Billionmail/BillionMail · error
task %d: update database threads failed: %w
Error message
task %d: update database threads failed: %w
What it means
The final step of UpdateTaskThreads writes the new thread count to the database (g.DB update on the task row). If the Update() call errors, it is wrapped with %w as "task %d: update database threads failed: <cause>". The task lookup succeeded but the persistence step failed, so the in-memory pool state may have changed while the DB did not.
Source
Thrown at core/internal/service/batch_mail/task_executor.go:1562
releaseMsg := fmt.Sprintf("task %d: old pool released", taskId)
g.Log().Info(context.Background(), releaseMsg)
}(oldPool, oldPoolSize, runningWorkers)
}
} else {
keepMsg := fmt.Sprintf("task %d: pool size keep unchanged (%d), only adjust rate controller", taskId, oldPoolSize)
g.Log().Info(context.Background(), keepMsg)
}
}
// update threads in database
_, err = g.DB().Model("email_tasks").
Where("id", taskId).
Data(g.Map{"threads": newThreads}).
Update()
if err != nil {
return fmt.Errorf("task %d: update database threads failed: %w", taskId, err)
}
return nil
}
View on GitHub (pinned to fc36c76c05)
Solutions
- Inspect the wrapped cause via errors.Is/As to distinguish connectivity, lock-timeout, or schema errors.
- Check DB health and re-run the update once connectivity is restored.
- Retry with backoff for transient errors (deadlock/serialization); consider optimistic locking if contention is common.
- Verify the tasks table schema matches the entity (run pending migrations).
Example fix
// before: fire and forget
_ = e.UpdateTaskThreads(taskId, threads)
// after
if err := e.UpdateTaskThreads(taskId, threads); err != nil {
log.Printf("persist threads for task %d failed: %v", taskId, err)
// reconcile pool size from DB or retry
} Defensive patterns
Strategy: try-catch
Validate before calling
// Go: pre-check the row is updatable before pool resize
count, err := g.DB().Model("tasks").Where("id", taskId).Count()
if err != nil || count == 0 { return fmt.Errorf("task %d not updatable", taskId) } Type guard
func isTransientDBErr(err error) bool {
msg := err.Error()
return strings.Contains(msg, "deadlock") || strings.Contains(msg, "connection reset") || strings.Contains(msg, "i/o timeout")
} Try / catch
if err := executor.UpdateTaskThreads(taskId, threads); err != nil {
var wrapped error
if strings.Contains(err.Error(), "update database threads failed") && errors.As(err, &wrapped) {
log.Printf("db write failed for task %d: %v", taskId, wrapped)
if isTransientDBErr(wrapped) { /* retry with backoff */ }
}
return err
} Prevention
- Verify the in-memory pool and DB threads value agree after any failed update (reconcile on error).
- Keep schema migrations in sync with the entity definitions.
- Size the DB connection pool for bulk-send workloads to avoid pool exhaustion.
- Wrap ORM writes in a helper with uniform logging and retry for transient errors.
When it happens
Trigger: The ORM Update() fails: DB connection dropped mid-operation, unique/lock contention on the task row, table schema mismatch, or context cancellation during the write.
Common situations: Postgres connection pool exhaustion under bulk-send load; deadlock on the tasks row with a concurrent update; migration drift (missing column or changed type) after a version upgrade.
Related errors
- get task info failed: %w
- failed to get all domains: %w
- fail to check domain: %w
- failed to get all emails: %w
- failed to get all mailboxes: %w
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/8359a9f89dee01ff.
Report an issue: GitHub.