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

org.jsimpledb.kv.mvcc.MutableView 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.mvcc;

import com.google.common.base.Preconditions;

import java.io.Closeable;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.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.SizeEstimating;
import org.jsimpledb.util.SizeEstimator;

/**
 * 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 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, SizeEstimating { private final KVStore kv; @GuardedBy("this") private /*final*/ Writes writes; @GuardedBy("this") private Reads reads; // Constructors /** * Constructor. * * @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 KVStore getKVStore() { return this.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; } // 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 Iterator getRange(byte[] minKey, byte[] maxKey, boolean reverse) { // Build iterator 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"); // 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"); // 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) { // 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) { return this.kv.encodeCounter(value); } @Override public long decodeCounter(byte[] bytes) { return this.kv.decodeCounter(bytes); } @Override public synchronized void adjustCounter(byte[] key, long amount) { // 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); } // SizeEstimating /** * Add the estimated size of this instance (in bytes) to the given estimator. * *

* The size estimate returned by this method does not include the underlying {@link KVStore}. * * @param estimator size estimator */ @Override public synchronized void addTo(SizeEstimator estimator) { estimator .addObjectOverhead() .addReferenceField() // kv .addField(this.reads) // reads .addField(this.writes); // writes } // 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 : "") + "]"; } // Internal methods // Apply accumulated counter adjustments to the value, if any 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) 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; // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Already tracked? final KeyRange readRange = new KeyRange(minKey, maxKey); if (this.reads.contains(readRange)) return; // Subtract out the part of the read range that did not really go through to k/v store due to puts or removes final KeyRanges readRanges = new KeyRanges(readRange); final Set putKeys = (maxKey != null ? this.writes.getPuts().subMap(minKey, maxKey) : this.writes.getPuts().tailMap(minKey)).keySet(); for (byte[] key : putKeys) readRanges.remove(new KeyRange(key)); readRanges.remove(this.writes.getRemoves()); // Record reads if (!readRanges.isEmpty()) this.reads.add(readRanges); } // RangeIterator private class RangeIterator implements Iterator, Closeable { private final boolean reverse; // iteration direction private final byte[] limit; // limit of iteration; exclusive if forward, inclusive if reverse private byte[] cursor; // current position; inclusive if forward, exclusive if reverse private KVPair next; // the next k/v pair queued up, or null if not found yet private byte[] removeKey; // key to remove if remove() is invoked private boolean finished; // Position in underlying k/v store private Iterator kviter; // k/v store iterator, if any left private KVPair kvnext; // next kvstore pair, if already retrieved // Position in puts private KVPair putnext; // next put pair, if already retrieved private boolean putdone; // no more pairs left in puts RangeIterator(byte[] minKey, byte[] maxKey, boolean reverse) { // Realize minKey if (minKey == null) minKey = ByteUtil.EMPTY; // Initialize cursors this.kviter = MutableView.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; // Find the next underlying k/v pair, if we don't already have it if (this.kviter != null && this.kvnext == null) { 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; synchronized (MutableView.this) { ranges = MutableView.this.writes.getRemoves().findKey(this.kvnext.getKey()); } if (ranges[0] == ranges[1] && ranges[0] != null) { final KeyRange removeRange = ranges[0]; // Find the end of the remove range (if any) final byte[] removeRangeEnd = this.reverse ? removeRange.getMin() : removeRange.getMax(); 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) { Map.Entry putEntry; synchronized (MutableView.this) { 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 = this.cursor; } else { skipMin = this.cursor; skipMax = this.next != null ? ByteUtil.getNextKey(this.next.getKey()) : this.limit; } if (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) { try { ((AutoCloseable)this.kviter).close(); } catch (Exception e) { // ignore; } this.kviter = null; } this.kvnext = null; } // Closeable @Override public synchronized void close() { this.closeKVStoreIterator(); this.putdone = true; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy