mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
12 hours ago 3847676be90a0b5b53d25f1a85ac37a038574d22
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);
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);
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