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

io.permazen.kv.xodus.XodusKVStore Maven / Gradle / Ivy


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

package io.permazen.kv.xodus;

import com.google.common.base.Preconditions;

import io.permazen.kv.AbstractKVStore;
import io.permazen.kv.CloseableKVStore;
import io.permazen.kv.KVPair;
import io.permazen.kv.KVStore;
import io.permazen.util.ByteUtil;
import io.permazen.util.CloseableIterator;

import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;

import jetbrains.exodus.ArrayByteIterable;
import jetbrains.exodus.ByteIterable;
import jetbrains.exodus.env.Cursor;
import jetbrains.exodus.env.Environment;
import jetbrains.exodus.env.Store;
import jetbrains.exodus.env.StoreConfig;
import jetbrains.exodus.env.Transaction;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Straightforward {@link KVStore} view of a Xodus {@link Store} viewed within an open {@link Transaction}.
 *
 * 

* Instances must be {@link #close}'d when no longer needed to avoid leaking resources. */ public class XodusKVStore extends AbstractKVStore implements CloseableKVStore { private final Logger log = LoggerFactory.getLogger(this.getClass()); private final TransactionType txType; private final Transaction tx; private final Store store; private final AtomicBoolean closed = new AtomicBoolean(); // Constructors /** * Convenience constructor. * *

* Equivalent to: {@code SnapshotXodusKVStore(env, storeName, true, txType)}. * * @param env Xodus environment * @param storeName Xodus store name * @param txType transaction type * @throws IllegalArgumentException if {@code env}, {@code storeName}, or {@code txType} is null */ public XodusKVStore(Environment env, String storeName, TransactionType txType) { this(env, storeName, true, txType); } /** * Constructor. * *

* The specified {@link Store} will be created if necessary. * * @param env Xodus environment * @param storeName Xodus store name * @param keyPrefixing if creating a new store, true to create the store with key prefixing * ({@link StoreConfig#WITHOUT_DUPLICATES_WITH_PREFIXING}), or false to create the store without key prefixing * ({@link StoreConfig#WITHOUT_DUPLICATES}) * @param txType transaction type * @throws IllegalArgumentException if {@code env}, {@code storeName}, or {@code txType} is null */ @SuppressWarnings("this-escape") public XodusKVStore(Environment env, String storeName, boolean keyPrefixing, TransactionType txType) { Preconditions.checkArgument(env != null, "null env"); Preconditions.checkArgument(storeName != null, "null storeName"); Preconditions.checkArgument(txType != null, "null txType"); this.txType = txType; this.tx = this.txType.apply(env); boolean success = false; try { this.store = env.openStore(storeName, keyPrefixing ? StoreConfig.WITHOUT_DUPLICATES_WITH_PREFIXING : StoreConfig.WITHOUT_DUPLICATES, this.tx); success = true; } finally { if (!success) this.tx.abort(); } if (this.log.isTraceEnabled()) this.log.trace("created {}", this); } // Used by readOnlySnapshot() private XodusKVStore(TransactionType txType, Transaction tx, Store store) { Preconditions.checkArgument(txType != null, "null txType"); Preconditions.checkArgument(tx != null, "null tx"); Preconditions.checkArgument(store != null, "null store"); this.txType = txType; this.tx = tx; this.store = store; } /** * Get the {@link TransactionType} associated with this instance. * * @return associated transaction type */ public TransactionType getTransactionType() { return this.txType; } /** * Get the {@link Transaction} associated with this instance. * * @return associated transaction */ public Transaction getTransaction() { return this.tx; } /** * Get the {@link Store} associated with this instance. * * @return associated store */ public Store getStore() { return this.store; } /** * Determine if this instance is closed. * * @return true if closed, false if still open */ public boolean isClosed() { return this.closed.get(); } /** * Return a read-only snapshot containing the same data as this instance. * *

* Though based on the same underlying data, the returned instance and this instance retain no references to each other. * * @return read-only snapshot * @throws IllegalStateException if this instance is closed */ public XodusKVStore readOnlySnapshot() { Preconditions.checkState(!this.closed.get(), "transaction closed"); return new XodusKVStore(TransactionType.READ_ONLY, this.tx.getReadonlySnapshot(), this.store); } // KVStore @Override public byte[] get(byte[] key) { key.getClass(); Preconditions.checkState(!this.closed.get(), "transaction closed"); final ByteIterable bytes = this.store.get(this.tx, new ArrayByteIterable(key)); return bytes != null ? XodusKVStore.get(bytes, true) : null; } @Override public KVPair getAtLeast(byte[] minKey, byte[] maxKey) { Preconditions.checkState(!this.closed.get(), "transaction closed"); try (Cursor cursor = this.store.openCursor(this.tx)) { final boolean found = minKey != null && minKey.length > 0 ? cursor.getSearchKeyRange(new ArrayByteIterable(minKey)) != null : cursor.getNext(); if (!found) return null; final byte[] key = XodusKVStore.get(cursor.getKey(), true); if (maxKey != null && ByteUtil.compare(key, maxKey) >= 0) return null; return new KVPair(key, XodusKVStore.get(cursor.getValue(), true)); } } @Override public KVPair getAtMost(byte[] maxKey, byte[] minKey) { Preconditions.checkState(!this.closed.get(), "transaction closed"); try (Cursor cursor = this.store.openCursor(this.tx)) { // It's possible somebody could be simultaneously inserting keys just after maxKey, in which case we // could be tricked into returning a key > maxKey. This is unlikely, but make sure it can't affect us. while (true) { if (maxKey != null) cursor.getSearchKeyRange(new ArrayByteIterable(maxKey)); if (!cursor.getPrev()) return null; final byte[] key = XodusKVStore.get(cursor.getKey(), true); if (maxKey != null && ByteUtil.compare(key, maxKey) >= 0) continue; if (minKey != null && ByteUtil.compare(key, minKey) < 0) return null; return new KVPair(key, XodusKVStore.get(cursor.getValue(), true)); } } } // Note Xodus closes all associated Cursors when a Transaction is closed, so we don't need to track the returned iterators @Override public CloseableIterator getRange(byte[] minKey, byte[] maxKey, boolean reverse) { Preconditions.checkState(!this.closed.get(), "transaction closed"); return new XodusIter(this.store.openCursor(this.tx), minKey, maxKey, reverse); } @Override public void put(byte[] key, byte[] value) { key.getClass(); value.getClass(); Preconditions.checkState(!this.closed.get(), "transaction closed"); Preconditions.checkState(!this.txType.isReadOnly(), "read-only transaction"); this.store.put(this.tx, new ArrayByteIterable(key), new ArrayByteIterable(value)); } @Override public void remove(byte[] key) { key.getClass(); Preconditions.checkState(!this.closed.get(), "transaction closed"); Preconditions.checkState(!this.txType.isReadOnly(), "read-only transaction"); this.store.delete(this.tx, new ArrayByteIterable(key)); } @Override public void removeRange(byte[] minKey, byte[] maxKey) { Preconditions.checkState(!this.closed.get(), "transaction closed"); Preconditions.checkState(!this.txType.isReadOnly(), "read-only transaction"); try (Cursor cursor = this.store.openCursor(this.tx)) { boolean found = minKey != null && minKey.length > 0 ? cursor.getSearchKeyRange(new ArrayByteIterable(minKey)) != null : cursor.getNext(); while (found) { if (maxKey != null && ByteUtil.compare(XodusKVStore.get(cursor.getKey(), false), maxKey) >= 0) break; cursor.deleteCurrent(); found = cursor.getNext(); } } } // Utility // Extract byte[] array from a ByteIterable. If result could be mutated, copy must be true. private static byte[] get(ByteIterable bytes, boolean copy) { final int len = bytes.getLength(); final byte[] data = bytes.getBytesUnsafe(); if (data.length == len && !copy) return data; final byte[] result = new byte[len]; System.arraycopy(data, 0, result, 0, len); return result; } // Object /** * Finalize this instance. Invokes {@link #close} to close any unclosed iterators. */ @Override @SuppressWarnings("deprecation") protected void finalize() throws Throwable { try { if (!this.closed.get()) { this.log.warn(this + " leaked without invoking close()"); this.close(); } } finally { super.finalize(); } } @Override public String toString() { return this.getClass().getSimpleName() + "[store=\"" + this.store.getName() + "\"" + ",type=" + this.txType + ",tx=" + this.tx + "]"; } // Closeable /** * Close this instance, discarding any changes. * *

* This closes the underlying {@link Transaction} and any unclosed iterators returned from {@link #getRange getRange()}. * This method just invokes {@link #close(boolean) close(false)}. */ @Override public void close() { this.close(false); } /** * Close this instance, optionally comitting any changes. * *

* This closes or commits the underlying {@link Transaction}, and closes any unclosed iterators returned from * {@link #getRange getRange()}. This instance will end up being closed even if commit fails. * * @param commit true to commit changes (if any) * @return true if already closed or successfully closed/commited, false if {@code commit} is true but the commit fails */ public boolean close(boolean commit) { if (!this.closed.compareAndSet(false, true)) return true; if (this.log.isTraceEnabled()) this.log.trace("closing {}", this); if (commit) { if (this.tx.commit()) return true; this.tx.abort(); return false; } this.tx.abort(); return true; } // XodusIter final class XodusIter implements CloseableIterator { private final Cursor cursor; private final byte[] minKey; private final byte[] maxKey; private final boolean reverse; private KVPair next; // the next key/value pair to return, if known private byte[] removeKey; // the key to delete if remove() is invoked private boolean removable; // if true, we can use cursor.deleteCurrent() to delete it private boolean finished; // iteration has completed private boolean closed; private XodusIter(Cursor cursor, byte[] minKey, byte[] maxKey, boolean reverse) { // Sanity checks Preconditions.checkArgument(minKey == null || maxKey == null || ByteUtil.compare(minKey, maxKey) <= 0, "minKey > maxKey"); // Initialize if (minKey != null && minKey.length == 0) minKey = null; this.cursor = cursor; this.minKey = minKey; this.maxKey = maxKey; this.reverse = reverse; if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("created {}", this); if (this.reverse) { if (this.maxKey != null) { final boolean found = this.cursor.getSearchKeyRange(new ArrayByteIterable(this.maxKey)) != null; assert !found || ByteUtil.compare(XodusKVStore.get(this.cursor.getKey(), false), this.maxKey) >= 0 : "cusor.getSearchKeyRange() returned " + ByteUtil.toString(XodusKVStore.get(this.cursor.getKey(), false)) + " < " + ByteUtil.toString(this.maxKey); if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("initial seek to {} -> {}", ByteUtil.toString(this.maxKey), found); } } else if (this.minKey != null) { final boolean found = this.cursor.getSearchKeyRange(new ArrayByteIterable(this.minKey)) != null; if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("initial seek to {} -> {}", ByteUtil.toString(this.minKey), found); if (found) { final byte[] key = XodusKVStore.get(cursor.getKey(), true); assert ByteUtil.compare(key, this.minKey) >= 0 : "cusor.getSearchKeyRange() returned " + ByteUtil.toString(key) + " < " + ByteUtil.toString(this.minKey); if (maxKey != null && ByteUtil.compare(key, maxKey) >= 0) this.finished = true; else this.next = new KVPair(key, XodusKVStore.get(cursor.getValue(), true)); } else { if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("initial seek failed -> DONE"); this.finished = true; } } } // Iterator @Override public synchronized boolean hasNext() { Preconditions.checkState(!this.closed, "iterator closed"); return this.next != null || this.findNext(); } @Override public synchronized KVPair next() { Preconditions.checkState(!this.closed, "iterator closed"); if (this.next == null && !this.findNext()) throw new NoSuchElementException(); assert this.next != null; final KVPair pair = this.next; this.removeKey = pair.getKey(); this.removable = true; this.next = null; return pair; } @Override public synchronized void remove() { Preconditions.checkState(!this.closed, "iterator closed"); Preconditions.checkState(this.removeKey != null); Preconditions.checkState(!XodusKVStore.this.txType.isReadOnly(), "read-only transaction"); if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("remove {}", ByteUtil.toString(this.removeKey)); if (this.removable) this.cursor.deleteCurrent(); else XodusKVStore.this.remove(this.removeKey); this.removeKey = null; this.removable = false; } private boolean findNext() { // Sanity check assert Thread.holdsLock(this); assert this.next == null; if (this.finished) return false; // Advance Xodus cursor this.removable = false; // this.remove() can no longer use cursor.deleteCurrent() byte[] key; if (this.reverse) { while (true) { // Any more keys? if (!this.cursor.getPrev()) { if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("seek previous -> DONE"); this.finished = true; return false; } key = XodusKVStore.get(this.cursor.getKey(), true); // It's possible somebody could be simultaneously inserting keys just after maxKey, in which case we // could be tricked into returning a key > maxKey. This is unlikely, but make sure it can't affect us. if (this.maxKey != null && ByteUtil.compare(key, this.maxKey) >= 0) { if (XodusKVStore.this.log.isTraceEnabled()) { XodusKVStore.this.log.trace("seek previous -> skip over " + ByteUtil.toString(key) + " >= " + ByteUtil.toString(this.maxKey)); } this.cursor.getSearchKeyRange(new ArrayByteIterable(this.maxKey)); // recalibrate and try again continue; } // Check lower bound if (this.minKey != null && ByteUtil.compare(key, this.minKey) < 0) { if (XodusKVStore.this.log.isTraceEnabled()) { XodusKVStore.this.log.trace("seek previous -> " + ByteUtil.toString(key) + " < bound " + ByteUtil.toString(this.minKey) + " -> DONE"); } this.finished = true; return false; } break; } } else { // Any more keys? if (!this.cursor.getNext()) { if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("seek next -> DONE"); this.finished = true; return false; } key = XodusKVStore.get(this.cursor.getKey(), true); // Check lower bound assert this.minKey == null || ByteUtil.compare(key, this.minKey) >= 0 : "cusor.getNext() returned " + ByteUtil.toString(key) + " < " + ByteUtil.toString(this.minKey); // Check upper bound if (this.maxKey != null && ByteUtil.compare(key, this.maxKey) >= 0) { if (XodusKVStore.this.log.isTraceEnabled()) { XodusKVStore.this.log.trace("seek next -> " + ByteUtil.toString(key) + " >= bound " + ByteUtil.toString(this.minKey) + " -> DONE"); } this.finished = true; return false; } } // We found the next pair this.next = new KVPair(key, XodusKVStore.get(cursor.getValue(), true)); if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("seek {} -> {}", this.reverse ? "previous" : "next", this.next); // Done return true; } // Closeable @Override public void close() { synchronized (this) { if (this.closed) return; this.closed = true; } if (XodusKVStore.this.log.isTraceEnabled()) XodusKVStore.this.log.trace("closing {}", this); try { this.cursor.close(); } catch (Throwable e) { XodusKVStore.this.log.debug("caught exception closing Xodus cursor (ignoring)", e); } } // Object @Override public String toString() { return XodusKVStore.class.getSimpleName() + "." + this.getClass().getSimpleName() + "[minKey=" + ByteUtil.toString(this.minKey) + ",maxKey=" + ByteUtil.toString(this.maxKey) + (this.reverse ? ",reverse" : "") + "]"; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy