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

Valery Kharseko
15 hours ago a141034da9ca73beb15ca36dcf8bd6c57f322905
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
/*
 * 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.
 * <p>
 * {@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.
 * <p>
 * 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.
 * <p>
 * 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.
 * <p>
 * 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.
 * <p>
 * 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<ResultCode> CONFLICT_CODES = Arrays.asList(
      ResultCode.NO_SUCH_OBJECT,          // 32, all four overloads
      ResultCode.ENTRY_ALREADY_EXISTS,    // 68, the Add and ModifyDN overloads
      ResultCode.NOT_ALLOWED_ON_RDN,      // 67, the Modify overload
      ResultCode.NOT_ALLOWED_ON_NONLEAF,  // 66, the Delete overload
      ResultCode.UNWILLING_TO_PERFORM,    // 53, the ModifyDN overload
      ResultCode.OBJECTCLASS_VIOLATION);  // 65, the ModifyDN overload
 
  /**
   * Codes conflict resolution does not solve. {@code OTHER} is the default of
   * {@code ds-cfg-server-error-result-code}, and the property is an integer with no upper
   * limit, so a value no result code is named after is a setting like any other.
   * {@code UNAVAILABLE} is not here: it is a failure of the server whatever is configured,
   * which {@link #unavailableIsAServerFailureWhateverIsConfigured()} covers.
   */
  private static final List<ResultCode> CODES_OUTSIDE_THE_CONFLICT_SET = Arrays.asList(
      ResultCode.OTHER,                       // 80, the default
      ResultCode.CONSTRAINT_VIOLATION,        // 19
      ResultCode.INSUFFICIENT_ACCESS_RIGHTS,  // 50
      ResultCode.valueOf(9999));              // a value no result code is named after
 
  @DataProvider(name = "conflictResultCodes")
  public Object[][] conflictResultCodes()
  {
    return rowsOf(CONFLICT_CODES);
  }
 
  @DataProvider(name = "codesOutsideTheConflictSet")
  public Object[][] codesOutsideTheConflictSet()
  {
    return rowsOf(CODES_OUTSIDE_THE_CONFLICT_SET);
  }
 
  /**
   * Every result code an administrator could put in {@code ds-cfg-server-error-result-code}.
   * The property is an integer with {@code lower-limit="0"}, so {@code UNDEFINED} (-1) is not
   * one of them - it is the null object of {@link ResultCode} rather than a setting - and a
   * value no result code is named after is.
   */
  private static List<ResultCode> configurableCodes()
  {
    final List<ResultCode> codes = new ArrayList<>();
    for (ResultCode code : ResultCode.values())
    {
      if (code.intValue() >= 0)
      {
        codes.add(code);
      }
    }
    codes.add(ResultCode.valueOf(9999));
    return codes;
  }
 
  private static Object[][] rowsOf(List<ResultCode> codes)
  {
    final Object[][] rows = new Object[codes.size()][];
    for (int i = 0; i < codes.size(); i++)
    {
      rows[i] = new Object[] { codes.get(i) };
    }
    return rows;
  }
 
  /** A result which is neither {@code UNAVAILABLE} nor the configured code. */
  @DataProvider(name = "resultAndAnotherConfiguredCode")
  public Object[][] resultAndAnotherConfiguredCode()
  {
    return new Object[][] {
      // a conflict under the default setting: the carve-out for CONFLICT_RESULT_CODES does
      // not come into it, the result simply is not what this server puts on an internal error
      { ResultCode.NO_SUCH_OBJECT, ResultCode.OTHER },
      { ResultCode.ENTRY_ALREADY_EXISTS, ResultCode.OTHER },
      { ResultCode.NOT_ALLOWED_ON_RDN, ResultCode.OTHER },
      { ResultCode.NOT_ALLOWED_ON_NONLEAF, ResultCode.OTHER },
      { ResultCode.UNWILLING_TO_PERFORM, ResultCode.OTHER },
      { ResultCode.OBJECTCLASS_VIOLATION, ResultCode.OTHER },
      // a conflict code while a different conflict code is configured - the deployment
      // this whole class is written about, meeting a conflict it did not configure
      { ResultCode.NO_SUCH_OBJECT, ResultCode.UNWILLING_TO_PERFORM },
      // and a failure which is not the configured code is not this server's either
      { ResultCode.OTHER, ResultCode.NO_SUCH_OBJECT },
      { ResultCode.CONSTRAINT_VIOLATION, ResultCode.UNWILLING_TO_PERFORM },
      { ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ResultCode.OTHER },
      { ResultCode.valueOf(9999), ResultCode.OTHER },
    };
  }
 
  /**
   * Sweeps every registered result code through the predicate against the list above. The
   * named tables below say what the rule is on the codes which matter; this says the set
   * behind it holds those and nothing else, so that a code put into
   * {@code CONFLICT_RESULT_CODES} - which stops a storage failure carrying it from being
   * retried - fails here as loudly as a code taken out of it.
   * <p>
   * 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.
   * <p>
   * 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();
  }
}