Skip to content

fix: Improve consistency of stub modification#3379

Merged
Mahoney merged 14 commits into
masterfrom
more-consistent-stub-editing
Apr 8, 2026
Merged

fix: Improve consistency of stub modification#3379
Mahoney merged 14 commits into
masterfrom
more-consistent-stub-editing

Conversation

@Mahoney

@Mahoney Mahoney commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

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

  • Recommended: Join WireMock Slack to get any help in #help-contributing or a project-specific channel like #wiremock-java
  • The PR request is well described and justified, including the body and the references
  • The PR title represents the desired changelog entry
  • The repository's code style is followed (see the contributing guide)
  • Test coverage that demonstrates that the change works as expected
  • For new features, there's necessary documentation in this pull request or in a subsequent PR to wiremock.org

@Mahoney Mahoney requested a review from a team as a code owner March 31, 2026 16:32
@Mahoney Mahoney force-pushed the more-consistent-stub-editing branch 3 times, most recently from 91d60d6 to 731e7d1 Compare April 2, 2026 21:45
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.
@Mahoney Mahoney force-pushed the more-consistent-stub-editing branch from 731e7d1 to f04afd8 Compare April 3, 2026 14:48
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.
@Mahoney Mahoney force-pushed the more-consistent-stub-editing branch from f04afd8 to 96b30fb Compare April 3, 2026 14:52
Mahoney and others added 6 commits April 3, 2026 15:55
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>
Comment on lines +183 to +190
scenarios.onStubMappingAdded(mapping);

if (mapping.shouldBePersisted()) {
try {
save(mapping);
} catch (Exception e) {
store.remove(mapping.getId());
scenarios.onStubMappingRemoved(mapping);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. pushing persistence down into the stub mappings implementation is a great change imo

@Mahoney Mahoney marked this pull request as draft April 3, 2026 15:32
@Mahoney

Mahoney commented Apr 3, 2026

Copy link
Copy Markdown
Collaborator Author

Should have marked this as a draft to start with...

Comment thread wiremock-core/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java Outdated
}

@Override
public void saveMappings() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Mahoney Mahoney force-pushed the more-consistent-stub-editing branch from 29e7c14 to 50e2c64 Compare April 7, 2026 09:57
Mahoney added 5 commits April 7, 2026 14:12
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.
@Mahoney Mahoney marked this pull request as ready for review April 7, 2026 17:41
@Mahoney Mahoney force-pushed the more-consistent-stub-editing branch from 35c2bfb to 582d49a Compare April 7, 2026 18:00
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.
@Mahoney Mahoney force-pushed the more-consistent-stub-editing branch from 582d49a to 383ef97 Compare April 7, 2026 18:02
@Mahoney Mahoney merged commit 85c9d08 into master Apr 8, 2026
5 checks passed
@Mahoney Mahoney deleted the more-consistent-stub-editing branch April 8, 2026 09:31
@Mahoney Mahoney mentioned this pull request Apr 9, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants