All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jsimpledb.kv.simple.SimpleKVDatabase Maven / Gradle / Ivy

There is a newer version: 3.6.1
Show newest version

/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package org.jsimpledb.kv.simple;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.SortedSet;

import org.jsimpledb.kv.KVDatabase;
import org.jsimpledb.kv.KVPair;
import org.jsimpledb.kv.KVStore;
import org.jsimpledb.kv.KeyRange;
import org.jsimpledb.kv.RetryTransactionException;
import org.jsimpledb.kv.StaleTransactionException;
import org.jsimpledb.kv.TransactionTimeoutException;
import org.jsimpledb.kv.mvcc.AtomicKVStore;
import org.jsimpledb.kv.mvcc.LockManager;
import org.jsimpledb.kv.mvcc.Mutations;
import org.jsimpledb.kv.util.KeyWatchTracker;
import org.jsimpledb.kv.util.NavigableMapKVStore;
import org.jsimpledb.util.ByteUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Simple implementation of the {@link KVDatabase} interface that provides a concurrent, transactional view
 * of an underlying {@link KVStore} with strong ACID semantics (Durability must be provided by the {@link KVStore}).
 *
 * 

* The ACID semantics, as well as {@linkplain #getWaitTimeout wait timeouts} and {@linkplain #getHoldTimeout hold timeouts}, * are provided by the {@link LockManager} class. If the wait timeout is exceeded, a {@link RetryTransactionException} * is thrown. If the hold timeout is exceeded, a {@link TransactionTimeoutException} is thrown. * *

* Instances wrap an underlying {@link KVStore} which provides persistence and from which committed data is read and written. * During a transaction, all mutations are recorded internally; if/when the transaction is committed, those mutations are * applied to the underlying {@link KVStore} all at once, and this operation is bracketed by calls to * {@link #preCommit preCommit()} and {@link #postCommit postCommit()}. If the underlying {@link KVStore} * is a {@link AtomicKVStore}, then {@link AtomicKVStore#mutate AtomicKVStore.mutate()} is used. * *

* {@linkplain SimpleKVTransaction#watchKey Key watches} are supported. * *

* Instances implement {@link Serializable} if the underlying {@link KVStore} is; this is the case when the default * constructor, which uses a {@link NavigableMapKVStore}, is used. However, key watches and open transactions are not * remembered across a (de)serialization cycle. * * @see LockManager */ public class SimpleKVDatabase implements KVDatabase, Serializable { /** * Default {@linkplain #getWaitTimeout wait timeout} for newly created transactions in milliseconds * ({@value DEFAULT_WAIT_TIMEOUT}). */ public static final long DEFAULT_WAIT_TIMEOUT = 500; /** * Default {@linkplain #getHoldTimeout hold timeout} in milliseconds ({@value DEFAULT_HOLD_TIMEOUT}). */ public static final long DEFAULT_HOLD_TIMEOUT = 5000; private static final long serialVersionUID = -6960954436594742251L; /** * The {@link KVStore} for the committed data. */ protected final KVStore kv; protected /*final*/ transient Logger log = LoggerFactory.getLogger(this.getClass()); private /*final*/ transient LockManager lockManager = new LockManager(this); private /*final*/ transient KeyWatchTracker keyWatchTracker; private long waitTimeout; /** * Constructor. Uses an internal in-memory {@link NavigableMapKVStore} and the default wait and hold timeouts. */ public SimpleKVDatabase() { this(new NavigableMapKVStore()); } /** * Constructor taking caller-supplied storage. Will use the default wait and hold timeouts. * * @param kv {@link KVStore} for the committed data, or null for an in-memory {@link KVStore} */ public SimpleKVDatabase(KVStore kv) { this(kv, DEFAULT_WAIT_TIMEOUT, DEFAULT_HOLD_TIMEOUT); } /** * Constructor taking timeout settings. Uses an internal in-memory {@link KVStore}. * * @param waitTimeout how long a thread will wait for a lock before throwing {@link RetryTransactionException} * in milliseconds, or zero for unlimited * @param holdTimeout how long a thread may hold a contestested lock before throwing {@link RetryTransactionException} * in milliseconds, or zero for unlimited * @throws IllegalArgumentException if {@code waitTimeout} or {@code holdTimeout} is negative */ public SimpleKVDatabase(long waitTimeout, long holdTimeout) { this(null, waitTimeout, holdTimeout); } /** * Primary constructor. * * @param kv {@link KVStore} for the committed data, or null for an in-memory {@link NavigableMapKVStore} * @param waitTimeout how long a thread will wait for a lock before throwing {@link RetryTransactionException} * in milliseconds, or zero for unlimited * @param holdTimeout how long a thread may hold a contestested lock before throwing {@link RetryTransactionException} * in milliseconds, or zero for unlimited * @throws IllegalArgumentException if {@code waitTimeout} or {@code holdTimeout} is negative */ public SimpleKVDatabase(KVStore kv, long waitTimeout, long holdTimeout) { this.kv = kv != null ? kv : new NavigableMapKVStore(); this.setWaitTimeout(waitTimeout); this.setHoldTimeout(holdTimeout); } /** * Get the wait timeout for newly created transactions. * *

* The wait timeout limits how long a thread will wait for a contested lock before giving up and throwing * {@link RetryTransactionException}. * * @return wait timeout in milliseconds */ public synchronized long getWaitTimeout() { return this.waitTimeout; } /** * Set the wait timeout for newly created transactions. Default is {@link #DEFAULT_WAIT_TIMEOUT}. * * @param waitTimeout how long a thread will wait for a lock before throwing {@link RetryTransactionException} * in milliseconds (default), or zero for unlimited * @throws IllegalArgumentException if {@code waitTimeout} is negative */ public synchronized void setWaitTimeout(long waitTimeout) { Preconditions.checkArgument(waitTimeout >= 0, "waitTimeout < 0"); this.waitTimeout = waitTimeout; } /** * Get the hold timeout configured for this instance. * *

* The hold timeout limits how long a thread may hold on to a contested lock before being forced to release * all of its locks; after that, the next attempted operation will fail with {@link RetryTransactionException}. * * @return hold timeout in milliseconds */ public long getHoldTimeout() { return this.lockManager.getHoldTimeout(); } /** * Set the hold timeout for this instance. Default is {@link #DEFAULT_HOLD_TIMEOUT}. * * @param holdTimeout how long a thread may hold a contestested lock before throwing {@link RetryTransactionException} * in milliseconds, or zero for unlimited * @throws IllegalArgumentException if {@code holdTimeout} is negative */ public void setHoldTimeout(long holdTimeout) { this.lockManager.setHoldTimeout(holdTimeout); } // KVDatabase @Override public synchronized void start() { } @Override public synchronized void stop() { if (this.keyWatchTracker != null) { this.keyWatchTracker.close(); this.keyWatchTracker = null; } } @Override public SimpleKVTransaction createTransaction(Map options) { return this.createTransaction(); // no options supported yet } @Override public synchronized SimpleKVTransaction createTransaction() { return new SimpleKVTransaction(this, this.waitTimeout); } // Key Watches synchronized ListenableFuture watchKey(byte[] key) { if (this.keyWatchTracker == null) this.keyWatchTracker = new KeyWatchTracker(); return this.keyWatchTracker.register(key); } // Subclass hooks /** * Invoked during transaction commit just prior to writing changes to the underlying {@link KVStore}. * *

* {@link SimpleKVDatabase} guarantees this method and {@link #postCommit postCommit()} will be invoked in matching pairs, * and that this instance will be locked when these methods are invoked. * *

* The implementation in {@link SimpleKVDatabase} does nothing. * * @param tx the transaction about to be committed * @throws RetryTransactionException if this transaction must be retried and is no longer usable */ protected void preCommit(SimpleKVTransaction tx) { } /** * Invoked during transaction commit just after writing changes to the underlying {@link KVStore}. * *

* {@link SimpleKVDatabase} guarantees this method and {@link #preCommit preCommit()} will be invoked in matching pairs, * and that this instance will be locked when these methods are invoked. * *

* This method is invoked even if the underlying {@link KVStore} throws an exception while changes were being written to it. * In that case, {@code successful} will be false. * *

* The implementation in {@link SimpleKVDatabase} does nothing. * * @param tx the transaction that was committed * @param successful true if all changes were written back successfully, * false if the underlying {@link KVStore} threw an exception during commit update */ protected void postCommit(SimpleKVTransaction tx, boolean successful) { } /** * Apply mutations to the underlying {@link KVStore}. */ void applyMutations(final Iterable mutations) { if (this.kv instanceof AtomicKVStore) { ((AtomicKVStore)this.kv).mutate(new Mutations() { @Override public Iterable getRemoveRanges() { return Iterables.filter(mutations, Del.class); } @Override public Iterable> getPutPairs() { return Iterables.transform(Iterables.filter(mutations, Put.class), Put::toMapEntry); } @Override public Iterable> getAdjustPairs() { return Collections.>emptySet(); } }, true); } else { for (Mutation mutation : mutations) mutation.apply(this.kv); } } /** * Verify that the given transaction is still usable. * *

* This method is invoked at the start of the {@link KVStore} data access and {@link SimpleKVTransaction#commit commit()} * methods of the {@link SimpleKVTransaction} associated with this instance. This allows for any checks which depend on * a consistent view of the transaction and database together. This instance's lock will be held when this method is invoked. * Note: transaction state is also protected by this instance's lock. * *

* The implementation in {@link SimpleKVDatabase} does nothing. * * @param tx the transaction being accessed * @throws StaleTransactionException if this instance is no longer usable * @throws RetryTransactionException if this transaction should be retried * @throws TransactionTimeoutException if the transaction has timed out */ protected void checkState(SimpleKVTransaction tx) { assert Thread.holdsLock(this); } private void checkUsable(SimpleKVTransaction tx) { if (tx.stale) throw new StaleTransactionException(tx); if (this.lockManager.checkHoldTimeout(tx.lockOwner) == -1) { this.rollback(tx); throw new TransactionTimeoutException(tx, "transaction taking too long: hold timeout of " + this.lockManager.getHoldTimeout() + "ms has expired"); } } // SimpleKVTransaction hooks synchronized byte[] get(SimpleKVTransaction tx, byte[] key) { // Sanity check Preconditions.checkArgument(key.length == 0 || key[0] != (byte)0xff, "key starts with 0xff"); this.checkUsable(tx); this.checkState(tx); // Check transaction mutations final Mutation mutation = tx.findMutation(key); if (mutation != null) return mutation instanceof Put ? ((Put)mutation).getValue() : null; // Read from underlying store this.getLock(tx, key, ByteUtil.getNextKey(key), false); return this.kv.get(key); } synchronized KVPair getAtLeast(SimpleKVTransaction tx, byte[] minKey, final byte[] maxKey) { // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Sanity check this.checkUsable(tx); this.checkState(tx); if (maxKey != null && ByteUtil.compare(minKey, maxKey) >= 0) return null; // Save original min key for locking purposes final byte[] originalMinKey = minKey; // Look for a mutation starting before minKey but containing it if (minKey.length > 0) { final Mutation overlap = tx.findMutation(minKey); if (overlap != null) { if (overlap instanceof Put) { final Put put = (Put)overlap; assert Arrays.equals(put.getKey(), minKey); return new KVPair(put.getKey(), put.getValue()); } assert overlap instanceof Del; final byte[] max = overlap.getMax(); if (max == null || (maxKey != null && ByteUtil.compare(max, maxKey) >= 0)) return null; minKey = max; } } // Get read lock this.getLock(tx, originalMinKey, maxKey, false); // Find whichever is first: a transaction Put, or an underlying store entry not covered by a transaction Delete SortedSet mutations = maxKey != null ? tx.mutations.headSet(Mutation.key(maxKey)) : tx.mutations; while (true) { // Get the next mutation and kvstore entry >= minKey (if they exist) assert minKey != null; mutations = mutations.tailSet(Mutation.key(minKey)); final Mutation mutation = !mutations.isEmpty() ? mutations.first() : null; final KVPair entry = this.kv.getAtLeast(minKey, maxKey); assert entry == null || ByteUtil.compare(entry.getKey(), minKey) >= 0; assert entry == null || maxKey == null || ByteUtil.compare(entry.getKey(), maxKey) < 0; // Handle the case where neither is found if (mutation == null && entry == null) return null; // Check for whether mutation or kvstore wins (i.e., which is first) if (mutation != null && (entry == null || mutation.compareTo(entry.getKey()) <= 0)) { if (mutation instanceof Del) { if ((minKey = mutation.getMax()) == null || (maxKey != null && ByteUtil.compare(minKey, maxKey) >= 0)) return null; continue; } final Put put = (Put)mutation; return new KVPair(put.getKey(), put.getValue()); } else return entry; } } synchronized KVPair getAtMost(SimpleKVTransaction tx, byte[] maxKey, byte[] minKey) { // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Sanity check this.checkUsable(tx); this.checkState(tx); if (maxKey != null && ByteUtil.compare(minKey, maxKey) >= 0) return null; // Get read lock this.getLock(tx, minKey, maxKey, false); // Find whichever is first: a transaction addition, or an underlying store entry not covered by a transaction deletion SortedSet mutations = tx.mutations; while (true) { // Get the next mutation and kvstore entry < maxKey (if they exist) if (maxKey != null) mutations = mutations.headSet(Mutation.key(maxKey)); final Mutation mutation = !mutations.isEmpty() ? mutations.last() : null; final KVPair entry = this.kv.getAtMost(maxKey, minKey); assert entry == null || ByteUtil.compare(entry.getKey(), minKey) >= 0; assert entry == null || maxKey == null || ByteUtil.compare(entry.getKey(), maxKey) < 0; // Handle the case where neither is found if (mutation == null && entry == null) return null; // Check for whether mutation or kvstore wins (i.e., which is first) if (mutation != null && (entry == null || mutation.compareTo(entry.getKey()) >= 0)) { if (mutation instanceof Del) { if ((maxKey = mutation.getMin()) == null || ByteUtil.compare(minKey, maxKey) >= 0) return null; continue; } final Put put = (Put)mutation; if (ByteUtil.compare(put.getKey(), minKey) < 0) { assert entry == null; // because otherwise minKey > mutation >= entry so entry should not have been found return null; } return new KVPair(put.getKey(), put.getValue()); } else return entry; } } synchronized void put(SimpleKVTransaction tx, byte[] key, byte[] value) { // Sanity check if (value == null) throw new NullPointerException(); Preconditions.checkArgument(key.length == 0 || key[0] != (byte)0xff, "key starts with 0xff"); this.checkUsable(tx); this.checkState(tx); final byte[] keyNext = ByteUtil.getNextKey(key); // Check transaction mutations final Mutation mutation = tx.findMutation(key); if (mutation instanceof Put) { assert Arrays.equals(((Put)mutation).getKey(), key); // Replace Put with new Put tx.mutations.remove(mutation); tx.mutations.add(new Put(key, value)); } else if (mutation instanceof Del) { // Split [Del] -> [Del*, Put, Del*] *if needed final Del del = (Del)mutation; final byte[] delMin = del.getMin(); final byte[] delMax = del.getMax(); tx.mutations.remove(del); if (KeyRange.compare(delMin, key) < 0) tx.mutations.add(new Del(delMin, key)); if (KeyRange.compare(keyNext, delMax) < 0) tx.mutations.add(new Del(keyNext, delMax)); tx.mutations.add(new Put(key, value)); } else { // Add write lock and new tx mutation this.getLock(tx, key, keyNext, true); tx.mutations.add(new Put(key, value)); } } synchronized void remove(SimpleKVTransaction tx, byte[] key) { // Sanity check Preconditions.checkArgument(key.length == 0 || key[0] != (byte)0xff, "key starts with 0xff"); this.checkUsable(tx); this.checkState(tx); final byte[] keyNext = ByteUtil.getNextKey(key); // Check transaction mutations final Mutation mutation = tx.findMutation(key); if (mutation instanceof Put) { assert Arrays.equals(((Put)mutation).getKey(), key); // Replace Put with Del tx.mutations.remove(mutation); tx.mutations.add(new Del(key)); } else if (mutation == null) { // Add write lock and new tx mutation this.getLock(tx, key, keyNext, true); tx.mutations.add(new Del(key)); } } synchronized void removeRange(SimpleKVTransaction tx, byte[] minKey, byte[] maxKey) { // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Sanity check int diff = KeyRange.compare(minKey, maxKey); Preconditions.checkArgument(diff <= 0, "minKey > maxKey"); this.checkUsable(tx); this.checkState(tx); if (diff == 0) // range is empty return; final byte[] originalMinKey = minKey; final byte[] originalMaxKey = maxKey; // Deal with partial overlap at the left end of the range if (minKey.length > 0) { final Mutation leftMutation = tx.findMutation(minKey); if (leftMutation instanceof Put) { assert Arrays.equals(((Put)leftMutation).getKey(), minKey); tx.mutations.remove(leftMutation); // overwritten by this change } else if (leftMutation instanceof Del) { final Del del = (Del)leftMutation; tx.mutations.remove(del); // will merge into this change minKey = del.getMin(); // guaranteed to be <= minKey if (KeyRange.compare(del.getMax(), maxKey) > 0) // get higher of the two maxKeys maxKey = del.getMax(); } } // Deal with partial overlap at the right end of the range if (maxKey != null) { Mutation rightMutation = null; try { rightMutation = minKey != null ? tx.mutations.subSet(Mutation.key(minKey), Mutation.key(maxKey)).last() : tx.mutations.headSet(Mutation.key(maxKey)).last(); } catch (NoSuchElementException e) { // ignore } if (rightMutation instanceof Put) tx.mutations.remove(rightMutation); // overwritten by this change else if (rightMutation instanceof Del) { final Del del = (Del)rightMutation; tx.mutations.remove(del); // will merge into this change if (KeyRange.compare(del.getMax(), maxKey) > 0) // get higher of the two maxKeys maxKey = del.getMax(); } } // Remove all mutations in the middle if (originalMinKey.length == 0 && originalMaxKey == null) tx.mutations.clear(); else if (originalMinKey.length == 0) tx.mutations.headSet(Mutation.key(originalMaxKey)).clear(); else if (originalMaxKey == null) tx.mutations.tailSet(Mutation.key(originalMinKey)).clear(); else tx.mutations.subSet(Mutation.key(originalMinKey), Mutation.key(originalMaxKey)).clear(); // Add write lock and new tx mutation this.getLock(tx, minKey, maxKey, true); tx.mutations.add(new Del(minKey, maxKey)); } synchronized void commit(SimpleKVTransaction tx, boolean readOnly) { // Prevent use after commit() or rollback() invoked if (tx.stale) throw new StaleTransactionException(tx); tx.stale = true; // Sanity check locking here before releasing locks boolean allMutationsWereLocked = true; boolean assertionsEnabled = false; assert assertionsEnabled = true; if (assertionsEnabled) { for (Mutation mutation : tx.mutations) { if (!this.lockManager.isLocked(tx.lockOwner, mutation.getMin(), mutation.getMax(), true)) { allMutationsWereLocked = false; break; } } } // Release all locks if (!this.lockManager.release(tx.lockOwner)) { throw new TransactionTimeoutException(tx, "transaction taking too long: hold timeout of " + this.lockManager.getHoldTimeout() + "ms has expired"); } assert allMutationsWereLocked; // Check subclass state this.checkState(tx); // If transaction is read-only, or there are no mutations, there's no need to write anything if (readOnly || tx.mutations.isEmpty()) return; // Commit mutations this.preCommit(tx); boolean successful = false; try { // Apply mutations this.applyMutations(tx.mutations); successful = true; // Trigger key watches if (this.keyWatchTracker != null && this.keyWatchTracker.getNumKeysWatched() > 0) { for (Mutation mutation : tx.mutations) mutation.trigger(this.keyWatchTracker); } } finally { tx.mutations.clear(); this.postCommit(tx, successful); } } synchronized void rollback(SimpleKVTransaction tx) { // Prevent use after commit() or rollback() invoked if (tx.stale) return; tx.stale = true; // Release all locks this.lockManager.release(tx.lockOwner); } // Internal methods private /*synchronized*/ void getLock(SimpleKVTransaction tx, byte[] minKey, byte[] maxKey, boolean write) { // Attempt to get the lock LockManager.LockResult lockResult; try { lockResult = this.lockManager.lock(tx.lockOwner, minKey, maxKey, write, tx.waitTimeout); } catch (InterruptedException e) { this.rollback(tx); Thread.currentThread().interrupt(); throw new RetryTransactionException(tx, "transaction interrupted while waiting to acquire lock", e); } // Check result switch (lockResult) { case SUCCESS: break; case WAIT_TIMEOUT_EXPIRED: this.rollback(tx); throw new RetryTransactionException(tx, "could not acquire lock after " + tx.waitTimeout + "ms"); case HOLD_TIMEOUT_EXPIRED: this.rollback(tx); throw new TransactionTimeoutException(tx, "transaction taking too long: hold timeout of " + this.lockManager.getHoldTimeout() + "ms has expired"); default: throw new RuntimeException("internal error"); } } // Serialization private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { input.defaultReadObject(); this.log = LoggerFactory.getLogger(this.getClass()); this.lockManager = new LockManager(this); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy