From 97dbf50dd56ee7640411ac61e6aa288f208a4375 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 01 Sep 2026 08:54:30 +0000
Subject: [PATCH] [#874] Grant the offline tools a read-only JDBC transaction instead of refusing it (#880)
---
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java | 81 ++++++++++++++++++++++++++++++++++++++++
1 files changed, 81 insertions(+), 0 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
index 9d7cdac..22e8ea1 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java
@@ -22,6 +22,7 @@
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
+import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.TreeName;
@@ -320,6 +321,86 @@
}
}
+ /**
+ * A storage opened READ_ONLY must still hand out the write transaction {@code RootContainer.open()} asks for
+ * there - otherwise the offline export-ldif, verify-index and backendstat fail before reading anything - and
+ * that transaction must serve exactly what the open needs and nothing more: opening an existing tree, reads,
+ * cursors and record counts, while every mutation, including a delete through a cursor it opened, is
+ * refused (#874).
+ */
+ @Test
+ public void testReadOnlyTransactionReadsButRefusesWrites() throws Exception {
+ final JDBCStorage storage = new JDBCStorage(createBackendCfg(), null);
+ final TreeName tree = new TreeName("testReadOnlyTransaction", "tree");
+ final TreeName absent = new TreeName("testReadOnlyTransaction", "absent");
+ try {
+ storage.open(AccessMode.READ_WRITE);
+ storage.write(new WriteOperation() {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception {
+ txn.openTree(tree, true);
+ txn.put(tree, key(0), value(0));
+ txn.put(tree, key(1), value(1));
+ }
+ });
+ storage.close();
+
+ storage.open(AccessMode.READ_ONLY);
+ storage.write(new WriteOperation() {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception {
+ // what RootContainer.open() does through this transaction in read-only mode
+ txn.openTree(tree, false);
+ assertEquals(txn.read(tree, key(0)), value(0));
+ assertEquals(txn.getRecordCount(tree), 2);
+ try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(tree)) {
+ assertTrue(cursor.next());
+ assertEquals(cursor.getKey(), key(0));
+ try {
+ cursor.delete();
+ fail("delete() through a cursor of a read-only transaction must fail");
+ } catch (UnsupportedOperationException expected) {}
+ }
+
+ assertReadOnly("openTree(createOnDemand)", () -> txn.openTree(absent, true));
+ assertReadOnly("put", () -> txn.put(tree, key(2), value(2)));
+ assertReadOnly("update", () -> txn.update(tree, key(0), old -> value(3)));
+ assertReadOnly("delete", () -> txn.delete(tree, key(0)));
+ assertReadOnly("deleteTree", () -> txn.deleteTree(tree));
+ }
+ });
+
+ // nothing above reached the database
+ storage.close();
+ storage.open(AccessMode.READ_WRITE);
+ storage.read(new ReadOperation<Void>() {
+ @Override
+ public Void run(ReadableTransaction txn) throws Exception {
+ assertEquals(txn.getRecordCount(tree), 2);
+ assertEquals(txn.read(tree, key(0)), value(0));
+ return null;
+ }
+ });
+ } finally {
+ try {
+ storage.write(new WriteOperation() {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception {
+ txn.deleteTree(tree);
+ }
+ });
+ } catch (Exception ignored) {}
+ storage.close();
+ }
+ }
+
+ private static void assertReadOnly(String operation, Runnable mutation) {
+ try {
+ mutation.run();
+ fail(operation + " must fail on a read-only storage");
+ } catch (ReadOnlyStorageException expected) {}
+ }
+
/** Buffer-served repositioning relies on the database collating keys in unsigned byte order. */
@Test
public void testCursorKeyOrderIsUnsigned() throws Exception {
--
Gitblit v1.10.0