refactor: unify download progress state management using SessionReset and effective total size passing#411
Conversation
… and effective total size passing
| func (ps *ProgressState) SessionReset() { | ||
| ps.mu.Lock() | ||
| defer ps.mu.Unlock() | ||
|
|
||
| ps.Downloaded.Store(0) | ||
| ps.VerifiedProgress.Store(0) | ||
| ps.SessionStartBytes = 0 | ||
| ps.StartTime = time.Now() | ||
| ps.SavedElapsed = 0 | ||
| ps.Done.Store(false) | ||
| ps.Paused.Store(false) | ||
| ps.Pausing.Store(false) | ||
| ps.Error.Store(nil) | ||
|
|
||
| // Clear mirrors error status | ||
| for i := range ps.Mirrors { | ||
| ps.Mirrors[i].Error = false | ||
| } | ||
|
|
||
| // Reset chunk tracking if initialized | ||
| if ps.BitmapWidth > 0 { | ||
| ps.ChunkBitmap = make([]byte, len(ps.ChunkBitmap)) | ||
| ps.ChunkProgress = make([]int64, ps.BitmapWidth) | ||
| } | ||
| } |
There was a problem hiding this comment.
SessionReset does not reset ActiveWorkers
After a failed concurrent download, ActiveWorkers may be non-zero if workers haven't fully decremented by the time d.Download() returns (e.g. via a deferred cleanup race). Leaving ActiveWorkers un-reset means the TUI could briefly display a stale connection count between the fallback detection and the moment the single-threaded downloader sets its own ActiveWorkers.
| func (ps *ProgressState) SessionReset() { | |
| ps.mu.Lock() | |
| defer ps.mu.Unlock() | |
| ps.Downloaded.Store(0) | |
| ps.VerifiedProgress.Store(0) | |
| ps.SessionStartBytes = 0 | |
| ps.StartTime = time.Now() | |
| ps.SavedElapsed = 0 | |
| ps.Done.Store(false) | |
| ps.Paused.Store(false) | |
| ps.Pausing.Store(false) | |
| ps.Error.Store(nil) | |
| // Clear mirrors error status | |
| for i := range ps.Mirrors { | |
| ps.Mirrors[i].Error = false | |
| } | |
| // Reset chunk tracking if initialized | |
| if ps.BitmapWidth > 0 { | |
| ps.ChunkBitmap = make([]byte, len(ps.ChunkBitmap)) | |
| ps.ChunkProgress = make([]int64, ps.BitmapWidth) | |
| } | |
| } | |
| func (ps *ProgressState) SessionReset() { | |
| ps.mu.Lock() | |
| defer ps.mu.Unlock() | |
| ps.Downloaded.Store(0) | |
| ps.VerifiedProgress.Store(0) | |
| ps.SessionStartBytes = 0 | |
| ps.StartTime = time.Now() | |
| ps.SavedElapsed = 0 | |
| ps.Done.Store(false) | |
| ps.Paused.Store(false) | |
| ps.Pausing.Store(false) | |
| ps.Error.Store(nil) | |
| ps.ActiveWorkers.Store(0) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/engine/types/progress.go
Line: 227-251
Comment:
**`SessionReset` does not reset `ActiveWorkers`**
After a failed concurrent download, `ActiveWorkers` may be non-zero if workers haven't fully decremented by the time `d.Download()` returns (e.g. via a deferred cleanup race). Leaving `ActiveWorkers` un-reset means the TUI could briefly display a stale connection count between the fallback detection and the moment the single-threaded downloader sets its own `ActiveWorkers`.
```suggestion
func (ps *ProgressState) SessionReset() {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.Downloaded.Store(0)
ps.VerifiedProgress.Store(0)
ps.SessionStartBytes = 0
ps.StartTime = time.Now()
ps.SavedElapsed = 0
ps.Done.Store(false)
ps.Paused.Store(false)
ps.Pausing.Store(false)
ps.Error.Store(nil)
ps.ActiveWorkers.Store(0)
```
How can I resolve this? If you propose a fix, please make it concise.| originalStartTime := time.Now().Add(-5 * time.Second) | ||
| ps.StartTime = originalStartTime | ||
|
|
||
| // Call SetTotalSize with the SAME size | ||
| ps.SetTotalSize(100) | ||
|
|
||
| // Verify StartTime was NOT reset to Now | ||
| if !ps.StartTime.Equal(originalStartTime) { | ||
| t.Errorf("StartTime was reset despite same size: got %v, want %v", ps.StartTime, originalStartTime) | ||
| } | ||
|
|
||
| // Call SetTotalSize with a DIFFERENT size | ||
| ps.SetTotalSize(200) | ||
|
|
||
| // Verify StartTime WAS reset (should be later than original) | ||
| if !ps.StartTime.After(originalStartTime) { | ||
| t.Errorf("StartTime was NOT reset for new size: got %v, want > %v", ps.StartTime, originalStartTime) | ||
| } |
There was a problem hiding this comment.
Direct access to mutex-protected field in test
ps.StartTime is protected by ps.mu (as documented in the struct comment). The test writes to it directly (ps.StartTime = originalStartTime) and reads it back without holding the lock. While safe in a sequential test, Go's -race detector can flag unsynchronized accesses even when no goroutine is concurrently running, and it sets an inconsistent precedent for future test authors.
Consider adding a helper or a lock-guarded setter, or at minimum using ps.mu.Lock()/ps.mu.Unlock() around the direct field accesses in the test to align with the struct's locking discipline.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/engine/types/progress_test.go
Line: 51-68
Comment:
**Direct access to mutex-protected field in test**
`ps.StartTime` is protected by `ps.mu` (as documented in the struct comment). The test writes to it directly (`ps.StartTime = originalStartTime`) and reads it back without holding the lock. While safe in a sequential test, Go's `-race` detector can flag unsynchronized accesses even when no goroutine is concurrently running, and it sets an inconsistent precedent for future test authors.
Consider adding a helper or a lock-guarded setter, or at minimum using `ps.mu.Lock()`/`ps.mu.Unlock()` around the direct field accesses in the test to align with the struct's locking discipline.
How can I resolve this? If you propose a fix, please make it concise.
Greptile Summary
This PR unifies download progress state management by introducing a
SessionReset()method (replacing ad-hoc three-field resets), adding an idempotency guard toSetTotalSize(preventing clock reset when the confirmed size matches the known size), and threadingeffectiveTotalSizeconsistently to both concurrent and single-threaded downloaders. The removal of the post-downloadcfg.State.SetTotalSizecall is safe because the concurrent downloader already callsSetTotalSizeinternally when it receives response headers.Confidence Score: 5/5
Safe to merge — all findings are minor style/hardening suggestions with no blocking correctness issues.
The core logic changes are correct and well-tested. The removed
SetTotalSizecall was verified to be redundant (the concurrent downloader updates state internally). The only open items are a missingActiveWorkersreset inSessionReset()(practically safe since workers exit before the fallback) and inconsistent mutex usage in the new tests.internal/engine/types/progress.go — consider resetting
ActiveWorkersinSessionReset()Important Files Changed
effectiveTotalSizeinstead ofcfg.TotalSizeto both downloaders and replaces the three-line manual reset withSessionReset(). Removal of the post-downloadcfg.State.SetTotalSizecall is safe — the concurrent downloader already callsSetTotalSizeinternally (line 672 of concurrent/downloader.go), so the post-download call was redundant.SessionReset()(comprehensive state wipe for fallback) and an idempotency guard toSetTotalSize(prevents clock reset when the same size is re-confirmed). Minor gap:ActiveWorkersis not reset inSessionReset().TestProgressState_SetTotalSize_IdempotentandTestProgressState_SessionResetwith good coverage. Tests directly access mutex-protected struct fields without holding the lock, which is inconsistent with the struct's locking discipline.Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "test: add idempotency tests for SetTotal..." | Re-trigger Greptile