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

Valery Kharseko
10 hours ago 0793e27bbc9cb1affc6fefcbb3c5aed924b5144e
[#1078] Stop the MakeLDIF generator when a template import ends before its reader (#1082)
3 files modified
1 files added
185 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java 2 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/types/LDIFImportConfig.java 10 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java 53 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/types/LDIFImportConfigTestCase.java 120 ●●●●● patch | view | raw | blame | history
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));
  }
}