Skip to content

refactor: make LifecycleManager the sole owner of all download management logic#321

Merged
SuperCoolPencil merged 14 commits intomainfrom
dumb-worker-pool
Apr 4, 2026
Merged

refactor: make LifecycleManager the sole owner of all download management logic#321
SuperCoolPencil merged 14 commits intomainfrom
dumb-worker-pool

Conversation

@SuperCoolPencil
Copy link
Copy Markdown
Member

@SuperCoolPencil SuperCoolPencil commented Apr 4, 2026

Greptile Summary

This PR promotes download management orchestration (Resume, ResumeBatch, Cancel, UpdateURL) from the worker pool level into LifecycleManager, making the pool a pure executor with no knowledge of DB persistence or event emission. The EngineHooks struct gains ExtractPausedConfig, Cancel, and UpdateURL hooks in place of the former pool-owned Resume. SetLifecycleHooks is refactored to accept a LifecycleHooks struct rather than individual function parameters, which is a cleaner API.

Key highlights:

  • Data race fixed: pool.UpdateURL previously wrote ad.config.URL outside the write lock; it is now correctly performed under p.mu.Lock().
  • New test coverage: manager_test.go adds tests for Resume (hot path, cold path, still-pausing guard, disk hydration), Cancel (from pool, DB-only, completed, not found), and UpdateURL — addressing the coverage gap noted in prior review comments.
  • Pool decoupling: ExtractPausedConfig atomically hands config out of the pool; the pool no longer emits DownloadResumedMsg or knows about state.LoadState.
  • SuppressNotifications: Added to the utils package and wired into both testutil.init() and cmd/main_test.go TestMain to prevent desktop notification spam during tests.

Minor remaining gaps: two inline comments in pause_resume.go describe what the code does without explaining why, and the UpdateURL DB-only path (when no pool hook is wired) has no dedicated test.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues found; the pool data race is fixed, the new orchestration methods are well-tested, and the architectural separation is clean.

All remaining findings are P2 style/quality concerns (two what-only comments, one missing edge-case test for the UpdateURL DB-only path). All previously flagged P0/P1 issues from prior review threads are resolved in this PR.

internal/processing/pause_resume.go for comment quality; internal/processing/manager_test.go for the missing UpdateURL DB-only path test

Important Files Changed

Filename Overview
internal/processing/pause_resume.go New file consolidating Resume/ResumeBatch/Cancel/UpdateURL orchestration into LifecycleManager; hot/cold resume paths and event emission are well-structured, with minor what-only comment issues
internal/processing/manager.go Unchanged lifecycle core; SetEngineHooks and enqueueResolved logic untouched by this refactor
internal/processing/manager_test.go Strong new test coverage for Resume (hot/cold/pausing/hydration), Cancel (pool/DB-only/completed/not-found), and UpdateURL; missing DB-only UpdateURL path test
internal/download/pool.go pool.UpdateURL data race fixed (write now under p.mu.Lock()); Cancel and ExtractPausedConfig are clean pure-executor implementations that emit no events
internal/download/pool_test.go Old Resume tests replaced with ExtractPausedConfig tests; comment about LifecycleManager test location updated and now accurate
internal/core/local_service.go SetLifecycleHooks refactored to take LifecycleHooks struct; Cancel and UpdateURL now properly route through lifecycle manager hooks
cmd/root.go Hook wiring updated to include Cancel/UpdateURL/ExtractPausedConfig; SetLifecycleHooks uses new struct form; lowercase 'surge' error message fix
cmd/autoresume_test.go Test hook setup updated to match new EngineHooks/LifecycleHooks struct signatures
cmd/startup_test.go Same hook-wiring update as autoresume_test.go
cmd/main_test.go SuppressNotifications set in TestMain to prevent notification spam during cmd integration tests
cmd/server.go Minor: 'Surge server' -> 'surge server' in error message for consistent capitalization
internal/engine/types/models.go CancelResult struct added to carry cancel metadata (filename, destPath, completed, wasQueued) from pool back to LifecycleManager
internal/testutil/state_db.go SuppressNotificationsInTests extracted to testutil init() so any test importing testutil suppresses desktop notifications automatically
internal/utils/notify.go SuppressNotifications exported flag added; ensureIcon uses sync.Once to avoid TOCTOU race on icon file creation
internal/core/test_helpers_test.go Test helpers updated to match new SetLifecycleHooks struct signature

Sequence Diagram

sequenceDiagram
    participant Caller
    participant LocalService
    participant LifecycleManager
    participant WorkerPool
    participant DB

    Note over Caller,DB: Resume (hot path — pool still holds download)
    Caller->>LocalService: Resume(id)
    LocalService->>LifecycleManager: Resume(id)
    LifecycleManager->>WorkerPool: GetStatus(id)
    WorkerPool-->>LifecycleManager: status (guard: reject if "pausing")
    LifecycleManager->>WorkerPool: ExtractPausedConfig(id)
    WorkerPool-->>LifecycleManager: *DownloadConfig (atomically removed)
    LifecycleManager->>DB: LoadState(url, destPath)
    DB-->>LifecycleManager: savedState (hydrateConfigFromDisk)
    LifecycleManager->>WorkerPool: AddConfig(cfg)
    LifecycleManager->>LocalService: PublishEvent(DownloadResumedMsg)

    Note over Caller,DB: Cancel
    Caller->>LocalService: Delete(id)
    LocalService->>LifecycleManager: Cancel(id)
    LifecycleManager->>WorkerPool: Cancel(id)
    WorkerPool-->>LifecycleManager: CancelResult\{filename, destPath, completed\}
    LifecycleManager->>DB: GetDownload(id)
    DB-->>LifecycleManager: entry (supplement missing metadata)
    LifecycleManager->>LocalService: PublishEvent(DownloadRemovedMsg)

    Note over Caller,DB: UpdateURL
    Caller->>LocalService: UpdateURL(id, newURL)
    LocalService->>LifecycleManager: UpdateURL(id, newURL)
    LifecycleManager->>WorkerPool: UpdateURL(id, newURL)
    WorkerPool-->>LifecycleManager: nil (in-memory only)
    LifecycleManager->>DB: UpdateURL(id, newURL)
    DB-->>LifecycleManager: nil
    LifecycleManager-->>LocalService: nil
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: internal/processing/pause_resume.go
Line: 90-95

Comment:
**"What" comments should explain "why"**

Two inline comments in this file label *what* the code does without explaining *why* it must work that way:

`// Guard: still transitioning to paused` (line 90) doesn't explain the failure mode being avoided — that `ExtractPausedConfig` returns `nil` while the worker is mid-shutdown, so without this guard the call would silently fall through to a cold-path resume, potentially enqueuing a duplicate config or losing the in-memory byte progress.

`// Mechanical cancel via pool` (line 245) doesn't explain the ordering rationale — that the pool is cancelled first because it holds the live context and in-memory state, and the DB is queried afterwards to supplement metadata for downloads that may have no live pool entry at all.

Suggested rewrites:
```go
// Reject the resume if the worker is still mid-shutdown: ExtractPausedConfig
// would return nil, and we would fall through to a cold-path re-enqueue that
// either errors or silently duplicates the in-flight download.
```
```go
// Cancel the live pool entry first: the pool holds the running context and
// in-memory state. We then query the DB to fill in identity fields for
// downloads that are DB-only (paused in a prior session) or already completed.
```

