Skip to content

[#939] Pin isServerFailure() directly, so the conflict result codes are guarded - #960

Merged
vharseko merged 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:test/939-pin-isserverfailure
Sep 9, 2026
Merged

[#939] Pin isServerFailure() directly, so the conflict result codes are guarded#960
vharseko merged 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:test/939-pin-isserverfailure

Conversation

@vharseko

@vharseko vharseko commented Sep 8, 2026

Copy link
Copy Markdown
Member

Fixes #939.

ds-cfg-server-error-result-code is a plain integer which is not validated as a result code, so it can be set to a code conflict resolution owns. #892 (issue #889) kept such a change out of isServerFailure() so that solveNamingConflict() gets it first, and CONFLICT_RESULT_CODES is the list that carve-out reads.

Only one of the six codes in that list is exercised, and only in one direction:

So today, removing any of the six from the set leaves the whole suite green while putting the bug back: a replica whose administrator set the code to 32 would take every NO_SUCH_OBJECT conflict for a failure of the server, retry it in place, hold its ServerState back over it and give up on it once the give-up delay is spent - for a conflict solveNamingConflict() would have solved on the attempt it never got.

The test

IsServerFailureTest is a table of the predicate itself, extends DirectoryServerTestCase with no startServer() in it, so it runs in about a second. Not an end-to-end test per code: each would need a scenario failing with exactly that code which nothing but conflict resolution can apply, and OBJECTCLASS_VIOLATION has none a replayed operation reaches through the server's own paths - the schema checks which raise it are skipped for synchronization operations, which is the half of #910 that #938 could not write.

Two things had to be right for it to guard anything:

  • The codes are written out, not read from the set. A data provider fed by CONFLICT_RESULT_CODES agrees with it whatever it holds - which is precisely what leaves the set unguarded today.
  • A hand-written list only ever notices a code taken out of the set. So every result code an administrator could configure is swept through the predicate against that list, and a code put into the set is as visible as one taken out. UNDEFINED (-1) is left out of the sweep: the property has lower-limit="0", so it is not a setting. The sweep uses soft assertions, so a drift in both directions at once is reported in one run.

The named tables say the rest of the rule: a code which does not belong in the set - the default 80 among them - still reports the storage failing; a conflict which arrives while a different code is configured is conflict resolution's; and UNAVAILABLE is a failure of the server whatever is configured.

The production change

isServerFailure() becomes package private with a line in its javadoc saying why. @VisibleForTesting is what the three members of this class already exposed for the tests carry, and all three are public.

CONFLICT_RESULT_CODES stays private. An earlier round of this branch opened it to assert set equality directly; sweeping the predicate proves the same thing through observable behaviour, over every configurable code rather than a hand-picked few, and needs no second widening.

Checked against the mutations it exists for

All runs against the committed version.

mutation result
baseline 23/23
drop NO_SUCH_OBJECT and OBJECTCLASS_VIOLATION from the set 3 failures - both named rows and the sweep
add OTHER (80) to the set 2 - the row for 80 and the sweep
add INVALID_DN_SYNTAX (34) to the set 1 - the sweep alone
add 19 and drop 66 at once 3 - the sweep names both codes in one run
drop the UNAVAILABLE disjunct 66 of the swept codes, in unavailableIsAServerFailureWhateverIsConfigured

The last two are why both halves are kept: the sweep cannot see the UNAVAILABLE disjunct going (with result == configured == UNAVAILABLE the invariant still holds through the second clause), and the named tables cannot see a code they do not name.

UpdateOperationTest.changeConflictResolutionCanNotSolveOnTheServerErrorCodeIsRetried was run against this branch as well: 1/1.

What this does not cover

Found while writing this

Three issues, none touched here:

…flict result codes are guarded

CONFLICT_RESULT_CODES keeps a change whose result code conflict resolution owns
from being taken for a failure of the server when an administrator sets
ds-cfg-server-error-result-code to that code. Only UNWILLING_TO_PERFORM has an
end-to-end test of it, and that one passes with the code removed from the set
(OpenIdentityPlatform#910), because the change is retried either way. So removing any of the six
left the whole suite green while putting the bug back: a replica whose
administrator set the code to 32 would take every NO_SUCH_OBJECT conflict for a
failure of the server, retry it in place, hold its ServerState back over it and
give up on it once the give-up delay is spent - for a conflict
solveNamingConflict() would have solved on the attempt it never got.

IsServerFailureTest is a table of the predicate itself, with no server in it.
The codes are written out rather than read from CONFLICT_RESULT_CODES: a data
provider fed by the set under test agrees with it whatever it holds, which is
what leaves the set unguarded today. Every result code an administrator could
configure is then swept through the predicate against that list, so a code put
into the set - which stops a storage failure carrying it from being retried -
is as visible as one taken out of it, and the set stays private. UNDEFINED (-1)
is left out of that sweep: the property has lower-limit="0", so it is not a
setting. The named tables say the rest of the rule - a code which does not
belong in the set, the default 80 among them, still reports the storage
failing; a conflict which arrives while a different code is configured is
conflict resolution's; and UNAVAILABLE is a failure of the server whatever is
configured.

isServerFailure() becomes package private, with a line in its javadoc saying
why. @VisibleForTesting is what the three members of this class already exposed
for the tests carry, and all three are public. CONFLICT_RESULT_CODES stays
private: sweeping the predicate proves what the set holds through behaviour
rather than by reading it.

Checked against the mutations it exists for, all against this version:

  dropping NO_SUCH_OBJECT and OBJECTCLASS_VIOLATION      3 failures
  adding OTHER (80) to the set                           2 failures
  adding INVALID_DN_SYNTAX (34) to the set               1 failure, the sweep alone
  adding 19 and dropping 66 at once                      3, both codes in one run
  dropping the UNAVAILABLE disjunct                      66 of the swept codes

Fixes OpenIdentityPlatform#939
@vharseko
vharseko requested a review from maximthomas September 8, 2026 09:43
@vharseko vharseko added replication tests Test suites: fixing, enabling, un-disabling labels Sep 8, 2026
@vharseko vharseko added java and removed java labels Sep 8, 2026
@vharseko
vharseko merged commit a141034 into OpenIdentityPlatform:master Sep 9, 2026
24 checks passed
@vharseko
vharseko deleted the test/939-pin-isserverfailure branch September 9, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

replication tests Test suites: fixing, enabling, un-disabling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Five of the six codes in CONFLICT_RESULT_CODES are never exercised: pin isServerFailure() directly

2 participants