diff options
author | Shawn O. Pearce <sop@google.com> | 2010-06-07 12:59:29 -0700 |
---|---|---|
committer | Shawn O. Pearce <sop@google.com> | 2010-06-07 12:59:29 -0700 |
commit | 099e8d35121a774afd57bfdafd763646f2e8bbf1 (patch) | |
tree | f92d45c9542e13cb5f7b3a3999fd52ce03498d96 | |
parent | fb190fe5de8151f0633016167f09fc805bc62286 (diff) | |
download | gwtorm-099e8d35121a774afd57bfdafd763646f2e8bbf1.tar.gz |
Update missing Javadoc around the NoSQL API
Change-Id: I6863f7b2241fc13ae169a7d19e013cdfb76334b0
Signed-off-by: Shawn O. Pearce <sop@google.com>
5 files changed, 204 insertions, 23 deletions
diff --git a/src/main/java/com/google/gwtorm/nosql/IndexFunction.java b/src/main/java/com/google/gwtorm/nosql/IndexFunction.java index 4678500..edc83b9 100644 --- a/src/main/java/com/google/gwtorm/nosql/IndexFunction.java +++ b/src/main/java/com/google/gwtorm/nosql/IndexFunction.java @@ -34,8 +34,8 @@ public abstract class IndexFunction<T> { * Should this object exist in the index? * <p> * Objects that shouldn't appear in this index are skipped because field - * values are currently {@code null} or because one or more constants doesn't - * match as expected. + * values are currently {@code null}, or because one or more field values do + * not match the constants used in the query that defines the index. * * @param object the object to read fields from. * @return true if the object should be indexed by this index. diff --git a/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java b/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java index b52a65f..7ba92bb 100644 --- a/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java +++ b/src/main/java/com/google/gwtorm/nosql/NoSqlAccess.java @@ -47,6 +47,13 @@ public abstract class NoSqlAccess<T, K extends Key<?>> extends protected abstract ResultSet<T> scan(String indexName, byte[] fromKey, byte[] toKey, int limit) throws OrmException; + /** + * Lookup a query index by its unique name. + * + * @param indexName the name of the index. + * @return the IndexFunction for {@code indexName} + * @throws OrmException if no IndexFunction exists for the supplied name. + */ protected IndexFunction<T> getQueryIndex(String indexName) throws OrmException { for (IndexFunction<T> f : getQueryIndexes()) { @@ -60,15 +67,29 @@ public abstract class NoSqlAccess<T, K extends Key<?>> extends throw new OrmException("No index named " + indexName); } - // -- These are all provided by AccessGen when builds a subclass -- + // -- These are all provided by AccessGen when it builds a subclass -- + /** @return name of the relation in the schema. */ protected abstract String getRelationName(); + /** @return encoder/decoder for the object data. */ protected abstract ProtobufCodec<T> getObjectCodec(); + /** @return encoder/decoder for the primary key of the object. */ protected abstract IndexFunction<T> getKeyIndex(); + /** @return array of indexes to support queries. */ protected abstract IndexFunction<T>[] getQueryIndexes(); + /** + * Encode the primary key of the object. + * <p> + * This method is similar to {@link #getKeyIndex()}, except it can encode the + * key without needing an object instance. This is suitable for key based + * lookup of the row. + * + * @param dst builder the key components will be added into. + * @param key the object primary key. + */ protected abstract void encodeKey(IndexKeyBuilder dst, K key); } diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java index 7578d70..19cd2c5 100644 --- a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java +++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java @@ -239,9 +239,17 @@ public abstract class GenericAccess<T, K extends Key<?>> extends } } - private void pruneOldIndexes(final T oldObj, T newObj) throws OrmException { - // Prune any old index records which no longer match. - // + /** + * Remove old secondary index rows that are no longer valid for an object. + * + * @param oldObj an old copy of the object, prior to the current update taking + * place. If null the method does nothing and simply returns. + * @param newObj the new copy of the object. Index rows that are still valid + * for {@code #newObj} are left alone. If null, all index rows for + * {@code oldObj} are removed. + * @throws OrmException the data store is unable to remove an index row. + */ + protected void pruneOldIndexes(final T oldObj, T newObj) throws OrmException { if (oldObj != null) { for (IndexFunction<T> f : getQueryIndexes()) { if (f.includes(oldObj)) { @@ -308,7 +316,19 @@ public abstract class GenericAccess<T, K extends Key<?>> extends } } - private boolean matches(IndexFunction<T> f, T obj, byte[] exp) { + /** + * Determine if an object still matches the index row. + * <p> + * This method checks that the object's fields still match the criteria + * necessary for it to be part of the index defined by {@code f}. It also + * formats the index key and validates it is still identical to {@code exp}. + * + * @param f the function that defines the index. + * @param obj the object instance being tested; must not be null. + * @param exp the index row key, as scanned from the index. + * @return true if the object still matches the data encoded in {@code #exp}. + */ + protected boolean matches(IndexFunction<T> f, T obj, byte[] exp) { return f.includes(obj) && Arrays.equals(exp, indexRowKey(f, obj)); } @@ -320,19 +340,43 @@ public abstract class GenericAccess<T, K extends Key<?>> extends return b.toByteArray(); } - private byte[] indexRowKey(IndexFunction<T> f, T obj) { + /** + * Generate the row key for an object's secondary index row. + * <p> + * The default implementation uses the relation name, '.', the index name, a + * delimiter, the indexed fields encoded, a delimiter, and then the encoded + * primary key (without the relation name prefix). + * <p> + * The object's primary key is always appended onto the end of the secondary + * index row key to ensure that objects with the same field values still get + * distinct rows in the secondary index. + * + * @param idx function that describes the index. + * @param obj the object the index record should reference. + * @return the encoded secondary index row key. + */ + protected byte[] indexRowKey(IndexFunction<T> idx, T obj) { IndexKeyBuilder b = new IndexKeyBuilder(); b.add(getRelationName()); b.add('.'); - b.add(f.getName()); + b.add(idx.getName()); b.delimiter(); - f.encode(b, obj); + idx.encode(b, obj); b.delimiter(); getKeyIndex().encode(b, obj); return b.toByteArray(); } - private byte[] indexRowData(T obj) { + /** + * Generate the data to store in a secondary index row for an object. + * <p> + * The default implementation of this method stores the encoded primary key, + * and the current system timestamp. + * + * @param obj the object the index record should reference. + * @return the encoded secondary index row data. + */ + protected byte[] indexRowData(T obj) { final long now = System.currentTimeMillis(); final IndexKeyBuilder b = new IndexKeyBuilder(); diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericDatabase.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericDatabase.java index 90828f9..3ee0f2f 100644 --- a/src/main/java/com/google/gwtorm/nosql/generic/GenericDatabase.java +++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericDatabase.java @@ -17,9 +17,36 @@ package com.google.gwtorm.nosql.generic; import com.google.gwtorm.client.OrmException; import com.google.gwtorm.client.Schema; import com.google.gwtorm.nosql.NoSqlDatabase; +import com.google.gwtorm.nosql.NoSqlSchema; import java.util.concurrent.TimeUnit; +/** + * Base class for generic NoSQL typed databases. + * <p> + * The generic types provide basic NoSQL implementation assuming a handful of + * primitive operations are available inside of the implementation's extension + * of {@link GenericSchema}. All relations are stored within the same key space, + * using the relation name as a prefix for the row's primary or secondary key. + * <p> + * Applications should use the database class to create instances of their + * Schema extension interface, and thus open and connect to the data store. + * <p> + * Creating a new database instance is expensive, due to the type analysis and + * code generation performed to implement the Schema and Access interfaces. + * Applications should create and cache their database instance for the life of + * the application. + * <p> + * Database instances are thread-safe, but returned Schema instances are not. + * <p> + * This class must be further extended by the NoSQL implementation to configure + * the connectivity with the data store and supply the correct subclass of + * {@link NoSqlSchema} that knows how to interact with the data store. + * + * @param <T> type of the application's Schema. + * @param <S> type of the implementation's base for Schema implementations. + * @param <A> type of the implementation's base for Access implementations. + */ public abstract class GenericDatabase<T extends Schema, S extends GenericSchema, A extends GenericAccess> extends NoSqlDatabase<T, S, A> { private static final long DEFAULT_FOSSIL_AGE = diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java index 79e65bd..34dc09a 100644 --- a/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java +++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java @@ -21,10 +21,18 @@ import com.google.gwtorm.nosql.CounterShard; import com.google.gwtorm.nosql.IndexKeyBuilder; import com.google.gwtorm.nosql.IndexRow; import com.google.gwtorm.nosql.NoSqlSchema; +import com.google.gwtorm.schema.SequenceModel; import java.util.Map; -/** Base implementation for {@link Schema} in a {@link GenericDatabase}. */ +/** + * Base implementation for {@link Schema} in a {@link GenericDatabase}. + * <p> + * NoSQL implementors must extend this class and provide implementations for the + * abstract methods declared here. Each schema instance will wrap one thread's + * connection to the data store. Therefore, unlike database, this class does not + * need to be thread-safe. + */ public abstract class GenericSchema extends NoSqlSchema { private final GenericDatabase<?, ?, ?> db; @@ -33,10 +41,28 @@ public abstract class GenericSchema extends NoSqlSchema { db = d; } + /** @return the database that created this schema instance. */ public GenericDatabase<?, ?, ?> getDatabase() { return db; } + /** + * Allocate a new unique value from a pool of values. + * <p> + * This method is only required to return a unique value per invocation. + * Implementors may override the method to provide an implementation that + * returns values out of order. + * <p> + * The default implementation of this method stores a {@link CounterShard} + * under the row key {@code ".sequence." + poolName}, and updates it through + * the atomic semantics of {@link #atomicUpdate(byte[], AtomicUpdate)}. If the + * row does not yet exist, it is initialized and the value 1 is returned. + * + * @param poolName name of the value pool to allocate from. This is typically + * the name of a sequence in the schema. + * @return a new unique value. + * @throws OrmException a unique value cannot be obtained. + */ @Override protected long nextLong(final String poolName) throws OrmException { IndexKeyBuilder b = new IndexKeyBuilder(); @@ -87,40 +113,103 @@ public abstract class GenericSchema extends NoSqlSchema { upsert(key, data); } + /** + * Update a single row, inserting it if it does not exist. + * <p> + * Unlike insert, this method always succeeds. + * + * @param key key of the row to update, or insert if missing. + * @param data data to store at this row. + * @throws OrmException the data store cannot process the request, for example + * due to a network connectivity problem. + */ public abstract void upsert(byte[] key, byte[] data) throws OrmException; + /** + * Delete the row stored under the given key. + * <p> + * If the row does not exist, this method must complete successfully anyway. + * The intent of the caller is to ensure the row does not exist when the + * method completes, and a row that did not exist satisfies that intent. + * + * @param key the key to delete. + * @throws OrmException the data store cannot perform the removal. + */ public abstract void delete(byte[] key) throws OrmException; /** * Atomically read and update a single row. * <p> - * Unlike the schema atomicUpdate, this method handles missing rows. - * Implementations must be logically equivalent to the following, but + * Unlike schema's atomicUpdate() method, this method must handle missing + * rows. Implementations must be logically equivalent to the following, but * performed atomically within the scope of the single row key: * * <pre> * byte[] oldData = get(key); * byte[] newData = update.update(oldData); - * if (newData != null) + * if (newData != null) { * upsert(key, newData); - * else if (oldData != null) remove(key); + * } else if (oldData != null) { + * remove(key); + * } * return data; * </pre> + * <p> + * Secondary index row updates are assumed to never be part of the atomic + * update transaction. This is an intentional design decision to fit with many + * NoSQL product's limitations to support only single-row atomic updates. + * <p> + * The {@code update} method may be invoked multiple times before the + * operation is considered successful. This permits an implementation to + * perform an opportunistic update attempt, and retry the update if the same + * row was modified by another concurrent worker. * * @param key the row key to read, update and return. - * @param update action to perform on the row's data element. May be passed in - * null if the row doesn't exist, and should return the new row data, - * or null to remove the row. - * @return the return value of {@code update}. + * @param update action to perform on the row's data element. The action may + * be passed null if the row doesn't exist. * @throws OrmException the database cannot perform the update. */ public abstract byte[] atomicUpdate(byte[] key, AtomicUpdate<byte[]> update) throws OrmException; - public void maybeFossilCollectIndexRow(long now, byte[] key, IndexRow r) - throws OrmException { - if (r.getTimestamp() + db.getMaxFossilAge() <= now) { + /** + * Check (and delete) an index row if its a fossil. + * <p> + * As index rows are written ahead of the main data row being written out, + * scans sometimes see an index row that does not match the data row. These + * are ignored for a short period ({@link GenericDatabase#getMaxFossilAge()}) + * to allow the primary data row to eventually get written out. If however the + * writer never finished the update, these index rows are stale and need to be + * pruned. Any index row older than the fossil age is removed by this method. + * + * @param now timestamp when the current scan started. + * @param key the index row key. + * @param row the index row data. + */ + public void maybeFossilCollectIndexRow(long now, byte[] key, IndexRow row) { + if (row.getTimestamp() + db.getMaxFossilAge() <= now) { + fossilCollectIndexRow(key, row); + } + } + + /** + * Delete the given fossil index row. + * <p> + * This method is logically the same as {@link #delete(byte[])}, but its + * separated out to permit asynchronous delivery of the delete events since + * these are arriving during an index scan and are less time-critical than + * other delete operations. + * <p> + * The default implementation of this method calls {@link #delete(byte[])}. + * + * @param key index key to remove. + * @param row the index row data. + */ + protected void fossilCollectIndexRow(byte[] key, IndexRow row) { + try { delete(key); + } catch (OrmException e) { + // Ignore a fossil delete error. } } |