fix: Improve consistency of stub modification#3379
Conversation
91d60d6 to
731e7d1
Compare
There's a classic two phase commit problem here. For all stub modifications, WireMockApp first mutates StubMappings, then calls MappingsSaver to persist the change. If the call to MappingsSaver fails to persist the change then what is running, and what is persisted, are out of sync until the persisted changes reload. There is of course no 100% robust solution to committing transactions on two independent data stores. However, catching any exception in the calls to MappingsSaver and reverting the in-memory changes to StubMappings seems superior to not doing so. The fix moves persistence into AbstractStubMappings itself. Each mutation method now follows the pattern: run before-listeners, update the store, attempt to save via protected save()/remove() hooks, revert the store silently on failure, and only then run after-listeners. StoreBackedStubMappings overrides the hooks to delegate to a MappingsSaver. This means after-listeners which trigger external side effects only fire once the save has succeeded, and rollback is a direct store revert with no listener involvement. WireMockApp.addStubMapping(), editStubMapping(), and removeStubMapping() are now simple delegations to StubMappings. It is of course possible that the changes were, in fact, persisted despite the exception being thrown; in which case the running state and the persisted state will still be out of sync. I believe there is no solution to this - it could be further mitigated by rechecking, but not ultimately solved. Breaking change: third-party implementations of StubMappings that extend AbstractStubMappings will inherit the new behaviour automatically. **Direct implementors of the StubMappings interface will lose the save behaviour that WireMockApp previously provided**, and will need to implement it themselves if required.
731e7d1 to
f04afd8
Compare
Introduce InMemoryStubMappings (non-saving) alongside StoreBackedStubMappings (saving) to cleanly separate persistence from in-memory operations. Both share the same underlying store and scenarios. WireMockApp now uses InMemoryStubMappings as a load target so that loading mappings from disk never triggers a save back. StoreBackedStubMappings handles all persistence internally via save/remove hooks, eliminating direct MappingsSaver usage from WireMockApp operations (saveMappings, resetMappings, importStubs, removeStubMappings). The updateMappings bulk operation now persists inserted/removed stubs with rollback on failure, matching the pattern already established for addMapping, editMapping and removeMapping. After-listeners only fire after successful persistence.
f04afd8 to
96b30fb
Compare
The previous name didn't communicate that this StubMappings instance must only be used for loading stubs from files (where persistence should be skipped to avoid re-saving stubs already on disk). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ackedStubMappings The two-subclass hierarchy (InMemoryStubMappings with no-op persistence, StoreBackedStubMappings with MappingsSaver delegation) added complexity for what was essentially a null check. AbstractStubMappings now accepts an optional MappingsSaver directly, eliminating the template method pattern and the need for StoreBackedStubMappings as a separate class. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If save() succeeds but remove() fails, the rollback reverts in-memory state but already-persisted saves remain on disk. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
addMapping may trigger persistence, which needs the file metadata to determine the target file path. The metadata must be registered first. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use a no-op implementation instead of null checks, removing conditional logic from the persistence delegation methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The class is no longer abstract, so the old name was misleading. StoreBackedStubMappings accurately describes what it is. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| scenarios.onStubMappingAdded(mapping); | ||
|
|
||
| if (mapping.shouldBePersisted()) { | ||
| try { | ||
| save(mapping); | ||
| } catch (Exception e) { | ||
| store.remove(mapping.getId()); | ||
| scenarios.onStubMappingRemoved(mapping); |
There was a problem hiding this comment.
would it be simpler to execute the scenario operations after the catch blocks? then you dont have to call the inverse operation in the catch block?
There was a problem hiding this comment.
I should hold off looking at this too hard yet, it's more a POC - it's the architectural breaking change that's most interesting to comment on
There was a problem hiding this comment.
makes sense. pushing persistence down into the stub mappings implementation is a great change imo
|
Should have marked this as a draft to start with... |
| } | ||
|
|
||
| @Override | ||
| public void saveMappings() { |
There was a problem hiding this comment.
From what I can tell, this has changed the way mappings are saved. Before it was a single batched operation but now it calls editMapping for each stub. I guess this means everything is then done individually rather than an atomic operation - before/after listener, stub persistence.
What happens if stub persistence fails part way through - say the 10th stub of 20? The first 9 are persisted and all the listeners fired but the remaining 10 are not so we have a partial save and not full batch rollback ?
There was a problem hiding this comment.
Good catch. I've added a single method to MappingsSaver, mutate, so that we can add, update and remove in an atomic operation if desired. I've also rejigged the whole StoreBackedStubMappings.updateMappings method to follow the same patterns as the other persist-then-update-in-memory-state methods.
29e7c14 to
50e2c64
Compare
Source of duplication
Makes it clear that the only difference between `nonPersistingStubMappings` and `stubMappings` is the `MappingsSaver` implementation they use.
In several cases we can try and persist the mapping change to the MappingsSaver early to avoid needing to roll back some of the work if it fails.
35c2bfb to
582d49a
Compare
Save and remove stubs to disk before applying scenario updates and in-memory store mutations, so that persistence failures are caught before the system state has changed.
582d49a to
383ef97
Compare
There's a classic two phase commit problem here. For all stub
modifications, WireMockApp first mutates StubMappings, then calls
MappingsSaver to persist the change. If the call to MappingsSaver
fails to persist the change then what is running, and what is persisted,
are out of sync until the persisted changes reload.
There is of course no 100% robust solution to committing transactions on
two independent data stores. However, catching any exception in the
calls to MappingsSaver and reverting the in-memory changes to
StubMappings seems superior to not doing so.
The fix moves persistence into AbstractStubMappings itself. Each
mutation method now follows the pattern: run before-listeners, update
the store, attempt to save via protected save()/remove() hooks,
revert the store silently on failure, and only then run after-listeners.
StoreBackedStubMappings overrides the hooks to delegate to a
MappingsSaver. This means after-listeners which trigger external
side effects only fire once the save has succeeded, and rollback
is a direct store revert with no listener involvement.
WireMockApp.addStubMapping(), editStubMapping(), and
removeStubMapping() are now simple delegations to StubMappings.
It is of course possible that the changes were, in fact, persisted
despite the exception being thrown; in which case the running state and
the persisted state will still be out of sync. I believe there is no
solution to this - it could be further mitigated by rechecking, but not
ultimately solved.
Breaking change: third-party implementations of StubMappings that
extend AbstractStubMappings will inherit the new behaviour
automatically. Direct implementors of the StubMappings interface will
lose the save behaviour that WireMockApp previously provided, and will
need to implement it themselves if required.
Submitter checklist
#help-contributingor a project-specific channel like#wiremock-java