[#939] Pin isServerFailure() directly, so the conflict result codes are guarded - #960
Merged
vharseko merged 1 commit intoSep 9, 2026
Conversation
…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
maximthomas
approved these changes
Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #939.
ds-cfg-server-error-result-codeis 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 ofisServerFailure()so thatsolveNamingConflict()gets it first, andCONFLICT_RESULT_CODESis the list that carve-out reads.Only one of the six codes in that list is exercised, and only in one direction:
UpdateOperationTest.changeConflictResolutionCanNotSolveOnTheServerErrorCodeIsRetried([#889] Keep a change the replay could not apply out of the ServerState #892) sets the code to 53 - and No test for a ModifyDN conflict solved while server-error-result-code is one of the conflict codes #910 already recorded that it passes with 53 removed from the set, because the change is retried either way.NamingConflictTest.modifyDnConflictIsSolvedWhileTheServerErrorCodeIsOneOfTheConflictCodes([#910] Test that a ModifyDN conflict is still solved while server-error-result-code is one of the conflict codes #938, open) watches 53 in the direction that discriminates.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_OBJECTconflict 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 conflictsolveNamingConflict()would have solved on the attempt it never got.The test
IsServerFailureTestis a table of the predicate itself,extends DirectoryServerTestCasewith nostartServer()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, andOBJECTCLASS_VIOLATIONhas 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:
CONFLICT_RESULT_CODESagrees with it whatever it holds - which is precisely what leaves the set unguarded today.UNDEFINED(-1) is left out of the sweep: the property haslower-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
UNAVAILABLEis a failure of the server whatever is configured.The production change
isServerFailure()becomes package private with a line in its javadoc saying why.@VisibleForTestingis what the three members of this class already exposed for the tests carry, and all three arepublic.CONFLICT_RESULT_CODESstays 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.
NO_SUCH_OBJECTandOBJECTCLASS_VIOLATIONfrom the setOTHER(80) to the setINVALID_DN_SYNTAX(34) to the setUNAVAILABLEdisjunctunavailableIsAServerFailureWhateverIsConfiguredThe last two are why both halves are kept: the sweep cannot see the
UNAVAILABLEdisjunct going (withresult == configured == UNAVAILABLEthe invariant still holds through the second clause), and the named tables cannot see a code they do not name.UpdateOperationTest.changeConflictResolutionCanNotSolveOnTheServerErrorCodeIsRetriedwas run against this branch as well: 1/1.What this does not cover
solveNamingConflict()it is the union of: a code dropped from both at once leaves the table still describing the predicate correctly. Recorded in the class javadoc rather than papered over.replay()answersSUCCESSandNO_OPERATIONbefore consulting it at all, which is A change which never applied is recorded as replayed when server-error-result-code is set to NO_OPERATION or SUCCESS #953;BUSYis answered ahead of it inside the retry loop, but reaches it once the in-place attempts are spent.Found while writing this
Three issues, none touched here:
server-error-result-codeis set toNO_OPERATIONorSUCCESS.solveNamingConflict(ModifyDNOperation)when both the moved entry and its new parent are gone.