/* * 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.replication.plugin; import static org.assertj.core.api.Assertions.*; import static org.opends.server.TestCaseUtils.*; import java.util.ArrayList; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import org.forgerock.opendj.ldap.DN; import org.opends.server.TestCaseUtils; import org.opends.server.backends.MemoryBackend; import org.opends.server.replication.ReplicationTestCase; import org.opends.server.replication.server.ReplServerFakeConfiguration; import org.opends.server.replication.server.ReplicationServer; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** * Tests that {@link MultimasterReplication#forEachDomain} gives every domain its turn * whatever one of them threw, and reports the failures the way it says it does: the first * is thrown once the loop is over, the others are suppressed under it where it records * suppression, and an instance thrown by two domains - the error a JVM out of memory hands * out as often as it is asked for one - is thrown once and suppresses nothing (issue #986). *

* Two domains, on backends of their own, neither of them started: the action is what * throws, and it never touches the domain it is given. */ @SuppressWarnings("javadoc") public class ForEachDomainTest extends ReplicationTestCase { private static final int RS_ID = 613; private static final String SECOND_BACKEND_ID = "test2"; private static final String SECOND_ROOT_DN_STRING = "o=" + SECOND_BACKEND_ID; private DN firstBaseDN; private DN secondBaseDN; private ReplicationServer replicationServer; private LDAPReplicationDomain firstDomain; private LDAPReplicationDomain secondDomain; @BeforeMethod public void setUpLocal() throws Exception { firstBaseDN = DN.valueOf(TEST_ROOT_DN_STRING); secondBaseDN = DN.valueOf(SECOND_ROOT_DN_STRING); TestCaseUtils.initializeTestBackend(true); TestCaseUtils.initializeMemoryBackend(SECOND_BACKEND_ID, SECOND_ROOT_DN_STRING, true); final int rsPort = TestCaseUtils.findFreePort(); replicationServer = new ReplicationServer(new ReplServerFakeConfiguration( rsPort, "forEachDomainTestDb", 0, RS_ID, 0, 100, new TreeSet())); final SortedSet replServers = new TreeSet<>(); replServers.add("localhost:" + rsPort); firstDomain = MultimasterReplication.createNewDomain(new DomainFakeCfg(firstBaseDN, 1, replServers)); secondDomain = MultimasterReplication.createNewDomain(new DomainFakeCfg(secondBaseDN, 2, replServers)); } @AfterMethod public void tearDown() throws Exception { try { MultimasterReplication.deleteDomain(firstBaseDN); MultimasterReplication.deleteDomain(secondBaseDN); } finally { try { remove(replicationServer); } finally { final MemoryBackend backend = (MemoryBackend) getServerContext().getBackendConfigManager() .getLocalBackendById(SECOND_BACKEND_ID); if (backend != null) { backend.clearMemoryBackend(); backend.finalizeBackend(); getServerContext().getBackendConfigManager().deregisterLocalBackend(backend); } } } } @Test public void theFirstFailureIsThrownOnceTheLoopIsOverWithTheOthersSuppressedUnderIt() { final RuntimeException first = new RuntimeException("first"); final RuntimeException second = new RuntimeException("second"); final List visited = new ArrayList<>(); final Throwable thrown = catchThrowable(() -> MultimasterReplication.forEachDomain(domain -> { visited.add(domain); throw visited.size() == 1 ? first : second; })); assertThat(thrown).as("the failure thrown once the loop is over must be the first one met") .isSameAs(first); assertThat(thrown.getSuppressed()).as("the failure met after the first must be suppressed under it") .containsExactly(second); assertThat(visited).as("every domain must get its turn whatever the one before it threw") .containsExactlyInAnyOrder(firstDomain, secondDomain); } @Test public void aFailureThrownByTwoDomainsIsThrownOnceAndSuppressesNothing() { // The error a JVM out of memory prepared beforehand is handed out to every domain alike. final OutOfMemoryError theOneError = new OutOfMemoryError("prepared beforehand"); final List visited = new ArrayList<>(); final Throwable thrown = catchThrowable(() -> MultimasterReplication.forEachDomain(domain -> { visited.add(domain); throw theOneError; })); assertThat(thrown).as("the failure thrown once the loop is over must be the one both domains threw") .isSameAs(theOneError); assertThat(thrown.getSuppressed()).as("a failure must not be suppressed under itself").isEmpty(); assertThat(visited).as("every domain must get its turn whatever the one before it threw") .containsExactlyInAnyOrder(firstDomain, secondDomain); } @Test public void aFirstFailureWhichRecordsNoSuppressionStillGivesEveryDomainItsTurn() { final Error first = new ErrorWhichRecordsNoSuppression(); final RuntimeException second = new RuntimeException("second"); final List visited = new ArrayList<>(); final Throwable thrown = catchThrowable(() -> MultimasterReplication.forEachDomain(domain -> { visited.add(domain); if (visited.size() == 1) { throw first; } throw second; })); assertThat(thrown).as("the failure thrown once the loop is over must be the first one met") .isSameAs(first); assertThat(thrown.getSuppressed()).as("a failure which records no suppression must have none recorded") .isEmpty(); assertThat(visited).as("every domain must get its turn whatever the one before it threw") .containsExactlyInAnyOrder(firstDomain, secondDomain); } /** * An error which keeps no record of the failures suppressed under it: the shape of the * error a JVM out of memory prepared beforehand, which was made without its constructor * and so keeps no list to record them in. */ private static final class ErrorWhichRecordsNoSuppression extends Error { private static final long serialVersionUID = 1L; private ErrorWhichRecordsNoSuppression() { super("records no suppression", null, false, false); } } }