package io.fathom.cloud.state; import io.fathom.cloud.CloudException; import io.fathom.cloud.state.StateStore.StateNode; import java.nio.ByteBuffer; import java.util.Random; import com.google.common.base.Charsets; import com.google.common.hash.Hashing; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.GeneratedMessage; import com.google.protobuf.Message; public class StringHashPrimaryKeyStrategy implements PrimaryKeyIndex { private final StateNode parentNode; private final FieldDescriptor idField; private final FieldDescriptor keyField; public StringHashPrimaryKeyStrategy(StateNode parentNode, FieldDescriptor idField, FieldDescriptor keyField) { this.parentNode = parentNode; this.idField = idField; this.keyField = keyField; } <T extends Message> String getKey(T item) { return (String) item.getField(keyField); } public <T extends GeneratedMessage> T find(NumberedItemCollection<T> collection, String key) throws CloudException { byte[] hash = Hashing.murmur3_128().hashString(key, Charsets.UTF_8).asBytes(); ByteBuffer buffer = ByteBuffer.wrap(hash); long v = buffer.getLong(); T item = collection.find(v, StoreOptions.ShowDeleted); if (item != null) { if (key.equals(getKey(item))) { if (!ItemStates.isDeleted(item)) { return item; } } } long seed = buffer.getLong(); Random random = new Random(seed); while (true) { long delta = random.nextLong(); long id = v ^ delta; item = collection.find(id); if (item != null) { if (key.equals(getKey(item))) { if (!ItemStates.isDeleted(item)) { return item; } } } if (item == null) { return null; } } } @Override public <T extends GeneratedMessage> long createId(NumberedItemCollection<T> collection, GeneratedMessage.Builder item) throws DuplicateValueException, CloudException { String keyValue = (String) item.getField(keyField); if (keyValue == null) { throw new IllegalArgumentException("String key cannot be null: " + keyField); } byte[] hash = Hashing.murmur3_128().hashString(keyValue, Charsets.UTF_8).asBytes(); ByteBuffer buffer = ByteBuffer.wrap(hash); long v = buffer.getLong(); T existing = collection.find(v, StoreOptions.ShowDeleted); if (existing == null) { return v; } if (keyValue.equals(getKey(existing))) { if (!ItemStates.isDeleted(existing)) { throw new DuplicateValueException(); } } long seed = buffer.getLong(); Random random = new Random(seed); while (true) { long delta = random.nextLong(); long id = v ^ delta; existing = collection.find(id, StoreOptions.ShowDeleted); if (existing == null) { return id; } if (keyValue.equals(getKey(existing))) { if (!ItemStates.isDeleted(existing)) { throw new DuplicateValueException(); } } } } // private boolean hasChild(long id) throws StateStoreException { // return parentNode.hasChild(Long.toHexString(id)); // } private <T extends GeneratedMessage> String findItemKey(NumberedItemCollection<T> collection, long id) throws CloudException { T item = collection.find(id); if (item == null) { return null; } String key = getKey(item); if (key == null) { throw new IllegalStateException(); } return key; } @Override public boolean allowDelete() { // We can't allow delete (we have to use tombstones instead) // Otherwise we wouldn't know when to stop the hash walk return false; } }