feat: add durable Worker execution and WSS server

This commit is contained in:
2026-09-17 08:22:24 +08:00
parent 9701b7a071
commit cd0dc7011b
31 changed files with 3007 additions and 16 deletions

139
pulse/worker/service.go Normal file
View File

@@ -0,0 +1,139 @@
package worker
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"sort"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
)
var StaleSession = fault.New("STALE_SESSION", "connection is expired or superseded")
var LeaseLost = fault.New("LEASE_LOST", "attempt no longer grants execution authority")
var GateClosed = fault.New("EXECUTION_PAUSED", "queue or execution gate is paused")
type Service struct{ store Store }
func New(store Store) *Service { return &Service{store: store} }
func (s *Service) Authorize(ctx context.Context, t auth.Token) error {
return s.store.Authorize(ctx, t)
}
func NewSecret() (string, core.Digest, error) {
var raw [32]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", core.Digest{}, err
}
return base64.RawURLEncoding.EncodeToString(raw[:]), core.Digest(sha256.Sum256(raw[:])), nil
}
func SecretDigest(secret string) (core.Digest, error) {
if len(secret) != 43 {
return core.Digest{}, fault.Unauthorized
}
raw, err := base64.RawURLEncoding.Strict().DecodeString(secret)
if err != nil || len(raw) != 32 {
return core.Digest{}, fault.Unauthorized
}
return core.Digest(sha256.Sum256(raw)), nil
}
func NormalizeConnect(in ConnectInput) (ConnectInput, error) {
if in.InstanceID.Validate() != nil || in.QueueID.Validate() != nil || in.Capacity < 1 || in.Capacity > 256 || len(in.Capabilities) < 1 || len(in.Capabilities) > 32 {
return in, fault.Invalid("invalid worker identity, queue, capacity or capability count")
}
if _, err := SecretDigest(in.InstanceSecret); err != nil {
return in, fault.Invalid("instance_secret must encode 32 random bytes")
}
in.Capabilities = append([]Capability(nil), in.Capabilities...)
for _, c := range in.Capabilities {
if !service.ValidName(c.Handler, 128) || !service.ValidName(c.HandlerVersion, 64) || !service.ValidName(c.PayloadVersion, 32) {
return in, fault.Invalid("invalid worker capability")
}
}
sort.Slice(in.Capabilities, func(i, j int) bool {
a, b := in.Capabilities[i], in.Capabilities[j]
if a.Handler != b.Handler {
return a.Handler < b.Handler
}
if a.HandlerVersion != b.HandlerVersion {
return a.HandlerVersion < b.HandlerVersion
}
return a.PayloadVersion < b.PayloadVersion
})
for i := 1; i < len(in.Capabilities); i++ {
if in.Capabilities[i] == in.Capabilities[i-1] {
return in, fault.Invalid("duplicate worker capability")
}
}
return in, nil
}
func (s *Service) Connect(ctx context.Context, t auth.Token, in ConnectInput) (Binding, Welcome, error) {
in, err := NormalizeConnect(in)
if err != nil {
return Binding{}, Welcome{}, err
}
return s.store.Connect(ctx, t, in)
}
func (s *Service) Capacity(ctx context.Context, t auth.Token, b Binding, in CapacityInput) (Welcome, error) {
if in.Sequence == 0 || in.Capacity > 256 {
return Welcome{}, fault.Invalid("invalid capacity sequence or value")
}
return s.store.Capacity(ctx, t, b, in)
}
func (s *Service) Claim(ctx context.Context, t auth.Token, b Binding) (*Assignment, error) {
return s.store.Claim(ctx, t, b)
}
func validRef(ref AttemptRef) error {
if ref.TaskID.Validate() != nil || ref.AttemptID.Validate() != nil || ref.Epoch == 0 {
return fault.Invalid("invalid attempt identity")
}
_, err := SecretDigest(ref.LeaseSecret)
return err
}
func validCommand(message core.ID, ref AttemptRef) error {
if message.Validate() != nil {
return fault.Invalid("invalid operation message id")
}
return validRef(ref)
}
func (s *Service) Start(ctx context.Context, t auth.Token, b Binding, message core.ID, ref AttemptRef) (CommandResult, error) {
if err := validCommand(message, ref); err != nil {
return CommandResult{}, err
}
return s.store.Start(ctx, t, b, message, ref)
}
func (s *Service) Renew(ctx context.Context, t auth.Token, b Binding, message core.ID, ref AttemptRef) (CommandResult, error) {
if err := validCommand(message, ref); err != nil {
return CommandResult{}, err
}
return s.store.Renew(ctx, t, b, message, ref)
}
func (s *Service) Finish(ctx context.Context, t auth.Token, b Binding, message core.ID, in FinishInput) (CommandResult, error) {
if err := validCommand(message, in.Ref); err != nil {
return CommandResult{}, err
}
if in.Outcome != "SUCCEEDED" && in.Outcome != "FAILED" && in.Outcome != "UNKNOWN" {
return CommandResult{}, fault.Invalid("invalid result outcome")
}
if len(in.Result) == 0 || len(in.Result) > 256<<10 || bytes.Equal(bytes.TrimSpace(in.Result), []byte("null")) {
return CommandResult{}, fault.Invalid("result must be non-null JSON up to 256 KiB")
}
if _, err := jsonvalue.Canonical(in.Result); err != nil {
return CommandResult{}, fault.Invalid("invalid result JSON")
}
if (in.Outcome == "SUCCEEDED" && in.ErrorCode != "") || (in.Outcome != "SUCCEEDED" && !service.ValidName(in.ErrorCode, 64)) {
return CommandResult{}, fault.Invalid("error_code must match the outcome")
}
return s.store.Finish(ctx, t, b, message, in)
}
func (s *Service) Resume(ctx context.Context, t auth.Token, b Binding, ref AttemptRef) (LeaseStatus, error) {
if err := validRef(ref); err != nil {
return LeaseStatus{}, err
}
return s.store.Resume(ctx, t, b, ref)
}

View File

@@ -0,0 +1,38 @@
package worker
import (
"strings"
"testing"
core "git.yuntuo.hk/k2safe/orbit/model"
)
func TestRegistrationAndSecretBoundaries(t *testing.T) {
secret, digest, err := NewSecret()
if err != nil {
t.Fatal(err)
}
parsed, err := SecretDigest(secret)
if err != nil || parsed != digest {
t.Fatal("secret round trip")
}
for _, bad := range []string{secret + "=", secret + "\n", strings.Repeat("!", 43), secret[:42]} {
if _, err := SecretDigest(bad); err == nil {
t.Fatal("malformed secret accepted")
}
}
input := ConnectInput{InstanceID: core.ID(strings.Repeat("1", 32)), InstanceSecret: secret, QueueID: core.ID(strings.Repeat("2", 32)), Capacity: 1, Capabilities: []Capability{{"z", "v1", "v1"}, {"a", "v1", "v1"}}}
normalized, err := NormalizeConnect(input)
if err != nil || normalized.Capabilities[0].Handler != "a" || input.Capabilities[0].Handler != "z" {
t.Fatalf("normalization changed caller input: %v", err)
}
input.Capabilities = append(input.Capabilities, input.Capabilities[0])
if _, err := NormalizeConnect(input); err == nil {
t.Fatal("duplicate capability accepted")
}
input = normalized
input.Capacity = 257
if _, err := NormalizeConnect(input); err == nil {
t.Fatal("excess capacity accepted")
}
}

124
pulse/worker/types.go Normal file
View File

