/* * Copyright 2013 Gordon Burgett and individual contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xflatdb.xflat.engine; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.xflatdb.xflat.Cursor; import org.xflatdb.xflat.DuplicateKeyException; import org.xflatdb.xflat.KeyNotFoundException; import org.xflatdb.xflat.XFlatException; import org.xflatdb.xflat.db.Engine; import org.xflatdb.xflat.db.EngineBase; import org.xflatdb.xflat.db.EngineState; import org.xflatdb.xflat.query.XPathQuery; import org.xflatdb.xflat.query.XPathUpdate; import org.xflatdb.xflat.transaction.Isolation; import org.xflatdb.xflat.transaction.Transaction; import org.xflatdb.xflat.transaction.WriteConflictException; import org.xflatdb.xflat.util.DocumentFileWrapper; import org.hamcrest.Matcher; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.xflatdb.xflat.XFlatConstants; import org.xflatdb.xflat.transaction.TransactionException; import org.xflatdb.xflat.transaction.TransactionOptions; import org.xflatdb.xflat.transaction.TransactionStateException; /** * This is an engine that caches the entire table in memory as a JDOM {@link Document}. * @author gordon */ public class CachedDocumentEngine extends EngineBase implements Engine { /** The radix of transaction IDs when serialized to an attribute, ex. 16 for hexadecimal */ private static final int TRANSACTION_ID_RADIX = 10; //TODO: can we replace this by taking a table lock on spin-up? private final AtomicBoolean operationsReady = new AtomicBoolean(false); private ConcurrentMap<String, Row> cache = null; private ConcurrentMap<String, Row> uncommittedRows = null; private final Object syncRoot = new Object(); private DocumentFileWrapper file; public DocumentFileWrapper getFile(){ return file; } public CachedDocumentEngine(File file, String tableName){ super(tableName); this.file = new DocumentFileWrapper(file); } public CachedDocumentEngine(DocumentFileWrapper file, String tableName){ super(tableName); this.file = file; } 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 XFlatException, DuplicateKeyException { Transaction tx = ensureWriteReady(); try{ long txId = getTxId(tx); RowData rData = new RowData(txId, data, id); if(tx == null){ //transactionless means auto-commit rData.commitId = txId; } Row newRow = new Row(id, rData); Row row; row = this.cache.putIfAbsent(id, newRow); if(row != null){ synchronized(row){ //see if all the data was from after this transaction RowData chosen = row.chooseMostRecentCommitted(tx, txId); if(chosen == null || chosen.data == null){ //we're good to insert our transactional data row.rowData.put(txId, rData); if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(id, row); } else{ throw new DuplicateKeyException(id); } } } else if(tx != null || this.getTransactionManager().anyOpenTransactions()){ //may still be uncommitted this.uncommittedRows.put(id, newRow); } setLastActivity(System.currentTimeMillis()); dumpCache(); }finally{ writeComplete(); } } @Override public Element readRow(String id) { this.ensureSpunUp(); Row row = this.cache.get(id); if(row == null){ return null; } setLastActivity(System.currentTimeMillis()); //lock the row synchronized(row){ Transaction tx = this.getTransactionManager().getTransaction(); //we want either the most recent for this transaction or, if null, the most recent globally. RowData ret = row.chooseMostRecentCommitted(tx, Long.MAX_VALUE); if(ret == null || ret.data == null){ return null; } //clone the data return ret.data.clone(); } } @Override public Cursor<Element> queryTable(XPathQuery query) { query.setConversionService(this.getConversionService()); this.ensureSpunUp(); TableCursor ret = new TableCursor(this.cache.values(), query, getTransactionManager().getTransaction()); this.openCursors.put(ret, ""); setLastActivity(System.currentTimeMillis()); return ret; } @Override public Element replaceRow(String id, Element data) throws KeyNotFoundException { Transaction tx = ensureWriteReady(); try{ RowData ret; long txId = getTxId(tx); Row row = this.cache.get(id); if(row == null){ throw new KeyNotFoundException(id); } synchronized(row){ ret = row.chooseMostRecentCommitted(tx, txId); if(ret == null || ret.data == null){ throw new KeyNotFoundException(id); } RowData newData = new RowData(txId, data, id); if(tx == null){ //transactionless means auto-commit newData.commitId = txId; } row.rowData.put(txId, newData); if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(id, row); } setLastActivity(System.currentTimeMillis()); dumpCache(); return ret.data; }finally{ writeComplete(); } } @Override public boolean update(String id, XPathUpdate update) throws KeyNotFoundException { Transaction tx = ensureWriteReady(); try{ Row row = this.cache.get(id); if(row == null){ throw new KeyNotFoundException(id); } long txId = getTxId(tx); update.setConversionService(this.getConversionService()); boolean ret; //lock the row synchronized(row){ RowData data = row.chooseMostRecentCommitted(tx, txId); 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(), row.rowId); if(tx == null){ //transactionless means auto-commit newData.commitId = txId; } int updates = update.apply(newData.rowElement); ret = updates > 0; if(ret){ //no need to put a new version if no data was modified row.rowData.put(txId, newData); if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(id, row); } } } setLastActivity(System.currentTimeMillis()); if(ret) dumpCache(); return ret; }finally{ writeComplete(); } } @Override public int update(XPathQuery query, XPathUpdate update) { Transaction tx = ensureWriteReady(); try{ query.setConversionService(this.getConversionService()); update.setConversionService(this.getConversionService()); Matcher<Element> rowMatcher = query.getRowMatcher(); long txId = getTxId(tx); int rowsUpdated = 0; for(Row row : this.cache.values()){ synchronized(row){ RowData rData = row.chooseMostRecentCommitted(tx, txId); if(rData == null || rData.data == null){ continue; } if(!rowMatcher.matches(rData.rowElement)) continue; //apply to a copy, store the copy as a transactional state. RowData newData = new RowData(txId, rData.data.clone(), row.rowId); if(tx == null){ //transactionless means auto-commit newData.commitId = txId; } int updates = update.apply(newData.rowElement); if(updates > 0){ //no need to put a new version if no data was modified row.rowData.put(txId, newData); if(newData.commitId == -1 && (tx != null || this.getTransactionManager().anyOpenTransactions())) this.uncommittedRows.put(row.rowId, row); } rowsUpdated = updates > 0 ? rowsUpdated + 1 : rowsUpdated; } } setLastActivity(System.currentTimeMillis()); if(rowsUpdated > 0){ dumpCache(); } return rowsUpdated; }finally{ writeComplete(); } } @Override public boolean upsertRow(String id, Element data) { Transaction tx = ensureWriteReady(); try{ long txId = getTxId(tx); RowData newData = new RowData(txId, data, id); if(tx == null){ //transactionless means auto-commit newData.commitId = txId; } Row newRow = new Row(id, newData); boolean didInsert = false; synchronized(newRow){ Row existingRow = this.cache.putIfAbsent(id, newRow); //takes care of the insert if(existingRow != null){ synchronized(existingRow){ //we inserted if the most recent committed was null or had null data RowData mostRecent = existingRow.chooseMostRecentCommitted(tx, txId); didInsert = mostRecent == null || mostRecent.data == null; //takes care of the "or update" existingRow.rowData.put(txId, newData); if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(id, existingRow); } } else{ didInsert = true; if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(id, newRow); } } setLastActivity(System.currentTimeMillis()); dumpCache(); return didInsert; //if none existed, then we inserted }finally{ writeComplete(); } } @Override public void deleteRow(String id) throws KeyNotFoundException { Transaction tx = ensureWriteReady(); try{ Row row = this.cache.get(id); if(row == null){ throw new KeyNotFoundException(id); } long txId = getTxId(tx); RowData newData = new RowData(txId, null, id); if(tx == null){ newData.commitId = txId; } synchronized(row){ RowData rData = row.chooseMostRecentCommitted(tx, txId); if(rData == null || rData.data == null){ //already deleted throw new KeyNotFoundException(id); } //a RowData that is null means it was deleted. row.rowData.put(txId, newData); if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(row.rowId, row); } setLastActivity(System.currentTimeMillis()); dumpCache(); }finally{ writeComplete(); } } @Override public int deleteAll(XPathQuery query) { Transaction tx = ensureWriteReady(); try{ query.setConversionService(this.getConversionService()); long txId = getTxId(tx); Matcher<Element> rowMatcher = query.getRowMatcher(); Iterator<Map.Entry<String, Row>> it = this.cache.entrySet().iterator(); int numRemoved = 0; while(it.hasNext()){ Map.Entry<String, Row> entry = it.next(); Row row = entry.getValue(); synchronized(row){ RowData rData = row.chooseMostRecentCommitted(tx, txId); if(rData == null || rData.data == null){ continue; } if(rowMatcher.matches(rData.rowElement)){ RowData newData = new RowData(txId, null, row.rowId); if(tx == null){ newData.commitId = txId; } row.rowData.put(txId, newData); if(tx != null || this.getTransactionManager().anyOpenTransactions()) this.uncommittedRows.put(row.rowId, row); numRemoved++; } } } setLastActivity(System.currentTimeMillis()); if(numRemoved > 0) dumpCache(); return numRemoved; }finally{ writeComplete(); } } //</editor-fold> private void updateTask(boolean cleanAll){ Set<Long> remainingTransactions = new HashSet<>(); Set<Row> rowsToRemove = new HashSet<>(); synchronized(syncRoot){ if(this.currentlyCommitting.get() != -1){ if(this.getTransactionManager().isTransactionCommitted(this.currentlyCommitting.get()) == -1 && !this.getTransactionManager().isTransactionReverted(this.currentlyCommitting.get())){ //the transaction is neither committed nor reverted, it is in the process of committing. //We'll have to come back to this update later when it is finished. return; } } //What are we cleaning? If cleanAll, then inspect the ENTIRE cache, not just uncommitted data. Iterable<Row> toClean; if(cleanAll) toClean = this.cache.values(); else toClean = this.uncommittedRows.values(); Iterator<Row> it = toClean.iterator(); while(it.hasNext()){ Row row = it.next(); synchronized(row){ if(row.cleanup()){ rowsToRemove.add(row); //fully committed, we can remove it from uncommitted rows. if(!cleanAll) it.remove(); } else{ boolean isFullyCommitted = true; //remember the remaining transactions for(RowData data : row.rowData.values()){ if(data.commitId == -1){ isFullyCommitted = false; remainingTransactions.add(data.transactionId); } } if(!cleanAll && isFullyCommitted){ //fully committed, we can remove it from uncommitted rows. it.remove(); } } } } if(rowsToRemove.size() > 0){ //we have to lock the table in order to actually remove any rows. try{ this.getTableLock(); for(Row row : rowsToRemove){ //doublecheck - do another cleanup, don't want to be sloppy here. if(row.cleanup()){ this.cache.remove(row.rowId); } else{ //remember the remaining transactions for(RowData data : row.rowData.values()){ if(data.commitId == -1){ remainingTransactions.add(data.transactionId); } } } } } finally{ this.releaseTableLock(); } } } //outside the synchronized block due to a deadlock issue //unbind the engine from all transactions except any of the remaining transactions or any that are open. this.getTransactionManager().unbindEngineExceptFrom(this, remainingTransactions); } private AtomicLong currentlyCommitting = new AtomicLong(-1); @Override public void commit(Transaction tx, TransactionOptions options) throws TransactionException { super.commit(tx, options); synchronized(syncRoot){ if(!currentlyCommitting.compareAndSet(-1, tx.getTransactionId())){ //see if this transaction is completely finished committing, or if it reverted if(this.getTransactionManager().isTransactionCommitted(tx.getTransactionId()) == -1){ throw new TransactionStateException("Cannot commit two transactions simultaneously"); } else{ //the transaction successfully committed, we can move on. currentlyCommitting.set(-1); } } Iterator<Row> it = this.uncommittedRows.values().iterator(); while(it.hasNext()){ Row row = it.next(); if(log.isTraceEnabled()) this.log.trace("committing row " + row.rowId); synchronized(row){ if(options.getIsolationLevel() == Isolation.SNAPSHOT){ //check for conflicts for(RowData data : row.rowData.values()){ if(data.commitId > tx.getTransactionId() && data.transactionId != tx.getTransactionId()){ //committed data after our own transaction began throw new WriteConflictException(String.format("Conflicting data in table %s, row %s", this.getTableName(), row.rowId)); } } } //don't remove the row, only do that in cleanup. //We don't want to cleanup cause we still might need the old data, //just set the transaction status to committed. RowData got = row.rowData.get(tx.getTransactionId()); if(got != null){ got.commitId = tx.getCommitId(); } } } //we must immediately dump the cache, we cannot say we are committed //until the data is on disk. That is, if the transaction is durable. lastModified.set(System.currentTimeMillis()); dumpCacheNow(options.isDurable()); currentlyCommitting.compareAndSet(tx.getTransactionId(), -1); } } @Override public void revert(long txId, boolean isRecovering){ super.revert(txId, isRecovering); synchronized(syncRoot){ boolean mustDump = false; Iterable<Row> toRevert; if(isRecovering) //need to revert over the entire cache. toRevert = this.cache.values(); else //need to revert only over the uncommitted rows. toRevert = this.uncommittedRows.values(); Iterator<Row> it = toRevert.iterator(); while(it.hasNext()){ Row row = it.next(); synchronized(row){ //remove the row data, since it's now uncommitted. RowData got = row.rowData.remove(txId); if(got != null && got.commitId != -1){ //this transaction was persisted to the DB. We're going to need //to dump the cache at the end. mustDump = true; } } } if(mustDump){ lastModified.set(System.currentTimeMillis()); this.dumpCacheNow(true); } //else we can leave dumping the cache for the cleanup task. //reset the currently committing if that was set currentlyCommitting.compareAndSet(txId, -1); } } @Override protected boolean spinUp() { if(!this.state.compareAndSet(EngineState.Uninitialized, EngineState.SpinningUp)){ return false; } this.getTableLock(); try{ synchronized(syncRoot){ //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", XFlatConstants.xFlatNs); for(int i = rowList.size() - 1; i >= 0; i--){ Element row = rowList.get(i); if(row.getChildren().isEmpty()){ continue; } String id = getId(row); Row newRow = null; for(Element data : row.getChildren()){ //default it to zero so that we know it's committed but if we don't get an actual //value for the commit then we have the lowest value. long txId = 0; long commitId = 0; String a = data.getAttributeValue("tx", XFlatConstants.xFlatNs); if(a != null && !"".equals(a)){ try{ txId = Long.parseLong(a, TRANSACTION_ID_RADIX); }catch(NumberFormatException ex){ //just leave it as 0. } } a = data.getAttributeValue("commit", XFlatConstants.xFlatNs); if(a != null && !"".equals(a)){ try{ commitId = Long.parseLong(a, TRANSACTION_ID_RADIX); }catch(NumberFormatException ex){ //just leave it as 0. } } if("delete".equals(data.getName()) && XFlatConstants.xFlatNs.equals(data.getNamespace())){ //it's a delete marker data = null; } else{ data = data.clone(); } RowData rData = new RowData(txId, data, id); rData.commitId = commitId; if(newRow == null) newRow = new Row(id, rData); else newRow.rowData.put(txId, rData); } if(newRow != null) this.cache.put(id, newRow); } } catch (JDOMException | IOException ex) { throw new XFlatException("Error building document cache", ex); } } this.state.set(EngineState.SpunUp); if(operationsReady.get()){ this.state.set(EngineState.Running); synchronized(operationsReady){ operationsReady.notifyAll(); } } return true; } } finally{ this.releaseTableLock(); } } @Override protected boolean beginOperations() { //could happen before spin up complete, in that case spinUp will handle the notifying operationsReady.set(true); //schedule the update task this.getExecutorService().scheduleWithFixedDelay(new Runnable(){ int runCount = -1; @Override public void run() { if(state.get() == EngineState.SpinningDown || state.get() == EngineState.SpunDown){ throw new RuntimeException("task termination"); } runCount = (runCount + 1) % 10; //every 10 iterations, clean the entire cache. //Also do this on the first iteration. updateTask(runCount == 0); } }, 500, 500, TimeUnit.MILLISECONDS); if(this.state.compareAndSet(EngineState.SpunUp, EngineState.Running)){ synchronized(operationsReady){ operationsReady.notifyAll(); } return true; } return false; } /** * Overrides ensureWriteReady to additionally check if the * engine has fully finished spinning up */ @Override protected Transaction ensureWriteReady(){ Transaction tx = super.ensureWriteReady(); ensureSpunUp(); return tx; } private void ensureSpunUp(){ //check if we're not yet running, if so wait until we are running if(!operationsReady.get() || state.get() != EngineState.Running){ synchronized(operationsReady){ while(!operationsReady.get() && this.state.get() != EngineState.Running){ try { operationsReady.wait(); } catch (InterruptedException ex) { if(operationsReady.get()){ //oh ok we're all good to go return; } throw new XFlatException("Interrupted while waiting for engine to be ready"); } } } } } private ConcurrentMap<Cursor<Element>, String> openCursors = new ConcurrentHashMap<>(); @Override protected boolean spinDown(final SpinDownEventHandler completionEventHandler) { try{ this.getTableLock(); //not much to do since everything's in the cache, just dump the cache //and set read-only mode. if(!this.state.compareAndSet(EngineState.Running, EngineState.SpinningDown)){ //we're in the wrong state. return false; } synchronized(syncRoot){ if(log.isTraceEnabled()) log.trace(String.format("Table %s Spinning down", this.getTableName())); //do the transactional data cleanup task, ensuring we clean the entire cache. updateTask(true); final AtomicReference<ScheduledFuture<?>> cacheDumpTask = new AtomicReference<>(null); if(this.cache != null){ //schedule immediate dump cacheDumpTask.set(this.getExecutorService().schedule( new Runnable(){ @Override public void run() { int failures = 0; do{ try{ dumpCacheNow(true); } catch(Exception ex){ log.warn("Unable to dump cached data", ex); } //give it 3 attempts }while(++failures < 3); } }, 0, TimeUnit.MILLISECONDS)); } if(openCursors.isEmpty() && (cacheDumpTask.get() == null || cacheDumpTask.get().isDone())){ this.state.set(EngineState.SpunDown); if(completionEventHandler != null) completionEventHandler.spinDownComplete(new SpinDownEvent(CachedDocumentEngine.this)); //we're ok to finish our spin down now return forceSpinDown(); } Runnable spinDownTask = new Runnable(){ @Override public void run() { if(!openCursors.isEmpty()) return; if(cacheDumpTask.get() != null && !cacheDumpTask.get().isDone()){ return; } if(!state.compareAndSet(EngineState.SpinningDown, EngineState.SpunDown)){ throw new RuntimeException("cancel task - in wrong state"); } if(completionEventHandler != null) completionEventHandler.spinDownComplete(new SpinDownEvent(CachedDocumentEngine.this)); //we're ok to finish our spin down now forceSpinDown(); throw new RuntimeException("Scheduled Task Complete"); } }; this.getExecutorService().scheduleWithFixedDelay( spinDownTask, 5, 10, TimeUnit.MILLISECONDS); return true; } } finally{ this.releaseTableLock(); } } @Override public boolean forceSpinDown() { //drop all remaining references to the cache, replace with a cache //that throws exceptions on access. this.cache = new InactiveCache<>(); EngineState old = this.state.getAndSet(EngineState.SpunDown); if(old != EngineState.SpunDown){ log.warn(String.format("Table %s improperly spun down", this.getTableName())); } return true; } private boolean isSpinningDown(){ return this.state.get() == EngineState.SpunDown || this.state.get() == EngineState.SpinningDown; } private AtomicReference<Future<?>> scheduledDump = new AtomicReference<>(null); private AtomicLong lastDump = new AtomicLong(0); private AtomicLong lastModified = new AtomicLong(System.currentTimeMillis()); private AtomicInteger dumpFailures = new AtomicInteger(); private void dumpCache(){ long delay = 0; lastModified.set(System.currentTimeMillis()); //did we dump inside the last 250 ms? if(lastDump.get() + 250 > System.currentTimeMillis()) { //yes, dump at 250 ms delay = lastDump.get() + 250 - System.currentTimeMillis(); if(delay < 0) delay = 0; } if(scheduledDump.get() != null || isSpinningDown()){ //we're already scheduled to dump the cache return; } ScheduledFuture<?> dumpTask; synchronized(dumpSyncRoot){ if(scheduledDump.get() != null || isSpinningDown()){ return; } //dump the cache on a separate thread so we can remain responsive dumpTask = this.getExecutorService().schedule( new Runnable(){ @Override public void run() { try{ dumpCacheNow(false); } catch(XFlatException ex){ log.warn("Unable to dump cached data", ex); } } }, delay, TimeUnit.MILLISECONDS); scheduledDump.set(dumpTask); } if(dumpFailures.get() > 5){ //get this on the thread that is doing the writing, so someone notices while(!dumpTask.isDone()){ try { dumpTask.get(); } catch (InterruptedException | ExecutionException ex) { throw new XFlatException("An error occurred after attempting to write to disk " + dumpFailures.get() + " times", ex); } } } } private final Object dumpSyncRoot = new Object(); /** * Dumps the cache immediately, on this thread. * @param required true if a dump is absolutely required, false to allow this * method to choose not to dump if it feels that a dump is unnecessary. */ private void dumpCacheNow(boolean required){ synchronized(dumpSyncRoot){ if(!required && lastModified.get() < lastDump.get()){ //no need to dump return; } long lastDump = System.currentTimeMillis(); Document doc = new Document(); Element root = new Element("table", XFlatConstants.xFlatNs) .setAttribute("name", this.getTableName(), XFlatConstants.xFlatNs); doc.setRootElement(root); for(Row row : this.cache.values()){ synchronized(row){ Element rowEl = null; int nonDeleteData = 0; //put ALL committed data to disk, even some that might otherwise //be cleaned up, because we may be in the process of committing //one of N engines and will need all previous values if we revert. for(RowData rData : row.rowData.values()){ if(rData == null) continue; if(rData.commitId == -1) //uncommitted data is not put to disk continue; if(rowEl == null){ rowEl = new Element("row", XFlatConstants.xFlatNs); setId(rowEl, row.rowId); } Element dataEl; if(rData.data == null){ //the data was deleted - make sure we mark that on the row dataEl = new Element("delete", XFlatConstants.xFlatNs); } else{ dataEl = rData.data.clone(); nonDeleteData++; } dataEl.setAttribute("tx", Long.toString(rData.transactionId, TRANSACTION_ID_RADIX), XFlatConstants.xFlatNs); dataEl.setAttribute("commit", Long.toString(rData.commitId, TRANSACTION_ID_RADIX), XFlatConstants.xFlatNs); rowEl.addContent(dataEl); } //doublecheck - only write out an element if there's actually //any data to write. Delete marker elements don't count. if(rowEl != null && nonDeleteData > 0){ root.addContent(rowEl); } } } try{ this.file.writeFile(doc); } catch(FileNotFoundException ex){ //this is a transient issue that may be caused by another process opening the file quickly, //the message is generally "The requested operation cannot be performed on a file with a user-mapped section open" int failures = dumpFailures.incrementAndGet(); if(failures > 3) throw new XFlatException("Unable to dump cache to file", ex); try { Thread.sleep(50); } catch (InterruptedException interruptedEx) { } //try again dumpCacheNow(required); return; } catch(Exception ex) { dumpFailures.incrementAndGet(); throw new XFlatException("Unable to dump cache to file", ex); } finally { scheduledDump.set(null); this.lastDump.set(lastDump); } //success! dumpFailures.set(0); } } @Override protected boolean hasUncomittedData() { return this.uncommittedRows == null ? false : !this.uncommittedRows.isEmpty(); } private class TableCursor implements Cursor<Element>{ private final Iterable<Row> toIterate; private final 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(), tx, txId); } @Override public void close() { CachedDocumentEngine.this.openCursors.remove(this); } } private static class TableCursorIterator implements Iterator<Element>{ 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<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()){ Row next = toIterate.next(); synchronized(next){ RowData rData = next.chooseMostRecentCommitted(tx, txId); if(rData == null || rData.data == null){ continue; } if(rowMatcher.matches(rData.rowElement)){ //found a matching row peekCount++; this.peek = rData.data.clone(); return; } } } //no matching row peekCount++; this.peek = null; isFinished = true; } @Override public boolean hasNext() { if(isFinished) return false; while(peekCount <= returnCount){ peekNext(); } return !isFinished; } @Override public Element next() { if(isFinished){ throw new NoSuchElementException(); } while(peekCount <= returnCount){ //gotta peek peekNext(); } //try again if(isFinished){ throw new NoSuchElementException(); } Element ret = peek; returnCount++; return ret; } @Override public void remove() { throw new UnsupportedOperationException("Remove not supported on cursors."); } } }