Skip to content

Track a queued save with a flag, not by comparing timestamps - #328

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/project-thread-02u3c5
Sep 29, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/project-thread-02u3c5

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Before: IsSaveQueued() returned SaveQueuedTime > LastSaveTime. When QueueSave() ran right after Save(), the two DateTime.UtcNow reads could return the same value. The comparison then reported nothing queued, so the change was never written: SaveIfRequired() skipped it after the debounce, and Dispose() did not flush it either. On main's CI run 36567126443 this failed TestMultipleSavesOnlyWriteOnceWithinDebouncePeriod on macOS only. The test waited 3.1 s against a 3 s debounce, so the debounce had elapsed, but "Data2" was never saved.

After: QueueSave() sets a HasQueuedSave flag and a successful Save() clears it. IsSaveQueued() reads the flag. SaveQueuedTime still drives the debounce.

Why macOS only: .NET's DateTime.UtcNow on macOS comes from clock_gettime(CLOCK_REALTIME), which advances in whole microseconds. A Save followed immediately by a QueueSave fits inside one microsecond. Linux reads the clock in nanoseconds and Windows uses the precise system time, so on those runners the two reads never tie. A wall clock set back would drop a queued save the same way. Whether a save is outstanding depends on the order of the two calls, not on when each one happened, so a flag is the correct model. This is a bug in the library, not in the test: an application calling Save() and then QueueSave() could silently lose data.

How:

  • AppData.cs: adds the HasQueuedSave flag. The flag is cleared only after the write succeeds, so a failed write leaves the save queued.
  • New test TestQueueSaveIsNotLostWhenTheClockHasNotAdvancedSinceTheLastSave reproduces the tied or backwards clock on every platform by setting LastSaveTime to DateTime.MaxValue before QueueSave(). It fails deterministically against the old comparison.
  • New test TestSaveClearsQueuedSave pins the other half: a save clears the flag.
  • The failing test is unchanged. It fails because of this bug, not because of its timing.
  • CLAUDE.md records why the flag replaced the timestamp comparison.

I couldn't build or run the tests locally because there is no .NET SDK in this environment. CI will be the first to compile and run this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RTMFFSY8FY5CpAhf6ikNH1


Generated by Claude Code

IsSaveQueued answered SaveQueuedTime > LastSaveTime. DateTime.UtcNow
advances in whole microseconds on macOS, so a QueueSave issued straight
after a Save can read the same instant as the save did. The comparison
then says nothing is queued, and the change is never written: not by
SaveIfRequired after the debounce, and not by Dispose either.

That is what failed TestMultipleSavesOnlyWriteOnceWithinDebouncePeriod on
the macOS runner only: the debounce had elapsed (the test waited 3.1 s
against a 3 s debounce) but the queued "Data2" was never saved. Linux and
Windows read the clock finely enough that the two calls never tied.

A save is outstanding because QueueSave ran after the last Save, which is
an ordering fact, so it is now a flag set by QueueSave and cleared by a
successful Save. SaveQueuedTime still drives the debounce. A regression
test pins the tied-clock case deterministically.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTMFFSY8FY5CpAhf6ikNH1
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants