| | |
| | | } |
| | | |
| | | /** |
| | | * A session restart decided while a total update out of this replica is running stops the |
| | | * session that export streams over (issue #1041). |
| | | * <p> |
| | | * What the restart must leave alone is a total update into this replica: the data it is |
| | | * about to replace is read over the session, and the import is the thread the stop waits |
| | | * for. An export is not that: it streams out of a backend nothing is taking away, on a |
| | | * thread of its own, and a session stopped under it is the cut it reports to whoever asked |
| | | * for the total update - the same cut every other stop of the session is. The claim the |
| | | * restart makes for the import is not made here, and the session is stopped as it was |
| | | * before the claim. |
| | | * <p> |
| | | * The export holds the context by standing where it waits for its target to report the |
| | | * start of the total update: the target is a broker of this test, and reports nothing. |
| | | */ |
| | | @Test(timeOut = 120_000) |
| | | public void aRestartDecidedWhileAnExportRunsStopsTheSessionItStreamsOver() throws Exception |
| | | { |
| | | final Entry entry = TestCaseUtils.addEntry( |
| | | "dn: cn=renamedSince," + EXAMPLE_DN, |
| | | "objectClass: top", |
| | | "objectClass: person", |
| | | "cn: renamedSince", |
| | | "sn: renamedSince"); |
| | | final String entryUUID = getEntryUUID(entry.getName()); |
| | | waitUntil(() -> domain.getReplicaInfos().containsKey(EXPORTER_ID), |
| | | "the exporter is not in the replicas of the domain: nothing to export into"); |
| | | |
| | | final AtomicReference<Throwable> exportFailure = new AtomicReference<>(); |
| | | final Thread export = new Thread(() -> { |
| | | try |
| | | { |
| | | domain.initializeRemote(EXPORTER_ID, null); |
| | | } |
| | | catch (Throwable t) |
| | | { |
| | | exportFailure.set(t); |
| | | } |
| | | }, "export of " + EXAMPLE_DN); |
| | | |
| | | // The restart is held after its decision, before the stop: what the case reads is the |
| | | // decision the export was found by, not the session which is down a moment later. |
| | | final CountDownLatch stopHeld = new CountDownLatch(1); |
| | | final CountDownLatch releaseStop = new CountDownLatch(1); |
| | | domain.setServiceStopHook(() -> { |
| | | stopHeld.countDown(); |
| | | awaitUninterruptibly(releaseStop); |
| | | }); |
| | | final CSN csn = gen.newCSN(); |
| | | final AtomicReference<Throwable> replayFailure = new AtomicReference<>(); |
| | | final Thread replay = new Thread(() -> { |
| | | try |
| | | { |
| | | replayMsg(new ModifyMsg(csn, DN.valueOf("cn=movedAway," + EXAMPLE_DN), |
| | | generatemods("description", "replayed while the export was running"), entryUUID)); |
| | | } |
| | | catch (Throwable t) |
| | | { |
| | | replayFailure.set(t); |
| | | } |
| | | }, "replay of " + csn); |
| | | try |
| | | { |
| | | export.start(); |
| | | waitUntil(() -> domain.ieRunning() || exportFailure.get() != null, |
| | | "the export did not claim the import context"); |
| | | assertNull(exportFailure.get(), |
| | | "the export failed before it claimed the context: " + exportFailure.get()); |
| | | |
| | | // A change whose entryUUID search never runs spends its attempts in place and asks |
| | | // for the session to be restarted, the way it does in the case above. |
| | | ShortCircuitPlugin.registerShortCircuit( |
| | | OperationType.SEARCH, "PreParse", ResultCode.UNAVAILABLE.intValue()); |
| | | try |
| | | { |
| | | replay.start(); |
| | | assertTrue(stopHeld.await(30, TimeUnit.SECONDS), |
| | | "the restart left the session to the export: an export is not the owner a total" |
| | | + " update into this replica is"); |
| | | } |
| | | finally |
| | | { |
| | | ShortCircuitPlugin.deregisterShortCircuit(OperationType.SEARCH, "PreParse"); |
| | | } |
| | | assertTrue(domain.ieRunning(), "the export ended before the restart was decided"); |
| | | } |
| | | finally |
| | | { |
| | | releaseStop.countDown(); |
| | | domain.setServiceStopHook(null); |
| | | } |
| | | |
| | | replay.join(60_000); |
| | | assertFalse(replay.isAlive(), "the restart did not end"); |
| | | assertNull(replayFailure.get(), "the replay failed: " + replayFailure.get()); |
| | | export.join(60_000); |
| | | assertFalse(export.isAlive(), "the export did not end once the session it streams over" |
| | | + " was stopped"); |
| | | assertThat(exportFailure.get()) |
| | | .as("the export was not told that the session it streams over was cut") |
| | | .isInstanceOf(DirectoryException.class); |
| | | waitUntil(domain::isConnected, "the session was not started back after the restart"); |
| | | assertFalse(domain.ieRunning(), "the export which was cut left its context claimed"); |
| | | } |
| | | |
| | | /** |
| | | * Has the exporter start a total update into this replica, and returns once the backend |
| | | * of the domain is deregistered for it: from then on the import is reading the session, |
| | | * and a change replayed here is replayed into no backend. |