Skip to content

Commit bdddb3b

Browse files
authored
[#692] Restore partial import semantics for include/exclude branches (#689)
1 parent c27ff32 commit bdddb3b

3 files changed

Lines changed: 238 additions & 3 deletions

File tree

opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/OnDiskMergeImporter.java

Lines changed: 234 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*
1414
* Portions Copyright 2014 The Apache Software Foundation
1515
* Copyright 2015-2016 ForgeRock AS.
16-
* Portions Copyright 2023-2025 3A Systems, LLC
16+
* Portions Copyright 2023-2026 3A Systems, LLC
1717
*/
1818
package org.opends.server.backends.pluggable;
1919

@@ -102,6 +102,7 @@
102102
import org.opends.server.backends.pluggable.DN2ID.TreeVisitor;
103103
import org.opends.server.backends.pluggable.ImportLDIFReader.EntryInformation;
104104
import org.opends.server.backends.pluggable.OnDiskMergeImporter.BufferPool.MemoryBuffer;
105+
import org.opends.server.backends.pluggable.spi.AccessMode;
105106
import org.opends.server.backends.pluggable.spi.Cursor;
106107
import org.opends.server.backends.pluggable.spi.Importer;
107108
import org.opends.server.backends.pluggable.spi.ReadOperation;
@@ -118,6 +119,8 @@
118119
import org.opends.server.types.DirectoryException;
119120
import org.opends.server.types.Entry;
120121
import org.opends.server.types.InitializationException;
122+
import org.opends.server.types.ExistingFileBehavior;
123+
import org.opends.server.types.LDIFExportConfig;
121124
import org.opends.server.types.LDIFImportConfig;
122125
import org.opends.server.types.LDIFImportResult;
123126

@@ -156,6 +159,8 @@ static class StrategyImpl implements ImportStrategy
156159

157160
private static final String PHASE1_IMPORTER_THREAD_NAME = "PHASE1-IMPORTER-%d";
158161

162+
private static final String PHASE1_MIGRATOR_THREAD_NAME = "PHASE1-MIGRATOR-%d";
163+
159164
private static final String PHASE2_IMPORTER_THREAD_NAME = "PHASE2-IMPORTER-%d";
160165

161166
private static final String SORTER_THREAD_NAME = "PHASE1-SORTER-%d";
@@ -190,6 +195,23 @@ public LDIFImportResult importLDIF(LDIFImportConfig importConfig)
190195
{
191196
logger.info(NOTE_IMPORT_STARTING, DirectoryServer.getVersionString(), BUILD_ID, REVISION);
192197

198+
// A partial import (include and/or exclude branches) only replaces the
199+
// imported branches: existing entries outside of the imported scope must
200+
// be preserved. Snapshot them into temporary LDIF files before the
201+
// import deletes the entry containers, so that they can be fed through
202+
// the import pipeline together with the user-provided LDIF file.
203+
final List<File> migrationFiles;
204+
try
205+
{
206+
migrationFiles = exportEntriesToPreserve(importConfig);
207+
}
208+
catch (Exception e)
209+
{
210+
throw new ExecutionException(e);
211+
}
212+
213+
try
214+
{
193215
final long startTime = System.currentTimeMillis();
194216
final int maxThreadCount = importConfig.getThreadCount() == 0
195217
? getDefaultNumberOfThread()
@@ -212,7 +234,34 @@ public LDIFImportResult importLDIF(LDIFImportConfig importConfig)
212234
final AbstractTwoPhaseImportStrategy importStrategy =
213235
new ExternalSortAndImportStrategy(entryContainers, dbStorage, tempDir, bufferPool, sorter);
214236
importer = new OnDiskMergeImporter(PHASE2_IMPORTER_THREAD_NAME, importStrategy);
215-
importer.doImport(source);
237+
if (migrationFiles.isEmpty())
238+
{
239+
importer.doImport(source);
240+
}
241+
else
242+
{
243+
final List<String> migrationPaths = new ArrayList<>(migrationFiles.size());
244+
for (File migrationFile : migrationFiles)
245+
{
246+
migrationPaths.add(migrationFile.getAbsolutePath());
247+
}
248+
final LDIFImportConfig migrationConfig = new LDIFImportConfig(migrationPaths);
249+
try
250+
{
251+
// Entries come straight from this backend: no filtering and no
252+
// schema validation must be applied to them.
253+
migrationConfig.setValidateSchema(false);
254+
try (final LDIFReaderSource migrationSource =
255+
new LDIFReaderSource(rootContainer, migrationConfig, PHASE1_MIGRATOR_THREAD_NAME, threadCount))
256+
{
257+
importer.doImport(new CompositeSource(Arrays.asList((Source) migrationSource, source)));
258+
}
259+
}
260+
finally
261+
{
262+
migrationConfig.close();
263+
}
264+
}
216265
}
217266
finally
218267
{
@@ -246,6 +295,150 @@ public LDIFImportResult importLDIF(LDIFImportConfig importConfig)
246295
{
247296
throw new ExecutionException(e);
248297
}
298+
}
299+
finally
300+
{
301+
for (File migrationFile : migrationFiles)
302+
{
303+
migrationFile.delete();
304+
}
305+
}
306+
}
307+
308+
/**
309+
* Exports the entries which must survive a partial import to temporary
310+
* LDIF files, mimicking the migration performed by the previous generation
311+
* importer: when include and/or exclude branches are configured, only the
312+
* imported branches are replaced, so existing entries outside of the
313+
* include branches, as well as existing entries below the exclude branches
314+
* of an imported branch, are re-imported from the snapshot files.
315+
*
316+
* @return the temporary LDIF files containing the entries to preserve
317+
*/
318+
private List<File> exportEntriesToPreserve(LDIFImportConfig importConfig) throws Exception
319+
{
320+
if (importConfig.clearBackend())
321+
{
322+
return Collections.emptyList();
323+
}
324+
final Set<DN> includeBranches = importConfig.getIncludeBranches();
325+
final Set<DN> excludeBranches = importConfig.getExcludeBranches();
326+
if (includeBranches.isEmpty() && excludeBranches.isEmpty())
327+
{
328+
return Collections.emptyList();
329+
}
330+
331+
final List<File> migrationFiles = new ArrayList<>();
332+
boolean storageOpen = false;
333+
try
334+
{
335+
for (EntryContainer entryContainer : rootContainer.getEntryContainers())
336+
{
337+
final DN baseDN = entryContainer.getBaseDN();
338+
if (excludeBranches.contains(baseDN))
339+
{
340+
// This entire base DN is excluded: the import does not touch it.
341+
continue;
342+
}
343+
344+
final List<DN> includesUnderBase = new ArrayList<>();
345+
for (DN includeBranch : includeBranches)
346+
{
347+
if (includeBranch.isSubordinateOrEqualTo(baseDN))
348+
{
349+
includesUnderBase.add(includeBranch);
350+
}
351+
}
352+
if (!includeBranches.isEmpty() && includesUnderBase.isEmpty())
353+
{
354+
// Nothing is imported under this base DN: the import does not touch it.
355+
continue;
356+
}
357+
358+
// Everything under this base DN which is not below an include branch
359+
// is replaced by the import, unless the base DN itself is included.
360+
final boolean wholeBaseReplaced = includesUnderBase.isEmpty() || includesUnderBase.contains(baseDN);
361+
if (!wholeBaseReplaced)
362+
{
363+
if (!storageOpen)
364+
{
365+
rootContainer.getStorage().open(AccessMode.READ_ONLY);
366+
storageOpen = true;
367+
}
368+
logger.info(NOTE_IMPORT_MIGRATION_START, "existing", baseDN);
369+
migrationFiles.add(
370+
exportBranches(Collections.singletonList(baseDN), new ArrayList<>(includesUnderBase)));
371+
}
372+
373+
// Existing entries below exclude branches located inside the
374+
// replaced scope must be preserved as well.
375+
final List<DN> excludedToPreserve = new ArrayList<>();
376+
for (DN excludeBranch : excludeBranches)
377+
{
378+
if (excludeBranch.isSubordinateOrEqualTo(baseDN)
379+
&& (wholeBaseReplaced || isUnderAnyOf(excludeBranch, includesUnderBase)))
380+
{
381+
excludedToPreserve.add(excludeBranch);
382+
}
383+
}
384+
if (!excludedToPreserve.isEmpty())
385+
{
386+
if (!storageOpen)
387+
{
388+
rootContainer.getStorage().open(AccessMode.READ_ONLY);
389+
storageOpen = true;
390+
}
391+
logger.info(NOTE_IMPORT_MIGRATION_START, "excluded", baseDN);
392+
migrationFiles.add(exportBranches(excludedToPreserve, Collections.<DN> emptyList()));
393+
}
394+
}
395+
return migrationFiles;
396+
}
397+
catch (Exception e)
398+
{
399+
for (File migrationFile : migrationFiles)
400+
{
401+
migrationFile.delete();
402+
}
403+
throw e;
404+
}
405+
finally
406+
{
407+
if (storageOpen)
408+
{
409+
rootContainer.getStorage().close();
410+
}
411+
}
412+
}
413+
414+
private static boolean isUnderAnyOf(DN dn, List<DN> branches)
415+
{
416+
for (DN branch : branches)
417+
{
418+
if (dn.isSubordinateOrEqualTo(branch))
419+
{
420+
return true;
421+
}
422+
}
423+
return false;
424+
}
425+
426+
private File exportBranches(List<DN> includeBranches, List<DN> excludeBranches) throws Exception
427+
{
428+
final File migrationFile = File.createTempFile("import-migration-", ".ldif");
429+
final LDIFExportConfig exportConfig =
430+
new LDIFExportConfig(migrationFile.getAbsolutePath(), ExistingFileBehavior.OVERWRITE);
431+
exportConfig.setIncludeBranches(includeBranches);
432+
exportConfig.setExcludeBranches(excludeBranches);
433+
try
434+
{
435+
new ExportJob(exportConfig).exportLDIF(rootContainer);
436+
}
437+
finally
438+
{
439+
exportConfig.close();
440+
}
441+
return migrationFile;
249442
}
250443

251444
private static int getDefaultNumberOfThread()
@@ -644,6 +837,45 @@ interface EntryProcessor
644837
boolean isCancelled();
645838
}
646839

840+
/** Chains {@link Source}s, processing them sequentially. */
841+
private static final class CompositeSource implements Source
842+
{
843+
private final List<Source> sources;
844+
845+
CompositeSource(final List<Source> sources)
846+
{
847+
this.sources = sources;
848+
}
849+
850+
@Override
851+
public void processAllEntries(EntryProcessor processor) throws InterruptedException, ExecutionException
852+
{
853+
for (Source source : sources)
854+
{
855+
source.processAllEntries(processor);
856+
}
857+
}
858+
859+
@Override
860+
public boolean isCancelled()
861+
{
862+
for (Source source : sources)
863+
{
864+
if (source.isCancelled())
865+
{
866+
return true;
867+
}
868+
}
869+
return false;
870+
}
871+
872+
@Override
873+
public void close()
874+
{
875+
// The chained sources are closed by their respective owners.
876+
}
877+
}
878+
647879
/** Extract LDAP {@link Entry}s from an LDIF file. */
648880
private static final class LDIFReaderSource implements Source
649881
{

opendj-server-legacy/src/messages/org/opends/messages/backend.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#
1313
# Copyright 2006-2010 Sun Microsystems, Inc.
1414
# Portions Copyright 2011-2016 ForgeRock AS.
15+
# Portions Copyright 2026 3A Systems, LLC
1516

1617

1718
#
@@ -1104,3 +1105,4 @@ ERR_SERVICE_DISCOVERY_CONFIG_MANAGER_ADD_MECHANISM_611=An error occurred while a
11041105
Service Discovery Mechanism '%s' : %s
11051106
ERR_SERVICE_DISCOVERY_CONFIG_MANAGER_INIT_MECHANISM_614=Service Discovery Mechanism '%s' initialization failed : %s
11061107
ERR_SERVICE_DISCOVERY_CONFIG_MANAGER_LISTENER_615=Registering Service Discovery Manager's listener failed : %s
1108+
NOTE_IMPORT_MIGRATION_START_616=Migrating %s entries for base DN %s so that they are preserved by the partial import

opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2006-2009 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718
package org.opends.server.tasks;
1819

@@ -347,7 +348,7 @@ public Object[][] createBadData() throws Exception
347348
* @param taskEntry The task entry.
348349
* @param expectedState The expected completion state of the task.
349350
*/
350-
@Test(dataProvider = "importexport", groups = "slow")
351+
@Test(dataProvider = "importexport")
351352
public void testImportExport(Entry taskEntry, TaskState expectedState)
352353
throws Exception
353354
{

0 commit comments

Comments
 (0)