package slopbucket; import jelectrum.EventLog; import java.io.File; import java.io.RandomAccessFile; import java.util.TreeMap; import java.nio.channels.FileChannel; import java.util.Map; import java.io.IOException; import java.nio.MappedByteBuffer; import java.nio.ByteBuffer; import java.util.HashSet; import java.util.Set; import com.google.protobuf.ByteString; import org.junit.Assert; import jelectrum.TimeRecord; import bloomtime.DeterministicStream; import java.nio.channels.FileChannel; public class Slopbucket { private EventLog log; private static final long SEGMENT_FILE_SIZE=Integer.MAX_VALUE; public static final int MAX_TROUGHS=64; public static final int MAX_TROUGH_NAME_LEN=64; private static final int LOCATION_VERSION=0; private static final int LOCATION_NEXT_FREE=LOCATION_VERSION+8; private static final int LOCATION_TROUGH_TABLE_START=LOCATION_NEXT_FREE+8; private static final int LOCATION_FIRST_FREE=LOCATION_TROUGH_TABLE_START+ (MAX_TROUGH_NAME_LEN + 8) * MAX_TROUGHS; private static final int LOCATION_HASH_MAX=0; private static final int LOCATION_HASH_ITEMS=LOCATION_HASH_MAX+4; private static final int LOCATION_HASH_NEXT=LOCATION_HASH_ITEMS+4; private static final int LOCATION_HASH_START=LOCATION_HASH_NEXT+8; private static final int HASH_INITAL_SIZE=64*1024; private static final int HASH_MULTIPLCATION=64; private static final double HASH_FULL=0.5; private Object ptr_lock = new Object(); private Map<Integer, MappedByteBuffer> open_buffers; private Map<String, Integer> trough_map; private File slop_file; private FileChannel file_channel; public Slopbucket(File slop_file, EventLog log) throws IOException { this.log = log; this.slop_file = slop_file; RandomAccessFile raf = new RandomAccessFile(slop_file, "rw"); file_channel = raf.getChannel(); open_buffers = new TreeMap<>(); } private long getCurrentWriteLocation() { MappedByteBuffer mbb = getBufferMap(0); long v; v = mbb.getLong((int)LOCATION_NEXT_FREE); if (v == 0) return LOCATION_FIRST_FREE; return v; } private void setCurrentWriteLocation(long v) { MappedByteBuffer mbb = getBufferMap(0); mbb.putLong((int)LOCATION_NEXT_FREE, v); } public Map<String, Integer> getTroughMap() { if (trough_map != null) return trough_map; Map<String, Integer> m = loadTroughMap(); trough_map = m; return m; } protected Map<String, Integer> loadTroughMap() { TreeMap<String,Integer> map = new TreeMap<>(); MappedByteBuffer mbb = getBufferMap(0); synchronized(mbb) { for(int i=0; i<MAX_TROUGHS; i++) { mbb.position( (int)(LOCATION_TROUGH_TABLE_START + (8 + MAX_TROUGH_NAME_LEN) * i)); long ptr = mbb.getLong(); byte[] name = new byte[MAX_TROUGH_NAME_LEN]; mbb.get(name); int len =0; for(int j=0; (j<MAX_TROUGH_NAME_LEN) && (name[j] != 0); j++) { len++; } if (len > 0) { String name_str = new String(name, 0, len); map.put(name_str, i); } else { map.put("__FREE", i); } } } return map; } public void addTrough(String name) { if (name.length() > MAX_TROUGH_NAME_LEN) throw new RuntimeException("OVER MAX NAME LENGTH"); Map<String, Integer> troughs = getTroughMap(); if(troughs.containsKey(name)) return; if (!troughs.containsKey("__FREE")) throw new RuntimeException("TOO MANY TROUGHS"); int trough_idx = troughs.get("__FREE"); long hash_loc = makeNewHashTable(HASH_INITAL_SIZE); MappedByteBuffer mbb = getBufferMap(0); synchronized(mbb) { mbb.position( (int) (LOCATION_TROUGH_TABLE_START + (8 + MAX_TROUGH_NAME_LEN) * trough_idx)); mbb.putLong(hash_loc); mbb.put(name.getBytes()); } trough_map = null; } public long getTroughPtr(String name) { Map<String, Integer> troughs = getTroughMap(); if (!troughs.containsKey(name)) { throw new RuntimeException("Unable to find trough: " + name); } int idx=troughs.get(name); MappedByteBuffer mbb = getBufferMap(0); synchronized(mbb) { mbb.position( (int) (LOCATION_TROUGH_TABLE_START + (8 + MAX_TROUGH_NAME_LEN) * idx) ); return mbb.getLong(); } } protected long makeNewHashTable(int items) { items = Math.min(items, (int)(SEGMENT_FILE_SIZE / 8 - 2)); long hash_loc = allocateSpace((int) (items * 8 + LOCATION_HASH_START)); writeInt(hash_loc + LOCATION_HASH_MAX, items); log.log("Making new hash table of size: " + items + " at " + hash_loc); return hash_loc; } protected void writeLong(long position, long value) { int file = (int) (position / SEGMENT_FILE_SIZE); int offset_in_file = (int) (position % SEGMENT_FILE_SIZE); MappedByteBuffer mbb = getBufferMap(file); synchronized(mbb) { mbb.position(offset_in_file); mbb.putLong(value); } } protected void writeInt(long position, int value) { int file = (int) (position / SEGMENT_FILE_SIZE); int offset_in_file = (int) (position % SEGMENT_FILE_SIZE); MappedByteBuffer mbb = getBufferMap(file); synchronized(mbb) { mbb.position(offset_in_file); mbb.putInt(value); } } protected long allocateSpace(int size) { synchronized(ptr_lock) { long loc = getCurrentWriteLocation(); long new_end = loc + size + 4; //If this would go into the next segment, just go to next segment if ((loc / SEGMENT_FILE_SIZE) < (new_end / SEGMENT_FILE_SIZE)) { loc = (new_end / SEGMENT_FILE_SIZE) * SEGMENT_FILE_SIZE; } long current_write_location = loc + size + 4; setCurrentWriteLocation(current_write_location); return loc; } } protected MappedByteBuffer getBufferMap(int idx) { synchronized(open_buffers) { if (open_buffers.containsKey(idx)) { return open_buffers.get(idx); } MappedByteBuffer mbb = openFileInternal(idx); open_buffers.put(idx, mbb); return mbb; } } protected MappedByteBuffer openFileInternal(int idx) { try { long pos = idx * SEGMENT_FILE_SIZE; return file_channel.map(FileChannel.MapMode.READ_WRITE, pos, SEGMENT_FILE_SIZE); } catch(java.io.IOException e) { throw new RuntimeException(e); } } public synchronized void putKeyValue(String trough_name, ByteString key, ByteString value) { long t1 = System.nanoTime(); long pos = getTroughPtr(trough_name); TimeRecord.record(t1, "slop_get_trough_ptr"); putKeyValueTable(pos, new RecordEntry(key, value)); } public synchronized ByteString getKeyValue(String trough_name, ByteString key) { long pos = getTroughPtr(trough_name); RecordEntry re = getKeyValueTable(pos, key); if (re == null) return null; return re.getValue(); } public synchronized void addListEntry(String trough_name, ByteString key, ByteString value) { long trough_pos = getTroughPtr(trough_name); RecordEntry prev_entry = getKeyValueTable(trough_pos, key); long prev_location = 0; if (prev_entry != null) { prev_location = prev_entry.getDataLoc(); } byte[] new_data_buff = new byte[8 + value.size()]; ByteBuffer bb = ByteBuffer.wrap(new_data_buff); bb.putLong(prev_location); value.copyTo(new_data_buff, 8); ByteString new_data = ByteString.copyFrom(new_data_buff); RecordEntry re = new RecordEntry(key, new_data); re.storeItem(0L); putKeyValueTable(trough_pos, re); } public synchronized Set<ByteString> getList(String trough_name, ByteString key) { long trough_pos = getTroughPtr(trough_name); RecordEntry re = getKeyValueTable(trough_pos, key); HashSet<ByteString> set = new HashSet<ByteString>(); while(re != null) { ByteString val_obj = re.getValue(); ByteString val = val_obj.substring(8); set.add(val); long location = val_obj.asReadOnlyByteBuffer().getLong(); re = null; if (location > 0) { re = new RecordEntry(location); } } return set; } protected RecordEntry getKeyValueTable(long table_pos, ByteString key) { int hash_file = (int) (table_pos / SEGMENT_FILE_SIZE); MappedByteBuffer hash_mbb = getBufferMap(hash_file); int file_offset = (int) (table_pos % SEGMENT_FILE_SIZE); int max; int items; long next_ptr; synchronized(hash_mbb) { hash_mbb.position(file_offset + (int) LOCATION_HASH_MAX); max = hash_mbb.getInt(); items = hash_mbb.getInt(); next_ptr = hash_mbb.getLong(); } Assert.assertTrue("Max " + max + " items " + items + " table at " + table_pos + " file " + hash_file + " file offset " + file_offset ,max > items); Assert.assertTrue(max > 4); Assert.assertTrue(items >= 0); int hash = key.hashCode(); int loc = Math.abs(hash % max); if (loc < 0) loc = 0; //DeterministicStream det_stream = new DeterministicStream(key); //int loc = det_stream.nextInt(max); while(true) { Assert.assertTrue(loc >= 0); Assert.assertTrue(loc < max); synchronized(hash_mbb) { hash_mbb.position(file_offset + LOCATION_HASH_START + loc * 8); long ptr = hash_mbb.getLong(); if (ptr != 0) { RecordEntry re = new RecordEntry(ptr); if (re.getKey().equals(key)) return re; } if (ptr == 0) { if (next_ptr != 0) { return getKeyValueTable(next_ptr, key); } else { return null; } } } //loc = det_stream.nextInt(max); loc = (loc + 131 ) % max; } } protected void putKeyValueTable(long table_pos, RecordEntry put_re) { long t1=System.nanoTime(); int hash_file = (int) (table_pos / SEGMENT_FILE_SIZE); MappedByteBuffer hash_mbb = getBufferMap(hash_file); int file_offset = (int) (table_pos % SEGMENT_FILE_SIZE); int max; int items; long next_ptr; synchronized(hash_mbb) { hash_mbb.position(file_offset + (int) LOCATION_HASH_MAX); max = hash_mbb.getInt(); items = hash_mbb.getInt(); next_ptr = hash_mbb.getLong(); } Assert.assertTrue("Max " + max + " items " + items + " table at " + table_pos + " file " + hash_file + " file offset " + file_offset ,max > items); Assert.assertTrue(max > 4); Assert.assertTrue(items >= 0); //DeterministicStream det_stream = new DeterministicStream(key); //int loc = det_stream.nextInt(max); int hash = put_re.getKey().hashCode(); int loc = Math.abs(hash % max); if (loc < 0) loc = 0; double full = (double) items / (double) max; while(true) { Assert.assertTrue(loc >= 0); Assert.assertTrue(loc < max); synchronized(hash_mbb) { long t1_check = System.nanoTime(); hash_mbb.position(file_offset + LOCATION_HASH_START + loc * 8); long ptr = hash_mbb.getLong(); TimeRecord.record(t1_check, "slop_get_ptr"); if ((ptr == 0) && (full >= HASH_FULL)) { // It isn't here and the hash is full, move on to next table if (next_ptr == 0) { next_ptr = makeNewHashTable(max * HASH_MULTIPLCATION); hash_mbb.position(file_offset + (int) LOCATION_HASH_NEXT); hash_mbb.putLong(next_ptr); } TimeRecord.record(t1, "slop_put_key_value_table_rec"); putKeyValueTable(next_ptr, put_re); return; } RecordEntry re = null; if (ptr != 0) { re = new RecordEntry(ptr); if (!re.getKey().equals(put_re.getKey())) { re = null; } } if ((ptr == 0) || (re!=null)) { //If we have an empty or a key match long data_loc = put_re.storeItem(ptr); hash_mbb.position(file_offset + LOCATION_HASH_START + loc * 8); hash_mbb.putLong(data_loc); if (ptr == 0) { hash_mbb.position(file_offset + LOCATION_HASH_ITEMS); items++; hash_mbb.putInt(items); } TimeRecord.record(t1, "slop_put_key_value_table_add"); return; } } //loc = det_stream.nextInt(max); loc = (loc + 131 ) % max; } } private Map<String, Long> getStats(String trough_name) { Map<String, Long> map = new TreeMap<String, Long>(); map.put("tables",0L); map.put("items",0L); map.put("key_size",0L); map.put("data_size",0L); long pos = getTroughPtr(trough_name); getTableStats(pos, map); return map; } private void getTableStats(long table_pos, Map<String, Long> map) { map.put("tables", map.get("tables") + 1); int hash_file = (int) (table_pos / SEGMENT_FILE_SIZE); MappedByteBuffer hash_mbb = getBufferMap(hash_file); int file_offset = (int) (table_pos % SEGMENT_FILE_SIZE); int max; int items; long next_ptr; synchronized(hash_mbb) { hash_mbb.position(file_offset + (int) LOCATION_HASH_MAX); max = hash_mbb.getInt(); items = hash_mbb.getInt(); next_ptr = hash_mbb.getLong(); hash_mbb.position(file_offset + (int) LOCATION_HASH_START); for(int i=0; i<max; i++) { long ptr = hash_mbb.getLong(file_offset + LOCATION_HASH_START + i*8); if (ptr != 0) { RecordEntry re = new RecordEntry(ptr); ByteString key = re.getKey(); ByteString value = re.getValue(); map.put("key_size", map.get("key_size") + key.size()); map.put("data_size", map.get("data_size") + value.size()); } } } map.put("items", map.get("items") + items); if (next_ptr != 0) { getTableStats(next_ptr, map); } } public void printStats() { for(Map.Entry<String, Integer> me : getTroughMap().entrySet()) { if (!me.getKey().equals("__FREE")) { System.out.println(me.getKey() + " - " +getStats(me.getKey())); } } } public class RecordEntry { int max_data_size; //Size of entire alloc ByteString key; ByteString value; long data_loc; public RecordEntry(long data_loc) { int file = (int) (data_loc / SEGMENT_FILE_SIZE); MappedByteBuffer mbb = getBufferMap(file); int offset = (int) (data_loc % SEGMENT_FILE_SIZE); synchronized(mbb) { mbb.position(offset); max_data_size = mbb.getInt(); int sz = mbb.getShort(); byte[] b = new byte[sz]; mbb.get(b); key = ByteString.copyFrom(b); sz = mbb.getInt(); b = new byte[sz]; mbb.get(b); value = ByteString.copyFrom(b); } this.data_loc = data_loc; } public RecordEntry(ByteString key, ByteString value) { data_loc = 0; max_data_size = 0; this.key = key; this.value = value; } public int getMaxDataSize() { return max_data_size; } public ByteString getKey() { return key; } public ByteString getValue() { return value; } public long getDataLoc() { return data_loc; } public int getMinAlloc() { return 4 + 2 + 4 + key.size() + value.size(); } private void save() { Assert.assertTrue(max_data_size >= getMinAlloc()); Assert.assertNotEquals(0, data_loc); Assert.assertNotNull(key); Assert.assertNotNull(value); int file = (int) (data_loc / SEGMENT_FILE_SIZE); MappedByteBuffer mbb = getBufferMap(file); int offset = (int) (data_loc % SEGMENT_FILE_SIZE); synchronized(mbb) { mbb.position(offset); mbb.putInt(max_data_size); mbb.putShort((short)key.size()); mbb.put(key.toByteArray()); mbb.putInt(value.size()); mbb.put(value.toByteArray()); } } public long storeItem(long prev_ptr) { //If someone already stored it, just return that location if (data_loc > 0) return data_loc; //If the prev_ptr is set and can store my item, use that if (prev_ptr > 0 ) { RecordEntry prev_entry = new RecordEntry(prev_ptr); if (getMinAlloc() <= prev_entry.getMaxDataSize()) { data_loc=prev_ptr; max_data_size = prev_entry.getMaxDataSize(); save(); return data_loc; } else { //If we have already stored it once and it doesn't fit in that space //go bigger so hopefully if it changes again it will fit max_data_size = getMinAlloc() * 2; data_loc = allocateSpace(max_data_size); save(); return data_loc; } } //If not, make a new space long t1_save = System.nanoTime(); max_data_size = getMinAlloc(); data_loc = allocateSpace(getMinAlloc()); save(); TimeRecord.record(t1_save, "slop_save_key_value"); return data_loc; } } public synchronized void flush(boolean close) throws java.io.IOException { for(MappedByteBuffer mbb : open_buffers.values()) { mbb.force(); } if (close) { open_buffers.clear(); file_channel.close(); } } }