feat(hitl): 增量1 —— JetStream KV checkpoint store(持久化中断地基)
HITL 持久化中断/恢复的地基:compose checkpoint 需要一个可持久化、抗重启的 存储后端。dispatcher 是"只说 NATS、无 DB"的纯净设计,故复用既有 JetStream (bus.js)开 KV 桶,不引 Redis、不破坏架构原则。 - shared/bus: 新增 KVHandle + Bus.Checkpoints(bucket, ttl)。薄封装把 NATS 细节(ErrKeyNotFound→ok=false、Delete 幂等)挡在 bus 内,对外是朴素 Get/Put/Delete;bus 无需反向依赖 eino。File 存储 + 桶级 TTL 兜底清理。 - dispatcher/eino: checkpointStore 把 CheckpointKV 适配成 compose.CheckPointStore (Get/Set + 可选 Delete)。CheckpointKV 是最小接口,bus.KVHandle 结构化满足。 - 测试: 内存桩往返(Set→Get→Delete→miss)+ 编译期契约断言 `var _ CheckpointKV = (*bus.KVHandle)(nil)` 钉死 bus↔eino 隐式契约。 零爆炸半径(纯新增)。go test ./... 全绿。 下一步增量2:审批节点改 compose.Interrupt + Orchestrator 识别中断置 waiting。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ package bus
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -622,3 +623,52 @@ func (b *Bus) ConsumeTasks(ctx context.Context, h TaskHandler) (drain func(conte
|
||||
drainWait(&wg, dctx)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ---- JetStream KV:临时持久态(dispatcher compose checkpoint 等)----
|
||||
|
||||
// KVHandle 是一个 JetStream KV 桶的薄封装,把 NATS 细节(ErrKeyNotFound 等)挡在 bus 内,
|
||||
// 对外暴露 Get([]byte,bool,error)/Put/Delete 的朴素键值语义——结构化满足调用方的最小接口,
|
||||
// 故 bus 无需反向依赖 dispatcher/eino。键带桶级 TTL 自动过期,进程重启后值仍在(File 存储)。
|
||||
type KVHandle struct{ kv jetstream.KeyValue }
|
||||
|
||||
// Checkpoints 打开(或创建)一个 KV 桶,键按 ttl 自动过期。
|
||||
// 用于 compose checkpoint 这类"可丢但最好留"的执行态:dispatcher 重启可复活在途审批,
|
||||
// 决定到达即恢复;TTL 兜底清理被遗弃的中断(如审批人始终不处理)。
|
||||
func (b *Bus) Checkpoints(ctx context.Context, bucket string, ttl time.Duration) (*KVHandle, error) {
|
||||
kv, err := b.js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
|
||||
Bucket: bucket,
|
||||
Storage: jetstream.FileStorage,
|
||||
TTL: ttl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kv bucket %q: %w", bucket, err)
|
||||
}
|
||||
return &KVHandle{kv: kv}, nil
|
||||
}
|
||||
|
||||
// Get 取键值;键不存在返回 (nil,false,nil)(非错误),其余 IO 错误如实上抛。
|
||||
func (h *KVHandle) Get(ctx context.Context, key string) ([]byte, bool, error) {
|
||||
e, err := h.kv.Get(ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value(), true, nil
|
||||
}
|
||||
|
||||
// Put 写入键值(覆盖)。
|
||||
func (h *KVHandle) Put(ctx context.Context, key string, val []byte) error {
|
||||
_, err := h.kv.Put(ctx, key, val)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除键(恢复/终态后显式清理 checkpoint);键不存在视为成功(幂等)。
|
||||
func (h *KVHandle) Delete(ctx context.Context, key string) error {
|
||||
err := h.kv.Delete(ctx, key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user