Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The change puts the decision and the report outside the operation the storage may replay, which is the shape #991 needs.
planIndexUpdates(AttributeIndex.java:1038) decides from the pre-change state, so a replayed untrust write reaches the same answer on every attempt.- The inner catch is gone from
VLVIndex.java:281-293: a conflict inside the untrust operation now reaches the storage's retry loop — on PDB and on JDBC — instead of committing an attempt which did nothing. conflictAtCommitOnWrite(ReplayedConfigChangeTest.java:924) makes a change with several writes testable, and the three new cases are red at the base for the right assertion each.
issue (blocking): The rebuild instruction is decided before the write and delivered only if the write commits.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:1006-1016, :942-953
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:290-292, :312-316
When the storage gives the write up (PDB after its retry budget, #937; JDBC after MAX_RETRIES), catch (Exception) at :1012 adds the server error and a stack trace; rebuildMessages + setAdminActionRequired(true) (:1006-1010) and the limit loop (:1002-1005) are skipped. What that road leaves: the index untrusted in memory (DefaultIndex.setTrusted :324 assigns before the tree write), TRUSTED intact on disk, and config.ldif already holding the raised limit — ConfigurationHandler.replaceEntry writes it at :642, before the listener loop, and nothing rolls it back. The log line says adminActionRequired=false, the client gets ERR_CONFIG_FILE_MODIFY_APPLY_FAILED with a stack trace naming no index, and the next restart trusts the index under the raised limit: the #991 end state, on a road where the base delivered NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD and adminActionRequired=true. Write 1 has the same shape for the added indexes. In VLVIndex the :290 catch rethrows, so the result carrying adminActionRequired=true (:248/:253/:263) is discarded whole.
The instruction is right on both roads, so say it before the write; only the limit belongs after the commit:
planIndexUpdates(updatedIndexes.values(), newConfiguration, indexesToUntrust, rebuildMessages);
for (LocalizableMessage rebuildMessage : rebuildMessages)
{
ccr.setAdminActionRequired(true);
ccr.addMessage(rebuildMessage);
}
entryContainer.getRootContainer().getStorage().write(/* untrust only, as now */);
for (final Index updatedIndex : updatedIndexes.values())
{
updatedIndex.setIndexEntryLimit(newConfiguration.getIndexEntryLimit());
}For VLVIndex, the :290 catch can report the way AttributeIndex :1012 does instead of throwing:
catch (final Exception e)
{
ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(getName()));
ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
ccr.setResultCode(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
return ccr;
}Pin: the NO_REPLAY twin of conflictAtCommitOnWrite in the harness, and one case per class on the failed road — red at the head (adminActionRequired false, no ordinal), green with the fix:
void failWithoutReplayOnWrite(int nth)
{
arm(ConflictPoint.NO_REPLAY, 1);
armedWrite = writes + nth;
}
backend.storage.failWithoutReplayOnWrite(3);
final ConfigChangeResult ccr = index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY), 8000));
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).as("the rebuild the raised limit needs, on the road which failed").isTrue();
assertThat(ordinalsOf(ccr)).contains(NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.ordinal());
assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).as("what the restart will read").contains(TRUSTED);suggestion (non-blocking): The untrust write in AttributeIndex opens a transaction with nothing to do.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:991
storage.write runs with an empty indexesToUntrust — a lowered limit, a confidentiality change which needs nothing — where VLVIndex.java:271 guards it and says why: a bounded storage can give that write up with nothing to give up, which puts the blocking issue's road in front of a change that asks for nothing. Pre-existing, but the PR aligns the two classes.
if (!indexesToUntrust.isEmpty())
{
entryContainer.getRootContainer().getStorage().write(...);
}suggestion (non-blocking): No case lowers or keeps the entry limit, so < → != at the comparison survives every case.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:1044, ReplayedConfigChangeTest.java:616-650
Every AttributeIndex case raises the limit (4000 → 8000). The mutant untrusts and reports on a lowered limit and nobody is red. A pre-existing gap, re-homed by the PR with the new cases around it.
final ConfigChangeResult ccr = index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY), 2000));
assertThat(ccr.adminActionRequired()).as("a lowered limit needs no rebuild").isFalse();
assertThat(ccr.getMessages()).isEmpty();
assertThat(cnIndex.isTrusted()).isTrue();
assertThat(cnIndex.getIndexEntryLimit()).isEqualTo(2000);suggestion (non-blocking): "A change asking for nothing opens no transaction" is asserted on the result, not on the storage.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:271, ReplayedConfigChangeTest.java:680-683
The again call asserts SUCCESS, no admin action, no messages; delete the if (requiresRebuild) guard and it stays green. writes() (:955) is what pins it, as aChangeWhichLeavesTheBaseDNsAloneOpensNoTransaction already does at :542-547.
final int writesBefore = backend.storage.writes();
final ConfigChangeResult again = vlvIndex.applyConfigurationChange(vlvIndexCfg("+sn"));
assertThat(backend.storage.writes()).as("a change which changes nothing opens no transaction").isEqualTo(writesBefore);suggestion (non-blocking): The raised-limit case arms the untrust write by its ordinal.
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java:629
conflictAtCommitOnWrite(3, 1) names the third write, and the comment above it explains why it is the third. #994 restructures the same method; once a write is merged or dropped the arm lands on another write and the case stays green pinning something else. The cheapest guard turns that red:
final int writesBefore = backend.storage.writes();
backend.storage.conflictAtCommitOnWrite(3, 1);
// ...
assertThat(backend.storage.writes()).as("the armed write was the last of three").isEqualTo(writesBefore + 3);note (non-blocking): A failure of the untrust operation now leaves VLVIndex.applyConfigurationChange as an exception, and no case pins the road the PR claims.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:271-296
Stated as deliberate at :274-279, recorded rather than argued: ConfigurationHandler.replaceEntry (:647-656) catches nothing, so on the give-up road and on any non-conflict failure the client gets the worker's uncaught-exception response instead of ERR_CONFIG_FILE_MODIFY_APPLY_FAILED. The blocking fix above closes the give-up half. What stays unpinned is the claim itself — "the conflict now reaches the retry loop": conflictAtFirstStorageAccess throws a raw RollbackException, nothing throws the StorageRuntimeException State.removeFlagsFromIndex produces from inside the operation. A harness point which does, plus attempts() == 2 and the flag gone, pins it:
writeOperation.run(txn);
throw new StorageRuntimeException(new RollbackException());note (non-blocking): index-entry-limit 0 means no limit, and < classifies it as the smallest limit.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:1044 (mirrors DefaultIndex.java:304; DefaultIndex.java:247 is where 0 means unlimited)
4000 → 0 answers "no rebuild" though every key over 4000 is undefined and stays so; 0 → 4000 answers "rebuild" though nothing needs one. Pre-existing — the PR copies the expression into planIndexUpdates — so a follow-up is fine; the lowered-limit case above is the case it needs.
final int oldLimit = updatedIndex.getIndexEntryLimit();
final int newLimit = newConfig.getIndexEntryLimit();
final boolean newLimitRequiresRebuild = oldLimit != 0 && (newLimit == 0 || oldLimit < newLimit);…hange outside the write which is replayed AttributeIndex.applyConfigurationChange called two helpers from inside a WriteOperation, and both wrote into the caller's ConfigChangeResult and mutated in-memory state no rollback undoes. Storage.write() may replay a rolled back operation - PDBStorage and JDBCStorage both do - so the operator was told to rebuild an index once per attempt, and setIndexEntryLimit(), which answers "the limit went up" by comparing the new limit with a field it then assigns, left the replay comparing the raised limit with itself: it found nothing to rebuild and committed an index whose entry limit was raised while the TRUSTED flag the rollback had put back was still stored. DefaultIndex.afterOpen reads that flag back after a restart and the index is used as if it were complete. The decision is now taken before the write, the write only removes the flag, and the entry limit and the messages are applied once it has committed - the shape EntryContainer.applyConfigurationAdd already uses for the index it adds. VLVIndex.applyConfigurationChange had the same shape, and the only thing keeping its replay from committing a trusted vlvIndex was the administrative action the rolled back attempt had left in the shared result - the same accident that repeated the message. It is decided before the write and published after it as well, and a conflict raised by the flag removal now reaches the retry loop instead of being caught and reported.
…report a give-up on the vlvIndex flag removal The first round reported the rebuild a raised entry limit needs only once the write which untrusts the index had committed. A write the storage gives up on - the bound of its retry loop spent, or a failure it does not replay at all - dropped that instruction together with the limit it could not apply, while config.ldif already held the raised limit: ConfigurationHandler .replaceEntry writes it before the listeners run and nothing rolls it back. The next open of the index applied the limit to a tree whose keys were given up under the lower one, with TRUSTED still stored, and the operator had been told nothing but a stack trace: the OpenIdentityPlatform#991 end state, on a road where the base delivered the message by the same accident which repeated it. What the indexes which stay are asked for is now decided and reported before any of the three writes, since the instruction holds whichever way they go, and only the limit itself waits for the untrust write to commit. That write is made only when there is something to untrust, as VLVIndex already did: a lowered limit opens no transaction a bounded storage could give up on. VLVIndex reported a give-up by throwing, which discarded the result carrying the rebuild it had asked for - ConfigurationHandler catches nothing a listener throws. It now reports it the way AttributeIndex does, with the message asked for before the write as well. Pinned: both give-up roads; a lowered limit, which needs no rebuild and opens no transaction; the write ordinals the replay cases arm; and the conflict the flag removal itself raises, in the form the PDB transaction raises it - a StorageRuntimeException wrapping the RollbackException, which the inner catch the first round removed used to swallow, and which the bare form the harness raised never reached.
ed6607c to
b7094cd
Compare
|
Round 2 pushed as b7094cd, on top of a rebase to current master 129fc4e (range-diff of the first commit is issue (blocking) — the rebuild instruction was delivered only if the write committed. Verified as described: any failure of a write — the bound spent, or a failure the engine does not replay at all ( On "write 1 has the same shape": the answer there needs the transaction (
Pins: suggestion — the untrust write with nothing to do. Guarded, third write only: suggestion — no case lowers the limit. suggestion — the suggestion — the arm by ordinal. note — "the conflict now reaches the retry loop" was unpinned. Agreed, and pinned — with a different harness point than the one in the note, because that one does not tell the base from the head. A throw placed after note — Runs: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: Round 1's blocking issue is closed the way it should be: the decision and the report now precede every write, and the give-up road is executed, not reasoned about.
ReplayedConfigChangeTestwith both round-2 hunks ofAttributeIndex.javaandVLVIndex.javareverted: exactly the three new cases red, each on its own assert (:756adminActionRequired,:815an exception in place of a result,:790before+3 writes), the other 16 green — 19/19 at head.failWithoutReplayOnWrite(nth)fails through the delegate's own classification (attempts()==1, the flag removal rolled back on disk), so the give-up cases pin what PDB does, not what the harness does.VLVIndex.applyConfigurationChangekeeps the old definition on the give-up road and theagainblock (:824-828) pins that a re-apply still sees a change.- The third write is opened only when there is something to untrust (
:1004), pinned bywrites()==before+2in the lowered-limit case.
suggestion (non-blocking): "Before any of the three writes" is pinned on the write-3 road only.
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java:741-761, opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:938-947
Moving the plan-and-report block back to just before if (!indexesToUntrust.isEmpty()) (:1004) is green on all 19 cases: no case combines a raised limit with a give-up on write 1 or 2. Under that mutant a 4000→8000 change whose (empty) add write gives up is round 1's shape again — config entry rewritten, index TRUSTED at 4000, no instruction in the result.
@Test
public void aRaisedEntryLimitIsReportedWhenTheWriteWhichAddsIndexesGivesUp() throws Exception
{
// Same fixture as aRaisedEntryLimitIsReportedWhenTheWriteWhichUntrustsTheIndexGivesUp, armed on the first write.
backend.storage.failWithoutReplayOnWrite(1);
final ConfigChangeResult ccr = index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY), 8000));
assertThat(ccr.getResultCode()).isNotEqualTo(ResultCode.SUCCESS);
assertThat(ccr.adminActionRequired()).as("the rebuild, asked for before the first write").isTrue();
assertThat(ordinalsOf(ccr)).contains(NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.ordinal());
assertThat(cnIndex.getIndexEntryLimit()).isEqualTo(4000);
assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).contains(TRUSTED);
}Pin: kills "report moved back before write 3" and pins the description's claim.
suggestion (non-blocking): The AttributeIndex give-up has no again twin; a re-apply after it is unpinned.
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java:741-761, opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:976, :1056
this.config = newConfiguration (:976) lands before write 3, so after a write-3 give-up config already holds 8000 while the live index holds 4000. The head re-detects because planIndexUpdates asks the live index (:1056); "compare config.getIndexEntryLimit() instead" is green on every case, and on a real re-apply it would apply 8000 to a trusted index with no report. VLVIndex has this twin (:824-828); the attribute index does not.
// After the asserts of aRaisedEntryLimitIsReportedWhenTheWriteWhichUntrustsTheIndexGivesUp:
final ConfigChangeResult again = index.applyConfigurationChange(indexCfg(newTreeSet(IndexType.EQUALITY), 8000));
assertThat(again.getResultCode()).isEqualTo(ResultCode.SUCCESS);
assertThat(again.adminActionRequired()).as("the limit the failed write did not apply is still a change").isTrue();
assertThat(ordinalsOf(again)).contains(NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.ordinal());
assertThat(cnIndex.getIndexEntryLimit()).isEqualTo(8000);
assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).doesNotContain(TRUSTED);suggestion (non-blocking): The four VLV definition publications moved behind the write are read by no case.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:309-323
The move is the fix (old definition kept on give-up), but only this.config is observed, through again. this.sortKeys = newSortKeys deleted is green on the three VLV cases, and so is putting the four assignments back at detection time as on the base: VLVIndex has no getters, sortKeys is read only in key encoding (:455, :835) and evaluate (:524). After a successful change the old keys would then serve an in-JVM rebuild.
// VLVIndex — for the test
List<SortKey> getSortKeys()
{
return sortKeys;
}
// aChangedSortOrderUntrustsTheVlvIndexWhenTheTransactionIsReplayed, after the flag asserts:
assertThat(vlvIndex.getSortKeys()).as("the definition the committed write published")
.containsExactly(new SortKey("sn", false));
// aChangedSortOrderIsReportedWhenTheWriteWhichUntrustsTheVlvIndexGivesUp, before `again`:
assertThat(vlvIndex.getSortKeys()).as("the definition the failed write did not publish")
.doesNotContain(new SortKey("sn", false));Or: rebuild the VLV index after the replayed case and evaluate() a search sorted +sn — non-null — and one sorted the old way — null (:524).
suggestion (non-blocking): The confidentiality arm of planIndexUpdates is reached by no case.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:1061-1069
The cfg mock never stubs isConfidentialityEnabled() and the test crypto suite is not encrypted, so setConfidential(...) is false in every case; deleting the arm is green on all 19. Base code (129fc4e3:1013), unpinned there too — the PR only gave it a new home.
@Test
public void aConfidentialityChangeUntrustsTheIndexWhenTheTransactionIsReplayed() throws Exception
{
final BackendIndexCfg cfg = indexCfg(newTreeSet(IndexType.EQUALITY), 4000);
when(cfg.isConfidentialityEnabled()).thenReturn(true);
backend.storage.conflictAtCommitOnWrite(3, 1);
final ConfigChangeResult ccr = index.applyConfigurationChange(cfg);
assertThat(backend.storage.attempts()).isEqualTo(2);
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(ordinalsOf(ccr)).containsOnly(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).doesNotContain(TRUSTED);
}suggestion (if-minor): The give-up catches are pinned by contains(ordinal) and isNotEqualTo(SUCCESS) only.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:303-305, opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java:755-757, :816-820
Deleting ccr.addMessage(LocalizableMessage.raw(stackTraceToSingleLineString(e))), or returning any other non-SUCCESS code, is green; what the operator reads on the give-up road is exactly that code and that cause.
assertThat(ccr.getResultCode()).isEqualTo(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
assertThat(ccr.getMessages().toString()).as("the failure, next to the rebuild").contains(UnreplayableFailure.class.getSimpleName());question (non-blocking): JEStorage.write has no retry loop — is a JE issue wanted?
opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java:913-933
Single attempt, no RollbackException catch — same on the base, so nothing this PR changes: on JE a conflicting untrust write is a first-attempt give-up, and the report decided before the write is delivered on that road too. Worth an issue for a JE retry loop like PDB's, or is JE meant to give up first time?
… re-apply after a give-up, and what a give-up answers The second round moved the rebuild instruction before the first of the three writes, and pinned it on the road where the third gives up. Moved back to just before the third, every case stayed green: none combined a raised limit with a give-up on the write which opens the indexes the change adds. A give-up there - the write this change opens with nothing to add - is now pinned too: the instruction in the result, the limit not applied, TRUSTED still stored. AttributeIndex replaces the configuration it holds before the write which untrusts the index, so after a give-up that configuration already carries the raised limit while the index does not. The plan asks the index, which is what makes a re-apply of the same change a change again; asked of the configuration instead, it would apply the limit to a trusted index with no report and nothing was red. The give-up case now re-applies, as its vlvIndex twin already did. VLVIndex publishes its four definition fields after the write, and no case read them: the sort keys deleted, or published before the write as on the base, was green. A package-private getSortKeys() lets the replayed case pin the keys the committed write published and the give-up case pin the ones the failed write did not. Both give-up catches answered any non-SUCCESS code with any message and stayed green; they now have to answer the server error result code with the failure named next to the rebuild.
|
Round 3 pushed as 97c8773, on top of b7094cd. Test-only, but for one package-private getter on suggestion — "before any of the three writes" pinned on the write-3 road only. Verified: the report block moved back to just before suggestion — no suggestion — the four VLV publications are read by no case. Verified both ways: suggestion — the confidentiality arm of suggestion (if-minor) — the give-up catches pinned by question — Runs: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: Every suggestion of the previous round landed, each pinned on the road where it bites.
aRaisedEntryLimitIsReportedWhenTheWriteWhichAddsIndexesGivesUp(opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java:786-816) arms write 1 — "before any of the three writes" is now pinned per write, not on the write-3 road only.VLVIndex.getSortKeys()(opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:388) withcontainsExactly(new SortKey("cn", false))after the give-up and"sn"after the re-apply (ReplayedConfigChangeTest.java:888,:896): the block moved back to detection time goes red.- The
againtwin after the AttributeIndex give-up (ReplayedConfigChangeTest.java:777-783): the raised limit is a change on the second ask, applied, andpersistedFlagslosesTRUSTED. isEqualTo(serverErrorResultCode())pluscontains(UnreplayableFailure.class.getSimpleName())in both give-up cases: the failure travels next to the rebuild, and the catch cannot answer with any other code.- CI green on the five ubuntu cells at 97c8773 (run 35238460931).
suggestion (non-blocking): The publish-after-the-write placement is pinned for sortKeys only; the baseDN, scope and filter arms are reached by no case.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java:309-320, opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java:1009-1017
Every BackendVLVIndexCfg the test passes comes from vlvIndexCfg(String), which fixes getBaseDN(), getScope() and getFilter() and varies only the sort order; no other test applies a VLV change. So baseDNChanged/scopeChanged/filterChanged are false in every case, and one arm hoisted above the write — say this.filter = newFilter above the try at :290 — survives the suite, while on the give-up road the index would then evaluate searches with a filter neither its tree nor its config holds. The whole-block hoist dies at :888; a one-field hoist does not. My round-2 wording named getSortKeys() as the cheap pin, so this is the remainder of that suggestion, not a new one.
// VLVIndex
SearchFilter getFilter()
{
return filter;
}
// ReplayedConfigChangeTest — the same give-up road, the change in the filter instead of the sort order
private BackendVLVIndexCfg vlvIndexCfg(String sortOrder, String filter)
{
final BackendVLVIndexCfg cfg = vlvIndexCfg(sortOrder);
when(cfg.getFilter()).thenReturn(filter);
return cfg;
}
backend.storage.failWithoutReplay();
final ConfigChangeResult ccr = vlvIndex.applyConfigurationChange(vlvIndexCfg("+cn", "(cn=*)"));
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(vlvIndex.getFilter().toString()).as("the filter the failed write did not publish")
.isEqualTo("(objectClass=*)");Kills the hoisted filter arm; scope and baseDN the same way, one fixture each — or one definition() accessor returning the four and a single isEqualTo per road.
suggestion (non-blocking): The confidentiality arm of planIndexUpdates is reached by no case.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java:1061-1069
No cfg mock stubs isConfidentialityEnabled(), and the test crypto suite is not encrypted, so setConfidential(false) compares equal in every case and the arm is dead; "arm deleted" is green on the whole class. Origin is the base — the PR re-homed the arm, it did not write it — so a follow-up is fine.
final BackendIndexCfg cfg = indexCfg(newTreeSet(IndexType.EQUALITY), 4000);
when(cfg.isConfidentialityEnabled()).thenReturn(true); // suite not encrypted: setConfidential(true) is a change
backend.storage.conflictAtCommitOnWrite(3, 1);
final ConfigChangeResult ccr = index.applyConfigurationChange(cfg);
assertThat(ordinalsOf(ccr)).containsOnly(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
assertThat(ccr.adminActionRequired()).isTrue();
assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).doesNotContain(TRUSTED);
assertThat(backend.storage.attempts()).isEqualTo(2);question (non-blocking): JEStorage.write has no replay loop (same at the base), so on JE a conflicting untrust write is a first-attempt give-up — the report-before-the-write delivers the instruction there too. Is a JE retry loop a follow-up issue, or does JE stay a first-attempt give-up by design?
…published fields, not sortKeys alone
Round 4 found the give-up/re-apply case pinned only the sort keys: every BackendVLVIndexCfg the
suite passes fixes getBaseDN(), getScope() and getFilter() and varies only the sort order, so a
hoist of any one of those three assignments above the write - the same accident round 2 fixed for
sortKeys - would survive unnoticed.
aChangedSortOrderIsReportedWhenTheWriteWhichUntrustsTheVlvIndexGivesUp now arms a change which
touches all four fields at once, and reads them back through three new package-private getters -
getBaseDN(), getScope(), getFilter() - next to the getSortKeys() round 3 added: the failed write
still returns the old values, the re-apply the new ones.
Verified: hoisting this.filter = newFilter above the write - the mutant the review's own snippet
proposed - turns the give-up assertion red (expected "(objectClass=*)" but was "(sn=*)"); reverted,
ReplayedConfigChangeTest is 20/20. baseDN and scope are guarded by the identical
if (xChanged) { this.x = ...; } shape, so the same hoist dies on the corresponding assertion by
construction.
|
Round 4 pushed as 728dd78, on top of 97c8773. Test-only, plus three package-private getters on suggestion — the publish-after-the-write placement is pinned for suggestion — the confidentiality arm of question — is a JE retry loop a follow-up, or first-attempt give-up by design? Follow-up, already filed. PR description updated to mention the fourth round's pin next to the third's. |
Fixes #991.
AttributeIndex
applyConfigurationChangecalledcreateIndexandupdateIndexfrom inside aWriteOperation, and both wrote into the caller'sConfigChangeResultand mutated in-memory state no rollback undoes.Storage.write()may replay a rolled back operation —PDBStorageandJDBCStorageboth do — so:setIndexEntryLimit()answers "the limit went up" by comparing the new limit with a field it then assigns, so the replay compared the raised limit with itself, found nothing to rebuild, and committed an index whose entry limit was raised while theTRUSTEDflag the rollback had put back was still stored.DefaultIndex.afterOpenreads that flag back after a restart and the index is used as if it were complete.Now what the indexes which stay are asked for is decided before any of the three writes —
updateIndexbecomesplanIndexUpdates, which readsgetIndexEntryLimit()rather than the return value of the setter — and reported there as well, because the instruction holds whichever way the writes go:ConfigurationHandler.replaceEntryhas already written the raised limit to config.ldif when the listener runs, and the next open of the index applies it to a tree whose keys were given up under the lower one. A write the storage gives up on therefore leaves the rebuild instruction in the result next to the failure, where the first round dropped it together with the limit. The untrust write does onlysetTrusted(txn, false), and only when there is something to untrust — a lowered limit opens no transaction, asVLVIndexalready did not; the entry limit is applied once that write has committed.createIndexanswers whether the index it opened has to be rebuilt instead of reporting it, and the report follows the write, asEntryContainer.applyConfigurationAdd:204-225already does for the index it adds and for the same reason: that answer needs the transaction.VLVIndex
VLVIndex.applyConfigurationChangehad the same shape and is included here rather than tracked on its own, since the analysis and the fix are one and the same. Every question it asked was asked of the configuration it holds, and both that configuration and the four fields around it were assigned inside the write, so a replay finds nothing changed.Worth stating precisely, because it is not what one would expect: the replay does still remove the flag today, and what keeps it doing so is that the rolled back attempt left
adminActionRequiredin the sharedConfigChangeResult— the same accident that repeats the message. The observable defect is therefore the duplicated message; the persisted flag rides on that duplication. Decided and reported before the write and published after it, neither depends on the other any more.A conflict raised by the flag removal itself was also caught inside the operation and turned into a message, so the storage was never told to replay it and committed an attempt which had done nothing. It now reaches the retry loop. What the storage gives up on is reported the way
AttributeIndexreports it — server error, the stack trace as a message — with the result built so far, rather than thrown:ConfigurationHandlercatches nothing a listener throws and would discard the rebuild the result asks for together with the exception.Tests
Eight tests in
ReplayedConfigChangeTest, which already carries the replaying storage from #907:anIndexAddedByAChangeIsReportedOnceWhenTheTransactionIsReplayedaRaisedEntryLimitUntrustsTheIndexWhenTheTransactionIsReplayedaRaisedEntryLimitIsReportedWhenTheWriteWhichUntrustsTheIndexGivesUp— and re-applies the change after the give-up, which is a change again because the plan asks the index, not the configuration the failed change had already put in placeaRaisedEntryLimitIsReportedWhenTheWriteWhichAddsIndexesGivesUp— the same instruction when the first write gives upaLoweredEntryLimitNeedsNoRebuildAndOpensNoTransactionaChangedSortOrderUntrustsTheVlvIndexWhenTheTransactionIsReplayedaChangedSortOrderIsReportedWhenTheWriteWhichUntrustsTheVlvIndexGivesUpaConflictRaisedByTheRemovalOfTheVlvIndexFlagIsReplayedThe harness gains
conflictAtCommitOnWrite(nth, conflicts)andfailWithoutReplayOnWrite(nth), since an index change makes three writes and each test has to arm the one it is about;conflictAsTheTransactionReportsIt(conflicts), which raises the first-access conflict in the formPDBStorage's own transaction raises it — aStorageRuntimeExceptionwrapping theRollbackException, the only form an inner catch of the former ever met; and a vlvIndex to open where a test asks for one.VLVIndexgains a package-privategetSortKeys(), so that the definition a committed write published, and the one a failed write did not, can be read.Red at the base for the assertion each is about: two messages instead of one;
[TRUSTED, COMPACTED]still stored after the raised limit; the vlvIndex conflict swallowed (one attempt, server error, flag intact); the give-up roads answered with the limit an attempt which rolled back had assigned, or with an exception in place of a result. The pins added in the third round are each red under the mutant they are about: the report moved back before the third write, the plan asking the configuration instead of the index, the vlvIndex definition unpublished or published before the write, the give-up catches without the failure. The fourth round's pin is red under the same hoist onbaseDN,scopeorfilter: the vlvIndex give-up case now changes all four published fields at once, read back throughgetBaseDN(),getScope()andgetFilter()next togetSortKeys(). Green here:ReplayedConfigChangeTest20/20, andPDBTestCase,DefaultIndexTest,BulkCursorTest,OnDiskMergeImporterTest,VLVControlTestCase,ServerSideSortControlTestCase— 134 tests, no failures.Not in this change
index-entry-limit0 means no limit, and the comparisonplanIndexUpdatesinherits fromDefaultIndex.setIndexEntryLimitclassifies it as the smallest limit. Pre-existing; filed as #1059 rather than folded in, since the method is already shared by #994 and #1000 in flight.The confidentiality arm of
planIndexUpdatesis reached by no case here. It cannot be reached by the fixture's equality index: enabling confidentiality on it changes the index ID toequality:hash, which makes the change an addition and a removal rather than an update. #1000 replaces that arm — the decision moves to a comparison of the two configurations, and the writes change — so its pin belongs there.Note for the merge
#994 restructures the same method for #962 — it moves the publication of
config,indexingOptionsandindexIdToIndexesaround the same three writes — and #1000 restructures it again for #992, takingsetConfidentialout ofIndex, whichplanIndexUpdatescalls. The three conflict textually and whichever land later will need the rebase.