org.jsimpledb.kv.mvcc.SnapshotKVDatabase Maven / Gradle / Ivy
Show all versions of jsimpledb-kv Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb.kv.mvcc;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import org.jsimpledb.kv.CloseableKVStore;
import org.jsimpledb.kv.KVDatabase;
import org.jsimpledb.kv.KVTransactionException;
import org.jsimpledb.kv.RetryTransactionException;
import org.jsimpledb.kv.StaleTransactionException;
import org.jsimpledb.kv.util.CloseableForwardingKVStore;
import org.jsimpledb.kv.util.KeyWatchTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link KVDatabase} implementation based on an underlying {@link AtomicKVStore} that uses
* {@linkplain AtomicKVStore#snapshot snapshot} views and optimistic locking to provide concurrent
* transactions and linearizable ACID consistency.
*
*
* Instances implement a simple optimistic locking scheme for MVCC using {@link AtomicKVStore#snapshot}. Concurrent transactions
* do not contend for any locks until commit time. During each transaction, reads are noted and derive from the snapshot,
* while writes are batched up. At commit time, if any other transaction has committed writes since the transaction's
* snapshot was created, and any of those writes {@linkplain Reads#isConflict conflict} with any of the committing
* transaction's reads, a {@link RetryTransactionException} is thrown. Otherwise, the transaction is committed and its
* writes are applied.
*
*
* Each outstanding transaction's mutations are batched up in memory using a {@link Writes} instance. Therefore,
* the transaction load supported by this class is limited to what can fit in memory.
*
*
* {@linkplain SnapshotKVTransaction#watchKey Key watches} are supported.
*
* @see AtomicKVDatabase
*/
@ThreadSafe
public abstract class SnapshotKVDatabase implements KVDatabase {
// Locking order: (1) SnapshotKVTransaction, (2) SnapshotKVDatabase, (3) MutableView
protected final Logger log = LoggerFactory.getLogger(this.getClass());
/*
Open transactions (only) are contained in this.transactions; this.snapshot is the read-only view
of the underlying key/value store on which all of these open transactions are based. Each transaction has its
own MutableView of this.snapshot.
this.snapshot has one reference for being non-null; this reference is shared by all open transactions (if any).
It also has one reference for each mutableSnapshot() based on it (see createMutableSnapshot()); these references are
the responsibility of whoever called mutableSnapshot().
When a transaction is committed, the mutations are applied to the key/value store and this.snapshot is discarded
and replaced with a new snapshot of the key/value store, and the MutableView's associated with all other open
(and non-conflicting) transactions are updated with the new snapshot.
*/
@GuardedBy("this")
private final HashSet transactions = new HashSet<>();
@GuardedBy("this")
private SnapshotRefs snapshot; // created on-demand for each new version
@GuardedBy("this")
private AtomicKVStore kvstore;
@GuardedBy("this")
private KeyWatchTracker keyWatchTracker;
@GuardedBy("this")
private long currentVersion;
@GuardedBy("this")
private boolean started;
@GuardedBy("this")
private boolean stopping;
// Constructors
/**
* Default constructor.
*
*
* The underlying key/value store must still be configured before starting this instance.
*/
public SnapshotKVDatabase() {
}
/**
* Constructor.
*
* @param kvstore underlying key/value store
*/
public SnapshotKVDatabase(AtomicKVStore kvstore) {
this.kvstore = kvstore;
}
// Properties
/**
* Get the underlying {@link AtomicKVStore}.
*
* @return underlying key/value store
*/
protected synchronized AtomicKVStore getKVStore() {
return this.kvstore;
}
/**
* Configure the underlying {@link AtomicKVStore}.
*
*
* Required property; must be configured before {@link #start}ing.
*
* @param kvstore underlying key/value store
* @throws IllegalStateException if this instance is already started
*/
protected synchronized void setKVStore(AtomicKVStore kvstore) {
Preconditions.checkState(!this.started, "already started");
this.kvstore = kvstore;
}
/**
* Get the current MVCC version number.
*
* @return MVCC database version number
*/
public synchronized long getCurrentVersion() {
return this.currentVersion;
}
// KVDatabase
@Override
@PostConstruct
public synchronized void start() {
if (this.started)
return;
Preconditions.checkState(this.kvstore != null, "no KVStore configured");
this.kvstore.start();
this.started = true;
}
@Override
@PreDestroy
public void stop() {
// Set stopping flag to prevent new transactions from being created
synchronized (this) {
if (!this.started || this.stopping)
return;
this.log.info("stopping " + this);
this.stopping = true;
}
// Close any remaining open transactions, while not holding lock
this.closeTransactions();
// Finish up
synchronized (this) {
assert this.started;
if (this.snapshot != null) {
this.snapshot.unref();
this.snapshot = null;
}
this.kvstore.stop();
if (this.keyWatchTracker != null) {
this.keyWatchTracker.close();
this.keyWatchTracker = null;
}
this.stopping = false;
this.started = false;
}
}
@Override
public SnapshotKVTransaction createTransaction(Map options) {
return this.createTransaction(); // no options supported yet
}
/**
* Create a new transaction.
*
* @throws IllegalStateException if not {@link #start}ed or {@link #stop}ing
*/
@Override
public synchronized SnapshotKVTransaction createTransaction() {
// Sanity check
Preconditions.checkState(this.started, "not started");
Preconditions.checkState(!this.stopping, "stopping");
// Create new transaction
final MutableView view = new MutableView(this.getCurrentSnapshot().getKVStore());
final SnapshotKVTransaction tx = this.createSnapshotKVTransaction(view, this.currentVersion);
assert !this.transactions.contains(tx);
this.transactions.add(tx);
if (this.log.isTraceEnabled())
this.log.trace("created new transaction " + tx + " (new total " + this.transactions.size() + ")");
// Done
return tx;
}
// Key Watches
synchronized ListenableFuture watchKey(byte[] key) {
Preconditions.checkState(this.started, "not started");
if (this.keyWatchTracker == null)
this.keyWatchTracker = new KeyWatchTracker();
return this.keyWatchTracker.register(key);
}
// Object
@Override
public synchronized String toString() {
return this.getClass().getSimpleName()
+ "[kvstore=" + this.kvstore
+ ",started=" + this.started
+ ",currentVersion=" + this.currentVersion
+ "]";
}
// Subclass methods
/**
* Instantiate a new {@link SnapshotKVTransaction} instance.
*
*
* The implementation in {@link SnapshotKVDatabase} just invokes the {@link SnapshotKVTransaction}
* constructor using {@code this}. Subclasses may want to override this method to create a more specific subclass.
*
* @param view mutable view to be used for this transaction
* @param baseVersion the database version associated with {@code base}
* @return new transaction instance
* @throws KVTransactionException if an error occurs
*/
protected SnapshotKVTransaction createSnapshotKVTransaction(MutableView view, long baseVersion) {
return new SnapshotKVTransaction(this, view, baseVersion);
}
/**
* Forcibly fail all outstanding transactions due to {@link #stop} being invoked.
*
*
* Can be used by subclasses during the shutdown sequence to ensure everything is properly cleaned up.
*/
protected synchronized void closeTransactions() {
for (SnapshotKVTransaction tx : new ArrayList<>(this.transactions)) {
if (tx.error == null)
tx.error = new KVTransactionException(tx, "database was stopped");
this.cleanupTransaction(tx);
}
}
/**
* Log specified exception. Used to ensure exceptions are logged at the point they are thrown.
*
* @param e exception to log
* @return e
*/
protected KVTransactionException logException(KVTransactionException e) {
if (this.log.isDebugEnabled())
this.log.debug("throwing exception for " + e.getTransaction() + ": " + e);
return e;
}
/**
* Wrap a {@link RuntimeException} as needed.
*
*
* The implementation in {@link SnapshotKVDatabase} just returns {@code e}.
*
* @param tx transaction in which the exception occurred
* @param e original exception
* @return wrapped exception, or just {@code e}
*/
protected RuntimeException wrapException(SnapshotKVTransaction tx, RuntimeException e) {
return e;
}
// Package methods
/**
* Commit a transaction.
*/
synchronized void commit(SnapshotKVTransaction tx, boolean readOnly) {
assert Thread.holdsLock(tx);
try {
this.doCommit(tx, readOnly);
} finally {
tx.error = null; // from this point on, throw a StaleTransactionException if accessed
this.cleanupTransaction(tx);
}
}
/**
* Rollback a transaction.
*/
synchronized void rollback(SnapshotKVTransaction tx) {
assert Thread.holdsLock(tx);
if (this.log.isTraceEnabled())
this.log.trace("rolling back transaction " + tx);
tx.error = null; // from this point on, throw a StaleTransactionException if accessed
this.cleanupTransaction(tx);
}
// SnapshotKVTransaction Methods
synchronized CloseableKVStore createMutableSnapshot(Writes writes) {
final SnapshotRefs snapshotRefs = this.getCurrentSnapshot();
snapshotRefs.ref();
final MutableView view = new MutableView(snapshotRefs.getKVStore(), null, writes);
return new CloseableForwardingKVStore(view, snapshotRefs.getUnrefCloseable());
}
// Internal methods
private synchronized void doCommit(SnapshotKVTransaction tx, boolean readOnly) {
// Sanity checks
assert Thread.holdsLock(tx);
assert Thread.holdsLock(this);
// Debug
if (this.log.isTraceEnabled()) {
this.log.trace("committing transaction " + tx + " based on version "
+ tx.baseVersion + " (current version is " + this.currentVersion + ")");
}
// Remove transaction; if not there, it's already been invalidated
if (!this.transactions.remove(tx)) {
tx.throwErrorIfAny();
throw this.logException(new StaleTransactionException(tx));
}
assert tx.error == null;
assert this.snapshot != null;
// Grab transaction reads & writes, set to immutable
final Writes txWrites;
synchronized (tx.view) {
txWrites = tx.getMutableView().getWrites();
tx.view.disableReadTracking();
tx.view.setReadOnly();
}
// If transaction is (effectively) read-only, no need to create a new version
if (readOnly || txWrites.isEmpty()) {
if (this.log.isTraceEnabled())
this.log.trace("no mutations in " + tx + ", staying at version " + this.currentVersion);
return;
}
// Apply the transaction's mutations
if (this.log.isTraceEnabled()) {
this.log.trace("applying " + tx + " mutations and advancing version from "
+ this.currentVersion + " -> " + (this.currentVersion + 1));
}
this.kvstore.mutate(txWrites, true);
// Discard the obsolete snapshot and advance the database version
final SnapshotRefs oldSnapshot = this.snapshot;
this.snapshot = null;
this.currentVersion++;
// Check concurrent transactions and invalidate any that have conflicts, or rebase them on the new version
int numTx = this.transactions.size(); // only used for logging
for (Iterator i = this.transactions.iterator(); i.hasNext(); ) {
final SnapshotKVTransaction victim = i.next();
assert victim.error == null;
synchronized (victim.view) {
// Check for conflict
final boolean conflict = victim.view.getReads().isConflict(txWrites);
if (this.log.isTraceEnabled()) {
this.log.trace("ordering " + victim + " after " + tx + " writes in version " + this.currentVersion
+ " results in " + (conflict ? "" : "no ") + "conflict");
// if (conflict)
// this.log.trace("conflicts: {}", victim.view.getReads().getConflicts(txWrites));
}
if (conflict) {
// Mark transaction for failure
i.remove();
victim.error = new RetryTransactionException(victim, "transaction is based on version "
+ victim.baseVersion + " but the transaction committed at version "
+ this.currentVersion + " contains conflicting writes");
if (this.log.isTraceEnabled())
this.log.trace("removed conflicting transaction " + victim + " (new total " + --numTx + ")");
// This looks weird. What it's really doing is ensuring that any subsequent attempt to access the
// data in the transaction via iterators that have already been created will "fail fast" and throw the
// RetryTransactionException created above. This happens because those accesses go through victim.delegate().
victim.view.setKVStore(victim);
continue;
}
// There was no conflict, so we can safely "rebase" this transaction on the new snapshot
victim.view.setKVStore(this.getCurrentSnapshot().getKVStore());
}
}
// Close the old snapshot (but only after rebasing remaining transactions)
oldSnapshot.unref();
// Notify watches
if (this.keyWatchTracker != null)
this.keyWatchTracker.trigger(txWrites);
}
private void cleanupTransaction(SnapshotKVTransaction tx) {
// Debug
assert Thread.holdsLock(this);
if (this.log.isTraceEnabled())
this.log.trace("cleaning up transaction " + tx);
// Remove open transaction from version
if (this.transactions.remove(tx) && this.log.isTraceEnabled())
this.log.trace("removed transaction " + tx + " (new total " + this.transactions.size() + ")");
}
// Get current k/v snapshot, creating on demand if necessary
private SnapshotRefs getCurrentSnapshot() {
assert Thread.holdsLock(this);
if (this.snapshot == null) {
this.snapshot = new SnapshotRefs(this.kvstore.snapshot());
if (this.log.isTraceEnabled())
this.log.trace("created new snapshot for version " + this.currentVersion);
}
return this.snapshot;
}
}