Add runFetchAsync helper and migrate the tasks fetch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+45
-20
@@ -81,6 +81,37 @@ func (c *Controller) SetTasks(p tasks.Provider) {
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runFetchAsync launches a generation-guarded background fetch. The caller has
|
||||||
|
// already captured its dependencies and (for the *Locked callers) holds c.mu;
|
||||||
|
// this method only spawns the goroutine, which re-acquires the lock itself.
|
||||||
|
//
|
||||||
|
// Closure contract:
|
||||||
|
// - fetch performs the I/O and runs with NO lock held; it must not touch c
|
||||||
|
// fields without taking c.mu itself.
|
||||||
|
// - stale runs under the re-acquired c.mu and returns true to DISCARD the
|
||||||
|
// result (e.g. a newer generation superseded this one).
|
||||||
|
// - apply runs under the re-acquired c.mu and records the result (persisting
|
||||||
|
// itself when the role requires it); it must NOT call c.notify — the helper
|
||||||
|
// owns the post-unlock notify, and notify self-locks, so calling it under
|
||||||
|
// c.mu would deadlock.
|
||||||
|
//
|
||||||
|
// On a non-stale completion the helper notifies after releasing the lock.
|
||||||
|
func (c *Controller) runFetchAsync(timeout time.Duration, fetch func(ctx context.Context), stale func() bool, apply func()) {
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
fetch(ctx)
|
||||||
|
c.mu.Lock()
|
||||||
|
if stale() {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apply()
|
||||||
|
c.mu.Unlock()
|
||||||
|
c.notify()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// startTasksFetchLocked kicks off an asynchronous Today() fetch when a provider
|
// startTasksFetchLocked kicks off an asynchronous Today() fetch when a provider
|
||||||
// is set. Mirrors RequestCoach: generation-guarded, discards stale or
|
// is set. Mirrors RequestCoach: generation-guarded, discards stale or
|
||||||
// post-planning results, and notifies on completion. Caller holds mu.
|
// post-planning results, and notifies on completion. Caller holds mu.
|
||||||
@@ -94,26 +125,20 @@ func (c *Controller) startTasksFetchLocked() {
|
|||||||
gen := c.tasksGen
|
gen := c.tasksGen
|
||||||
c.tasksStatus = tasksPending
|
c.tasksStatus = tasksPending
|
||||||
provider := c.tasksProvider
|
provider := c.tasksProvider
|
||||||
go func() {
|
var list []tasks.Task
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tasksTimeout)
|
var err error
|
||||||
defer cancel()
|
c.runFetchAsync(tasksTimeout,
|
||||||
list, err := provider.Today(ctx)
|
func(ctx context.Context) { list, err = provider.Today(ctx) },
|
||||||
|
func() bool { return gen != c.tasksGen || c.runtimeState != domain.RuntimePlanning },
|
||||||
c.mu.Lock()
|
func() {
|
||||||
if gen != c.tasksGen || c.runtimeState != domain.RuntimePlanning {
|
if err != nil {
|
||||||
c.mu.Unlock()
|
c.tasksStatus = tasksError
|
||||||
return // stale or left planning: discard
|
c.tasksList = nil
|
||||||
}
|
} else {
|
||||||
if err != nil {
|
c.tasksStatus = tasksReady
|
||||||
c.tasksStatus = tasksError
|
c.tasksList = list
|
||||||
c.tasksList = nil
|
}
|
||||||
} else {
|
})
|
||||||
c.tasksStatus = tasksReady
|
|
||||||
c.tasksList = list
|
|
||||||
}
|
|
||||||
c.mu.Unlock()
|
|
||||||
c.notify()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetKnowledge injects the Knowledge source. Mirrors SetTasks. A nil source
|
// SetKnowledge injects the Knowledge source. Mirrors SetTasks. A nil source
|
||||||
|
|||||||
Reference in New Issue
Block a user