-
-
Notifications
You must be signed in to change notification settings - Fork 2
fix: functionality fixes — health analyzer, depth sort, history race, multi-disk #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,10 +28,19 @@ public sealed class SpeedTestHistoryService | |
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase | ||
| }; | ||
|
|
||
| // FUNC-M4: Serialize all file operations to prevent concurrent SaveAsync | ||
| // calls from racing (load-modify-save is not atomic). A SemaphoreSlim(1,1) | ||
| // acts as an async-compatible mutex. | ||
| private readonly SemaphoreSlim _fileLock = new(1, 1); | ||
|
|
||
| /// <summary> | ||
| /// Loads all saved results from disk. Returns empty list on any error. | ||
| /// </summary> | ||
| public async Task<List<SpeedTestResult>> LoadAsync(CancellationToken ct = default) | ||
| => await LoadCoreAsync(ct).ConfigureAwait(false); | ||
|
|
||
| /// <summary>Internal load without locking — called from within locked sections.</summary> | ||
| private async Task<List<SpeedTestResult>> LoadCoreAsync(CancellationToken ct) | ||
|
Comment on lines
39
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lock Line 39 still lets public reads bypass Suggested fix public async Task<List<SpeedTestResult>> LoadAsync(CancellationToken ct = default)
- => await LoadCoreAsync(ct).ConfigureAwait(false);
+{
+ await _fileLock.WaitAsync(ct).ConfigureAwait(false);
+ try
+ {
+ return await LoadCoreAsync(ct).ConfigureAwait(false);
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
+}🤖 Prompt for AI Agents |
||
| { | ||
| try | ||
| { | ||
|
|
@@ -68,9 +77,10 @@ public async Task<List<SpeedTestResult>> LoadAsync(CancellationToken ct = defaul | |
| /// </summary> | ||
| public async Task SaveAsync(SpeedTestResult result, CancellationToken ct = default) | ||
| { | ||
| await _fileLock.WaitAsync(ct).ConfigureAwait(false); | ||
| try | ||
| { | ||
| var all = await LoadAsync(ct).ConfigureAwait(false); | ||
| var all = await LoadCoreAsync(ct).ConfigureAwait(false); | ||
| all.Add(result); | ||
|
|
||
| // Trim per engine: keep only the most recent MaxPerEngine entries. | ||
|
|
@@ -104,13 +114,18 @@ public async Task SaveAsync(SpeedTestResult result, CancellationToken ct = defau | |
| { | ||
| Log.Warning(ex, "Access denied saving speed test history"); | ||
| } | ||
| finally | ||
| { | ||
| _fileLock.Release(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Clears history for a specific engine, or all history if engine is null. | ||
| /// </summary> | ||
| public async Task ClearAsync(string? engine = null, CancellationToken ct = default) | ||
| { | ||
| await _fileLock.WaitAsync(ct).ConfigureAwait(false); | ||
| try | ||
| { | ||
| if (engine == null) | ||
|
|
@@ -120,7 +135,7 @@ public async Task ClearAsync(string? engine = null, CancellationToken ct = defau | |
| return; | ||
| } | ||
|
|
||
| var all = await LoadAsync(ct).ConfigureAwait(false); | ||
| var all = await LoadCoreAsync(ct).ConfigureAwait(false); | ||
| var filtered = all.Where(r => !string.Equals(r.Engine, engine, StringComparison.OrdinalIgnoreCase)).ToList(); | ||
|
|
||
| if (filtered.Count == 0) | ||
|
|
@@ -151,6 +166,10 @@ public async Task ClearAsync(string? engine = null, CancellationToken ct = defau | |
| { | ||
| Log.Warning(ex, "Access denied clearing speed test history"); | ||
| } | ||
| finally | ||
| { | ||
| _fileLock.Release(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary>JSON-serializable DTO for history entries.</summary> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: laurentiu021/SystemManager
Length of output: 8857
The instance-level lock does not prevent cross-instance writes; make it static or use a shared coordinator.
The service is instantiated manually in
SpeedTestViewModel(line 19:private readonly SpeedTestHistoryService _history = new();), not registered as a singleton. Each ViewModel instance therefore has its own_fileLock, so concurrent saves from different ViewModels still race. Consider a static lock, a shared file coordinator, or registering the service as singleton in DI.🤖 Prompt for AI Agents