feat: add durable safe retries with uncertainty tracking and SDK examples
This commit is contained in:
@@ -216,14 +216,14 @@ func (s *TaskStore) Submit(ctx context.Context, token auth.Token, key string, in
|
||||
return out, err
|
||||
}
|
||||
|
||||
const taskColumns = `id,namespace_id,queue_id,state,revision,handler,handler_version,payload_version,payload,result,COALESCE(business_key,''),available_at,start_deadline_at,deadline_at,recovery_safety,policy_snapshot,attempt_count,cancel_requested_at,completed_at,created_at,updated_at`
|
||||
const taskColumns = `id,namespace_id,queue_id,state,revision,handler,handler_version,payload_version,payload,result,COALESCE(business_key,''),available_at,start_deadline_at,deadline_at,recovery_safety,policy_snapshot,attempt_count,cancel_requested_at,completed_at,created_at,updated_at,effect_uncertain`
|
||||
|
||||
type scanner interface{ Scan(...any) error }
|
||||
|
||||
func scanTask(row scanner) (service.TaskView, error) {
|
||||
var v service.TaskView
|
||||
var policy, payload, result []byte
|
||||
err := row.Scan(&v.ID, &v.NamespaceID, &v.QueueID, &v.State, &v.Revision, &v.Handler, &v.HandlerVersion, &v.PayloadVersion, &payload, &result, &v.BusinessKey, &v.AvailableAt, &v.StartDeadlineAt, &v.DeadlineAt, &v.RecoverySafety, &policy, &v.AttemptCount, &v.CancelRequestedAt, &v.CompletedAt, &v.CreatedAt, &v.UpdatedAt)
|
||||
err := row.Scan(&v.ID, &v.NamespaceID, &v.QueueID, &v.State, &v.Revision, &v.Handler, &v.HandlerVersion, &v.PayloadVersion, &payload, &result, &v.BusinessKey, &v.AvailableAt, &v.StartDeadlineAt, &v.DeadlineAt, &v.RecoverySafety, &policy, &v.AttemptCount, &v.CancelRequestedAt, &v.CompletedAt, &v.CreatedAt, &v.UpdatedAt, &v.EffectUncertain)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func (s *TaskStore) ListTasks(ctx context.Context, token auth.Token, in service.
|
||||
if !p.AllowsQueue(in.QueueID) {
|
||||
return fault.Forbidden
|
||||
}
|
||||
q := `SELECT id,queue_id,state,revision,handler,handler_version,COALESCE(business_key,''),available_at,deadline_at,attempt_count,created_at,updated_at FROM pulse_tasks WHERE namespace_id=? AND queue_id=?`
|
||||
q := `SELECT id,queue_id,state,revision,handler,handler_version,COALESCE(business_key,''),available_at,deadline_at,attempt_count,created_at,updated_at,effect_uncertain FROM pulse_tasks WHERE namespace_id=? AND queue_id=?`
|
||||
args := []any{token.NamespaceID, in.QueueID}
|
||||
if in.State != "" {
|
||||
q += " AND state=?"
|
||||
@@ -297,7 +297,7 @@ func (s *TaskStore) ListTasks(ctx context.Context, token auth.Token, in service.
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var v service.TaskSummary
|
||||
if err := rows.Scan(&v.ID, &v.QueueID, &v.State, &v.Revision, &v.Handler, &v.HandlerVersion, &v.BusinessKey, &v.AvailableAt, &v.DeadlineAt, &v.AttemptCount, &v.CreatedAt, &v.UpdatedAt); err != nil {
|
||||
if err := rows.Scan(&v.ID, &v.QueueID, &v.State, &v.Revision, &v.Handler, &v.HandlerVersion, &v.BusinessKey, &v.AvailableAt, &v.DeadlineAt, &v.AttemptCount, &v.CreatedAt, &v.UpdatedAt, &v.EffectUncertain); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Items = append(out.Items, v)
|
||||
@@ -357,7 +357,17 @@ func (s *TaskStore) CancelTask(ctx context.Context, token auth.Token, id core.ID
|
||||
if owner.Valid {
|
||||
return fault.Conflict
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `UPDATE pulse_tasks SET state='CANCELLED',cancel_requested_at=UTC_TIMESTAMP(6),completed_at=UTC_TIMESTAMP(6),updated_at=UTC_TIMESTAMP(6),revision=revision+1 WHERE namespace_id=? AND id=? AND revision=? AND state='WAITING'`, token.NamespaceID, id, in.ExpectedRevision)
|
||||
now, err := dbNow(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next := pulse.TaskCancelled
|
||||
var completed *time.Time = &now
|
||||
if task.EffectUncertain {
|
||||
next = pulse.TaskNeedsReview
|
||||
completed = nil
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `UPDATE pulse_tasks SET state=?,cancel_requested_at=?,completed_at=?,updated_at=?,revision=revision+1 WHERE namespace_id=? AND id=? AND revision=? AND state='WAITING'`, next, now, completed, now, token.NamespaceID, id, in.ExpectedRevision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -372,7 +382,7 @@ func (s *TaskStore) CancelTask(ctx context.Context, token auth.Token, id core.ID
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := emitEvent(ctx, tx, token.NamespaceID, id, out.Task.Revision, "task", "task.cancelled", map[string]any{"task_id": id, "state": out.Task.State}); err != nil {
|
||||
if err := emitEvent(ctx, tx, token.NamespaceID, id, out.Task.Revision, "task", resultEvent(out.Task.State), map[string]any{"task_id": id, "state": out.Task.State, "reason": "CANCEL_REQUESTED", "effect_uncertain": out.Task.EffectUncertain}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAudit(ctx, tx, token, identity, id, "task", "task.cancel", in.Reason, map[string]any{"state": task.State, "revision": fmt.Sprint(task.Revision)}, map[string]any{"state": out.Task.State, "revision": fmt.Sprint(out.Task.Revision)}); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user