Skip to content
Browse files

Got a lot of coding done for transactions. Still need to work out the…

… tests.

Signed-off-by: gburgett <gordon.burgett@gmail.com>
  • Loading branch information...
1 parent 88840d0 commit 3bcc00998fff7b8f339e97339d43dd3c0278072e @gburgett committed Jan 19, 2013
Showing with 955 additions and 94 deletions.
  1. +3 −1 java/XFlat/src/org/gburgett/xflat/Cursor.java
  2. +2 −2 java/XFlat/src/org/gburgett/xflat/DatabaseConfig.java
  3. +1 −1 java/XFlat/src/org/gburgett/xflat/TableConfig.java
  4. +1 −1 java/XFlat/src/org/gburgett/xflat/db/ConvertingTable.java
  5. +171 −8 java/XFlat/src/org/gburgett/xflat/db/EngineBase.java
  6. +22 −2 java/XFlat/src/org/gburgett/xflat/db/ShardedEngineBase.java
  7. +29 −6 java/XFlat/src/org/gburgett/xflat/db/TableMetadata.java
  8. +1 −1 java/XFlat/src/org/gburgett/xflat/db/TableMetadataFactory.java
  9. +1 −1 java/XFlat/src/org/gburgett/xflat/db/XFlatDatabase.java
  10. +268 −50 java/XFlat/src/org/gburgett/xflat/engine/CachedDocumentEngine.java
  11. +2 −1 java/XFlat/src/org/gburgett/xflat/engine/IdShardedEngine.java
  12. +11 −12 java/XFlat/src/org/gburgett/xflat/engine/InactiveCache.java
  13. +1 −1 java/XFlat/src/org/gburgett/xflat/query/EmptyCursor.java
  14. +23 −0 java/XFlat/src/org/gburgett/xflat/transaction/IllegalTransactionStateException.java
  15. +203 −0 java/XFlat/src/org/gburgett/xflat/transaction/ThreadContextTransactionManager.java
  16. +45 −0 java/XFlat/src/org/gburgett/xflat/transaction/Transaction.java
  17. +22 −0 java/XFlat/src/org/gburgett/xflat/transaction/TransactionException.java
  18. +63 −0 java/XFlat/src/org/gburgett/xflat/transaction/TransactionManager.java
  19. +46 −0 java/XFlat/src/org/gburgett/xflat/transaction/TransactionOptions.java
  20. +33 −0 java/XFlat/src/org/gburgett/xflat/transaction/TransactionValues.java
  21. +4 −4 java/XFlat/test/org/gburgett/xflat/db/DatabaseIntegrationTest.java
  22. +2 −2 java/XFlat/test/org/gburgett/xflat/db/XFlatDatabaseTest.java
  23. +1 −1 java/XFlat/test/org/gburgett/xflat/engine/IdShardedEngineIntegrationTests.java
View
4 java/XFlat/src/org/gburgett/xflat/Cursor.java
@@ -11,5 +11,7 @@
* @author gordon
*/
public interface Cursor<T> extends Iterable<T>, AutoCloseable {
-
+
+ @Override
+ void close() throws XflatException;
}
View
4 java/XFlat/src/org/gburgett/xflat/DatabaseConfig.java
@@ -141,10 +141,10 @@ public DatabaseConfig setDefaultTableConfig(TableConfig tableConfig){
/**
* The default configuration used by the Database.
*/
- public static DatabaseConfig defaultConfig = new DatabaseConfig()
+ public static DatabaseConfig Default = new DatabaseConfig()
.setThreadCount(4)
.setPojoConverterClass("org.gburgett.xflat.convert.converters.JAXBPojoConverter")
- .setDefaultTableConfig(TableConfig.defaultConfig)
+ .setDefaultTableConfig(TableConfig.Default)
.setIdGeneratorStrategy(Arrays.asList(
UuidIdGenerator.class,
TimestampIdGenerator.class,
View
2 java/XFlat/src/org/gburgett/xflat/TableConfig.java
@@ -95,7 +95,7 @@ public TableConfig sharded(ShardsetConfig<?> config){
* The default configuration used by the database when no configuration
* is specified.
*/
- public static TableConfig defaultConfig = new TableConfig()
+ public static TableConfig Default = new TableConfig()
.setIdGenerator(null)
.setInactivityShutdownMs(3000);
View
2 java/XFlat/src/org/gburgett/xflat/db/ConvertingTable.java
@@ -432,7 +432,7 @@ public ConvertingCursor(Cursor<Element> rowCursor){
}
@Override
- public void close() throws Exception {
+ public void close() throws XflatException {
this.rowCursor.close();
}
}
View
179 java/XFlat/src/org/gburgett/xflat/db/EngineBase.java
@@ -4,12 +4,20 @@
*/
package org.gburgett.xflat.db;
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gburgett.xflat.convert.ConversionService;
+import org.gburgett.xflat.transaction.Transaction;
+import org.gburgett.xflat.transaction.TransactionManager;
+import org.jdom2.Attribute;
import org.jdom2.Element;
/**
@@ -123,6 +131,7 @@ public SpinDownEvent(Engine source){
//</editor-fold>
+ //<editor-fold desc="dependencies">
private ScheduledExecutorService executorService;
protected ScheduledExecutorService getExecutorService(){
return executorService;
@@ -141,6 +150,21 @@ protected void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
+ private TransactionManager transactionManager;
+ /**
+ * Gets the transactionManager.
+ */
+ public TransactionManager getTransactionManager(){
+ return this.transactionManager;
+ }
+ /**
+ * Sets the transactionManager.
+ */
+ public void setTransactionManager(TransactionManager transactionManager){
+ this.transactionManager = transactionManager;
+ }
+
+ //</editor-fold>
/**
@@ -178,16 +202,155 @@ protected void setId(Element row, String id){
row.setAttribute("id", id, XFlatDatabase.xFlatNs);
}
+
+
+ /**
+ * Checks whether this engine has any transactional updates in an uncommitted
+ * or unreverted state.
+ * If so, returns true.
+ * @return true if this engine has uncommitted transactional data, false otherwise.
+ */
+ protected abstract boolean hasUncomittedData();
+
/**
- * Wraps some element data in a row element with the given ID
- * @param data The data to wrap
- * @param id The ID to use for the row
- * @return The data wrapped in a row element
+ * Represents one row in the database. The row contains a set of
+ * {@link RowData} which represents the committed and uncommitted data in
+ * the row. The row data is mapped by its transaction ID.
+ * <p/>
+ * The Row should always be locked before any reading or modification of
+ * the data.
*/
- protected Element wrapInRow(Element data, String id){
- Element row = new Element("row", XFlatDatabase.xFlatNs).setContent(data);
- setId(row, id);
+ protected class Row{
+ /**
+ * The ID of this row.
+ */
+ public final String rowId;
+
+ /**
+ * A SortedMap of the committed and uncommitted data in the row.
+ * Always lock the row before accessing this data.
+ */
+ public final SortedMap<Long, RowData> rowData = new TreeMap<>();
+
+ public Row(String id){
+ this.rowId = id;
+ }
+
+ public Row(String id, RowData data){
+ this.rowId = id;
+ this.rowData.put(data.transactionId, data);
+ }
+
+ /**
+ * Chooses the most recent committed RowData that was committed before the given transaction.
+ * If the transaction is null, this will choose the most recent committed
+ * RowData globally.
+ * <p/>
+ * ALWAYS invoke this while synchronized on the Row.
+ * @param currentTransaction The current transaction, or null.
+ * @return The most recent committed RowData in this row, committed before the transaction.
+ */
+ public RowData chooseMostRecentCommitted(Transaction currentTransaction){
+ if(currentTransaction == null){
+ return chooseMostRecentCommitted(null, Long.MAX_VALUE);
+ }
+
+ return chooseMostRecentCommitted(currentTransaction, currentTransaction.getTransactionId());
+ }
+
+ /**
+ * Chooses the most recent committed RowData that was committed before the given transaction ID.
+ * This prevents dirty reads in a non-transactional context by having a synchronizing transaction ID
+ * which can be obtained from {@link TransactionManager#transactionlessCommitId() }
+ * <p/>
+ * ALWAYS invoke this while synchronized on the Row.
+ * @param snapshotId The Transaction ID representing the time at which a snapshot of the data should be obtained.
+ * @return The most recent committed RowData in this row, committed before the given snapshot.
+ */
+ public RowData chooseMostRecentCommitted(Long snapshotId){
+ return chooseMostRecentCommitted(null, snapshotId);
+ }
+
+ private RowData chooseMostRecentCommitted(Transaction currentTransaction, long currentTxId){
+
+ RowData ret = null;
+ long retCommitId = -1;
+
+ Iterator<RowData> it = rowData.values().iterator();
+ while(it.hasNext()){
+ RowData data = it.next();
+
+ //if we're in a transaction, see if this row is the version for this transaction.
+ //if the transaction is reverted we don't want that, we want the most recent
+ //committed version
+ if(currentTransaction != null && !currentTransaction.isReverted()){
+
+ if(data.transactionId > -1 && currentTxId == data.transactionId){
+ //this row data is the data in the current transaction
+ return data;
+ }
+ }
+
+ if(data.commitId == -1){
+ //uncommitted row data - doublecheck with the transaction manager
+
+ data.commitId = transactionManager.isTransactionCommitted(data.transactionId);
+ }
+
+ if(data.commitId > -1){
+ //this row data has been committed
+ if(currentTxId > data.commitId){
+ //the current transaction is null or began after the transaction was committed
+
+ if(retCommitId < data.commitId){
+ //the last valid version we saw was before this version.
+
+ ret = data;
+ retCommitId = data.commitId;
+ }
+ }
+ }
+ else{
+ //check if reverted
+ if(transactionManager.isTransactionReverted(data.transactionId)){
+ //remove it from the row
+ it.remove();
+ }
+ }
+ }
+
+ return ret;
+ }
+
+
+ }
+
+ protected class RowData{
+ /**
+ * A snapshot of the data in the row, possibly uncommitted.
+ */
+ public Element data = null;
+
+ /**
+ * The ID of the transaction that created this data snapshot
+ */
+ public long transactionId = -1;
- return row;
+ /**
+ * The ID of the transaction commit that caused this row data to become
+ * committed. If the data is uncommitted, this is -1.
+ */
+ public long commitId = -1;
+
+ public RowData(long txId){
+ this.transactionId = txId;
+ }
+
+ public RowData(long txId, Element data){
+ this.data = data;
+ this.transactionId = txId;
+ }
}
+
+
}
View
24 java/XFlat/src/org/gburgett/xflat/db/ShardedEngineBase.java
@@ -124,7 +124,7 @@ private EngineBase getEngine(Interval<T> interval){
this.knownShards.put(interval, file);
metadata = this.getMetadataFactory().makeTableMetadata(name, file);
- metadata.config = TableConfig.defaultConfig; //not even really used for our purposes
+ metadata.config = TableConfig.Default; //not even really used for our purposes
TableMetadata weWereLate = openShards.putIfAbsent(interval, metadata);
if(weWereLate != null){
@@ -169,7 +169,7 @@ protected void update(){
Iterator<TableMetadata> it = openShards.values().iterator();
while(it.hasNext()){
TableMetadata table = it.next();
- if(table.getLastActivity() + 3000 < System.currentTimeMillis()){
+ if(table.canSpinDown()){
//remove right now - if between the check and the remove we got some activity
//then oh well, we can spin up a new instance.
it.remove();
@@ -184,6 +184,26 @@ protected void update(){
}
}
}
+
+ @Override
+ protected boolean hasUncomittedData() {
+ EngineState state = this.state.get();
+ if(state == EngineState.SpinningDown){
+ for(EngineBase e : this.spinningDownEngines.values()){
+ if(e.hasUncomittedData()){
+ return true;
+ }
+ }
+ }
+ else if(state == EngineState.Running){
+ for(TableMetadata table : this.openShards.values()){
+ if(table.hasUncommittedData()){
+ return true;
+ }
+ }
+ }
+ return false;
+ }
@Override
protected boolean spinUp() {
View
35 java/XFlat/src/org/gburgett/xflat/db/TableMetadata.java
@@ -47,8 +47,16 @@ public String getName(){
TableConfig config;
long lastActivity = System.currentTimeMillis();
- public long getLastActivity(){
- return lastActivity;
+
+
+ public boolean canSpinDown(){
+ EngineBase engine = this.engine.get();
+ return lastActivity + 3000 < System.currentTimeMillis() && engine == null || !engine.hasUncomittedData();
+ }
+
+ public boolean hasUncommittedData(){
+ EngineBase engine = this.engine.get();
+ return engine != null && engine.hasUncomittedData();
}
EngineState getEngineState(){
@@ -182,13 +190,29 @@ else if(state == EngineState.SpinningUp ||
public EngineBase spinDown(){
synchronized(this){
- final EngineBase engine = this.engine.getAndSet(null);
+ EngineBase engine = this.engine.getAndSet(null);
EngineState state;
if(engine == null ||
(state = engine.getState()) == EngineState.SpinningDown ||
- state == EngineState.SpunDown)
+ state == EngineState.SpunDown){
//another thread already spinning it down
return engine;
+
+ }
+ else{
+ if(engine.hasUncomittedData()){
+ //whoops! see if we can put it back quick
+ engine = this.engine.getAndSet(engine);
+ //continue like we were spinning this one down.
+
+ if(engine == null ||
+ (state = engine.getState()) == EngineState.SpinningDown ||
+ state == EngineState.SpunDown){
+
+ return engine;
+ }
+ }
+ }
Log l = LogFactory.getLog(getClass());
if(l.isTraceEnabled())
@@ -197,8 +221,7 @@ public EngineBase spinDown(){
if(engine.spinDown(new SpinDownEventHandler(){
@Override
- public void spinDownComplete(SpinDownEvent event) {
- engine.forceSpinDown();
+ public void spinDownComplete(SpinDownEvent event) {
}
}))
{
View
2 java/XFlat/src/org/gburgett/xflat/db/TableMetadataFactory.java
@@ -126,7 +126,7 @@ private TableMetadata makeNewTableMetadata(String name, File engineFile, TableCo
TableMetadata ret = new TableMetadata(name, db, engineFile);
- config = config == null ? TableConfig.defaultConfig : config;
+ config = config == null ? TableConfig.Default : config;
ret.config = config;
//make ID Generator
View
2 java/XFlat/src/org/gburgett/xflat/db/XFlatDatabase.java
@@ -117,7 +117,7 @@ public void run() {
//the engine cache
private ConcurrentHashMap<String, TableMetadata> tables = new ConcurrentHashMap<>();
- private DatabaseConfig config = DatabaseConfig.defaultConfig;
+ private DatabaseConfig config = DatabaseConfig.Default;
public void setConfig(DatabaseConfig config){
if(this.state.get() != DatabaseState.Uninitialized){
throw new XflatException("Cannot configure database after initialization");
View
318 java/XFlat/src/org/gburgett/xflat/engine/CachedDocumentEngine.java
@@ -31,8 +31,10 @@
import org.gburgett.xflat.db.XFlatDatabase;
import org.gburgett.xflat.query.XpathQuery;
import org.gburgett.xflat.query.XpathUpdate;
+import org.gburgett.xflat.transaction.Transaction;
import org.gburgett.xflat.util.DocumentFileWrapper;
import org.hamcrest.Matcher;
+import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
@@ -45,7 +47,9 @@
private final AtomicBoolean operationsReady = new AtomicBoolean(false);
- private ConcurrentMap<String, Element> cache = null;
+ private ConcurrentMap<String, Row> cache = null;
+
+ private ConcurrentMap<String, Row> uncommittedRows = null;
private DocumentFileWrapper file;
public DocumentFileWrapper getFile(){
@@ -62,24 +66,55 @@ public CachedDocumentEngine(DocumentFileWrapper file, String tableName){
this.file = file;
}
+ private void setTxId(Element data, long txId){
+ data.setAttribute("tx", Long.toString(txId), XFlatDatabase.xFlatNs);
+ }
+
+ private long getTxId(Transaction tx){
+ return tx != null ?
+ tx.getTransactionId() :
+ //transactionless insert, get a new ID
+ this.getTransactionManager().transactionlessCommitId();
+ }
+
//<editor-fold desc="interface methods">
@Override
public void insertRow(String id, Element data) throws DuplicateKeyException {
ensureReady();
- Element row = wrapInRow(data, id);
- Element existed = this.cache.putIfAbsent(id, row);
- if(existed != null){
- throw new DuplicateKeyException(id);
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
+ RowData rData = new RowData(txId, data);
+ if(tx == null){
+ //transactionless means auto-commit
+ rData.commitId = txId;
}
+ Row row = new Row(id, rData);
+ row = this.cache.putIfAbsent(id, row);
+ if(row != null){
+ synchronized(row){
+ //see if all the data was from after this transaction
+ RowData chosen = row.chooseMostRecentCommitted(tx);
+ if(chosen == null || chosen.data == null){
+ //we're good to insert our transactional data
+ row.rowData.put(txId, rData);
+ this.uncommittedRows.put(id, row);
+ }
+ else{
+ throw new DuplicateKeyException(id);
+ }
+ }
+ }
+
setLastActivity(System.currentTimeMillis());
dumpCache();
}
@Override
public Element readRow(String id) {
- Element row = this.cache.get(id);
+ Row row = this.cache.get(id);
if(row == null){
return null;
}
@@ -88,15 +123,23 @@ public Element readRow(String id) {
//lock the row
synchronized(row){
+ Transaction tx = this.getTransactionManager().getTransaction();
+ RowData ret = row.chooseMostRecentCommitted(tx);
+
+ if(ret == null || ret.data == null){
+ return null;
+ }
+
//clone the data
- return row.getChildren().get(0).clone();
+ return ret.data.clone();
}
}
@Override
public Cursor<Element> queryTable(XpathQuery query) {
query.setConversionService(this.getConversionService());
- TableCursor ret = new TableCursor(this.cache.values(), query);
+
+ TableCursor ret = new TableCursor(this.cache.values(), query, getTransactionManager().getTransaction());
this.openCursors.put(ret, "");
setLastActivity(System.currentTimeMillis());
@@ -108,12 +151,29 @@ public Element readRow(String id) {
public void replaceRow(String id, Element data) throws KeyNotFoundException {
ensureReady();
- Element row = wrapInRow(data, id);
- Element replaced = this.cache.replace(id, row);
- if(replaced == null){
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
+
+ Row row = this.cache.get(id);
+ if(row == null){
throw new KeyNotFoundException(id);
}
+ synchronized(row){
+ RowData toReplace = row.chooseMostRecentCommitted(tx);
+ if(toReplace == null || toReplace.data == null){
+ throw new KeyNotFoundException(id);
+ }
+
+ RowData newData = new RowData(txId, data);
+ if(tx == null){
+ //transactionless means auto-commit
+ newData.commitId = txId;
+ }
+ row.rowData.put(txId, newData);
+ }
+
setLastActivity(System.currentTimeMillis());
dumpCache();
}
@@ -122,19 +182,39 @@ public void replaceRow(String id, Element data) throws KeyNotFoundException {
public boolean update(String id, XpathUpdate update) throws KeyNotFoundException {
ensureReady();
- Element row = this.cache.get(id);
+ Row row = this.cache.get(id);
if(row == null){
throw new KeyNotFoundException(id);
}
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
update.setConversionService(this.getConversionService());
- boolean ret = false;
+ boolean ret;
try {
//lock the row
synchronized(row){
- int updates = update.apply(row);
- ret = updates > 0;
+ RowData data = row.chooseMostRecentCommitted(tx);
+ if(data == null || data.data == null){
+ throw new KeyNotFoundException(id);
+ }
+ else{
+ //apply to a copy, store the copy as a transactional state.
+ RowData newData = new RowData(txId, data.data.clone());
+ if(tx == null){
+ //transactionless means auto-commit
+ newData.commitId = txId;
+ }
+
+ int updates = update.apply(newData.data);
+ ret = updates > 0;
+ if(ret){
+ //no need to put a new version if no data was modified
+ row.rowData.put(txId, newData);
+ }
+ }
}
} catch (JDOMException ex) {
if(log.isDebugEnabled())
@@ -160,14 +240,37 @@ public int update(XpathQuery query, XpathUpdate update) {
Matcher<Element> rowMatcher = query.getRowMatcher();
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
+
int rowsUpdated = 0;
- for(Element row : this.cache.values()){
+ for(Row row : this.cache.values()){
synchronized(row){
- if(!rowMatcher.matches(row))
+ RowData rData = row.chooseMostRecentCommitted(tx);
+ if(rData == null || rData.data == null){
continue;
+ }
+
+ if(!rowMatcher.matches(rData.data))
+ continue;
+
try {
- int updates = update.apply(row);
+ //apply to a copy, store the copy as a transactional state.
+ RowData newData = new RowData(txId, rData.data.clone());
+ if(tx == null){
+ //transactionless means auto-commit
+ newData.commitId = txId;
+ }
+
+ int updates = update.apply(newData.data);
+
+ if(updates > 0){
+ //no need to put a new version if no data was modified
+ row.rowData.put(txId, newData);
+ }
+
rowsUpdated = updates > 0 ? rowsUpdated + 1 : rowsUpdated;
}
catch (JDOMException ex) {
@@ -190,8 +293,22 @@ public int update(XpathQuery query, XpathUpdate update) {
public boolean upsertRow(String id, Element data) {
ensureReady();
- Element row = wrapInRow(data, id);
- Element existed = this.cache.put(id, row);
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
+ RowData newData = new RowData(txId, data);
+ if(tx == null){
+ //transactionless means auto-commit
+ newData.commitId = txId;
+ }
+
+ Row newRow = new Row(id, newData);
+
+ Row existed = this.cache.putIfAbsent(id, newRow); //takes care of the insert
+ synchronized(existed){
+ //takes care of the "or update"
+ existed.rowData.put(txId, newData);
+ }
setLastActivity(System.currentTimeMillis());
dumpCache();
@@ -203,12 +320,31 @@ public boolean upsertRow(String id, Element data) {
public void deleteRow(String id) throws KeyNotFoundException {
ensureReady();
- Element removed = this.cache.remove(id);
+ Row toRemove = this.cache.get(id);
- if(removed == null){
+ if(toRemove == null){
throw new KeyNotFoundException(id);
}
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
+ RowData newData = new RowData(txId, null);
+ if(tx == null){
+ newData.commitId = txId;
+ }
+
+
+ synchronized(toRemove){
+ RowData rData = toRemove.chooseMostRecentCommitted(tx);
+ if(rData == null || rData.data == null){
+ throw new KeyNotFoundException(id);
+ }
+
+ //a RowData that is null means it was deleted.
+ toRemove.rowData.put(txId, newData);
+ }
+
setLastActivity(System.currentTimeMillis());
dumpCache();
}
@@ -219,17 +355,31 @@ public int deleteAll(XpathQuery query) {
query.setConversionService(this.getConversionService());
+ Transaction tx = this.getTransactionManager().getTransaction();
+ long txId = getTxId(tx);
+
Matcher<Element> rowMatcher = query.getRowMatcher();
- Iterator<Map.Entry<String,Element>> it = this.cache.entrySet().iterator();
+ Iterator<Map.Entry<String, Row>> it = this.cache.entrySet().iterator();
int numRemoved = 0;
while(it.hasNext()){
- Map.Entry<String, Element> entry = it.next();
- Element row = entry.getValue();
+ Map.Entry<String, Row> entry = it.next();
+
+ Row row = entry.getValue();
synchronized(row){
- if(rowMatcher.matches(row)){
- it.remove();
+ RowData rData = row.chooseMostRecentCommitted(tx);
+ if(rData == null || rData.data == null){
+ continue;
+ }
+
+ if(rowMatcher.matches(rData.data)){
+ RowData newData = new RowData(txId, null);
+ if(tx == null){
+ newData.commitId = txId;
+ }
+ row.rowData.put(txId, newData);
+
numRemoved++;
}
}
@@ -253,16 +403,48 @@ protected boolean spinUp() {
//concurrency level 4 - don't expect to need more than this.
this.cache = new ConcurrentHashMap<>(16, 0.75f, 4);
+ this.uncommittedRows = new ConcurrentHashMap<>(16, 0.75f, 4);
if(file.exists()){
try {
Document doc = this.file.readFile();
List<Element> rowList = doc.getRootElement().getChildren("row", XFlatDatabase.xFlatNs);
- //copy to array to avoid concurrent modification exception
- Element[] rowArr = new Element[rowList.size()];
- for(Element row : rowList.toArray(rowArr)){
- row.detach();
- this.cache.put(getId(row), row);
+
+ for(int i = rowList.size() - 1; i >= 0; i--){
+ Element row = rowList.get(i).detach();
+
+ if(row.getChildren().isEmpty()){
+ continue;
+ }
+ Element data = row.getChildren().get(0);
+
+ String id = getId(row);
+ long txId = -1;
+ long commitId = -1;
+
+ String a = row.getAttributeValue("tx", XFlatDatabase.xFlatNs);
+ if(a != null && !"".equals(a)){
+ try{
+ txId = Long.parseLong(a);
+ }catch(NumberFormatException ex){
+ //just leave it as -1.
+ }
+ }
+ a = row.getAttributeValue("commit", XFlatDatabase.xFlatNs);
+ if(a != null && !"".equals(a)){
+ try{
+ commitId = Long.parseLong(a);
+ }catch(NumberFormatException ex){
+ //just leave it as -1.
+ }
+ }
+
+ RowData rData = new RowData(txId, data);
+ rData.commitId = commitId;
+
+ Row newRow = new Row(id, rData);
+
+ this.cache.put(id, newRow);
}
} catch (JDOMException | IOException ex) {
throw new XflatException("Error building document cache", ex);
@@ -409,7 +591,7 @@ public void run() {
public boolean forceSpinDown() {
//drop all remaining references to the cache, replace with a cache
//that throws exceptions on access.
- this.cache = new InactiveCache();
+ this.cache = new InactiveCache<>();
this.state.set(EngineState.SpunDown);
@@ -495,9 +677,26 @@ private void dumpCacheNow(){
.setAttribute("name", this.getTableName(), XFlatDatabase.xFlatNs);
doc.setRootElement(root);
- for(Element e : this.cache.values()){
- synchronized(e){
- root.addContent(e.clone());
+ //get a transaction ID so we are taking a snapshot of the committed data at this point in time.
+ long snapshotId = getTransactionManager().transactionlessCommitId();
+
+ for(Row row : this.cache.values()){
+ synchronized(row){
+ RowData rData = row.chooseMostRecentCommitted(snapshotId);
+ if(rData == null || rData.data == null){
+ //the data was deleted
+ continue;
+ }
+
+
+ Element rowEl = new Element("row", XFlatDatabase.xFlatNs);
+ setId(rowEl, row.rowId);
+ rowEl.setAttribute("tx", Long.toString(rData.transactionId), XFlatDatabase.xFlatNs);
+ rowEl.setAttribute("commit", Long.toString(rData.commitId), XFlatDatabase.xFlatNs);
+
+ rowEl.addContent(rData.data.clone());
+
+ root.addContent(rowEl);
}
}
@@ -517,55 +716,78 @@ private void dumpCacheNow(){
dumpFailures.set(0);
}
}
+
+ @Override
+ protected boolean hasUncomittedData() {
+ return this.uncommittedRows == null ? true : this.uncommittedRows.isEmpty();
+ }
+
private class TableCursor implements Cursor<Element>{
- private final Iterable<Element> toIterate;
+ private final Iterable<Row> toIterate;
private final XpathQuery filter;
- public TableCursor(Iterable<Element> toIterate, XpathQuery filter){
+ private final Transaction tx;
+ private final long txId;
+
+ public TableCursor(Iterable<Row> toIterate, XpathQuery filter, Transaction tx){
this.filter = filter;
this.toIterate = toIterate;
+ this.tx = tx;
+ this.txId = getTxId(tx);
}
@Override
public Iterator<Element> iterator() {
- return new TableCursorIterator(toIterate.iterator(), filter.getRowMatcher());
+ return new TableCursorIterator(toIterate.iterator(), filter.getRowMatcher(), tx, txId);
}
@Override
- public void close() throws Exception {
+ public void close() {
CachedDocumentEngine.this.openCursors.remove(this);
}
}
private static class TableCursorIterator implements Iterator<Element>{
- private final Iterator<Element> toIterate;
+ private final Iterator<Row> toIterate;
private final Matcher<Element> rowMatcher;
+ private final Transaction tx;
+ private final long txId;
+
private Element peek = null;
private boolean isFinished = false;
private int peekCount = 0;
private int returnCount = 0;
- public TableCursorIterator(Iterator<Element> toIterate, Matcher<Element> rowMatcher){
+ public TableCursorIterator(Iterator<Row> toIterate, Matcher<Element> rowMatcher, Transaction tx, long txId){
this.toIterate = toIterate;
this.rowMatcher = rowMatcher;
+ this.tx = tx;
+ this.txId = txId;
}
private void peekNext(){
while(toIterate.hasNext()){
- Element next = toIterate.next();
+ Row next = toIterate.next();
synchronized(next){
- if(rowMatcher.matches(next)){
+ RowData rData = next.chooseMostRecentCommitted(tx);
+ if(rData == null || rData.data == null){
+ continue;
+ }
+
+ if(rowMatcher.matches(rData.data)){
+ //found a matching row
peekCount++;
- this.peek = next;
+ this.peek = rData.data.clone();
return;
}
}
}
+ //no matching row
peekCount++;
this.peek = null;
isFinished = true;
@@ -600,11 +822,7 @@ public Element next() {
}
Element ret = peek;
- synchronized(ret){
- //lock the row
- ret = ret.getChildren().get(0).clone();
- }
-
+
returnCount++;
return ret;
}
View
3 java/XFlat/src/org/gburgett/xflat/engine/IdShardedEngine.java
@@ -205,6 +205,7 @@ public Integer act(Engine engine) {
return count;
}
+
/**
* A cursor that queries across multiple engines.
@@ -306,7 +307,7 @@ public void remove() {
}
@Override
- public void close() throws Exception {
+ public void close() throws XflatException {
if(this.closed){
return;
}
View
23 java/XFlat/src/org/gburgett/xflat/engine/InactiveCache.java
@@ -8,13 +8,12 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
-import org.jdom2.Element;
/**
*
* @author gordon
*/
-public class InactiveCache implements ConcurrentMap<String, Element> {
+public class InactiveCache<T, U> implements ConcurrentMap<T, U> {
@Override
public int size() {
@@ -37,22 +36,22 @@ public boolean containsValue(Object value) {
}
@Override
- public Element get(Object key) {
+ public U get(Object key) {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public Element put(String key, Element value) {
+ public U put(T key, U value) {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public Element remove(Object key) {
+ public U remove(Object key) {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public void putAll(Map<? extends String, ? extends Element> m) {
+ public void putAll(Map<? extends T, ? extends U> m) {
throw new UnsupportedOperationException("Engine is no longer active");
}
@@ -62,22 +61,22 @@ public void clear() {
}
@Override
- public Set<String> keySet() {
+ public Set<T> keySet() {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public Collection<Element> values() {
+ public Collection<U> values() {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public Set<Map.Entry<String, Element>> entrySet() {
+ public Set<Map.Entry<T, U>> entrySet() {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public Element putIfAbsent(String key, Element value) {
+ public U putIfAbsent(T key, U value) {
throw new UnsupportedOperationException("Engine is no longer active");
}
@@ -87,12 +86,12 @@ public boolean remove(Object key, Object value) {
}
@Override
- public boolean replace(String key, Element oldValue, Element newValue) {
+ public boolean replace(T key, U oldValue, U newValue) {
throw new UnsupportedOperationException("Engine is no longer active");
}
@Override
- public Element replace(String key, Element value) {
+ public U replace(T key, U value) {
throw new UnsupportedOperationException("Engine is no longer active");
}
View
2 java/XFlat/src/org/gburgett/xflat/query/EmptyCursor.java
@@ -35,7 +35,7 @@ public void remove() {
}
@Override
- public void close() throws Exception {
+ public void close() {
//do nothing
}
View
23 java/XFlat/src/org/gburgett/xflat/transaction/IllegalTransactionStateException.java
@@ -0,0 +1,23 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+/**
+ * An exception that occurs on an attempt to commit or roll back a transaction that
+ * has already been resolved.
+ * @author Gordon
+ */
+public class IllegalTransactionStateException extends TransactionException {
+
+ /**
+ * Constructs an instance of
+ * <code>TransactionException</code> with the specified detail message.
+ *
+ * @param msg the detail message.
+ */
+ IllegalTransactionStateException(String msg) {
+ super(msg);
+ }
+}
View
203 java/XFlat/src/org/gburgett/xflat/transaction/ThreadContextTransactionManager.java
@@ -0,0 +1,203 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import org.jdom2.Element;
+
+/**
+ *
+ * @author Gordon
+ */
+public class ThreadContextTransactionManager implements TransactionManager {
+
+ AtomicLong lastId = new AtomicLong();
+
+ /**
+ * Generates a new Transaction ID. The ID is composed of the lower
+ * 48 bits of {@link System#currentTimeMillis() } plus a 16-bit uniquifier.
+ * unfortunately this means we have a y10k problem :P I'll let my descendants
+ * deal with it.
+ * @return A new ID for a transaction.
+ */
+ protected long generateNewId(){
+ long id;
+ long last;
+ do{
+ //bitshifting current time millis still gets us at least to the year 10,000 before it overflows.
+ id = System.currentTimeMillis() << 16;
+ last = lastId.get();
+ if((last & 0xFFFFFFFFFFFF0000l) == (id & 0xFFFFFFFFFFFF0000l)){
+ //the last ID was at the same millisecond as our new ID, need to use uniquifier.
+ int u = (int)(last & 0xFFFFl) + 1;
+ if(u > 0xFFFF){
+ try {
+ //we can't roll over, need to slow down rate of transaction generation.
+ Thread.sleep(1);
+ } catch (InterruptedException ex) {
+ //don't care
+ }
+ //try again, hopefully currentTimeMillis rolled over.
+ continue;
+ }
+
+ id = id | u;
+ }
+ }while(!lastId.compareAndSet(last, id));
+
+ return id;
+ }
+
+ private Map<Thread, ThreadedTransaction> currentTransactions = new ConcurrentHashMap<>();
+
+ private Map<Long, ThreadedTransaction> committedTransactions = new ConcurrentHashMap<>();
+
+
+ @Override
+ public Transaction getTransaction() {
+ return currentTransactions.get(Thread.currentThread());
+ }
+
+ @Override
+ public Transaction openTransaction() {
+ return openTransaction(TransactionOptions.Default);
+ }
+
+ @Override
+ public Transaction openTransaction(TransactionOptions options) {
+ if(currentTransactions.get(Thread.currentThread()) != null){
+ throw new IllegalStateException("Transaction already open on current thread.");
+ }
+
+ ThreadedTransaction ret = new ThreadedTransaction(generateNewId(), options);
+ if(currentTransactions.put(Thread.currentThread(), ret) != null){
+ //how could this happen? I dunno, programs surprise me all the time.
+ throw new IllegalStateException("Transaction already open on current thread");
+ }
+
+ return ret;
+ }
+
+ @Override
+ public long isTransactionCommitted(long transactionId) {
+ ThreadedTransaction tx = committedTransactions.get(transactionId);
+ return tx == null ? -1 : tx.getCommitId();
+ }
+
+ @Override
+ public boolean isTransactionReverted(long transactionId) {
+ //if we find it in the current transactions, check the transaction
+ for(Transaction tx : currentTransactions.values()){
+ if(tx.getTransactionId() == transactionId){
+ return tx.isReverted();
+ }
+ }
+
+ //otherwise it might be in the committed transactions, if so it is not reverted.
+ if(committedTransactions.get(transactionId) != null){
+ return false;
+ }
+
+ //if we lost it then it's reverted.
+ return true;
+ }
+
+ @Override
+ public long transactionlessCommitId() {
+ return generateNewId();
+ }
+
+
+
+ protected class ThreadedTransaction implements Transaction{
+
+ private TransactionOptions options;
+
+ private AtomicBoolean isCompleted = new AtomicBoolean(false);
+ private AtomicBoolean isRollbackOnly = new AtomicBoolean(false);
+
+ private final long id;
+
+ private long commitId = -1;
+ @Override
+ public long getCommitId(){
+ return commitId;
+ }
+
+ protected ThreadedTransaction(long id, TransactionOptions options){
+ this.options = options;
+ if(this.options.getReadOnly()){
+ this.isRollbackOnly.set(true);
+ }
+ this.id = id;
+ }
+
+ @Override
+ public void commit() throws TransactionException {
+ if(this.isRollbackOnly.get()){
+ throw new IllegalTransactionStateException("Cannot commit a rollback-only transaction");
+ }
+ if(!this.isCompleted.compareAndSet(false, true)){
+ throw new IllegalTransactionStateException("Cannot commit a completed transaction");
+ }
+
+ commitId = generateNewId();
+ committedTransactions.put(id, this);
+ }
+
+ @Override
+ public void rollback() {
+ if(this.isCompleted.get()){
+ throw new IllegalTransactionStateException("Cannot rollback a completed transaction");
+ }
+
+ doRollback();
+ }
+
+ private void doRollback(){
+ //do nothing
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ this.isRollbackOnly.set(true);
+ }
+
+ @Override
+ public long getTransactionId() {
+ return this.id;
+ }
+
+ @Override
+ public void close() {
+ if(isCompleted.compareAndSet(false, true)){
+ doRollback();
+ }
+
+ //remove the transaction from the current transactions map
+ Iterator<ThreadedTransaction> it = currentTransactions.values().iterator();
+ while(it.hasNext()){
+ if(it.next() == this){
+ it.remove();
+ }
+ }
+ }
+
+ @Override
+ public boolean isCommitted() {
+ return commitId > -1;
+ }
+
+ @Override
+ public boolean isReverted() {
+ return isCompleted.get() && commitId > -1;
+ }
+ }
+
+}
View
45 java/XFlat/src/org/gburgett/xflat/transaction/Transaction.java
@@ -0,0 +1,45 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+import java.io.Closeable;
+
+/**
+ *
+ * @author Gordon
+ */
+public interface Transaction extends AutoCloseable {
+
+ void commit() throws TransactionException;
+
+ void rollback();
+
+ void setRollbackOnly();
+
+ /**
+ * Gets the ID of this transaction. A Transaction's ID is linked to the time
+ * it was created, so a transaction with a higher ID is guaranteed to have
+ * been created later. Transaction IDs are also valid across
+ * @return
+ */
+ long getTransactionId();
+
+ /**
+ * Gets the commit ID of this transaction. A transaction has a commit ID if
+ * it has been committed. This commit ID is also linked to the time it was
+ * created, and can be compared to other transaction IDs to see if this
+ * transaction was committed before, during, or after another transaction.
+ * @return The transaction's commit ID, or -1 if uncommitted.
+ */
+ long getCommitId();
+
+ boolean isCommitted();
+
+ boolean isReverted();
+
+ @Override
+ void close();
+
+}
View
22 java/XFlat/src/org/gburgett/xflat/transaction/TransactionException.java
@@ -0,0 +1,22 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+/**
+ *
+ * @author Gordon
+ */
+public class TransactionException extends RuntimeException {
+
+ /**
+ * Constructs an instance of
+ * <code>TransactionException</code> with the specified detail message.
+ *
+ * @param msg the detail message.
+ */
+ TransactionException(String msg) {
+ super(msg);
+ }
+}
View
63 java/XFlat/src/org/gburgett/xflat/transaction/TransactionManager.java
@@ -0,0 +1,63 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+/**
+ *
+ * @author Gordon
+ */
+public interface TransactionManager {
+
+ /**
+ * Gets the current transaction, or null if none exists. The current
+ * transaction is defined as the transaction retrieved by the last call to
+ * {@link #openTransaction(org.gburgett.xflat.transaction.TransactionOptions) }
+ * in this context (usually a thread context).
+ * @return The current transaction, or null.
+ */
+ public Transaction getTransaction();
+
+ /**
+ * Opens a new transaction, using the {@link TransactionOptions#Default} options.
+ * If a transaction is already open in this context, an IllegalStateException
+ * is thrown.
+ * @return
+ */
+ public Transaction openTransaction();
+
+ /**
+ * Opens a new transaction, using the given TransactionOptions. If a
+ * transaction is already open in this context, an IllegalStateException
+ * is thrown.
+ * @param options
+ * @return
+ */
+ public Transaction openTransaction(TransactionOptions options);
+
+ /**
+ * Gets a new commit ID for a transactionless write operation.
+ * All transactionless writes can be thought of as transactions that are
+ * automatically committed. This allows us to provide isolation between
+ * transactions and transactionless writes.
+ * @return
+ */
+ public long transactionlessCommitId();
+
+ /**
+ * Checks to see if the given transaction ID has been committed. If so,
+ * returns the transaction's commit ID. Otherwise returns -1.
+ * @param transactionId The ID of the transaction to check.
+ * @return the transaction's commit ID if committed, -1 otherwise.
+ */
+ public long isTransactionCommitted(long transactionId);
+
+ /**
+ * Checks to see if the given transaction ID has been reverted. If so,
+ * returns true, otherwise false.
+ * @param transactionId The ID of the transaction to check.
+ * @return true if the transaction is reverted, false otherwise.
+ */
+ public boolean isTransactionReverted(long transactionId);
+}
View
46 java/XFlat/src/org/gburgett/xflat/transaction/TransactionOptions.java
@@ -0,0 +1,46 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+/**
+ *
+ * @author Gordon
+ */
+public class TransactionOptions {
+
+ private boolean readOnly;
+ /**
+ * Gets whether this transaction is Read Only. A ReadOnly transaction
+ * cannot be committed. Write operations during a ReadOnly transaction
+ * throw an exception.
+ */
+ public boolean getReadOnly(){
+ return this.readOnly;
+ }
+ /**
+ * Sets whether this transaction is Read Only. A ReadOnly transaction
+ * cannot be committed. Write operations during a ReadOnly transaction
+ * throw an exception.
+ */
+ public TransactionOptions setReadOnly(boolean readOnly){
+ TransactionOptions ret = new TransactionOptions(this);
+ ret.readOnly = readOnly;
+ return ret;
+ }
+
+ private TransactionOptions(){
+ }
+
+ private TransactionOptions(TransactionOptions other){
+
+ }
+
+ /**
+ * The default transaction options.
+ */
+ public static final TransactionOptions Default = new TransactionOptions()
+ .setReadOnly(false);
+
+}
View
33 java/XFlat/src/org/gburgett/xflat/transaction/TransactionValues.java
@@ -0,0 +1,33 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.gburgett.xflat.transaction;
+
+/**
+ *
+ * @author Gordon
+ */
+public class TransactionValues {
+ private long transactionId;
+ /**
+ * Gets the transactionId.
+ */
+ public long getTransactionId(){
+ return this.transactionId;
+ }
+ private long commitId;
+ /**
+ * Gets the commitId.
+ */
+ public long getCommitId(){
+ return this.commitId;
+ }
+
+ public TransactionValues(long transactionId){
+ this(transactionId, -1);
+ }
+ public TransactionValues(long transactionId, long commitId){
+
+ }
+}
View
8 java/XFlat/test/org/gburgett/xflat/db/DatabaseIntegrationTest.java
@@ -142,7 +142,7 @@ public void testInsertMany_QueriesAll() throws Exception {
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
- db.configureTable("Foo", TableConfig.defaultConfig
+ db.configureTable("Foo", TableConfig.Default
.setIdGenerator(IntegerIdGenerator.class));
db.Initialize();
@@ -191,7 +191,7 @@ public void testInsertMany_DeleteMatching() throws Exception {
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
- db.configureTable("Foo", TableConfig.defaultConfig
+ db.configureTable("Foo", TableConfig.Default
.setIdGenerator(IntegerIdGenerator.class));
db.Initialize();
@@ -289,7 +289,7 @@ public void testInsert_Resume_ValidatesConfig() throws Exception {
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
- db.configureTable("Foo", TableConfig.defaultConfig
+ db.configureTable("Foo", TableConfig.Default
.setIdGenerator(TimestampIdGenerator.class));
db.Initialize();
@@ -311,7 +311,7 @@ public void testInsert_Resume_ValidatesConfig() throws Exception {
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
//use a different ID generator
- db.configureTable("Foo", TableConfig.defaultConfig
+ db.configureTable("Foo", TableConfig.Default
.setIdGenerator(IntegerIdGenerator.class));
boolean didThrow = false;
View
4 java/XFlat/test/org/gburgett/xflat/db/XFlatDatabaseTest.java
@@ -93,7 +93,7 @@ public void testGetTable_NoActivity_TableIsSpunDown() throws Exception {
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
- db.configureTable("Foo", TableConfig.defaultConfig.setInactivityShutdownMs(10));
+ db.configureTable("Foo", TableConfig.Default.setInactivityShutdownMs(10));
db.Initialize();
try{
@@ -135,7 +135,7 @@ public void testGetTable_NoActivity_SpinsUpNewEngine() throws Exception {
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
- db.configureTable("Foo", TableConfig.defaultConfig.setInactivityShutdownMs(10));
+ db.configureTable("Foo", TableConfig.Default.setInactivityShutdownMs(10));
db.Initialize();
try{
View
2 java/XFlat/test/org/gburgett/xflat/engine/IdShardedEngineIntegrationTests.java
@@ -42,7 +42,7 @@ private XFlatDatabase getDatabase(String testName){
File dbDir = new File(workspace, testName);
XFlatDatabase ret = new XFlatDatabase(dbDir);
- ret.configureTable(tbl, TableConfig.defaultConfig
+ ret.configureTable(tbl, TableConfig.Default
.setIdGenerator(IntegerIdGenerator.class)
.sharded(ShardsetConfig.create(XpathQuery.Id, Integer.class, NumericIntervalProvider.forInteger(2, 100))));

0 comments on commit 3bcc009

Please sign in to comment.
Something went wrong with that request. Please try again.