From 2c443ee5664c0307d7b7f50491fa6130a83faef5 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 17 Jul 2026 05:43:08 +0000
Subject: [PATCH] [#708] Fix OutOfMemory during replication initialize with JDBC backend (#711)

---
 opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/TestCase.java |  156 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 153 insertions(+), 3 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 efcc1a8..03316e2 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
@@ -11,23 +11,43 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
- * Copyright 2024-2025 3A Systems, LLC.
+ * Copyright 2024-2026 3A Systems, LLC.
  */
 package org.opends.server.backends.jdbc;
 
+import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.server.config.server.JDBCBackendCfg;
 import org.opends.server.backends.pluggable.PluggableBackendImplTestCase;
+import org.opends.server.backends.pluggable.spi.AccessMode;
+import org.opends.server.backends.pluggable.spi.Cursor;
+import org.opends.server.backends.pluggable.spi.ReadOperation;
+import org.opends.server.backends.pluggable.spi.ReadableTransaction;
+import org.opends.server.backends.pluggable.spi.TreeName;
+import org.opends.server.backends.pluggable.spi.WriteOperation;
+import org.opends.server.backends.pluggable.spi.WriteableTransaction;
 import org.testcontainers.DockerClientFactory;
 import org.testcontainers.containers.JdbcDatabaseContainer;
 import org.testng.SkipException;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
 
 import java.sql.Connection;
 import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
 
 import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
 import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 
 
@@ -50,14 +70,35 @@
 				throw new SkipException(getContainerDockerCommand());
 			}
 		}
-		try(Connection ignored = DriverManager.getConnection(createBackendCfg().getDBDirectory())){
-
+		try(Connection con = DriverManager.getConnection(createBackendCfg().getDBDirectory())){
+			dropStaleTrees(con);
 		} catch (Exception e) {
 			throw new SkipException(getContainerDockerCommand());
 		}
 		super.setUp();
 	}
 
+	/**
+	 * Backend test classes sharing one database map the same tree names to the same tables,
+	 * so a previous run may leave trees behind — including entries encrypted with a lost cipher key.
+	 */
+	static void dropStaleTrees(Connection con) throws SQLException {
+		final List<String> stale = new ArrayList<>();
+		try (final ResultSet rs = con.getMetaData().getTables(null, null, null, new String[]{"TABLE"})) {
+			while (rs.next()) {
+				final String name = rs.getString("TABLE_NAME");
+				if (name.toLowerCase().startsWith("opendj_")) {
+					stale.add(name);
+				}
+			}
+		}
+		try (final Statement st = con.createStatement()) {
+			for (final String name : stale) {
+				st.execute("drop table " + name);
+			}
+		}
+	}
+
 	@Override
 	protected Backend createBackend() {
 		return new Backend();
@@ -87,4 +128,113 @@
 	protected abstract String getBackendId();
 
 	protected abstract String getJdbcUrl();
+
+	private static ByteString key(int i) {
+		return ByteString.valueOfUtf8(String.format("key%02d", i));
+	}
+
+	private static ByteString value(int i) {
+		return ByteString.valueOfUtf8("value" + i);
+	}
+
+	/** Cursor operations must keep working when the tree spans several "fetchsize" batches. */
+	@Test
+	public void testCursorCrossesFetchSizeBatches() throws Exception {
+		System.setProperty("org.openidentityplatform.opendj.jdbc.fetchsize", "2");
+		final Storage storage = new Storage(createBackendCfg(), null);
+		final TreeName tree = new TreeName("testCursorBatch", "tree");
+		try {
+			storage.open(AccessMode.READ_WRITE);
+			storage.write(new WriteOperation() {
+				@Override
+				public void run(WriteableTransaction txn) throws Exception {
+					txn.openTree(tree, true);
+					for (int i = 0; i < 7; i++) {
+						txn.put(tree, key(i), value(i));
+					}
+				}
+			});
+			storage.read(new ReadOperation<Void>() {
+				@Override
+				public Void run(ReadableTransaction txn) throws Exception {
+					try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(tree)) {
+						for (int i = 0; i < 7; i++) {
+							assertTrue(cursor.next(), "next() at " + i);
+							assertEquals(cursor.getKey(), key(i));
+							assertEquals(cursor.getValue(), value(i));
+						}
+						assertFalse(cursor.next());
+						assertFalse(cursor.isDefined());
+						try {
+							cursor.getKey();
+							fail("getKey() on undefined cursor must fail");
+						} catch (NoSuchElementException expected) {}
+
+						assertTrue(cursor.positionToKeyOrNext(key(3)));
+						assertEquals(cursor.getKey(), key(3));
+						assertTrue(cursor.positionToKeyOrNext(ByteString.valueOfUtf8("key031")));
+						assertEquals(cursor.getKey(), key(4));
+						assertTrue(cursor.next());
+						assertEquals(cursor.getKey(), key(5));
+						assertTrue(cursor.next());
+						assertEquals(cursor.getKey(), key(6));
+						assertFalse(cursor.next());
+						assertFalse(cursor.positionToKeyOrNext(ByteString.valueOfUtf8("z")));
+						assertFalse(cursor.isDefined());
+
+						assertTrue(cursor.positionToKey(key(5)));
+						assertEquals(cursor.getValue(), value(5));
+						assertTrue(cursor.next());
+						assertEquals(cursor.getKey(), key(6));
+						assertFalse(cursor.positionToKey(ByteString.valueOfUtf8("key99")));
+						assertFalse(cursor.isDefined());
+
+						assertTrue(cursor.positionToIndex(0));
+						assertEquals(cursor.getKey(), key(0));
+						assertTrue(cursor.positionToIndex(5));
+						assertEquals(cursor.getKey(), key(5));
+						assertTrue(cursor.next());
+						assertEquals(cursor.getKey(), key(6));
+						assertFalse(cursor.positionToIndex(7));
+						assertFalse(cursor.positionToIndex(-1));
+
+						assertTrue(cursor.positionToLastKey());
+						assertEquals(cursor.getKey(), key(6));
+						assertFalse(cursor.next());
+
+						assertTrue(cursor.positionToKey(key(0)));
+						try {
+							cursor.delete();
+							fail("delete() on read-only cursor must fail");
+						} catch (UnsupportedOperationException expected) {}
+					}
+					return null;
+				}
+			});
+			storage.write(new WriteOperation() {
+				@Override
+				public void run(WriteableTransaction txn) throws Exception {
+					try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(tree)) {
+						assertTrue(cursor.positionToKey(key(3)));
+						cursor.delete();
+						assertTrue(cursor.next());
+						assertEquals(cursor.getKey(), key(4));
+					}
+					assertNull(txn.read(tree, key(3)));
+					assertEquals(txn.getRecordCount(tree), 6);
+				}
+			});
+		} finally {
+			System.clearProperty("org.openidentityplatform.opendj.jdbc.fetchsize");
+			try {
+				storage.write(new WriteOperation() {
+					@Override
+					public void run(WriteableTransaction txn) throws Exception {
+						txn.deleteTree(tree);
+					}
+				});
+			} catch (Exception ignored) {}
+			storage.close();
+		}
+	}
 }

--
Gitblit v1.10.0