opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
@@ -602,6 +602,8 @@ catch (Exception e) { logger.error(ERR_LDIFIMPORT_CANNOT_OPEN_REJECTS_FILE, rejectFile, getExceptionMessage(e)); // No file is open yet, but a template import has already started generating its entries. importConfig.close(); return TaskState.STOPPED_BY_ERROR; } } opendj-server-legacy/src/main/java/org/opends/server/types/LDIFImportConfig.java
@@ -91,6 +91,12 @@ private BufferedWriter skipWriter; /** The input stream to use to read the data to import. */ private InputStream ldifInputStream; /** * The input stream this config created for itself, which it must close: a backend closes it * with the reader, but an import which ends before a backend took that reader leaves it open. * A stream handed to the config belongs to the caller and is not held here. */ private InputStream ownedInputStream; /** The buffer size to use when reading data from the LDIF file. */ private int bufferSize = DEFAULT_BUFFER_SIZE; @@ -205,6 +211,8 @@ public LDIFImportConfig(TemplateFile templateFile) { this(MakeLDIFInputStream.newStartedInputStream(templateFile)); // The generator thread is already running and stops only when this stream is closed. ownedInputStream = ldifInputStream; } @@ -1031,7 +1039,7 @@ @Override public void close() { StaticUtils.close(reader, rejectWriter, skipWriter); StaticUtils.close(reader, ownedInputStream, rejectWriter, skipWriter); } /** opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java
@@ -21,6 +21,9 @@ import java.io.IOException; import java.io.Writer; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import java.util.UUID; import org.forgerock.opendj.ldap.ResultCode; @@ -32,6 +35,7 @@ import org.opends.server.core.AddOperation; import org.opends.server.core.BackendConfigManager; import org.opends.server.core.DirectoryServer; import org.opends.server.tools.makeldif.MakeLDIFInputStream; import org.opends.server.types.Entry; import org.opends.server.types.LDIFImportConfig; import org.forgerock.opendj.ldap.schema.ObjectClass; @@ -536,6 +540,55 @@ assertNull(importConfig.getSkipWriter(), "The skip writer was opened after all"); } /** * A template import which cannot open its reject file must stop the generator thread its config * started when it was built. That failure is the first return after the config exists, and it * returns before the import is announced and before the try whose finally closes the config. */ @Test public void testTemplateImportWhichCannotOpenItsRejectFileStopsItsGenerator() throws Exception { File templateFile = File.createTempFile("import-test", ".template"); try { Files.write(templateFile.toPath(), Arrays.asList(template), StandardCharsets.UTF_8); Entry taskEntry = TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-template-file: " + templateFile.getPath(), // The task stores the path as it is given, and a directory cannot be opened for writing. "ds-task-import-reject-file: " + ldifFile.getParent(), "ds-task-import-overwrite-rejects: TRUE"); testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60); LDIFImportConfig importConfig = importConfigOf(getDoneTask(taskEntry.getName())); assertNotNull(importConfig, "The task never built an import config"); assertNull(importConfig.getRejectWriter(), "The reject writer was opened after all"); Thread generator = generatorOf(importConfig); generator.join(10000); assertFalse(generator.isAlive(), "The generator is still running after the import ended"); } finally { templateFile.delete(); } } /** The thread generating the entries of a template import, which neither class exposes. */ private static Thread generatorOf(LDIFImportConfig importConfig) throws Exception { Field ldifInputStream = LDIFImportConfig.class.getDeclaredField("ldifInputStream"); ldifInputStream.setAccessible(true); Field generatorThread = MakeLDIFInputStream.class.getDeclaredField("generatorThread"); generatorThread.setAccessible(true); return (Thread) generatorThread.get(ldifInputStream.get(importConfig)); } /** The config an import task worked with, which the task keeps to itself. */ private static LDIFImportConfig importConfigOf(Task task) throws Exception { opendj-server-legacy/src/test/java/org/opends/server/types/LDIFImportConfigTestCase.java
New file @@ -0,0 +1,120 @@ /* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2026 3A Systems, LLC. */ package org.opends.server.types; import static org.testng.Assert.*; import java.io.ByteArrayInputStream; import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Random; import org.forgerock.i18n.LocalizableMessage; import org.opends.server.TestCaseUtils; import org.opends.server.core.DirectoryServer; import org.opends.server.tools.makeldif.MakeLDIFInputStream; import org.opends.server.tools.makeldif.TemplateFile; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; /** Tests which resources closing an {@link LDIFImportConfig} releases. */ public class LDIFImportConfigTestCase extends TypesTestCase { /** * Far more entries than the ten the MakeLDIF input stream queues, so that the generator cannot * finish on its own and waits for a reader which never comes. */ private static final String[] TEMPLATE = { "define suffix=dc=example,dc=com", "", "branch: [suffix]", "subordinateTemplate: person:100", "", "template: person", "rdnAttr: uid", "objectClass: top", "objectClass: person", "uid: user.<sequential:0>", "cn: user", "sn: user", "" }; private String resourcePath; @BeforeClass public void setUp() throws Exception { // The template file resolves its resource directory against the server root. TestCaseUtils.startServer(); resourcePath = DirectoryServer.getInstanceRoot() + File.separator + "config" + File.separator + "MakeLDIF"; } /** * A template import which ends before any backend asked for its reader - the reject file cannot * be opened, the backend cannot be locked - must still stop the generator thread the config * started when it was built: nothing else ever closes the stream that thread feeds. */ @Test public void testClosingATemplateConfigWhichWasNeverReadStopsItsGenerator() throws Exception { TemplateFile templateFile = new TemplateFile(resourcePath, new Random(1)); templateFile.parse(TEMPLATE, new ArrayList<LocalizableMessage>()); LDIFImportConfig importConfig = new LDIFImportConfig(templateFile); Thread generator = generatorOf(importConfig); generator.join(1000); assertTrue(generator.isAlive(), "The generator finished on its own, so the template does not show whether closing stops it"); importConfig.close(); generator.join(10000); assertFalse(generator.isAlive(), "The generator is still running after the import config was closed"); } /** * A config which was handed its input stream - a replication domain passes the stream the total * update arrives on - does not own it, and must leave it open for the one who does. */ @Test public void testClosingAConfigLeavesOpenTheStreamItWasHanded() throws Exception { final boolean[] closed = { false }; ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]) { @Override public void close() { closed[0] = true; } }; new LDIFImportConfig(inputStream).close(); assertFalse(closed[0], "The config closed an input stream it did not create"); } /** The thread generating the entries the config was built on, which neither class exposes. */ private static Thread generatorOf(LDIFImportConfig importConfig) throws Exception { Field ldifInputStream = LDIFImportConfig.class.getDeclaredField("ldifInputStream"); ldifInputStream.setAccessible(true); Field generatorThread = MakeLDIFInputStream.class.getDeclaredField("generatorThread"); generatorThread.setAccessible(true); return (Thread) generatorThread.get(ldifInputStream.get(importConfig)); } }