package net.sitemorph.protostore; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Message; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import static net.sitemorph.protostore.DbFieldCrudStore.setStatementValue; /** * URN keyed data store using columnar storage like the field iterator but uses * internal UUID. Also supports sort order. * * The goal of this class is to allow UUID based crud and avoid db locks with * multiple front end. * * TODO(dka) Implement update based on both vector and ID to prevent theoretical * race condition that could occur between 'read' of an updated value vector and * a concurrent read before the immediately proceeding write. Note though that * this second read would then error when updating as it would be out of date. * This can be fixed by using the update counter or a transaction which acquires * a write lock for the table. * * TODO(dka) Implement different vector start clocks as a best effort way of * avoiding the situation where there is a create, delete create with the * same random uuid and both have the same vector clock but are different * messages. This is only a remote possibility assuming that random UUID * reuse is low. * * @author damien@sitemorph.net * * TODO create an example on the github documentations * TODO consider making the name provider a factory * * TODO modify to use single statement vector update. Should not be used for * relaxed locking scenarios until resolved. */ public class DbUrnFieldStore<T extends Message> implements CrudStore<T> { private Connection connection; private PreparedStatement create, readAll, update, delete, readUrn; private String tableName; private Message.Builder prototype; private FieldDescriptor urnField; private Map<FieldDescriptor, PreparedStatement> readIndexes; private SortOrder sortDirection; private FieldDescriptor sortField; private FieldDescriptor vectorField; private DbUrnFieldStore() { readIndexes = Maps.newHashMap(); } /** * Create a urn based object with a defined urn field. * * @param builder to build from * @return the constructed object with urn set. * @throws CrudException */ @Override public T create(Message.Builder builder) throws CrudException { try { // set the uuid UUID uid = UUID.randomUUID(); CrudIterator<T> prior = read(prototype.clone() .setField(urnField, uid.toString())); while (prior.hasNext()) { prior.close(); uid = UUID.randomUUID(); prior = read(prototype.clone() .setField(urnField, uid.toString())); } prior.close(); builder.setField(urnField, uid.toString()); if (null != vectorField) { InMemoryStore.setInitialVector(builder, vectorField); } Descriptor descriptor = prototype.getDescriptorForType(); List<FieldDescriptor> fields = descriptor.getFields(); int offset = 1; for (FieldDescriptor field : fields) { setStatementValue(create, offset++, field, builder.hasField(field)? builder.getField(field) : null); } create.executeUpdate(); return (T) builder.build(); } catch (SQLException e) { throw new CrudException("Error creating new urn crud object", e); } } /** * Read from the store using either primary or secondary indexes if set up. * If no value is specified in either a primary or secondary index field all * records are returned. If multiple secondary index fields are set then it * it an implementation decision which to use to index the result. * * @param builder with either urn or secondary index set. * @return iterator over results. * @throws CrudException */ @Override public CrudIterator<T> read(Message.Builder builder) throws CrudException { try { if (builder.hasField(urnField)) { readUrn.setString(1, builder.getField(urnField).toString()); return new DbFieldIterator<T>(builder, readUrn.executeQuery()); } for (Map.Entry<FieldDescriptor, PreparedStatement> index : readIndexes.entrySet()) { FieldDescriptor field = index.getKey(); PreparedStatement statement = index.getValue(); if (builder.hasField(field)) { Object value = builder.getField(field); setStatementValue(statement, 1, field, value); return new DbFieldIterator<T>(builder, statement.executeQuery()); } } return new DbFieldIterator<T>(builder, readAll.executeQuery()); } catch (SQLException e) { throw new CrudException("Error reading urn fields records.", e); } } @Override public T update(Message.Builder builder) throws CrudException { if(!builder.hasField(urnField)) { throw new CrudException("Can't update message due to missing urn"); } // write the update try { Descriptor descriptor = builder.getDescriptorForType(); List<FieldDescriptor> fields = descriptor.getFields(); int offset = 1; long vector = -1; for (FieldDescriptor field : fields) { if (field.equals(urnField)) { // skip the urn field as it is set in the where continue; } if (null != vectorField && field.equals(vectorField)) { // update the vector vector = (Long) builder.getField(vectorField); InMemoryStore.updateVector(builder, vectorField); } Object value = builder.hasField(field)? builder.getField(field) : null; setStatementValue(update, offset++, field, value); } update.setString(offset++, builder.getField(urnField).toString()); if (null != vectorField) { update.setLong(offset, vector); } int updated = update.executeUpdate(); // test and set using update where old value to new value if (1 != updated) { throw new MessageVectorException( builder.getDescriptorForType().getName() + " : " + builder.getField(urnField) + " not updated to to vector mismatch"); } return (T) builder.build(); } catch (SQLException e) { throw new CrudException("Error updating urn crud value", e); } } @Override public void delete(T message) throws CrudException { if(!message.hasField(urnField)) { throw new CrudException("Can't update message due to missing urn"); } try { delete.setString(1, message.getField(urnField).toString()); if (null != vectorField) { Long vector = (Long)message.getField(vectorField); delete.setLong(2, vector); } int updated = delete.executeUpdate(); if (1 != updated) { throw new MessageVectorException("Delete failed due to missing or " + "vector clock mismatch"); } } catch (SQLException e) { throw new CrudException("Error deleting urn crud value: " + e.getMessage(), e); } } @Override public void close() throws CrudException { try { create.close(); readAll.close(); update.close(); delete.close(); for (Map.Entry<FieldDescriptor, PreparedStatement> index : readIndexes.entrySet()) { index.getValue().close(); } } catch (SQLException e) { throw new CrudException("Error closing Db Urn Field Store", e); } } public static class Builder<F extends Message> { private DbUrnFieldStore<F> result; private Set<String> indexes = Sets.newHashSet(); public Builder() { result = new DbUrnFieldStore<F>(); } public DbUrnFieldStore<F> build() throws CrudException { if (null == result.prototype) { throw new CrudException("Protobuf prototype required but not set."); } if (null == result.tableName) { throw new CrudException("Table name required but not set"); } if (null == result.urnField) { throw new CrudException("Required urn field not set"); } if (null == result.connection) { throw new CrudException("Connection null. Please provide a connector"); } Descriptor descriptor = result.prototype.getDescriptorForType(); List<FieldDescriptor> fields = descriptor.getFields(); for (String index : indexes) { boolean found = false; for (FieldDescriptor field : fields) { if (field.getName().equals(index)) { found = true; break; } } if (!found) { throw new CrudException("An undefined index field was specified: " + index); } } // Create StringBuilder create = new StringBuilder(); create.append("INSERT INTO ") .append(result.tableName) .append(" ("); for (FieldDescriptor field : fields) { create.append(field.getName()) .append(", "); } create.delete(create.length() - 2, create.length()); create.append(") VALUES ("); for (int i = 0; i < fields.size(); i++) { create.append("?, "); } create.delete(create.length() - 2, create.length()); create.append(")"); try { result.create = result.connection.prepareStatement(create.toString()); } catch (SQLException e) { throw new CrudException("Error generating create of Urn Store", e); } // Read all try { result.readAll = DbFieldCrudStore.getStatement(result.connection, result.tableName, fields, null, result.sortField, result.sortDirection); // read indexes for (FieldDescriptor field : fields) { if (indexes.contains(field.getName())) { result.readIndexes.put(field, DbFieldCrudStore.getStatement(result.connection, result.tableName, fields, field, result.sortField, result.sortDirection)); } } result.readUrn = DbFieldCrudStore.getStatement(result.connection, result.tableName, fields, result.urnField, result.sortField, result.sortDirection); } catch (SQLException e) { throw new CrudException("Error generating read of Urn Store", e); } // Update try { StringBuilder update = new StringBuilder(); update.append("UPDATE ") .append(result.tableName) .append(" SET "); for (FieldDescriptor field : fields) { if (field.equals(result.urnField)) { continue; } update.append(field.getName()) .append(" = ?, "); } update.delete(update.length() - 2, update.length()); update.append(" WHERE ") .append(result.urnField.getName()) .append(" = ?"); if (null != result.vectorField) { update.append(" AND ") .append(result.vectorField.getName()) .append(" = ?"); } result.update = result.connection.prepareStatement(update.toString()); } catch (SQLException e) { throw new CrudException("Error creating update for urn store", e); } // Delete try { StringBuilder delete = new StringBuilder(); delete.append("DELETE FROM ") .append(result.tableName) .append(" WHERE ") .append(result.urnField.getName()) .append(" = ?"); if (null != result.vectorField) { delete.append(" AND ") .append(result.vectorField.getName()) .append(" = ?"); } result.delete = result.connection.prepareStatement(delete.toString()); } catch (SQLException e) { throw new CrudException("Error creating delete for urn store", e); } return result; } public Builder<F> setConnection(Connection connection) { result.connection = connection; return this; } public Builder<F> setTableName(String tableName) { result.tableName = tableName; return this; } public Builder<F> setUrnColumn(String urnColumn) throws CrudException { return setUrnField(urnColumn); } public Builder<F> setUrnField(String urnField) throws CrudException { // look for the urn field descriptor in the field definition list Descriptor descriptor = result.prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(urnField)) { result.urnField = field; break; } } if (null == result.urnField) { StringBuilder fields = new StringBuilder(); for (FieldDescriptor field : descriptor.getFields()) { fields.append(field.getName()) .append(", "); } throw new CrudException("Error locating urn field by name " + urnField + " in field list " + fields.toString()); } return this; } public Builder<F> setVectorField(String fieldName) throws CrudException { Descriptor descriptor = result.prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(fieldName)) { result.vectorField = field; return this; } } throw new CrudException("Error locating vector field: " + fieldName); } public Builder<F> setPrototype(Message.Builder prototype) { result.prototype = prototype; return this; } public Builder<F> addIndexField(String indexField) { indexes.add(indexField); return this; } public Builder<F> setSortOrder(String fieldName, SortOrder direction) throws CrudException { result.sortDirection = direction; Descriptor descriptor = result.prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(fieldName)) { result.sortField = field; break; } } if (null == result.sortField) { throw new CrudException("Error locating sort field name: " + fieldName); } return this; } } }