fix(progress): always report status transitions and poll for control signals
This commit is contained in:
parent
763e267bf8
commit
01d62ffa13
5 changed files with 122 additions and 26 deletions
|
|
@ -39,27 +39,32 @@ type ProgressReporter struct {
|
|||
onPause ActionFunc
|
||||
onDeleteFiles ActionFunc
|
||||
onStreamRequested ActionFunc
|
||||
onWatchingChanged func(watching bool)
|
||||
|
||||
mu sync.Mutex
|
||||
latest map[string]*Task // taskID -> task with latest progress
|
||||
mu sync.Mutex
|
||||
latest map[string]*Task // taskID -> task with latest progress
|
||||
lastReported map[string]TaskStatus // taskID -> last status sent to API
|
||||
lastCheckAt time.Time // last time we reported for control-signal polling
|
||||
}
|
||||
|
||||
// NewProgressReporter creates a reporter that flushes every interval.
|
||||
// Accepts *agent.Client directly (backwards compatible).
|
||||
func NewProgressReporter(ac *agent.Client, interval time.Duration) *ProgressReporter {
|
||||
return &ProgressReporter{
|
||||
reporter: ac,
|
||||
interval: interval,
|
||||
latest: make(map[string]*Task),
|
||||
reporter: ac,
|
||||
interval: interval,
|
||||
latest: make(map[string]*Task),
|
||||
lastReported: make(map[string]TaskStatus),
|
||||
}
|
||||
}
|
||||
|
||||
// NewProgressReporterWithTransport creates a reporter using a Transport.
|
||||
func NewProgressReporterWithTransport(t agent.Transport, interval time.Duration) *ProgressReporter {
|
||||
return &ProgressReporter{
|
||||
reporter: &transportStatusAdapter{t: t},
|
||||
interval: interval,
|
||||
latest: make(map[string]*Task),
|
||||
reporter: &transportStatusAdapter{t: t},
|
||||
interval: interval,
|
||||
latest: make(map[string]*Task),
|
||||
lastReported: make(map[string]TaskStatus),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +92,12 @@ func (r *ProgressReporter) SetStreamRequestedHandler(fn ActionFunc) { r.onStream
|
|||
// SetWatchingFunc sets the function that checks if someone is viewing downloads.
|
||||
func (r *ProgressReporter) SetWatchingFunc(fn WatchingFunc) { r.isWatching = fn }
|
||||
|
||||
// SetWatchingChangedHandler sets a callback invoked when the server's watching flag changes.
|
||||
// This allows the daemon to update its Watching state from status responses (not just heartbeats).
|
||||
func (r *ProgressReporter) SetWatchingChangedHandler(fn func(watching bool)) {
|
||||
r.onWatchingChanged = fn
|
||||
}
|
||||
|
||||
// Track registers a task for progress tracking.
|
||||
func (r *ProgressReporter) Track(task *Task) {
|
||||
r.mu.Lock()
|
||||
|
|
@ -99,6 +110,7 @@ func (r *ProgressReporter) Untrack(taskID string) {
|
|||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.latest, taskID)
|
||||
delete(r.lastReported, taskID)
|
||||
}
|
||||
|
||||
// Run starts the periodic flush loop. Blocks until ctx is cancelled.
|
||||
|
|
@ -123,23 +135,38 @@ func (r *ProgressReporter) flush(ctx context.Context) {
|
|||
for _, t := range r.latest {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
// Snapshot lastReported under the same lock
|
||||
lastReported := make(map[string]TaskStatus, len(r.lastReported))
|
||||
for k, v := range r.lastReported {
|
||||
lastReported[k] = v
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
// When nobody is watching, only report final states (completed/failed).
|
||||
// This saves ~99% of API requests when the user isn't on the downloads page.
|
||||
// When nobody is watching, only report final states, status transitions,
|
||||
// and periodic check-ins (every 30s) so we still receive control signals
|
||||
// (cancel/pause) from the server.
|
||||
watching := r.isWatching == nil || r.isWatching()
|
||||
controlCheckDue := time.Since(r.lastCheckAt) >= 30*time.Second
|
||||
|
||||
var reportable []*Task
|
||||
for _, task := range tasks {
|
||||
status := task.GetStatus()
|
||||
isFinal := status == StatusCompleted || status == StatusFailed
|
||||
isActive := status == StatusDownloading || status == StatusVerifying ||
|
||||
status == StatusOrganizing || status == StatusSeeding
|
||||
if isFinal || (watching && isActive) {
|
||||
status == StatusOrganizing || status == StatusSeeding ||
|
||||
status == StatusResolving
|
||||
// Always report status transitions so the DB reflects the current state.
|
||||
prev := lastReported[task.ID]
|
||||
isTransition := prev == "" || prev != status
|
||||
if isFinal || isTransition || (watching && isActive) || (controlCheckDue && isActive) {
|
||||
reportable = append(reportable, task)
|
||||
}
|
||||
}
|
||||
|
||||
if controlCheckDue {
|
||||
r.lastCheckAt = time.Now()
|
||||
}
|
||||
|
||||
if len(reportable) == 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -152,20 +179,27 @@ func (r *ProgressReporter) flush(ctx context.Context) {
|
|||
|
||||
// Fallback: individual requests
|
||||
for _, task := range reportable {
|
||||
statusAtReport := task.GetStatus() // capture before HTTP round-trip
|
||||
update := task.ToStatusUpdate()
|
||||
resp, err := r.reporter.ReportStatus(ctx, update)
|
||||
if err != nil {
|
||||
log.Printf("[%s] progress report failed: %v", task.ID[:8], err)
|
||||
continue
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.lastReported[task.ID] = statusAtReport
|
||||
r.mu.Unlock()
|
||||
r.handleResponse(task, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProgressReporter) flushBatch(ctx context.Context, batcher BatchStatusReporter, tasks []*Task) {
|
||||
updates := make([]agent.StatusUpdate, len(tasks))
|
||||
// Capture status before HTTP round-trip to avoid missed transitions
|
||||
statusAtReport := make([]TaskStatus, len(tasks))
|
||||
for i, task := range tasks {
|
||||
updates[i] = task.ToStatusUpdate()
|
||||
statusAtReport[i] = task.GetStatus()
|
||||
}
|
||||
|
||||
resp, err := batcher.BatchReportStatus(ctx, updates)
|
||||
|
|
@ -174,10 +208,20 @@ func (r *ProgressReporter) flushBatch(ctx context.Context, batcher BatchStatusRe
|
|||
return
|
||||
}
|
||||
|
||||
// Propagate watching flag from batch response
|
||||
if resp.Watching && r.onWatchingChanged != nil {
|
||||
r.onWatchingChanged(true)
|
||||
}
|
||||
|
||||
// Match results back to tasks by index (server returns in same order)
|
||||
if len(resp.Results) != len(tasks) {
|
||||
log.Printf("batch response mismatch: sent %d updates, got %d results", len(tasks), len(resp.Results))
|
||||
}
|
||||
r.mu.Lock()
|
||||
for i, task := range tasks {
|
||||
r.lastReported[task.ID] = statusAtReport[i]
|
||||
}
|
||||
r.mu.Unlock()
|
||||
for i, result := range resp.Results {
|
||||
if i < len(tasks) {
|
||||
r.handleResponse(tasks[i], &result)
|
||||
|
|
@ -186,6 +230,11 @@ func (r *ProgressReporter) flushBatch(ctx context.Context, batcher BatchStatusRe
|
|||
}
|
||||
|
||||
func (r *ProgressReporter) handleResponse(task *Task, resp *agent.StatusResponse) {
|
||||
// Propagate watching flag from status response to daemon
|
||||
if resp.Watching && r.onWatchingChanged != nil {
|
||||
r.onWatchingChanged(true)
|
||||
}
|
||||
|
||||
if resp.Cancelled {
|
||||
log.Printf("[%s] cancelled by user (via web)", task.ID[:8])
|
||||
r.Untrack(task.ID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue