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

io.permazen.kv.lmdb.LMDBKVDatabase Maven / Gradle / Ivy


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

package io.permazen.kv.lmdb;

import com.google.common.base.Preconditions;

import io.permazen.kv.KVDatabase;

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

import java.io.File;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;

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

import org.lmdbjava.Dbi;
import org.lmdbjava.DbiFlags;
import org.lmdbjava.Env;
import org.lmdbjava.EnvFlags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link KVDatabase} view of an LMDB database.
 *
 * 

* A {@linkplain #setDirectory database directory} is the only required configuration property. * Instances may be stopped and (re)started multiple times. * * @param buffer type */ @ThreadSafe public abstract class LMDBKVDatabase implements KVDatabase { // Lock order: (1) LMDBKVTransaction, (2) LMDBKVDatabase /** * Default LMDB database name ({@value #DEFAULT_DATABASE_NAME}). * * @see #setDatabaseName */ public static final String DEFAULT_DATABASE_NAME = "permazen"; protected final Logger log = LoggerFactory.getLogger(this.getClass()); @GuardedBy("this") private File directory; @GuardedBy("this") private String databaseName = DEFAULT_DATABASE_NAME; @GuardedBy("this") private Env.Builder builder; @GuardedBy("this") private EnumSet flags = EnumSet.noneOf(EnvFlags.class); @GuardedBy("this") private Env env; @GuardedBy("this") private Dbi db; @GuardedBy("this") private final HashSet> openTx = new HashSet<>(); // Constructors /** * Constructor. * * @param defaultBuilder the default builder * @throws IllegalArgumentException if {@code defaultBuilder} is null */ protected LMDBKVDatabase(Env.Builder defaultBuilder) { Preconditions.checkArgument(defaultBuilder != null, "null defaultBuilder"); this.builder = defaultBuilder; } // Configuration /** * Get the filesystem directory containing the database. * * @return database directory */ public synchronized File getDirectory() { return this.directory; } /** * Configure the filesystem directory containing the database. Required property. * * @param directory database directory * @throws IllegalStateException if this instance is already {@link #start}ed */ public synchronized void setDirectory(File directory) { Preconditions.checkState(this.env == null, "already started"); this.directory = directory; } /** * Get the LMDB database name to use. * * @return configured database name */ public synchronized String getDatabaseName() { return this.databaseName; } /** * Set the LMDB database name to use. * *

* Default value is {@link #DEFAULT_DATABASE_NAME}. * * @param databaseName database name * @throws IllegalStateException if this instance is already {@link #start}ed * @throws IllegalArgumentException if {@code databaseName} is null */ public synchronized void setDatabaseName(String databaseName) { Preconditions.checkState(this.env == null, "already started"); this.databaseName = databaseName; } /** * Get the {@link EnvFlags} that will be used when opening the associated {@link Env}. * * @return environment flags */ public synchronized EnumSet getEnvFlags() { return this.flags.clone(); } /** * Set the flags to be used when opening the associated {@link Env}. * *

* Default is empty set. * * @param flags environment flags * @throws IllegalStateException if this instance is already {@link #start}ed * @throws IllegalArgumentException if {@code flags} is null */ public synchronized void setEnvBuilder(EnumSet flags) { Preconditions.checkArgument(flags != null, "null flags"); Preconditions.checkState(this.env == null, "already started"); this.flags = flags.clone(); } /** * Get the {@link Env.Builder} that will be used when opening the associated {@link Env}. * * @return environment builder */ public synchronized Env.Builder getEnvBuilder() { return this.builder; } /** * Set a custom builder to be used when opening the associated {@link Env}. * * @param builder environment builder * @throws IllegalStateException if this instance is already {@link #start}ed * @throws IllegalArgumentException if {@code builder} is null */ public synchronized void setEnvBuilder(Env.Builder builder) { Preconditions.checkArgument(builder != null, "null builder"); Preconditions.checkState(this.env == null, "already started"); this.builder = builder; } // Accessors /** * Get the {@link Env} associated with this instance. * * @return the associated {@link Env} * @throws IllegalStateException if this instance is not {@link #start}ed */ public synchronized Env getEnv() { Preconditions.checkState(this.env != null, "not started"); return this.env; } // Lifecycle @Override @PostConstruct public synchronized void start() { // Already started? if (this.env != null) return; this.log.info("starting {}", this); // Check configuration Preconditions.checkState(this.directory != null, "no directory configured"); if (!this.directory.exists() && !this.directory.mkdirs()) throw new RuntimeException(String.format("failed to create directory %s", this.directory)); if (!this.directory.isDirectory()) throw new RuntimeException(String.format("file %s is not a directory", this.directory)); // Open environment if (this.log.isDebugEnabled()) this.log.debug("starting LMDB database {}", this); this.env = this.builder.open(this.directory, this.flags.toArray(new EnvFlags[this.flags.size()])); // Open database boolean ok = false; try { this.db = this.env.openDbi(this.databaseName, DbiFlags.MDB_CREATE); ok = true; } finally { if (!ok) this.env.close(); } } @Override @PreDestroy public void stop() { final ArrayList> cleanups = new ArrayList<>(); while (true) { // Close any remaining transactions - otherwise this.env.close() will throw an exception - but not while locked for (LMDBKVTransaction tx : cleanups) tx.rollback(); // Shutdown when no open transactions remain synchronized (this) { // Already stopped? if (this.env == null) return; // Any remaining open transactions? If so close them and restart cleanups.clear(); cleanups.addAll(this.openTx); this.openTx.clear(); if (!cleanups.isEmpty()) continue; // Shut down LMDB database this.log.info("stopping {}", this); try { if (this.log.isDebugEnabled()) this.log.debug("stopping LMDB database {}", this); //this.db.close(); // we're not supposed to use this this.env.close(); } catch (Throwable e) { this.log.error("caught exception closing database during shutdown (ignoring)", e); } this.env = null; return; } } } synchronized void transactionClosed(LMDBKVTransaction tx) { this.openTx.remove(tx); } // KVDatabase @Override public synchronized LMDBKVTransaction createTransaction() { return this.createTransaction(null); } @Override public synchronized LMDBKVTransaction createTransaction(Map options) { Preconditions.checkState(this.env != null, "not started"); final LMDBKVTransaction tx = this.doCreateTransaction(this.env, this.db, options); this.openTx.add(tx); return tx; } protected abstract LMDBKVTransaction doCreateTransaction(Env env, Dbi db, Map options); // Object /** * Finalize this instance. Invokes {@link #stop} to close any unclosed iterators. */ @Override @SuppressWarnings("deprecation") protected void finalize() throws Throwable { try { if (this.env != null) this.log.warn(this + " leaked without invoking stop()"); this.stop(); } finally { super.finalize(); } } @Override public String toString() { return this.getClass().getSimpleName() + "[dir=" + this.directory + ",database=\"" + this.databaseName + "\"" + ",env=" + this.env + "]"; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy