feat: support durable task cancellation without foreign keys

This commit is contained in:
2026-09-17 17:03:23 +08:00
parent 6805506450
commit 3ce7d2c104
48 changed files with 1642 additions and 164 deletions

View File

@@ -47,16 +47,17 @@ type workerFrame struct {
// 本类型不自动重连、不自动续租、不运行处理器。关闭连接不等于取消业务效果。
// 最多 32 个在途控制请求、8 个未读派发;超时关闭连接以便按原消息身份核对。
type WorkerConnection struct {
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
welcome WorkerWelcome
registration WorkerRegistration
assignments chan WorkerAssignment
done chan struct{}
mu sync.Mutex
err error
pending map[string]chan workerFrame
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
welcome WorkerWelcome
registration WorkerRegistration
assignments chan WorkerAssignment
cancellations chan WorkerCancellation
done chan struct{}
mu sync.Mutex
err error
pending map[string]chan workerFrame
}
// DialWorker 使用 Authorization 和固定子协议连接;ctx 只控制握手。
@@ -116,7 +117,7 @@ func (c *Client) DialWorker(ctx context.Context, registration WorkerRegistration
return nil, ErrWorkerProtocol
}
lifetime, cancel := context.WithCancel(context.Background())
w := &WorkerConnection{conn: conn, ctx: lifetime, cancel: cancel, welcome: welcome, registration: r, assignments: make(chan WorkerAssignment, 8), done: make(chan struct{}), pending: make(map[string]chan workerFrame)}
w := &WorkerConnection{conn: conn, ctx: lifetime, cancel: cancel, welcome: welcome, registration: r, assignments: make(chan WorkerAssignment, 8), cancellations: make(chan WorkerCancellation, 32), done: make(chan struct{}), pending: make(map[string]chan workerFrame)}
ok = true
go w.readLoop()
return w, nil
@@ -175,6 +176,20 @@ func (w *WorkerConnection) readLoop() {
}
continue
}
if f.Type == "task.cancel" {
var c WorkerCancellation
if !w.registration.CancellationNotifications || f.ReplyTo != "" || json.Unmarshal(f.Body, &c) != nil || !validID(c.TaskID) || !validID(c.AttemptID) || c.Epoch == 0 || c.RequestedAt.IsZero() {
w.fail(ErrWorkerProtocol)
return
}
select {
case w.cancellations <- c:
default:
w.fail(ErrWorkerBackpressure)
return
}
continue
}
if f.Type != "ack" && f.Type != "error" {
w.fail(ErrWorkerProtocol)
return
@@ -222,6 +237,28 @@ func (w *WorkerConnection) NextAssignment(ctx context.Context) (WorkerAssignment
}
}
// NextCancellation 等待特定尝试的停止提示;需注册 CancellationNotifications 并持续读取。
// 提示允许迟到或重复;只取消匹配 TaskID/AttemptID/Epoch 的处理器,结果仍通过 Finish 确认。
func (w *WorkerConnection) NextCancellation(ctx context.Context) (WorkerCancellation, error) {
if !w.registration.CancellationNotifications {
return WorkerCancellation{}, errors.New("cancellation notifications not enabled")
}
if err := ctx.Err(); err != nil {
return WorkerCancellation{}, err
}
select {
case <-ctx.Done():
return WorkerCancellation{}, ctx.Err()
case <-w.ctx.Done():
return WorkerCancellation{}, w.Err()
case c := <-w.cancellations:
if err := w.Err(); err != nil {
return WorkerCancellation{}, err
}
return c, nil
}
}
func (w *WorkerConnection) call(ctx context.Context, kind, id string, in, out any) (time.Time, error) {
started := time.Now()
if !validID(id) {
@@ -335,7 +372,7 @@ func (w *WorkerConnection) Finish(ctx context.Context, id string, in WorkerFinis
if in.Retryable && in.Outcome != WorkerFailed {
return out, errors.New("retryable is only valid for known failure")
}
if (in.Outcome != WorkerSucceeded && in.Outcome != WorkerFailed && in.Outcome != WorkerUnknown) || len(in.Result) > 256<<10 || bytes.Equal(bytes.TrimSpace(in.Result), []byte("null")) || validateWorkerJSON(in.Result) != nil {
if (in.Outcome != WorkerSucceeded && in.Outcome != WorkerFailed && in.Outcome != WorkerUnknown && in.Outcome != WorkerCancelled) || len(in.Result) > 256<<10 || bytes.Equal(bytes.TrimSpace(in.Result), []byte("null")) || validateWorkerJSON(in.Result) != nil {
return out, errors.New("invalid Worker result outcome or JSON")
}
if in.Outcome == WorkerSucceeded && in.ErrorCode != "" || in.Outcome != WorkerSucceeded && !workerName(in.ErrorCode, 64) {