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

org.jsimpledb.kv.mvcc.MutableView Maven / Gradle / Ivy

The newest version!

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

package org.jsimpledb.kv.mvcc;

import com.google.common.base.Preconditions;

import java.util.Arrays;
import java.util.Map;
import java.util.NoSuchElementException;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

import org.jsimpledb.kv.AbstractKVStore;
import org.jsimpledb.kv.KVPair;
import org.jsimpledb.kv.KVStore;
import org.jsimpledb.kv.KeyRange;
import org.jsimpledb.kv.KeyRanges;
import org.jsimpledb.util.ByteUtil;
import org.jsimpledb.util.CloseableIterator;

/**
 * Provides a mutable view of an underlying, read-only {@link KVStore}.
 *
 * 

* Instances intercept all operations to the underlying {@link KVStore}, recording mutations in a {@link Writes} instance * instead of applying them to the {@link KVStore}. Instances then provide a view of the mutated {@link KVStore} based those * mutations. Mutations that overwrite previous mutations are consolidated. * *

* Unlike writes, reads are passed through to the underlying {@link KVStore}, except where they intersect a previous write. * Reads may also be optionally recorded. * *

* In all cases, the underlying {@link KVStore} is never modified. * *

* Instances ensure that counter adjustment mutations never overlap put or remove mutations. * *

* Instances are thread safe; however, directly accessing the associated {@link Reads} or {@link Writes} is not thread safe * without first locking the containing instance. */ @ThreadSafe public class MutableView extends AbstractKVStore implements Cloneable { @GuardedBy("this") private KVStore kv; @GuardedBy("this") private /*final*/ Writes writes; @GuardedBy("this") private Reads reads; @GuardedBy("this") private boolean readOnly; // Constructors /** * Constructor. * *

* The instance will use a new, empty {@link Reads} instance for read tracking. * * @param kv underlying {@link KVStore} * @throws IllegalArgumentException if {@code kv} is null */ public MutableView(KVStore kv) { this(kv, new Reads(), new Writes()); } /** * Constructor using caller-provided {@link Reads} (optional) and {@link Writes}. * * @param kv underlying {@link KVStore} * @param reads recorded reads, or null for none * @param writes recorded writes * @throws IllegalArgumentException if {@code kv} is null * @throws IllegalArgumentException if {@code writes} is null */ public MutableView(KVStore kv, Reads reads, Writes writes) { Preconditions.checkArgument(kv != null, "null kv"); Preconditions.checkArgument(writes != null, "null writes"); this.kv = kv; this.reads = reads; this.writes = writes; } // Public methods /** * Get the underlying {@link KVStore} associated with this instance. * * @return underlying {@link KVStore} */ public synchronized KVStore getKVStore() { return this.kv; } /** * Swap out the underlying {@link KVStore} associated with this instance. * *

* Note: the new {@link KVStore} should have a consistent encoding of counter values as the previous {@link KVStore}, * otherwise a concurrent thread may read previously written counter values back incorrectly. * * @param kv new underlying {@link KVStore} * @throws IllegalArgumentException if {@code kv} is null */ public synchronized void setKVStore(KVStore kv) { Preconditions.checkArgument(kv != null, "null kv"); this.kv = kv; } /** * Get the {@link Reads} associated with this instance. * *

* This includes all keys explicitly or implicitly read by calls to * {@link #get get()}, {@link #getAtLeast getAtLeast()}, {@link #getAtMost getAtMost()}, and {@link #getRange getRange()}. * *

* The returned object should only be accessed while synchronized on this instance. * * @return reads recorded, or null if this instance is not configured to record reads */ public synchronized Reads getReads() { return this.reads; } /** * Get the {@link Writes} associated with this instance. * *

* The returned object should only be accessed while synchronized on this instance. * * @return writes recorded */ public synchronized Writes getWrites() { return this.writes; } /** * Disable read tracking and discard the {@link Reads} associated with this instance. * *

* Can be used to save some memory when read tracking information is no longer needed. */ public synchronized void disableReadTracking() { this.reads = null; } /** * Configure this instance as read-only. * *

* Any subsequent invocations of {@link #put put()}, {@link #remove remove()}, {@link #removeRange removeRange()}, * or {@link #adjustCounter adjustCounter()} will result in an {@link IllegalStateException}. */ public synchronized void setReadOnly() { this.readOnly = true; } // KVStore @Override public synchronized byte[] get(byte[] key) { // Check puts byte[] value = this.writes.getPuts().get(key); if (value != null) return this.applyCounterAdjustment(key, value).clone(); // Check removes if (this.writes.getRemoves().contains(key)) return null; // we can ignore adjustments of missing values // Read from underlying k/v store value = this.kv.get(key); // Record the read this.recordReads(key, ByteUtil.getNextKey(key)); // Apply counter adjustments if (value != null) // we can ignore adjustments of missing values value = this.applyCounterAdjustment(key, value).clone(); // Done return value; } @Override public synchronized CloseableIterator getRange(byte[] minKey, byte[] maxKey, boolean reverse) { return new RangeIterator(minKey, maxKey, reverse); } @Override public synchronized void put(byte[] key, byte[] value) { // Sanity check Preconditions.checkArgument(key != null, "null key"); Preconditions.checkArgument(value != null, "null value"); Preconditions.checkState(!this.readOnly, "instance is read-only"); // Overwrite any counter adjustment this.writes.getAdjusts().remove(key); // Record the put this.writes.getPuts().put(key.clone(), value.clone()); } @Override public synchronized void remove(byte[] key) { // Sanity check Preconditions.checkArgument(key != null, "null key"); Preconditions.checkState(!this.readOnly, "instance is read-only"); // Overwrite any counter adjustment this.writes.getAdjusts().remove(key); // Overwrite any put this.writes.getPuts().remove(key); // Record the remove this.writes.getRemoves().add(new KeyRange(key)); } @Override public synchronized void removeRange(byte[] minKey, byte[] maxKey) { // Sanity check Preconditions.checkState(!this.readOnly, "instance is read-only"); // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Overwrite any puts and counter adjustments if (maxKey != null) { this.writes.getPuts().subMap(minKey, maxKey).clear(); this.writes.getAdjusts().subMap(minKey, maxKey).clear(); } else { this.writes.getPuts().tailMap(minKey).clear(); this.writes.getAdjusts().tailMap(minKey).clear(); } // Record the remove this.writes.getRemoves().add(new KeyRange(minKey, maxKey)); } @Override public byte[] encodeCounter(long value) { final KVStore currentKV; synchronized (this) { currentKV = this.kv; } return currentKV.encodeCounter(value); } @Override public long decodeCounter(byte[] bytes) { final KVStore currentKV; synchronized (this) { currentKV = this.kv; } return currentKV.decodeCounter(bytes); } @Override public synchronized void adjustCounter(byte[] key, long amount) { // Sanity check Preconditions.checkState(!this.readOnly, "instance is read-only"); // Check puts final byte[] putValue = this.writes.getPuts().get(key); if (putValue != null) { final long value; try { value = this.kv.decodeCounter(putValue); } catch (IllegalArgumentException e) { return; // previously put value was not decodable, so ignore this adjustment } this.writes.getPuts().put(key, this.kv.encodeCounter(value + amount)); return; } // Check removes if (this.writes.getRemoves().contains(key)) return; // Calculate new, cumulative adjustment final Long oldAdjust = this.writes.getAdjusts().get(key); if (oldAdjust != null) amount += oldAdjust; // Record/update adjustment if (amount != 0) this.writes.getAdjusts().put(key, amount); else if (oldAdjust != null) this.writes.getAdjusts().remove(key); } // Cloneable /** * Clone this instance. * *

* The clone will have the same underlying {@link KVStore}, but its own {@link Reads} and {@link Writes}, * which will themselves be cloned from this instance's copies. * * @return clone of this instance */ @Override public synchronized MutableView clone() { final MutableView clone; try { clone = (MutableView)super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } if (this.reads != null) clone.reads = this.reads.clone(); clone.writes = this.writes.clone(); return clone; } // Object @Override public synchronized String toString() { return this.getClass().getSimpleName() + "[writes=" + this.writes + (this.reads != null ? ",reads=" + this.reads : "") + (this.readOnly ? ",r/o" : "") + "]"; } // Internal methods // Apply accumulated counter adjustments to the value, if any. If no adjustment necessary, returns same "value" object. private synchronized byte[] applyCounterAdjustment(byte[] key, byte[] value) { // Is there an adjustment of this key? assert key != null; final Long adjust = this.writes.getAdjusts().get(key); if (adjust == null || adjust == 0) return value; // Decode value we just read as a counter final long counterValue; try { counterValue = this.kv.decodeCounter(value); } catch (IllegalArgumentException e) { return value; // previous adjustment was bogus because value was not decodable } // Adjust counter value by accumulated adjustment value and re-encode final byte[] adjustedValue = this.kv.encodeCounter(counterValue + adjust); assert adjustedValue != null; return adjustedValue; } // Record that keys were read in the range [minKey, maxKey) private synchronized void recordReads(byte[] minKey, byte[] maxKey) { // Not tracking reads? if (this.reads == null) return; // Define the range final KeyRange range = new KeyRange(minKey != null ? minKey : ByteUtil.EMPTY, maxKey); // If read is entirely contained in a remove range, it did not really go through to k/v store if (this.writes.getRemoves().contains(range)) return; // If the read is of a single key and that key has been written, it did not really go through to k/v store if (range.isSingleKey() && this.writes.getPuts().containsKey(range.getMin())) return; // Add range this.reads.add(range); } // RangeIterator @ThreadSafe private class RangeIterator implements CloseableIterator { // Locking order: (1) RangeIterator (2) MutableView private final boolean reverse; // iteration direction private final byte[] limit; // limit of iteration; exclusive if forward, inclusive if reverse @GuardedBy("this") private KVStore kv; // underlying k/v store corresponding to this.kviter @GuardedBy("this") private byte[] cursor; // current position; inclusive if forward, exclusive if reverse @GuardedBy("this") private KVPair next; // the next k/v pair queued up, or null if not found yet @GuardedBy("this") private byte[] removeKey; // key to remove if remove() is invoked @GuardedBy("this") private boolean finished; // Position in underlying k/v store @GuardedBy("this") private CloseableIterator kviter; // k/v store iterator, if any left @GuardedBy("this") private KVPair kvnext; // next kvstore pair, if already retrieved // Position in puts @GuardedBy("this") private KVPair putnext; // next put pair, if already retrieved @GuardedBy("this") private boolean putdone; // no more pairs left in puts RangeIterator(byte[] minKey, byte[] maxKey, boolean reverse) { assert Thread.holdsLock(MutableView.this); // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Initialize cursor this.kv = MutableView.this.kv; this.kviter = this.kv.getRange(minKey, maxKey, reverse); this.cursor = reverse ? maxKey : minKey; this.limit = reverse ? minKey : maxKey; this.reverse = reverse; } @Override public synchronized boolean hasNext() { return this.next != null || this.findNext(); } @Override public synchronized KVPair next() { if (this.next == null && !this.findNext()) throw new NoSuchElementException(); final KVPair pair = this.next; assert pair != null; this.removeKey = pair.getKey(); this.next = null; return pair; } @Override public synchronized void remove() { Preconditions.checkState(this.removeKey != null); MutableView.this.remove(this.removeKey); this.removeKey = null; } private synchronized boolean findNext() { // Invariants & checks assert this.next == null; assert this.cursor != null || this.reverse; assert this.limit != null || !this.reverse; assert this.kviter != null || this.kvnext == null; // Exhausted? if (this.finished) return false; // Keep track of starting range of keys read from the underlying k/v store byte[] readStart; // Find the next underlying k/v pair, if we don't already have it. Whenever we access the underlying KVStore // we synchronize on this MutableView; this prevents it from changing out from under us while we're using it, // as well as avoiding races with other threads doing put(), remove(), etc. synchronized (MutableView.this) { // Detect if the underlying key/value store has been swapped out; if so, we must get a new iterator if (this.kviter != null && this.kv != MutableView.this.kv) { this.closeKVStoreIterator(); this.kv = MutableView.this.kv; this.kviter = this.reverse ? this.kv.getRange(this.limit, this.cursor, true) : this.kv.getRange(this.cursor, this.limit, false); } // Advance to the next key/value pair readStart = this.cursor; if (this.kviter != null && this.kvnext == null) { // Get removes final KeyRanges removes = MutableView.this.writes.getRemoves(); // Find next key/value pair that has not been removed while (true) { // Get next k/v pair in underlying key/value store, if any if (!this.kviter.hasNext()) { this.closeKVStoreIterator(); break; } this.kvnext = this.kviter.next(); assert this.kvnext != null; assert !this.isPastLimit(this.kvnext.getKey()); assert this.isPast(this.kvnext.getKey(), this.cursor) : "key " + ByteUtil.toString(this.kvnext.getKey()) + " is not past cursor " + ByteUtil.toString(this.cursor); // If k/v pair has been removed, skip past the matching remove range final KeyRange[] ranges = removes.findKey(this.kvnext.getKey()); if (ranges[0] == ranges[1] && ranges[0] != null) { final KeyRange removeRange = ranges[0]; // If the removed range contains the starting cursor as well, we can shrink our recorded read range final byte[] removeRangeEnd = this.reverse ? removeRange.getMin() : removeRange.getMax(); if (this.reverse) { final byte[] removeRangeStart = removeRange.getMax(); if (readStart != null && (removeRangeStart == null || ByteUtil.compare(readStart, removeRangeStart) <= 0)) readStart = removeRangeEnd; } else if (removeRange.contains(readStart)) readStart = removeRangeEnd; // Find the end of the remove range (if any) if (removeRangeEnd == null || this.isPastLimit(removeRangeEnd) || (this.reverse && Arrays.equals(removeRangeEnd, this.limit))) { this.closeKVStoreIterator(); break; } // Skip over it and restart iterator this.closeKVStoreIterator(); final byte[] iterMin; final byte[] iterMax; if (this.reverse) { iterMin = this.limit; iterMax = removeRangeEnd; } else { iterMin = removeRangeEnd; iterMax = this.limit; } this.kviter = MutableView.this.kv.getRange(iterMin, iterMax, this.reverse); continue; } // Got one break; } } // Find next put pair, if we don't already have it if (!this.putdone && this.putnext == null) { final Map.Entry putEntry; if (this.reverse) { putEntry = this.cursor != null ? MutableView.this.writes.getPuts().lowerEntry(this.cursor) : MutableView.this.writes.getPuts().lastEntry(); } else putEntry = MutableView.this.writes.getPuts().ceilingEntry(this.cursor); if (putEntry == null || this.isPastLimit(putEntry.getKey())) { this.putnext = null; this.putdone = true; } else this.putnext = new KVPair(putEntry.getKey().clone(), putEntry.getValue().clone()); } } // Figure out which pair appears first (k/v or put); if there's a tie, the put wins if (this.kvnext == null && this.putnext == null) this.next = null; else if (this.kvnext == null) { this.next = this.putnext; this.putnext = null; } else if (this.putnext == null) { this.next = this.kvnext; this.kvnext = null; } else { final int diff = reverse ? ByteUtil.compare(this.kvnext.getKey(), this.putnext.getKey()) : ByteUtil.compare(this.putnext.getKey(), this.kvnext.getKey()); if (diff <= 0) { this.next = this.putnext; this.putnext = null; if (diff == 0) this.kvnext = null; // the kvstore key was overridden by the put key } else { this.next = this.kvnext; this.kvnext = null; } } // Record that we read from everything we just scanned over in the underlying KVStore final byte[] skipMin; final byte[] skipMax; if (this.reverse) { skipMin = this.next != null ? this.next.getKey() : this.limit; skipMax = readStart; } else { skipMin = readStart; skipMax = this.next != null ? ByteUtil.getNextKey(this.next.getKey()) : this.limit; } if (skipMin != null && (skipMax == null || ByteUtil.compare(skipMin, skipMax) < 0)) MutableView.this.recordReads(skipMin, skipMax); // Finished? if (this.next == null) { this.finished = true; return false; } // Apply any counter adjustment to the retrieved value, if appropriate final byte[] adjustedValue = MutableView.this.applyCounterAdjustment(this.next.getKey(), this.next.getValue()); if (adjustedValue != this.next.getValue()) this.next = new KVPair(this.next.getKey(), adjustedValue); // Update cursor this.cursor = this.reverse ? this.next.getKey() : ByteUtil.getNextKey(this.next.getKey()); // Done return true; } private boolean isPastLimit(byte[] key) { return this.isPast(key, this.limit); } private boolean isPast(byte[] key, byte[] mark) { return this.reverse ? mark == null || ByteUtil.compare(key, mark) < 0 : mark != null && ByteUtil.compare(key, mark) >= 0; } private void closeKVStoreIterator() { assert Thread.holdsLock(this); if (this.kviter != null) { this.kviter.close(); this.kviter = null; } this.kvnext = null; } // Closeable @Override public synchronized void close() { this.closeKVStoreIterator(); this.putdone = true; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy