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

Valery Kharseko
2 days ago 1af0a1247d3e2d503a53723dd1d844c90c43ccc6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
/*
 * 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.backends.pluggable;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.opends.server.util.CollectionUtils.newTreeSet;
 
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.BooleanSupplier;
 
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationAddListener;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.config.server.ConfigurationDeleteListener;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.server.config.meta.BackendIndexCfgDefn.IndexType;
import org.forgerock.opendj.server.config.meta.BackendVLVIndexCfgDefn.Scope;
import org.forgerock.opendj.server.config.server.BackendIndexCfg;
import org.forgerock.opendj.server.config.server.BackendVLVIndexCfg;
import org.forgerock.opendj.server.config.server.PDBBackendCfg;
import org.forgerock.opendj.server.config.server.PluggableBackendCfg;
import org.mockito.ArgumentCaptor;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.pdb.PDBStorage;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.StorageInUseException;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.StorageStatus;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.UpdateFunction;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.ServerContext;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.types.RestoreConfig;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
 
import com.persistit.exception.RollbackException;
 
/**
 * Tests that a backend which fails to open gives back everything its opening took - see OpenDJ
 * issue #993.
 * <p>
 * {@link EntryContainer} registers itself and its two configuration managers as listeners of the
 * backend configuration, and every index it opens registers one of its own. Only
 * {@link EntryContainer#close()} takes them off again, and an entry container whose
 * {@link EntryContainer#open} failed is registered nowhere, so nothing will ever call it: the
 * listeners of a backend which is not running answer configuration changes for the life of the JVM.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public class FailedBackendOpenTest extends DirectoryServerTestCase
{
  private static final String BACKEND_ID = "FailedBackendOpenTest";
  private static final DN BASE_DN = DN.valueOf("dc=b993,dc=com");
  /** A second base DN of the same backend, sorting after {@link #BASE_DN}. */
  private static final DN SECOND_BASE_DN = DN.valueOf("dc=b993b,dc=com");
 
  private ServerContext serverContext;
  private AttributeType cnType;
  /** The index configuration the entry container's attribute index registers with. */
  private BackendIndexCfg indexCfg;
  /** The VLV index configuration the entry container's VLV index registers with. */
  private BackendVLVIndexCfg vlvIndexCfg;
 
  @BeforeClass
  public void startServer() throws Exception
  {
    TestCaseUtils.startServer();
    serverContext = TestCaseUtils.getServerContext();
    cnType = serverContext.getSchema().getAttributeType("cn");
  }
 
  /**
   * A VLV index whose filter does not parse fails {@code EntryContainer.open()} with a
   * {@link ConfigException}, after the attribute indexes ahead of it have opened and registered
   * their own listeners. The backend does not open, and nothing it registered may be left behind.
   */
  @Test
  public void aBackendWhichFailsToOpenLeavesNothingRegistered() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    when(vlvIndexCfg.getFilter()).thenReturn("(&(objectClass=*)");
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    // What an index the stored configuration names, and the schema no longer supports, does.
    openExpectingFailure(backend, "the backend was expected not to open with a VLV index whose filter does not parse");
 
    assertThat(stillRegisteredOn(cfg)).isEmpty();
    assertThat(stillRegisteredOn(indexCfg)).isEmpty();
  }
 
  /**
   * A VLV index registers itself as a listener of its configuration from its constructor, and only
   * reaches the entry container's map once it has opened. A failure in between is the one the
   * container has always caught, and the index it is closing is still not one it holds.
   */
  @Test
  public void anIndexWhichFailsToOpenLeavesNoListenerBehind() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    backend.storage.failOpeningTree("/dc=com,dc=b993/vlv.vlv1");
    // What a storage which cannot give the index its tree does.
    openExpectingFailure(backend, "the backend was expected not to open with a VLV index whose tree cannot be opened");
 
    assertThat(stillRegisteredOn(cfg)).isEmpty();
    assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty();
  }
 
  /**
   * The storage a failed open opened is given back along with the listeners.
   * {@code BackendConfigManager} releases the backend's shared lock and never calls
   * {@code closeBackend()} for a backend which did not open, so a volume left open here is one no
   * later attempt to enable that backend can take.
   */
  @Test
  public void aBackendWhichFailsToOpenGivesBackTheStorageItOpened() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    when(vlvIndexCfg.getFilter()).thenReturn("(&(objectClass=*)");
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    // What an index the stored configuration names, and the schema no longer supports, does.
    final int closesByTheFailedOpen = openExpectingFailure(backend,
        "the backend was expected not to open with a VLV index whose filter does not parse");
 
    assertThat(closesByTheFailedOpen).isEqualTo(1);
  }
 
  /**
   * A root container whose storage would not open has nothing of the storage's to give back: a
   * {@code Storage.open()} which threw returns what it took itself, and a storage which never
   * opened is not one to close. What is the root container's own - the listener it registered from
   * its constructor - it gives back all the same.
   */
  @Test
  public void aRootContainerWhichCouldNotOpenTheStorageDoesNotCloseIt() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    backend.storage.refuseToOpen();
    // What a storage whose volume another process holds does.
    final int closesByTheFailedOpen = openExpectingFailure(backend,
        "the backend was expected not to open over a storage whose volume is locked");
 
    assertThat(closesByTheFailedOpen).isEqualTo(0);
    assertThat(stillRegisteredOn(cfg)).isEmpty();
  }
 
  /**
   * The give-back of a failed open walks the entry containers the attempt had registered, which
   * every base DN but the last one leaves behind when a later one fails: the first base DN's
   * container is open, registered and answering configuration changes by the time the second one
   * cannot open its trees.
   */
  @Test
  public void aSecondBaseDNWhichFailsToOpenGivesBackTheFirst() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    // A tree set, as the configuration's own is: dc=b993 sorts before dc=b993b, so it is the one
    // opened - and registered - first.
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN, SECOND_BASE_DN));
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    backend.storage.failOpeningTree("/dc=com,dc=b993b/id2entry");
    // What a storage which cannot give the second container its trees does.
    openExpectingFailure(backend,
        "the backend was expected not to open with a second base DN whose trees cannot be opened");
 
    // The road this test is about: the first container had opened before the second one failed.
    assertThat(backend.storage.openedTrees()).contains("/dc=com,dc=b993/id2entry");
    assertThat(stillRegisteredOn(cfg)).isEmpty();
    assertThat(stillRegisteredOn(indexCfg)).isEmpty();
    assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty();
  }
 
  /**
   * An entry container which opened has registered everything it ever will, and it is registered
   * with the root container only after its highest entry ID has been read. A failure of that read
   * leaves a container which nothing holds, unless it is registered before anything else can
   * throw.
   */
  @Test
  public void anEntryContainerWhichOpenedButWasNotRegisteredIsGivenBack() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    // The read of the highest entry ID is the first cursor over id2entry once the container has
    // opened, which is when it registers itself: every cursor before that - the emptiness check of
    // EntryContainer.open() and the one each untrusted index makes as it opens - fails the open
    // itself, which the container catches.
    backend.storage.failOpeningCursor("/dc=com,dc=b993/id2entry", () -> anEntryContainerIsRegisteredOn(cfg));
    // What a storage which cannot position a cursor on the last entry does.
    openExpectingFailure(backend, "the backend was expected not to open when the highest entry ID cannot be read");
 
    assertThat(stillRegisteredOn(cfg)).isEmpty();
    assertThat(stillRegisteredOn(indexCfg)).isEmpty();
    assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty();
  }
 
  /**
   * The positive twin of the tests above: an entry container which opened is registered, once, as
   * a listener of the backend configuration, and so are its two configuration managers and each
   * index it opened. Without it, the registrations could be dropped and every test of a failed
   * open would stay green.
   */
  @Test
  public void anEntryContainerWhichOpenedIsRegisteredOnce() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    openExpectingSuccess(backend);
    final List<Object> registered;
    final List<Object> registeredOnIndex;
    final List<Object> registeredOnVLVIndex;
    try
    {
      registered = stillRegisteredOn(cfg);
      registeredOnIndex = stillRegisteredOn(indexCfg);
      registeredOnVLVIndex = stillRegisteredOn(vlvIndexCfg);
    }
    finally
    {
      backend.finalizeBackend();
    }
 
    assertThat(registered).filteredOn(listener -> listener instanceof EntryContainer).hasSize(1);
    // The two configuration managers, each once as an add listener and once as a delete listener.
    assertThat(registered)
        .filteredOn(listener -> listener.getClass().getSimpleName().endsWith("IndexCfgManager"))
        .hasSize(4);
    assertThat(registeredOnIndex).hasSize(1);
    assertThat(registeredOnVLVIndex).hasSize(1);
  }
 
  /**
   * {@code RootContainer.open} opens and registers its entry containers inside the write a storage
   * may replay after a transaction conflict. The attempt which replaces a rolled back one must find
   * the registry as the first one found it: an entry container left registered fails it with
   * {@code ERR_ENTRY_CONTAINER_ALREADY_REGISTERED}, so an ordinary write-write conflict becomes a
   * backend which does not start, and the container it left behind keeps the listeners it opened
   * with.
   */
  @Test
  public void aReplayedOpenLeavesOneSetOfEntryContainers() throws Exception
  {
    final TrackedBackend backend = new TrackedBackend();
    backend.setBackendID(BACKEND_ID);
    final PDBBackendCfg cfg = backendCfg(newTreeSet(BASE_DN));
    backend.configureBackend(cfg, serverContext);
    backend.storage.removeStorageFiles();
    backend.storage.conflictAtCommit(1);
    openExpectingSuccess(backend);
    try
    {
      assertThat(backend.storage.writeAttempts()).isEqualTo(2);
      assertThat(backend.getRootContainer().getBaseDNs()).containsOnly(BASE_DN);
    }
    finally
    {
      backend.finalizeBackend();
    }
 
    // Closing the backend takes back what the backend which is running registered, so anything
    // still registered here belongs to the attempt which was rolled back.
    assertThat(stillRegisteredOn(cfg)).isEmpty();
    assertThat(stillRegisteredOn(indexCfg)).isEmpty();
    assertThat(stillRegisteredOn(vlvIndexCfg)).isEmpty();
  }
 
  /**
   * Opens a backend which is not expected to open, and gives back what the attempt left behind:
   * the storage a failed open may have left open, or the backend itself when it opened after all -
   * its base DNs stay registered with the server otherwise, and every test which follows fails in
   * {@code openBackend()} on them rather than on what it is about.
   *
   * @return how many times the failed open closed the storage, before this method closed it
   */
  private static int openExpectingFailure(TrackedBackend backend, String expectation) throws Exception
  {
    try
    {
      backend.openBackend();
    }
    catch (InitializationException expected)
    {
      final int closesByTheFailedOpen = backend.storage.closeCalls();
      // A no-op once the failed open has given the storage back, and what keeps the tests which
      // follow runnable if it has not.
      backend.storage.close();
      return closesByTheFailedOpen;
    }
    backend.finalizeBackend();
    throw new AssertionError(expectation);
  }
 
  /**
   * Opens a backend which is expected to open. One which does not is left with its volume closed,
   * or every test which follows fails in {@code openBackend()} too and the one which actually
   * broke is lost among them.
   */
  private static void openExpectingSuccess(TrackedBackend backend) throws Exception
  {
    try
    {
      backend.openBackend();
    }
    catch (Exception failedToOpen)
    {
      backend.storage.close();
      throw failedToOpen;
    }
  }
 
  /** Every listener added to the backend configuration and not taken off it again. */
  private static List<Object> stillRegisteredOn(PluggableBackendCfg cfg) throws ConfigException
  {
    final List<Object> registered = new ArrayList<>();
 
    final ArgumentCaptor<ConfigurationChangeListener<PluggableBackendCfg>> changeAdded =
        captorFor(ConfigurationChangeListener.class);
    verify(cfg, atLeast(0)).addPluggableChangeListener(changeAdded.capture());
    registered.addAll(changeAdded.getAllValues());
    final ArgumentCaptor<ConfigurationChangeListener<PluggableBackendCfg>> changeRemoved =
        captorFor(ConfigurationChangeListener.class);
    verify(cfg, atLeast(0)).removePluggableChangeListener(changeRemoved.capture());
    removeEach(registered, changeRemoved.getAllValues());
 
    final ArgumentCaptor<ConfigurationAddListener<BackendIndexCfg>> indexAdded =
        captorFor(ConfigurationAddListener.class);
    verify(cfg, atLeast(0)).addBackendIndexAddListener(indexAdded.capture());
    registered.addAll(indexAdded.getAllValues());
    final ArgumentCaptor<ConfigurationAddListener<BackendIndexCfg>> indexAddRemoved =
        captorFor(ConfigurationAddListener.class);
    verify(cfg, atLeast(0)).removeBackendIndexAddListener(indexAddRemoved.capture());
    removeEach(registered, indexAddRemoved.getAllValues());
 
    final ArgumentCaptor<ConfigurationDeleteListener<BackendIndexCfg>> indexDeleteAdded =
        captorFor(ConfigurationDeleteListener.class);
    verify(cfg, atLeast(0)).addBackendIndexDeleteListener(indexDeleteAdded.capture());
    registered.addAll(indexDeleteAdded.getAllValues());
    final ArgumentCaptor<ConfigurationDeleteListener<BackendIndexCfg>> indexDeleteRemoved =
        captorFor(ConfigurationDeleteListener.class);
    verify(cfg, atLeast(0)).removeBackendIndexDeleteListener(indexDeleteRemoved.capture());
    removeEach(registered, indexDeleteRemoved.getAllValues());
 
    final ArgumentCaptor<ConfigurationAddListener<BackendVLVIndexCfg>> vlvAdded =
        captorFor(ConfigurationAddListener.class);
    verify(cfg, atLeast(0)).addBackendVLVIndexAddListener(vlvAdded.capture());
    registered.addAll(vlvAdded.getAllValues());
    final ArgumentCaptor<ConfigurationAddListener<BackendVLVIndexCfg>> vlvAddRemoved =
        captorFor(ConfigurationAddListener.class);
    verify(cfg, atLeast(0)).removeBackendVLVIndexAddListener(vlvAddRemoved.capture());
    removeEach(registered, vlvAddRemoved.getAllValues());
 
    final ArgumentCaptor<ConfigurationDeleteListener<BackendVLVIndexCfg>> vlvDeleteAdded =
        captorFor(ConfigurationDeleteListener.class);
    verify(cfg, atLeast(0)).addBackendVLVIndexDeleteListener(vlvDeleteAdded.capture());
    registered.addAll(vlvDeleteAdded.getAllValues());
    final ArgumentCaptor<ConfigurationDeleteListener<BackendVLVIndexCfg>> vlvDeleteRemoved =
        captorFor(ConfigurationDeleteListener.class);
    verify(cfg, atLeast(0)).removeBackendVLVIndexDeleteListener(vlvDeleteRemoved.capture());
    removeEach(registered, vlvDeleteRemoved.getAllValues());
 
    return registered;
  }
 
  /** Every listener added to an index configuration and not taken off it again. */
  private static List<Object> stillRegisteredOn(BackendIndexCfg cfg)
  {
    final List<Object> registered = new ArrayList<>();
    final ArgumentCaptor<ConfigurationChangeListener<BackendIndexCfg>> added =
        captorFor(ConfigurationChangeListener.class);
    verify(cfg, atLeast(0)).addChangeListener(added.capture());
    registered.addAll(added.getAllValues());
    final ArgumentCaptor<ConfigurationChangeListener<BackendIndexCfg>> removed =
        captorFor(ConfigurationChangeListener.class);
    verify(cfg, atLeast(0)).removeChangeListener(removed.capture());
    removeEach(registered, removed.getAllValues());
    return registered;
  }
 
  /** Every listener added to a VLV index configuration and not taken off it again. */
  private static List<Object> stillRegisteredOn(BackendVLVIndexCfg cfg)
  {
    final List<Object> registered = new ArrayList<>();
    final ArgumentCaptor<ConfigurationChangeListener<BackendVLVIndexCfg>> added =
        captorFor(ConfigurationChangeListener.class);
    verify(cfg, atLeast(0)).addChangeListener(added.capture());
    registered.addAll(added.getAllValues());
    final ArgumentCaptor<ConfigurationChangeListener<BackendVLVIndexCfg>> removed =
        captorFor(ConfigurationChangeListener.class);
    verify(cfg, atLeast(0)).removeChangeListener(removed.capture());
    removeEach(registered, removed.getAllValues());
    return registered;
  }
 
  /**
   * Whether an entry container is registered as a listener of the backend configuration, which is
   * the last thing {@code EntryContainer.open()} does.
   */
  private static boolean anEntryContainerIsRegisteredOn(PluggableBackendCfg cfg)
  {
    try
    {
      for (Object listener : stillRegisteredOn(cfg))
      {
        if (listener instanceof EntryContainer)
        {
          return true;
        }
      }
      return false;
    }
    catch (ConfigException declaredButNeverThrownByAMock)
    {
      throw new AssertionError(declaredButNeverThrownByAMock);
    }
  }
 
  /**
   * Takes one occurrence off the registrations for every removal, rather than every occurrence
   * for any: a listener registered twice and taken off once is still registered.
   */
  private static void removeEach(List<Object> registered, List<?> removed)
  {
    for (Object listener : removed)
    {
      registered.remove(listener);
    }
  }
 
  @SuppressWarnings({ "unchecked", "rawtypes" })
  private static <T> ArgumentCaptor<T> captorFor(Class<?> listenerClass)
  {
    return (ArgumentCaptor<T>) ArgumentCaptor.forClass((Class) listenerClass);
  }
 
  private PDBBackendCfg backendCfg(SortedSet<DN> baseDNs) throws ConfigException
  {
    final PDBBackendCfg cfg = mockCfg(PDBBackendCfg.class);
    when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config"));
    when(cfg.getBackendId()).thenReturn(BACKEND_ID);
    when(cfg.getDBDirectory()).thenReturn(BACKEND_ID);
    when(cfg.getDBDirectoryPermissions()).thenReturn("755");
    when(cfg.getDBCacheSize()).thenReturn(0L);
    when(cfg.getDBCachePercent()).thenReturn(20);
    when(cfg.getBaseDN()).thenReturn(baseDNs);
    when(cfg.listBackendIndexes()).thenReturn(new String[] { "cn" });
    when(cfg.listBackendVLVIndexes()).thenReturn(new String[] { "vlv1" });
 
    indexCfg = mock(BackendIndexCfg.class);
    when(indexCfg.getIndexType()).thenReturn(newTreeSet(IndexType.EQUALITY));
    when(indexCfg.getAttribute()).thenReturn(cnType);
    when(indexCfg.getIndexEntryLimit()).thenReturn(4000);
    when(indexCfg.getSubstringLength()).thenReturn(6);
    when(cfg.getBackendIndex("cn")).thenReturn(indexCfg);
 
    vlvIndexCfg = mock(BackendVLVIndexCfg.class);
    when(vlvIndexCfg.getName()).thenReturn("vlv1");
    when(vlvIndexCfg.getBaseDN()).thenReturn(baseDNs.first());
    when(vlvIndexCfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE);
    when(vlvIndexCfg.getFilter()).thenReturn("(objectClass=*)");
    when(vlvIndexCfg.getSortOrder()).thenReturn("+cn");
    when(cfg.getBackendVLVIndex("vlv1")).thenReturn(vlvIndexCfg);
    return cfg;
  }
 
  /** A backend whose storage the test can reach, and which counts what closes it. */
  private static final class TrackedBackend extends BackendImpl<PDBBackendCfg>
  {
    private TrackingStorage storage;
 
    @Override
    protected Storage configureStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException
    {
      storage = new TrackingStorage(new PDBStorage(cfg, serverContext));
      return storage;
    }
  }
 
  /**
   * Decorates a {@link Storage} so that the test can tell whether what opened it also gave it back.
   * {@link #close()} is answered once for each time the storage was opened, so that a test can
   * close what a failure left open without hiding whether it had already been closed.
   */
  private static final class TrackingStorage implements Storage
  {
    private final Storage delegate;
    private boolean open;
    private int closeCalls;
    /** The full name of the tree no transaction of this storage will open, if any. */
    private String failingTree;
    /** The full name of the tree one cursor of which no transaction of this storage will open, if any. */
    private String failingCursorTree;
    /** Once this holds, the next cursor over {@link #failingCursorTree} is the one refused. */
    private BooleanSupplier cursorRefusalDue;
    /** The full names of every tree a transaction of this storage opened. */
    private final Set<String> openedTrees = new HashSet<>();
    /** Whether this storage refuses to open at all, as one whose volume another process holds does. */
    private boolean refuseToOpen;
    /** How many write operations are still to be conflicted once they have run. */
    private int conflictsLeft;
    private int writeAttempts;
 
    TrackingStorage(Storage delegate)
    {
      this.delegate = delegate;
    }
 
    /** How many times this storage was asked to close, whether it was open or not. */
    int closeCalls()
    {
      return closeCalls;
    }
 
    /** Makes every transaction of this storage refuse to open the tree of the given full name. */
    void failOpeningTree(String treeName)
    {
      failingTree = treeName;
    }
 
    /**
     * Makes the transactions of this storage refuse to open one cursor over the tree of the given
     * full name: the first one asked for once the given condition holds.
     */
    void failOpeningCursor(String treeName, BooleanSupplier once)
    {
      failingCursorTree = treeName;
      cursorRefusalDue = once;
    }
 
    /** The full names of every tree a transaction of this storage opened. */
    Set<String> openedTrees()
    {
      return openedTrees;
    }
 
    /** Makes this storage refuse to open, as one whose volume another process holds does. */
    void refuseToOpen()
    {
      refuseToOpen = true;
    }
 
    /**
     * Makes the next write operations conflict once they have run, as PersistIt reports a
     * write-write conflict at commit time. The conflict is raised from within the single write the
     * delegate is asked for, so the replay is the delegate's own retry loop.
     */
    void conflictAtCommit(int conflicts)
    {
      conflictsLeft = conflicts;
    }
 
    /** How many times a write operation was run, the replays included. */
    int writeAttempts()
    {
      return writeAttempts;
    }
 
    @Override
    public void open(AccessMode accessMode) throws Exception
    {
      if (refuseToOpen)
      {
        throw new StorageInUseException("the volume is locked by another process");
      }
      delegate.open(accessMode);
      open = true;
    }
 
    @Override
    public void close()
    {
      closeCalls++;
      // Only what was opened is given back, so that a test can close what a failure left open
      // without closing the delegate twice.
      if (open)
      {
        open = false;
        delegate.close();
      }
    }
 
    @Override
    public void write(final WriteOperation writeOperation) throws Exception
    {
      delegate.write(new WriteOperation()
      {
        @Override
        public void run(WriteableTransaction txn) throws Exception
        {
          writeAttempts++;
          writeOperation.run(new TrackingTransaction(txn));
          if (conflictsLeft > 0)
          {
            conflictsLeft--;
            throw new RollbackException();
          }
        }
      });
    }
 
    @Override
    public <T> T read(ReadOperation<T> readOperation) throws Exception
    {
      return delegate.read(readOperation);
    }
 
    @Override
    public Importer startImport() throws ConfigException
    {
      return delegate.startImport();
    }
 
    @Override
    public void removeStorageFiles()
    {
      delegate.removeStorageFiles();
    }
 
    @Override
    public StorageStatus getStorageStatus()
    {
      return delegate.getStorageStatus();
    }
 
    @Override
    public Set<TreeName> listTrees()
    {
      return delegate.listTrees();
    }
 
    @Override
    public boolean supportsBackupAndRestore()
    {
      return delegate.supportsBackupAndRestore();
    }
 
    @Override
    public void createBackup(BackupConfig backupConfig) throws DirectoryException
    {
      delegate.createBackup(backupConfig);
    }
 
    @Override
    public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
    {
      delegate.removeBackup(backupDirectory, backupID);
    }
 
    @Override
    public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
    {
      delegate.restoreBackup(restoreConfig);
    }
 
    /**
     * A transaction which is every bit the one it decorates, except that it records the trees it
     * opens and refuses what the storage was told to refuse: one named tree - what a storage which
     * cannot give an index the tree it asks for does - or one cursor over one.
     */
    private final class TrackingTransaction implements WriteableTransaction
    {
      private final WriteableTransaction delegate;
 
      TrackingTransaction(WriteableTransaction delegate)
      {
        this.delegate = delegate;
      }
 
      @Override
      public void openTree(TreeName name, boolean createOnDemand)
      {
        if (name.toString().equals(failingTree))
        {
          throw new StorageRuntimeException("cannot open " + name);
        }
        delegate.openTree(name, createOnDemand);
        openedTrees.add(name.toString());
      }
 
      @Override
      public Cursor<ByteString, ByteString> openCursor(TreeName treeName)
      {
        if (treeName.toString().equals(failingCursorTree) && cursorRefusalDue.getAsBoolean())
        {
          failingCursorTree = null;
          throw new StorageRuntimeException("cannot open a cursor over " + treeName);
        }
        return delegate.openCursor(treeName);
      }
 
      @Override
      public void deleteTree(TreeName name)
      {
        delegate.deleteTree(name);
      }
 
      @Override
      public void put(TreeName treeName, ByteSequence key, ByteSequence value)
      {
        delegate.put(treeName, key, value);
      }
 
      @Override
      public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f)
      {
        return delegate.update(treeName, key, f);
      }
 
      @Override
      public boolean delete(TreeName treeName, ByteSequence key)
      {
        return delegate.delete(treeName, key);
      }
 
      @Override
      public ByteString read(TreeName treeName, ByteSequence key)
      {
        return delegate.read(treeName, key);
      }
 
      @Override
      public long getRecordCount(TreeName treeName)
      {
        return delegate.getRecordCount(treeName);
      }
 
      @Override
      public boolean treeExists(TreeName treeName)
      {
        return delegate.treeExists(treeName);
      }
    }
  }
}