/* * Copyright © 2016-2025 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.lmdbjava; import static jnr.ffi.Memory.allocateDirect; import static jnr.ffi.NativeType.ADDRESS; import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.Library.LIB; import static org.lmdbjava.Library.RUNTIME; import static org.lmdbjava.ResultCodeMapper.checkRc; import static org.lmdbjava.Txn.State.DONE; import static org.lmdbjava.Txn.State.READY; import static org.lmdbjava.Txn.State.RELEASED; import static org.lmdbjava.Txn.State.RESET; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; import java.util.Objects; import jnr.ffi.Pointer; /** * LMDB transaction. * * @param buffer type */ public final class Txn implements AutoCloseable { private final KeyVal keyVal; private final Txn parent; private final BufferProxy proxy; private final Pointer ptr; private final boolean readOnly; private final Env env; private State state; Txn(final Env env, final Txn parent, final BufferProxy proxy, final TxnFlagSet flags) { if (SHOULD_CHECK) { Objects.requireNonNull(flags); } this.proxy = proxy; this.keyVal = proxy.keyVal(); this.readOnly = flags.isSet(MDB_RDONLY_TXN); if (env.isReadOnly() && !this.readOnly) { throw new EnvIsReadOnly(); } this.env = env; this.parent = parent; if (parent != null && parent.isReadOnly() != this.readOnly) { throw new IncompatibleParent(); } final Pointer txnPtr = allocateDirect(RUNTIME, ADDRESS); final Pointer txnParentPtr = parent == null ? null : parent.ptr; checkRc(LIB.mdb_txn_begin(env.pointer(), txnParentPtr, flags.getMask(), txnPtr)); ptr = txnPtr.getPointer(0); state = READY; } /** Aborts this transaction. */ public void abort() { if (SHOULD_CHECK) { env.checkNotClosed(); } checkReady(); state = DONE; LIB.mdb_txn_abort(ptr); } /** * Closes this transaction by aborting if not already committed. * *

Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for * each read-only buffer (ie the key and value). */ @Override public void close() { if (SHOULD_CHECK) { env.checkNotClosed(); } if (state == RELEASED) { return; } if (state == READY) { LIB.mdb_txn_abort(ptr); } keyVal.close(); state = RELEASED; } /** Commits this transaction. */ public void commit() { if (SHOULD_CHECK) { env.checkNotClosed(); } checkReady(); state = DONE; checkRc(LIB.mdb_txn_commit(ptr)); } /** * Return the transaction's ID. * * @return A transaction ID, valid if input is an active transaction */ public long getId() { if (SHOULD_CHECK) { env.checkNotClosed(); } return LIB.mdb_txn_id(ptr); } /** * Obtains this transaction's parent. * * @return the parent transaction (may be null) */ public Txn getParent() { return parent; } /** * Whether this transaction is read-only. * * @return if read-only */ public boolean isReadOnly() { return readOnly; } /** * Fetch the buffer which holds a read-only view of the LMDI allocated memory. Any use of this * buffer must comply with the standard LMDB C "mdb_get" contract (ie do not modify, do not * attempt to release the memory, do not use once the transaction or cursor closes, do not use * after a write etc). * * @return the key buffer (never null) */ public T key() { return keyVal.key(); } /** Renews a read-only transaction previously released by {@link #reset()}. */ public void renew() { if (SHOULD_CHECK) { env.checkNotClosed(); } if (state != RESET) { throw new NotResetException(); } state = DONE; checkRc(LIB.mdb_txn_renew(ptr)); state = READY; } /** * Aborts this read-only transaction and resets the transaction handle, so it can be reused upon * calling {@link #renew()}. */ public void reset() { if (SHOULD_CHECK) { env.checkNotClosed(); } checkReadOnly(); if (state != READY && state != DONE) { throw new ResetException(); } state = RESET; LIB.mdb_txn_reset(ptr); } /** * Fetch the buffer which holds a read-only view of the LMDI allocated memory. Any use of this * buffer must comply with the standard LMDB C "mdb_get" contract (ie do not modify, do not * attempt to release the memory, do not use once the transaction or cursor closes, do not use * after a write etc). * * @return the value buffer (never null) */ public T val() { return keyVal.val(); } void checkReadOnly() { if (!readOnly) { throw new ReadOnlyRequiredException(); } } void checkReady() { if (state != READY) { throw new NotReadyException(); } } void checkWritesAllowed() { if (readOnly) { throw new ReadWriteRequiredException(); } } /** * Return the state of the transaction. * * @return the state */ State getState() { return state; } KeyVal kv() { return keyVal; } KeyVal newKeyVal() { return proxy.keyVal(); } Pointer pointer() { return ptr; } /** Transaction must abort, has a child, or is invalid. */ public static final class BadException extends LmdbNativeException { static final int MDB_BAD_TXN = -30_782; private static final long serialVersionUID = 1L; BadException() { super(MDB_BAD_TXN, "Transaction must abort, has a child, or is invalid"); } } /** Invalid reuse of reader locktable slot. */ public static final class BadReaderLockException extends LmdbNativeException { static final int MDB_BAD_RSLOT = -30_783; private static final long serialVersionUID = 1L; BadReaderLockException() { super(MDB_BAD_RSLOT, "Invalid reuse of reader locktable slot"); } } /** The proposed R-W transaction is incompatible with a R-O Env. */ public static class EnvIsReadOnly extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public EnvIsReadOnly() { super("Read-write Txn incompatible with read-only Env"); } } /** The proposed transaction is incompatible with its parent transaction. */ public static class IncompatibleParent extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public IncompatibleParent() { super("Transaction incompatible with its parent transaction"); } } /** Transaction is not in a READY state. */ public static final class NotReadyException extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public NotReadyException() { super("Transaction is not in ready state"); } } /** The current transaction has not been reset. */ public static class NotResetException extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public NotResetException() { super("Transaction has not been reset"); } } /** The current transaction is not a read-only transaction. */ public static class ReadOnlyRequiredException extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public ReadOnlyRequiredException() { super("Not a read-only transaction"); } } /** The current transaction is not a read-write transaction. */ public static class ReadWriteRequiredException extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public ReadWriteRequiredException() { super("Not a read-write transaction"); } } /** The current transaction has already been reset. */ public static class ResetException extends LmdbException { private static final long serialVersionUID = 1L; /** Creates a new instance. */ public ResetException() { super("Transaction has already been reset"); } } /** Transaction has too many dirty pages. */ public static final class TxFullException extends LmdbNativeException { static final int MDB_TXN_FULL = -30_788; private static final long serialVersionUID = 1L; TxFullException() { super(MDB_TXN_FULL, "Transaction has too many dirty pages"); } } /** Transaction states. */ enum State { READY, DONE, RESET, RELEASED } }