| | |
| | | * 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; |
| | | |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 |