@@ -0,0 +1,124 @@
// Package worker 定义执行控制契约;处理器本身在外部 Worker 进程执行。
package worker
import (
"context"
"encoding/json"
"time"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
)
// Capability 精确声明可处理的版本,不做静默版本降级。
type Capability struct {
Handler string `json:"handler"` // Handler 处理器名称。
HandlerVersion string `json:"handler_version"` // HandlerVersion 处理器版本。
PayloadVersion string `json:"payload_version"` // PayloadVersion 参数版本。
}
// ConnectInput 在每次连接时提交;同一进程恢复时保留实例和秘密。
type ConnectInput struct {
InstanceID core.ID `json:"instance_id"` // InstanceID 进程标识,重启换新值。
InstanceSecret string `json:"instance_secret"` // InstanceSecret 32 字节随机秘密,只保存摘要。
QueueID core.ID `json:"queue_id"` // QueueID 当前进程绑定的消费队列。
Capabilities []Capability `json:"capabilities"` // Capabilities 1–32 个精确版本组合,重连固定。
Capacity uint32 `json:"capacity"` // Capacity 总执行槽数量,1–256。
}
// Binding 是接入层持有的认证结果,不作为公开请求字段接受或写入日志。
type Binding struct {
InstanceID core.ID // InstanceID 已证明秘密的进程标识。
SessionID core.ID // SessionID 当前连接标识。
Epoch uint64 // Epoch 连接代次。
Proof core.Digest // Proof 进程秘密摘要,持久化验证旧会话隔离。
}
// Welcome 给出数据库时钟与控制参数;时间用于 SDK 计算保守的单调时钟期限。
type Welcome struct {
SessionID core.ID `json:"session_id"` // SessionID 本次连接标识。
Epoch uint64 `json:"epoch,string"` // Epoch 当前连接代次。
ServerTime time.Time `json:"server_time"` // ServerTime 本次响应对应的数据库时间。
ExpiresAt time.Time `json:"expires_at"` // ExpiresAt 会话有效期限,不能代替任务租约。
LeaseDurationMS uint64 `json:"lease_duration_ms"` // LeaseDurationMS 单次续租窗口,毫秒。
Draining bool `json:"draining"` // Draining 排空标记,跨重连保留。
}
// CapacityInput 使用覆盖值和序号,重复消息不会重复增加执行容量。
type CapacityInput struct {
Sequence uint64 `json:"sequence,string"` // Sequence 本会话内从一开始递增。
Capacity uint32 `json:"capacity"` // Capacity 总容量,零表示暂不领取;不释放已有授权。
Drain bool `json:"drain"` // Drain 为真后持久化排空,不能通过后续消息清除。
}
// Assignment 仅通过已认证执行连接交付,包含敏感尝试秘密。
type Assignment struct {
Task service.TaskView `json:"task"` // Task 受理快照及业务参数。
AttemptID core.ID `json:"attempt_id"` // AttemptID 本次尝试标识。
Epoch uint64 `json:"epoch,string"` // Epoch 任务执行代次,隔离旧尝试。
LeaseSecret string `json:"lease_secret"` // LeaseSecret 执行秘密,禁止普通日志及查询返回。
LeaseUntil time.Time `json:"lease_until"` // LeaseUntil 尚未开始时的预占有效期。
ServerTime time.Time `json:"server_time"` // ServerTime 领取时数据库时间。
}
// AttemptRef 证明对一次尝试的控制权;连接代次仍由 Binding 单独验证。
type AttemptRef struct {
TaskID core.ID `json:"task_id"` // TaskID 所属任务。
AttemptID core.ID `json:"attempt_id"` // AttemptID 目标尝试。
Epoch uint64 `json:"epoch,string"` // Epoch 目标执行代次。
LeaseSecret string `json:"lease_secret"` // LeaseSecret 原始执行秘密,只在受控连接传输。
}
// FinishInput 区分明确结果与未知效果,未知不能被解释为可以盲目重跑。
type FinishInput struct {
Ref AttemptRef `json:"ref"` // Ref 当前尝试授权证明。
Outcome string `json:"outcome"` // Outcome 为 SUCCEEDED、FAILED 或 UNKNOWN。
Result json.RawMessage `json:"result"` // Result 非 null JSON 结果或证据引用,最多 256 KiB。
ErrorCode string `json:"error_code,omitempty"` // ErrorCode FAILED/UNKNOWN 必填稳定分类,不包含原始异常正文。
}
// Receipt 是不可变的操作回执,不代表当前仍有执行权。
type Receipt struct {
Operation string `json:"operation"` // Operation start、renew 或 finish。
TaskState pulse.TaskState `json:"task_state"` // TaskState 提交该操作时的任务状态。
AttemptState pulse.AttemptState `json:"attempt_state"` // AttemptState 操作提交时的尝试状态。
TaskRevision uint64 `json:"task_revision,string"` // TaskRevision 操作提交时的任务版本。
LeaseUntil time.Time `json:"lease_until"` // LeaseUntil 操作提交时租约期限,历史值不能授权新执行。
Disposition string `json:"disposition"` // Disposition 为 APPLIED 或 EVIDENCE_ONLY。
}
// LeaseStatus 每次响应重新计算;处理器仅在 Active 为真、State 为 RUNNING 且期限内推进。
// LEASED 的 Active 只代表预占仍有效,不能代替开始授权。
type LeaseStatus struct {
Active bool `json:"active"` // Active 当前会话仍持有未过期的有效尝试。
State pulse.AttemptState `json:"state"` // State 当前尝试状态。
LeaseUntil time.Time `json:"lease_until"` // LeaseUntil 当前租约,不因重放旧回执自动延长。
ServerTime time.Time `json:"server_time"` // ServerTime 完成锁定后重新读取的数据库时间。
}
// CommandResult 将原回执与当前执行权分开,避免旧 start ACK 使过期尝试复活。
type CommandResult struct {
Receipt Receipt `json:"receipt"` // Receipt 对应稳定消息身份的原回执。
Lease LeaseStatus `json:"lease"` // Lease 当前授权判断。
Deduplicated bool `json:"deduplicated"` // Deduplicated 是否返回已保存的回执。
}
// RecoveryStats 统计本批恢复,不作为持久化成功率或容量指标。
type RecoveryStats struct {
Examined int
Requeued int
Failed int
NeedsReview int
}
type Store interface {
Authorize(context.Context, auth.Token) error
Connect(context.Context, auth.Token, ConnectInput) (Binding, Welcome, error)
Capacity(context.Context, auth.Token, Binding, CapacityInput) (Welcome, error)
Claim(context.Context, auth.Token, Binding) (*Assignment, error)
Start(context.Context, auth.Token, Binding, core.ID, AttemptRef) (CommandResult, error)
Renew(context.Context, auth.Token, Binding, core.ID, AttemptRef) (CommandResult, error)
Finish(context.Context, auth.Token, Binding, core.ID, FinishInput) (CommandResult, error)
Resume(context.Context, auth.Token, Binding, AttemptRef) (LeaseStatus, error)
}