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

io.permazen.kv.caching.CachingKVDatabase Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show newest version

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

package io.permazen.kv.caching;

import com.google.common.base.Preconditions;

import io.permazen.kv.KVDatabase;
import io.permazen.kv.KVTransaction;
import io.permazen.util.MovingAverage;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

/**
 * A wrapper around an inner {@link KVDatabase} that adds a caching layer to transactions by wrapping them
 * in a {@link CachingKVStore}.
 *
 * 

* See {@link CachingKVStore} for details on how caching is performed. * *

Consistency Assumptions

* *

* Warning: this class assumes that the underlying {@link KVDatabase} provides transactions guaranteeing * fully consistent reads: the data returned by any two read operations, no matter when they occur, must be consistent * within any given transaction. A corollary is that transactions must be fully isolated from each other. * Enabling assertions on this package may detect some violations of this assumption. * * @see CachingKVStore */ public class CachingKVDatabase extends AbstractCachingConfig implements KVDatabase { /** * Default for the initial round-trip time estimate in milliseconds. * * @see #setInitialRttEstimate */ public static final int DEFAULT_INITIAL_RTT_ESTIMATE_MILLIS = 50; /** * Default thread pool size when no {@link ExecutorService} is explicitly configured. * * @see #setExecutorService */ public static final int DEFAULT_THREAD_POOL_SIZE = 10; private static final double RTT_ESTIMATE_DECAY_FACTOR = 0.025; private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(); private KVDatabase inner; private int threadPoolSize = DEFAULT_THREAD_POOL_SIZE; private long initialRttEstimate = TimeUnit.MILLISECONDS.toNanos(DEFAULT_INITIAL_RTT_ESTIMATE_MILLIS); private ExecutorService executor; private boolean started; private boolean privateExecutor; private MovingAverage rtt; /** * Default constructor. * *

* Instances must be configured with an inner {@link KVDatabase} before starting. */ public CachingKVDatabase() { } /** * Primary constructor. * * @param inner inner {@link KVDatabase} */ public CachingKVDatabase(KVDatabase inner) { this.inner = inner; } // Accessors /** * Get the initial round trip time estimate. * *

* Default is {@value #DEFAULT_INITIAL_RTT_ESTIMATE_MILLIS} ms. * * @return initial round trip time estimate in nanoseconds */ public synchronized long getInitialRttEstimate() { return this.initialRttEstimate; } /** * Set the initial round trip time estimate. * *

* This is just an initial estimate. The actual value used adjusts dynamically as transactions occur * and actual RTT measurements are made. * *

* Default is {@value #DEFAULT_INITIAL_RTT_ESTIMATE_MILLIS} ms. * * @param initialRttEstimate initial round trip time estimate in nanoseconds * @throws IllegalStateException if this instance is already started * @throws IllegalArgumentException if {@code initialRttEstimate < 0} */ public synchronized void setInitialRttEstimate(long initialRttEstimate) { Preconditions.checkArgument(initialRttEstimate >= 0, "initialRttEstimate < 0"); Preconditions.checkState(!this.started, "already started"); this.initialRttEstimate = initialRttEstimate; } /** * Get the inner {@link KVDatabase}. * * @return the underlying {@link KVDatabase} */ public synchronized KVDatabase getKVDatabase() { return this.inner; } /** * Configure the underlying {@link KVDatabase}. * * @param inner the underlying {@link KVDatabase} * @throws IllegalStateException if this instance is already started */ public synchronized void setKVDatabase(KVDatabase inner) { Preconditions.checkState(!this.started, "already started"); this.inner = inner; } /** * Get the configured {@link ExecutorService}, if any. * * @return caller-supplied executor to use for background tasks, or null for none */ public synchronized ExecutorService getExecutorService() { return this.executor; } /** * Configure a {@link ExecutorService} to use for asynchronous queries to the underlying database. * *

* This property is optional; if none is configured, a private thread pool will be * automatically created and setup internally by {@link #start} and torn down by {@link #stop}. * If an outside executor is configured here, then it will not be shutdown by {@link #stop}. * * @param executor caller-supplied executor to use for background tasks, or null for none * @throws IllegalStateException if this instance is already started */ public synchronized void setExecutorService(ExecutorService executor) { Preconditions.checkState(!this.started, "already started"); this.executor = executor; } /** * Get the number of threads in the internally-created thread pool. * *

* This property is ignored if a custom {@link ExecutorService} is configured via * {@link #setExecutorService setExecutorService()}. * *

* Default value is {@value #DEFAULT_THREAD_POOL_SIZE}. * * @return number of threads in thread pool */ public synchronized int getThreadPoolSize() { return this.threadPoolSize; } /** * Set the number of threads in the internally-created thread pool. * *

* This property is ignored if a custom {@link ExecutorService} is configured via * {@link #setExecutorService setExecutorService()}. * *

* Default value is {@value #DEFAULT_THREAD_POOL_SIZE}. * * @param threadPoolSize number of threads in thread pool * @throws IllegalStateException if this instance is already started * @throws IllegalArgumentException if {@code threadPoolSize <= 0} */ public synchronized void setThreadPoolSize(int threadPoolSize) { Preconditions.checkArgument(threadPoolSize > 0, "threadPoolSize <= 0"); Preconditions.checkState(!this.started, "already started"); this.threadPoolSize = threadPoolSize; } // Lifecycle @Override @PostConstruct public synchronized void start() { Preconditions.checkState(this.inner != null, "no inner KVDatabase configured"); if (this.started) return; this.privateExecutor = this.executor == null; if (this.privateExecutor) { this.executor = Executors.newFixedThreadPool(this.threadPoolSize, r -> { final Thread thread = new Thread(r); thread.setName(this.getClass().getSimpleName() + "-" + THREAD_COUNTER.incrementAndGet()); return thread; }); } this.rtt = new MovingAverage(RTT_ESTIMATE_DECAY_FACTOR, this.initialRttEstimate); try { this.inner.start(); this.started = true; } finally { if (!this.started) this.shutdown(); } } @Override @PreDestroy public synchronized void stop() { if (!this.started) return; this.shutdown(); this.started = false; } private synchronized void shutdown() { if (this.executor != null) { if (this.privateExecutor) this.executor.shutdown(); this.executor = null; } this.inner.stop(); } // Transactions @Override public CachingKVTransaction createTransaction() { return this.createTransaction(this.inner::createTransaction); } @Override public CachingKVTransaction createTransaction(Map options) { return this.createTransaction(() -> this.inner.createTransaction(options)); } protected synchronized CachingKVTransaction createTransaction(Supplier innerTxCreator) { Preconditions.checkState(this.started, "not started"); return new CachingKVTransaction(this, innerTxCreator.get(), this.executor, (long)this.rtt.get()); } // RTT estimate /** * Get the current round trip time estimate. * * @return current RTT estimate in nanoseconds * @throws IllegalStateException if this instance has never {@link #start}ed */ public synchronized double getRttEstimate() { Preconditions.checkState(this.rtt != null, "instance has never started"); return this.rtt.get(); } synchronized void updateRttEstimate(double rtt) { this.rtt.add(rtt); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy