/* * 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.assertThat; import static org.opends.server.replication.plugin.LDAPReplicationDomain.isServerFailure; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.assertj.core.api.SoftAssertions; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.DirectoryServerTestCase; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** * Tests which result codes {@link LDAPReplicationDomain} counts as a failure of the server * rather than as a change only conflict resolution can apply. *
* {@code ds-cfg-server-error-result-code} is a plain integer which is not validated as a * result code, so an administrator can set it to a code {@code solveNamingConflict()} owns. * The replay must still leave such a change to conflict resolution: a replica which took * every one of them for a failure of the server would retry them in place, hold its * ServerState back over them and give up on them once the give-up delay is spent - for * conflicts conflict resolution would have solved on the attempt it never got. *
* Only {@code UNWILLING_TO_PERFORM} has an end-to-end test of that rule, because each of the * other codes would need a scenario which fails with exactly that code and which nothing but * conflict resolution can apply - and {@code OBJECTCLASS_VIOLATION} has none a replayed * operation reaches through the server's own paths, the schema checks which raise it being * skipped for synchronization operations. Forcing a code with {@code ShortCircuitPlugin} does * not stand in for one: the operation applies for real once that budget is spent, so such a * test passes with the code removed from the set (#910, #938). So the rule is pinned here on * the predicate itself, with no server in it. *
* The codes below are written out rather than read from {@code CONFLICT_RESULT_CODES}: a data * provider fed by the set under test agrees with it whatever it holds, which is exactly what * leaves the set unguarded today. Every result code which can be configured is then swept * through the predicate against that list, so a code put into the set is as visible as one * taken out of it, and the set itself stays private - nothing here reads it. *
* What this can not see is the set drifting away from the branches of * {@code solveNamingConflict()} it is the union of: a code dropped from both at once leaves * the table below still describing the predicate correctly. *
* Nor is the predicate the whole of the rule at the call site: {@code replay()} answers
* {@code SUCCESS} and {@code NO_OPERATION} before it consults this at all, so a change which
* came back with one of them never reaches the table below whatever is configured - that is
* issue #953. {@code BUSY} is answered ahead of it too, but only inside the retry loop: once
* the in-place attempts are spent, the predicate is asked about it after all.
*/
@SuppressWarnings("javadoc")
public class IsServerFailureTest extends DirectoryServerTestCase
{
/**
* The result codes the four {@code solveNamingConflict()} overloads dispatch on. Not
* everything they resolve: the ModifyDN overload answers {@code NOTHING_TO_DO} from three
* early returns before it ever looks at the result code.
*/
private static final List
* It speaks for the predicate, not for what the replay hands it: {@code SUCCESS} and
* {@code NO_OPERATION} never reach the method, and what it answers for them is what the
* rest of this sweep is written from rather than a rule of its own.
*
* Soft assertions so that a drift in both directions at once - a code added and another
* dropped - is reported in one run rather than one code per run.
*/
@Test
public void everyRegisteredCodeIsAServerFailureExactlyWhenConflictResolutionDoesNotOwnIt()
{
final SoftAssertions softly = new SoftAssertions();
for (ResultCode code : configurableCodes())
{
softly.assertThat(isServerFailure(code, code))
.as("%s (%d) set as server-error-result-code is a failure of this server unless "
+ "conflict resolution is the only thing which can solve it",
code, code.intValue())
.isEqualTo(!CONFLICT_CODES.contains(code));
}
softly.assertAll();
}
@Test(dataProvider = "conflictResultCodes")
public void conflictCodeConfiguredAsTheServerErrorCodeIsLeftToConflictResolution(ResultCode code)
{
assertThat(isServerFailure(code, code))
.as("%s (%d) set as server-error-result-code must not take a change away from "
+ "solveNamingConflict(), which is the only thing which can solve it",
code, code.intValue())
.isFalse();
}
@Test(dataProvider = "codesOutsideTheConflictSet")
public void codeOutsideTheConflictSetConfiguredAsTheServerErrorCodeIsAServerFailure(ResultCode code)
{
assertThat(isServerFailure(code, code))
.as("%s (%d) set as server-error-result-code is this server reporting an internal "
+ "error, and conflict resolution can not solve it",
code, code.intValue())
.isTrue();
}
/**
* "Whatever is configured" is every code which can be configured, rather than a handful of
* them: the predicate reads the configured code only after {@code UNAVAILABLE} has not
* matched, so a change which stops that short circuit for some code has to be looked for
* across all of them.
*/
@Test
public void unavailableIsAServerFailureWhateverIsConfigured()
{
final SoftAssertions softly = new SoftAssertions();
for (ResultCode serverErrorResultCode : configurableCodes())
{
softly.assertThat(isServerFailure(ResultCode.UNAVAILABLE, serverErrorResultCode))
.as("the backend being offline or rebuilt is a failure of the server while "
+ "server-error-result-code is %s (%d) just as much as it is by default",
serverErrorResultCode, serverErrorResultCode.intValue())
.isTrue();
}
softly.assertAll();
}
@Test(dataProvider = "resultAndAnotherConfiguredCode")
public void codeWhichIsNotTheConfiguredOneIsNotAServerFailure(
ResultCode result, ResultCode serverErrorResultCode)
{
assertThat(isServerFailure(result, serverErrorResultCode))
.as("%s (%d) is not what this server puts on an internal error - it puts %s (%d) - "
+ "so it is the operation which failed rather than the server",
result, result.intValue(), serverErrorResultCode, serverErrorResultCode.intValue())
.isFalse();
}
}