From 3847676be90a0b5b53d25f1a85ac37a038574d22 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 04 Aug 2026 08:33:38 +0000
Subject: [PATCH] [#819] Release the log when a changelog DB constructor cannot read its limits (#833)
---
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileReplicaDB.java | 12 +++
opendj-server-legacy/src/test/java/org/opends/server/replication/server/changelog/file/FileReplicaDBTest.java | 110 ++++++++++++++++++++++++++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangeNumberIndexDB.java | 21 +++++-
3 files changed, 137 insertions(+), 6 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangeNumberIndexDB.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangeNumberIndexDB.java
index 642c29d..fb69d03 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangeNumberIndexDB.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangeNumberIndexDB.java
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.replication.server.changelog.file;
@@ -121,11 +122,21 @@
{
this.changelogDB = changelogDB;
log = replicationEnv.getOrCreateCNIndexDB();
- final ChangeNumberIndexRecord newestRecord = readLastRecord();
- newestChangeNumber = getChangeNumber(newestRecord);
- // initialization of the lastGeneratedChangeNumber from the DB content
- // if DB is empty => last record does not exist => default to 0
- lastGeneratedChangeNumber = new AtomicLong(newestChangeNumber);
+ try
+ {
+ final ChangeNumberIndexRecord newestRecord = readLastRecord();
+ newestChangeNumber = getChangeNumber(newestRecord);
+ // initialization of the lastGeneratedChangeNumber from the DB content
+ // if DB is empty => last record does not exist => default to 0
+ lastGeneratedChangeNumber = new AtomicLong(newestChangeNumber);
+ }
+ catch (ChangelogException | RuntimeException e)
+ {
+ // This instance never escapes the failed constructor, so nobody could ever call shutdown()
+ // to release the reference taken on the log by getOrCreateCNIndexDB().
+ log.close();
+ throw e;
+ }
// Monitoring registration
DirectoryServer.deregisterMonitorProvider(dbMonitor);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileReplicaDB.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileReplicaDB.java
index ac4ec81..0dd267e 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileReplicaDB.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileReplicaDB.java
@@ -115,7 +115,17 @@
this.replicationServer = replicationServer;
this.replicationEnv = replicationEnv;
this.log = createLog(replicationEnv, cryptoSuite);
- this.csnLimits = new CSNLimits(readOldestCSN(), readNewestCSN());
+ try
+ {
+ this.csnLimits = new CSNLimits(readOldestCSN(), readNewestCSN());
+ }
+ catch (ChangelogException | RuntimeException e)
+ {
+ // This instance never escapes the failed constructor, so nobody could ever call shutdown()
+ // to release the reference taken on the log by createLog().
+ log.close();
+ throw e;
+ }
DirectoryServer.deregisterMonitorProvider(dbMonitor);
DirectoryServer.registerMonitorProvider(dbMonitor);
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/server/changelog/file/FileReplicaDBTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/server/changelog/file/FileReplicaDBTest.java
index 17be42f..5e3ce5e 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/server/changelog/file/FileReplicaDBTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/server/changelog/file/FileReplicaDBTest.java
@@ -12,10 +12,12 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.replication.server.changelog.file;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -23,6 +25,7 @@
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.util.time.TimeService;
import org.opends.server.TestCaseUtils;
@@ -359,6 +362,113 @@
}
}
+ /**
+ * Reproduces https://github.com/OpenIdentityPlatform/OpenDJ/issues/819: a constructor which
+ * cannot read the CSN limits must release the log it just opened, otherwise the Log instance
+ * stays pinned in the JVM-wide log cache together with its file handles.
+ */
+ @Test
+ public void testFailedConstructorReleasesLog() throws Exception
+ {
+ ReplicationServer replicationServer = null;
+ File testRoot = null;
+ ReplicationEnvironment dbEnv = null;
+ FileReplicaDB replicaDB = null;
+ try
+ {
+ TestCaseUtils.startServer();
+ replicationServer = configureReplicationServer(100000, 10);
+ testRoot = createCleanDir();
+ dbEnv = new ReplicationEnvironment(testRoot.getPath(), replicationServer, TimeService.SYSTEM);
+ replicaDB = new FileReplicaDB(1, TEST_ROOT_DN, replicationServer, createCryptoSuite(false), dbEnv);
+
+ final CSN[] csns = generateCSNs(1, 0, 2);
+ replicaDB.add(new DeleteMsg(TEST_ROOT_DN, csns[0], "uid"));
+ replicaDB.add(new DeleteMsg(TEST_ROOT_DN, csns[1], "uid"));
+ waitChangesArePersisted(replicaDB, 2);
+ replicaDB.shutdown();
+ replicaDB = null;
+
+ // A rotated log file is opened without validation (its bounds come from the file name),
+ // so reading the oldest CSN is the first operation touching its corrupted content.
+ final File logDirectory = findReplicaLogDirectory(testRoot);
+ assertNotNull(logDirectory, "could not find the replica DB log directory under " + testRoot);
+ final File corruptedLogFile = writeCorruptedReadOnlyLogFile(logDirectory);
+ try
+ {
+ replicaDB = new FileReplicaDB(1, TEST_ROOT_DN, replicationServer, createCryptoSuite(false), dbEnv);
+ org.testng.Assert.fail("Expected the constructor to fail on the corrupted log file " + corruptedLogFile);
+ }
+ catch (ChangelogException expected)
+ {
+ debugInfo("testFailedConstructorReleasesLog", "constructor failed as expected: " + expected.getMessage());
+ }
+
+ // Reopening once the corrupted file is gone must succeed: if the failed constructor leaked
+ // its reference, the log cache returns the stale Log instance which still contains the
+ // corrupted log file, and this constructor fails although the directory is sane again.
+ assertTrue(corruptedLogFile.delete());
+ replicaDB = new FileReplicaDB(1, TEST_ROOT_DN, replicationServer, createCryptoSuite(false), dbEnv);
+ assertLimits(replicaDB, csns[0], csns[1]);
+ }
+ finally
+ {
+ shutdown(replicaDB);
+ if (dbEnv != null)
+ {
+ dbEnv.shutdown();
+ }
+ remove(replicationServer);
+ TestCaseUtils.deleteDirectory(testRoot);
+ }
+ }
+
+ /** Returns the directory holding the log files of the single replica DB under the provided directory. */
+ private File findReplicaLogDirectory(File directory)
+ {
+ if (new File(directory, Log.HEAD_LOG_FILE_NAME).isFile())
+ {
+ return directory;
+ }
+ final File[] children = directory.listFiles();
+ if (children != null)
+ {
+ for (File child : children)
+ {
+ if (child.isDirectory())
+ {
+ final File found = findReplicaLogDirectory(child);
+ if (found != null)
+ {
+ return found;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Writes a log file named like a rotated log file but holding a record which cannot be decoded:
+ * a record length followed by garbage instead of an update message.
+ */
+ private File writeCorruptedReadOnlyLogFile(File logDirectory) throws Exception
+ {
+ final String key = new CSN(1, 1, 1).toString();
+ final File file = new File(logDirectory, key + "_" + key + ".log");
+ final ByteStringBuilder record = new ByteStringBuilder();
+ record.appendInt(8);
+ for (int i = 0; i < 8; i++)
+ {
+ record.appendByte(0xFF);
+ }
+ try (FileOutputStream output = new FileOutputStream(file))
+ {
+ output.write(record.toByteArray());
+ }
+ return file;
+ }
+
private void assertNextCSN(FileReplicaDB replicaDB, final CSN startCSN,
final PositionStrategy positionStrategy, final CSN expectedCSN)
throws ChangelogException
--
Gitblit v1.10.0