**Rule Used:** What: Comments must explain *why* code exists, not... ([source](https://app.greptile.com/review/custom-context?memory=4e45ef13-32c2-4753-8060-a44ef767144d))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: internal/processing/manager_test.go
Line: 911-964

Comment:
**Missing test: `UpdateURL` without a pool hook**

Both existing `UpdateURL` tests always wire `hooks.UpdateURL`. The second branch in `LifecycleManager.UpdateURL` — when `hooks.UpdateURL == nil` — calls `state.UpdateURL` directly (the DB-only path, for downloads that have no live pool entry). This path is reachable in production for cold/DB-only downloads and is not covered by any test.

A minimal addition:
```go
func TestLifecycleManager_UpdateURL_DBOnlyPath(t *testing.T) {
    tempDir := testutil.SetupStateDB(t)
    testutil.SeedMasterList(t, types.DownloadEntry{
        ID:       "db-only-id",
        URL:      "http://example.com/old.zip",
        URLHash:  state.URLHash("http://example.com/old.zip"),
        DestPath: filepath.Join(tempDir, "file.zip"),
        Filename: "file.zip",
        Status:   "paused",
    })

    mgr := newLifecycleManagerForTest()
    // No UpdateURL hook — exercises the DB-only path.
    mgr.SetEngineHooks(EngineHooks{})

    if err := mgr.UpdateURL("db-only-id", "http://example.com/new.zip"); err != nil {
        t.Fatalf("UpdateURL (DB-only path) failed: %v", err)
    }
    entry, err := state.GetDownload("db-only-id")
    if err != nil || entry == nil || entry.URL != "http://example.com/new.zip" {
        t.Errorf("DB URL not updated: %v", err)
    }
}
```

**Rule Used:** What: All code changes must include tests for edge... ([source](https://app.greptile.com/review/custom-context?memory=2b22782d-3452-4d55-b059-e631b2540ce8))

How can I resolve this? If you propose a fix, please make it concise.

Reviews (4): Last reviewed commit: "address ai comments" | Re-trigger Greptile

… and DownloadRemovedMsg

WorkerPool should be a pure executor with no event emission responsibilities.

Changes:
- Add CancelResult to engine/types/models.go (shared type to avoid import cycle)
- Cancel() now returns types.CancelResult with metadata for the caller
- Remove DownloadQueuedMsg emission from Add() - moved to LifecycleManager.enqueueResolved()
- Remove DownloadRemovedMsg emission from Cancel()
- Remove state.UpdateURL() call from UpdateURL() - caller handles DB persistence
- Delete Resume() method entirely - replaced by ExtractPausedConfig()
- Add ExtractPausedConfig() which atomically removes a paused download config
  from the pool's memory for re-enqueue by the LifecycleManager
EngineHooks updated:
- Remove Resume hook (pool no longer has Resume method)
- Add ExtractPausedConfig hook (hot-path resume via pool memory)
- Add Cancel hook (pool's mechanical cancel returns CancelResult)
- Add UpdateURL hook (pool's in-memory update only)

New LifecycleManager methods:
- Resume(): unified hot+cold path using ExtractPausedConfig then DB fallback
- ResumeBatch(): updated to use hot+cold path pattern
- Cancel(): calls pool Cancel, publishes DownloadRemovedMsg via event worker
- UpdateURL(): calls pool UpdateURL + state.UpdateURL for DB persistence

DownloadQueuedMsg is now emitted by enqueueResolved() after pool.Add()
so all lifecycle events originate from LifecycleManager, not the pool.
…ooks

LocalDownloadService.Delete() and UpdateURL() now delegate to lifecycle
manager hooks when available, instead of calling WorkerPool directly.

- Add deleteFunc and updateURLFunc fields to LocalDownloadService
- SetLifecycleHooks now accepts cancel and updateURL callbacks
- Delete() delegates to lifecycle.Cancel() which owns event emission
- UpdateURL() delegates to lifecycle.UpdateURL() which owns DB persistence
- Fallback paths kept for tests that don't wire lifecycle hooks
Update all wiring sites to match the new hook signatures:
- root.go: Remove Resume hook, add ExtractPausedConfig/Cancel/UpdateURL
- autoresume_test.go: Same wiring update
- startup_test.go: Same wiring update
- test_helpers_test.go: Same wiring update
- Cancel tests: check CancelResult.Found instead of consuming channel events
  (pool no longer emits DownloadRemovedMsg or any events)
- Remove pool.Resume tests (no longer has Resume method)
- Add TestWorkerPool_ExtractPausedConfig_* tests for new hot-path resume API
- UpdateURL test: verify only in-memory update (no DB persistence check)
- Remove TestWorkerPool_UpdateURL_PersistsToDB (now tested in processing layer)
- Remove unused events/state imports
utils.Notify now checks testing.Testing() and returns early when
running under go test. This prevents beeep.Notify from firing real
desktop notifications for every DownloadCompleteMsg/DownloadErrorMsg
that flows through the event worker during tests.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 4, 2026

Binary Size Analysis

⚠️ Size Increased

Version Human Readable Raw Bytes
Main 18.84 MB 19751204
PR 18.84 MB 19755300
Difference 4.00 KB 4096

Comment thread internal/processing/pause_resume.go
Comment thread internal/core/local_service.go
Comment thread internal/download/pool_test.go Outdated
Comment thread internal/processing/pause_resume.go
Comment thread internal/download/pool.go
@SuperCoolPencil SuperCoolPencil changed the title refactor; make LifecycleManager the sole owner of all download management logic refactor: make LifecycleManager the sole owner of all download management logic Apr 4, 2026
@SuperCoolPencil SuperCoolPencil merged commit 5f1e8b4 into main Apr 4, 2026
15 checks passed
@SuperCoolPencil SuperCoolPencil deleted the dumb-worker-pool branch April 4, 2026 20:51
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.

1 participant