Don't crash on concurrent access to telemetrySettings.json#178
Conversation
There was a problem hiding this comment.
Copilot wasn't able to review any files in this pull request.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: isourabh <2982389+isourabh@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The current LoadAsync exception handling can swallow cancellation and perform unintended side effects, and one of the new tests doesn’t yet assert the behavior its name implies.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
MSStore.CLI/Services/ConfigurationManager.cs:91
- LoadAsync's broad catch-all will also catch OperationCanceledException/TaskCanceledException from the passed CancellationToken (e.g., during retry delays or JSON I/O) and, when clearInvalidConfig is true, treat it like a parse failure and call ClearAsync. Cancellation should always propagate to avoid unintended side effects (including potentially overwriting a config file) when a caller cancels the operation.
catch
{
if (!clearInvalidConfig)
{
throw;
}
return await ClearAsync(ct);
}
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
Co-authored-by: isourabh <2982389+isourabh@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new unit test assertion style is likely to break compilation, and the telemetry-save IOException handling can unintentionally enable telemetry for a run when the settings file can’t be read/persisted under contention.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
…test style Co-authored-by: isourabh <2982389+isourabh@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new retry/fallback paths currently treat broad IOException cases as “file in use,” which can mask unrelated IO failures and diverges from the intended contention-only handling.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
MSStore.CLI.UnitTests/ConfigurationManagerUnitTests.cs:39
- In SaveAsyncWaitsForOtherProcessToReleaseTheFile, the lock-holding FileStream isn't in a using/try-finally. If an assertion fails before the explicit Dispose(), the file can remain locked and cascade failures into later tests/cleanup. Making it a
using varensures cleanup even on failure.
MSStore.CLI/Services/ConfigurationManager.cs:122
- OpenAsync retries on any IOException (excluding File/DirectoryNotFound). This can add avoidable startup delay and log noise for IO failures that will never resolve with backoff (e.g., path too long, media errors). Retrying only on sharing/lock violations keeps the retry logic aligned with the "file in use" goal.
catch (IOException ex) when (attempt < MaxOpenAttempts && ex is not FileNotFoundException and not DirectoryNotFoundException)
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
…etry defaults over an unread file Co-authored-by: isourabh <2982389+isourabh@users.noreply.github.com>
There was a problem hiding this comment.
🟢 Approval recommended
The concurrency fix is well-scoped, aligns with the stated behavior (no crash + no clobber under lock), and is backed by targeted unit tests; only a minor test-hygiene nit was found.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
MSStore.CLI.UnitTests/ConfigurationManagerUnitTests.cs:39
- In SaveAsyncWaitsForOtherProcessToReleaseTheFile, the lock FileStream is manually disposed later. If an assertion fails before
otherProcessFile.Dispose(), the handle can leak and keep the file locked, potentially causing cleanup or later tests to fail for the wrong reason. Using ausing vardeclaration ensures the handle is always released even on early test failure (you can still call Dispose early to release the lock before awaiting the save).
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
Alexandre Zollinger Chohfi (azchohfi)
left a comment
There was a problem hiding this comment.
Took a pass over this. The ConfigurationManager retry/backoff is sound and I verified the Unix errno constants are correct — dotnet/runtime maps EWOULDBLOCK through new IOException(msg, errorInfo.RawErrno), so HResult really is 11 on Linux and 35 on BSD/macOS.
Build is clean and the 5 new tests pass locally on Windows. Three things below: one behavioural change that reaches outside the diff and I think is a real bug, one suggestion about the root cause, and a test-flakiness nit.
|
Copilot please address the three review comments I just left on this PR. 1.
The root problem is that 2. Every open takes 3.
For context on what I checked: the build is clean, all 5 new tests pass locally on Windows, and the Unix errno constants are correct — dotnet/runtime maps |
…eterministic retry test Co-authored-by: azchohfi <527713+azchohfi@users.noreply.github.com>
Addressed all three in 2d819b6:
Full suite passes (199 tests, only the 2 pre-existing |
There was a problem hiding this comment.
🟡 Changes recommended
The new TryLoadAsync return-value contract is documented inconsistently with its behavior, and a unit test’s retry-signal logger can trigger on unrelated Information logs, weakening reliability.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Lite
…dAsync contract RetrySignalingLogger completed on any Information log, but ConfigurationManager also logs at Information when it creates the settings directory. On a machine that has never run the CLI, ClearAsync in the arrange step fired the signal before a single open retry had happened, so the test could proceed without ever observing the behaviour it exists to prove. Match the retry message instead. The TryLoadAsync docs claimed the flag reported whether the configuration was "actually read from disk", but it is also true for a missing file (created with defaults) and for repaired invalid content. Describe what it actually means: the stored state is known, and it is false only under contention. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99c1d195-484e-4482-bafe-1df997d5aafc
There was a problem hiding this comment.
🟡 Changes recommended
TryLoadAsync currently swallows all IOException (not just contention) and can misreport Readable, and a new reset failure message is logged at a level hidden by default so users may see a silent -1 exit.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Lite
…at never happened The repair path called LoadAsync(true), which swallows contention and returns a default instance. If the file was invalid on the first attempt and then became locked, TryLoadAsync reported Readable=true for a configuration it never read, which is the exact case ResetAsync relies on the flag to prevent. Recreate the file with ClearAsync instead, which surfaces the lock rather than hiding it. Readable is now also false for non-contention I/O failures, since the caller's question is whether the stored state is known, not why it isn't. The contract is documented that way and both failure paths log the underlying exception. ResetAsync reported its refusals only through LogError, but the default minimum log level is Critical, so `reconfigure --reset` exited -1 with nothing printed. Route both refusals through IAnsiConsole, matching ConfigureAsync, and assert in the credential-safety tests that the reason actually reaches the console. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99c1d195-484e-4482-bafe-1df997d5aafc
There was a problem hiding this comment.
🟢 Approval recommended
The concurrency crash is addressed with targeted retries and safe fallback logic, and the new behavior is backed by focused unit tests; remaining feedback is a minor logging deduplication nit.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Lite
LoadAsync already logs a warning for sharing/lock violations before rethrowing, so TryLoadAsync logging every IOException duplicated the entry under --verbose. Only report the I/O failures LoadAsync does not. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99c1d195-484e-4482-bafe-1df997d5aafc
There was a problem hiding this comment.
🔵 Needs a closer look
TryLoadAsync currently treats FileNotFoundException/DirectoryNotFoundException (e.g., delete-between-exists-and-open race) as Readable=false, contradicting the documented “missing file is known/default” contract and causing unnecessary aborts for callers like reset.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
MSStore.CLI/Services/ConfigurationManager.cs:139
- TryLoadAsync catches any IOException from LoadAsync(false) and returns (new T(), Readable=false). That includes FileNotFoundException/DirectoryNotFoundException if the file (or settings directory) is deleted between the File.Exists() check in LoadAsync and the subsequent open, which contradicts the interface docs that treat a missing file as a known/default state and can make callers (e.g., ResetAsync) abort unnecessarily on this race. Handle missing-file exceptions here by recreating defaults via ClearAsync and returning Readable=true, falling back to Readable=false only if recreation also fails.
- Files reviewed: 8/8 changed files
- Comments generated: 0 new
- Review effort level: Lite
LoadAsync checks File.Exists and then opens, so a file deleted in between throws FileNotFoundException. TryLoadAsync caught that as a generic IOException and reported Readable=false, contradicting the contract that a missing file is a known state and making ResetAsync abort on a benign race. Recreate the defaults instead, falling back to Readable=false only if recreation itself fails. Extract TryRecreateAsync, now that both the vanished and the invalid-content paths need it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99c1d195-484e-4482-bafe-1df997d5aafc
There was a problem hiding this comment.
🟢 Approval recommended
The change set addresses the reported contention crash with bounded retries, safe fallback behavior, and targeted unit tests covering the critical concurrency scenarios.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Will relook at this once the PR to route output to stdout is merged |
Two
msstoreprocesses running at once could kill one of them with an unhandledIOException, sinceProgram.Mainloads and rewritestelemetrySettings.jsonbefore the command line is even parsed andConfigurationManageropened the file with no retry or error handling.ConfigurationManagerLoadAsync/ClearAsync/SaveAsyncopen the file via a newOpenAsynchelper that retries a sharing violation up to 5 times with incremental backoff (50 ms × attempt).FileNotFoundException/DirectoryNotFoundExceptionare excluded so genuinely bad paths still fail fast.LoadAsynchandlesIOExceptionseparately from the existing "invalid config" path: contention now logs a warning and returns a default instance rather than falling through toClearAsync, which would truncate the file the other process is writing (and throw again from inside thecatch).ProgramCreateTelemetryClientAsyncignoresIOExceptionwhen persisting telemetry settings — this is incidental bookkeeping and should never fail the command being run.Tests
ConfigurationManagerUnitTestscovers a save that retries until an exclusive lock is released, a load under lock returning a default without clobbering the file, and concurrent load/save cycles.Retry bounds are deliberately short: worst case ~500 ms before falling back, which keeps startup cost negligible for the common uncontended case.