/*
* 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.db;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
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.jdom2.Element;
import org.xflatdb.xflat.EngineStateException;
import org.xflatdb.xflat.XFlatConstants;
import org.xflatdb.xflat.XFlatException;
import org.xflatdb.xflat.convert.ConversionService;
import org.xflatdb.xflat.db.EngineBase.RowData;
import org.xflatdb.xflat.transaction.Transaction;
import org.xflatdb.xflat.transaction.TransactionException;
import org.xflatdb.xflat.transaction.TransactionOptions;
import org.xflatdb.xflat.transaction.TransactionStateException;
/**
* The base class for Engine objects. The Database uses the functionality
* described here to manage engines, so all engine implementations must extend
* this base class.
* @author gordon
*/
public abstract class EngineBase implements Engine {
final private String tableName;
@Override
public String getTableName(){
return tableName;
}
protected Log log = LogFactory.getLog(getClass());
protected EngineBase(String tableName){
this.tableName = tableName;
}
//<editor-fold desc="transition functions">
protected AtomicReference<EngineState> state = new AtomicReference<>(EngineState.Uninitialized);
public EngineState getState(){
return state.get();
}
/**
* Initializes the engine and instructs it to begin acquiring the resources
* necessary to function. At this point the engine may begin to respond to
* read-only requests, but any write requests MUST block until {@link #beginOperations() } )
* is called.
*/
protected abstract boolean spinUp();
/**
* Instructs the engine to begin full read/write operations. At this point this
* engine has full control over the data files.
*/
protected abstract boolean beginOperations();
/**
* Instructs this Engine to wrap up its operations in preparation for being
* switched out. The engine must be set to read-only mode (write operations
* should throw an exception). The engine can stay alive until its outstanding
* cursors are closed, but must allow concurrent reads of the file.
* @param completionEventHandler An event handler that is notified when
* the Engine has finished spinning down.
*/
protected abstract boolean spinDown(SpinDownEventHandler completionEventHandler);
/**
* Forces this engine to immediately release all resources, even if there are
* still outstanding cursors or uncommitted data. Cursors that continue iterating will throw an
* exception that indicates the engine has spun down. This will be called after a normal
* spin down, or in case of some kind of error to reclaim resources.
*/
protected abstract boolean forceSpinDown();
private AtomicLong lastActivity = new AtomicLong();
/**
* Gets the date at which the last operation was performed on the engine.
* This is used by the Database to determine whether this engine needs to
* remain in its cache or if it can be spun down for later.
* @return The date at which the last write or read has occurred, or the date
* of engine creation if no operations have occurred.
*/
public long getLastActivity(){
return lastActivity.get();
}
/**
* Concurrently updates the lastActivity property to the greater of the
* existing value or the given time.
* @param time the time to update lastActivity to, if greater than the existing
* value.
*/
protected void setLastActivity(long time){
long existing;
do{
existing = lastActivity.get();
if(existing >= time){
//no need to update
return;
}
//ensure we compared to the latest value before setting.
}while(!lastActivity.compareAndSet(existing, time));
}
/**
* An event handler for the events fired when an Engine finishes spinning
* down.
*/
public static interface SpinDownEventHandler{
/**
* Called when the engine is completely finished spinning down,
* and is ready to release all filesystem references.
* @param event
*/
public void spinDownComplete(SpinDownEvent event);
}
/**
* An event object given to the {@link SpinDownEventHandler} when an
* engine finishes spinning down.
*/
public static class SpinDownEvent extends java.util.EventObject{
/**
* The Engine that has spun down.
*/
@Override
public Engine getSource(){
return (Engine)super.getSource();
}
/**
* Creates a new SpinDownEvent which says the given engine has finished
* spinning down.
* @param source The engine which has finished spinning down.
*/
public SpinDownEvent(Engine source){
super(source);
}
}
//</editor-fold>
//<editor-fold desc="dependencies">
private ScheduledExecutorService executorService;
protected ScheduledExecutorService getExecutorService(){
return executorService;
}
protected void setExecutorService(ScheduledExecutorService service){
this.executorService = service;
}
private ConversionService conversionService;
protected ConversionService getConversionService() {
return this.conversionService;
}
protected void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
private EngineTransactionManager transactionManager;
/**
* Gets the transactionManager.
*/
protected EngineTransactionManager getTransactionManager(){
return this.transactionManager;
}
/**
* Sets the transactionManager.
*/
protected void setTransactionManager(EngineTransactionManager transactionManager){
this.transactionManager = transactionManager;
}
private IdGenerator idGenerator;
/**
* Gets the ID generator which generates IDs for this table.
*/
protected IdGenerator getIdGenerator(){
return idGenerator;
}
/**
* Sets the ID generator which generates IDs for this table.
*/
protected void setIdGenerator(IdGenerator idGenerator){
this.idGenerator = idGenerator;
}
//</editor-fold>
private final AtomicLong tableLock = new AtomicLong(-1);
private int tableLockCount = 0;
private final Object tableLockSync = new Object();
private final AtomicInteger writesInProgress = new AtomicInteger(0);
/**
* Called before every write to ensure we are ready to write. <br/>
* This method also checks if there is a current table lock, and increments
* the {@link #writesInProgress} counter.
* <p/>
* If the engine is spinning down then we throw because engines are read-only
* when spinning down.
*/
protected Transaction ensureWriteReady() throws TransactionStateException {
//check if there is a write lock on the table
long tblLock = tableLock.get();
if(tblLock != -1 && tblLock != Thread.currentThread().getId()){
synchronized(tableLockSync){
tblLock = tableLock.get();
while(tblLock != -1 && tblLock != Thread.currentThread().getId()){
try {
tableLockSync.wait();
} catch (InterruptedException ex) {
}
tblLock = tableLock.get();
}
}
}
Transaction tx = this.transactionManager.getTransaction();
if(tx != null){
if(tx.isReadOnly()){
throw new TransactionStateException("Cannot write in a read-only transaction");
}
if(tx.isCommitted()){
throw new TransactionStateException("Cannot write in an already committed transaction");
}
if(tx.isReverted()){
throw new TransactionStateException("Cannot write in an already reverted transaction");
}
}
//check the engine state
EngineState state = this.state.get();
if(state == EngineState.SpunDown ||
state == EngineState.SpinningDown){
throw new EngineStateException("Write operations not supported on an engine that is spinning down", state);
}
//we're about to write, so the engine must be bound to the current transaction
this.transactionManager.bindEngineToCurrentTransaction(this);
//increment the number of writes in progress
int inprog = this.writesInProgress.incrementAndGet();
if(inprog < 1){
//dunno how we got here, try to correct
this.writesInProgress.compareAndSet(inprog, 1);
if(log.isTraceEnabled())
log.trace(String.format("Writes in progress was less than 1: %d", inprog));
}
return tx;
}
/**
* Called inside a finally block within every write operation -
* this is a synchronizing measure for write locks
*/
protected void writeComplete(){
//decrement the number of writes in progress
int inprog = this.writesInProgress.decrementAndGet();
if(inprog < 0){
this.writesInProgress.compareAndSet(inprog, 0);
if(log.isTraceEnabled())
log.trace(String.format("Writes in progress was less than 1: %d", inprog));
}
}
/**
* Obtains a write lock on the table for this thread.
* <p/>
* New write operations will block until the lock is released with {@link #releaseTableLock() }.
* This method will wait after obtaining the lock until all in-progress write operations
* have terminated.
* <p/>
* Since I don't exactly trust this to never throw an exception, it would of
* course be good practice to always use the following pattern:
* <pre>
* try{
* engine.getTableLock();
*
* //do stuff
* }
* finally{
* engine.releaseTableLock();
* }
* </pre>
*/
protected void getTableLock(){
long thread = Thread.currentThread().getId();
if(this.tableLock.get() == thread){
this.tableLockCount++;
return;
}
synchronized(tableLockSync){
while(!this.tableLock.compareAndSet(-1, thread)){
if(this.tableLock.get() == thread){
this.tableLockCount++;
return;
}
try {
//wait until we can obtain the lock for this thread.
tableLockSync.wait();
} catch (InterruptedException ex) {
}
}
this.tableLockCount++;
//spin wait on writes in progress - this should only decrement while we have a write lock
long start = System.currentTimeMillis();
long nanos = System.nanoTime();
while(this.writesInProgress.get() > 0){
//if we've been waiting longer than 500ms something is amiss
if(System.currentTimeMillis() - start > 500){
//release the lock before throwing
this.tableLock.compareAndSet(thread, -1);
this.tableLockCount--;
throw new XFlatException(String.format("Cannot obtain table lock - %d long running writes in progress", this.writesInProgress.get()));
}
//if we've been spin-waiting longer than 500ns then sleep the thread
if(System.nanoTime() - nanos > 500){
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
}
}
}
}
}
/**
* Releases a write lock on the table that was obtained by this thread.
* If the current thread did not own the lock then this method does nothing.
* <p/>
* ALWAYS call this in a finally block after calling {@link #getTableLock() }
*/
protected void releaseTableLock(){
if(this.tableLock.get() != Thread.currentThread().getId()){
return;
}
synchronized(tableLockSync){
if(this.tableLock.get() != Thread.currentThread().getId()){
return;
}
if(--this.tableLockCount == 0){
//last reentrant release encountered
if(this.tableLock.compareAndSet(Thread.currentThread().getId(), -1)){
//notify of lock released
tableLockSync.notifyAll();
}
}
}
}
/**
* Saves metadata to the given element. Metadata is things like indexes
* and other configuration.
* @param metadataElement The element from which metadata should be loaded.
*/
protected void saveMetadata(Element metadataElement){
}
/**
* Loads metadata from the given element. Metadata is things like indexes
* and other configurations the engine may need to save.
* @param metatdataElement The element to which metadata should be saved.
*/
protected void loadMetadata(Element metatdataElement){
}
/**
* gets the string ID from a row element.
* @param row The row whose ID is needed
* @return The ID attached to the row
*/
protected String getId(Element row) {
return row.getAttributeValue("id", XFlatConstants.xFlatNs);
}
/**
* set the Id of the given row element to the given ID
* @param row The row whose Id to set
* @param id The new value of the ID.
*/
protected void setId(Element row, String id){
row.setAttribute("id", id, XFlatConstants.xFlatNs);
}
//<editor-fold desc="transactions">
/**
* 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();
/**
* Called when a transaction is committed to write the committed data to disk.
* After this method returns, the data should be stored in non-volatile storage.
* @param tx
*/
public void commit(Transaction tx, TransactionOptions options)
throws TransactionException, TransactionStateException
{
if(tx.isCommitted() || tx.isReverted()){
throw new TransactionStateException("Cannot commit an already finished transaction.");
}
}
/**
* Called when a transaction is committed to revert the given transaction ID.
* This may be called even if a transaction was previously committed in this engine,
* because it was not fully committed across all engines.
* @param tx
* @param isRecovering true if this transaction is being reverted during recovery
* at startup.
*/
public void revert(long tx, boolean isRecovering){
}
//</editor-fold>
/**
* 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 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. This is
* <p/>
* ALWAYS invoke this while synchronized on the Row.
* @param currentTransaction The current transaction, or null.
* @param transactionId The transaction ID to use iff the current transaction is null.
* This is overwritten if the transaction is not null.
* @return The most recent committed RowData in this row, committed before the transaction.
*/
public RowData chooseMostRecentCommitted(Transaction currentTransaction, long transactionId){
if(currentTransaction != null){
transactionId = currentTransaction.getTransactionId();
}
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 && transactionId == data.transactionId){
//this row data is 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(transactionId > data.commitId){
//the current transaction is null or began after the transaction was committed
//check if the transaction is an in-progress commit
if(!transactionManager.isCommitInProgress(data.transactionId)){
//the transaction is wholly 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;
}
/**
* 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 EngineTransactionManager#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);
}
/**
* Cleans up the transactional data in this row.
* Returns true if this row can then be removed because it contains no data.
* @return true if this row has no RowData or its only RowData is "nothing".
*/
public boolean cleanup(){
RowData mostRecent = null;
long lowest = transactionManager.getLowestOpenTransaction();
Set<RowData> toRemove = null;
Iterator<RowData> it = rowData.values().iterator();
while(it.hasNext()){
RowData data = it.next();
if(data.commitId == -1){
data.commitId = transactionManager.isTransactionCommitted(data.transactionId);
if(data.commitId == -1){
//the data is uncommitted
if(transactionManager.isTransactionReverted(data.transactionId)){
//don't need this anymore
it.remove();
}
continue;
}
}
//the data might be committed
if(transactionManager.isCommitInProgress(data.transactionId))
continue;
//the data is committed
if(mostRecent == null){
mostRecent = data;
}
else{
if(data.commitId <= mostRecent.commitId){
//the most recent data is newer
if(mostRecent.commitId < lowest){
//there is no open transaction that would see this data instead of mostRecent
it.remove();
}
}
else{
//the data is newer
if(data.commitId < lowest){
//there is no open transaction that would see mostRecent instead of this data
if(toRemove == null){
toRemove = new HashSet<>();
}
toRemove.add(mostRecent);
mostRecent = data;
}
}
}
}
//remove the ones we couldn't remove during the iteration
if(toRemove != null && toRemove.size() > 0){
for(RowData data : toRemove){
rowData.remove(data.transactionId);
}
}
//if there's no more row datas, or there is only one row data and it's value is "nothing", then return true.
return rowData.isEmpty() || (rowData.size() == 1 && rowData.values().iterator().next().data == null);
}
}
protected class RowData{
/**
* A snapshot of the data in the row, possibly uncommitted.
*/
public Element data = null;
/**
* A "db:row" element that wraps the data. This is useful for queries.
*/
public Element rowElement = null;
/**
* The ID of the transaction that created this data snapshot
*/
public long transactionId = -1;
/**
* 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, String id){
if(data != null){
this.data = data;
this.rowElement = new Element("row", XFlatConstants.xFlatNs)
.setAttribute("id", id, XFlatConstants.xFlatNs)
.setContent(data);
}
this.transactionId = txId;
}
}
}