package shadowdb import ( "context" "time" ) // Opener opens, closes, and reaps session-scoped shadow tables. // Implementations: // // - InMemoryOpener (this package): reference implementation // backed by a ShadowDB handle. Suitable for tests and small // in-process deployments. // - Future SQL-backed openers belong in platform-layer packages // that hold a real driver (platform/internal/shadowdb/...). // core ships only the interface and the reference impl. // // Thread safety: all methods MUST be safe for concurrent use from // multiple goroutines. A single Opener instance is expected to // serve many Agent sessions. // // Lifecycle ownership: core does NOT spawn goroutines. Reap is a // blocking function call; the platform layer invokes it from its // own cron / watchdog / worker loop. This keeps core free of // runtime-infrastructure responsibilities and lets the platform // layer choose scheduling granularity (per-minute cron, event-driven // after Agent session ends, on-demand from admin HTTP). // // Opener 负责打开 / 关闭 / 回收 session 级影子表. 实现: // // - InMemoryOpener (本包): 基于 ShadowDB handle 的参考实现. // 适合测试与小型进程内部署. // - 未来 SQL-backed opener 属于持有真实 driver 的平台层包 // (platform/internal/shadowdb/...). core 只发接口和参考实现. // // 线程安全: 所有方法必须支持多 goroutine 并发. 单个 Opener 实例 // 预期服务多个 Agent session. // // 生命周期归属: core 不起 goroutine. Reap 是阻塞的函数调用, 平台 // 层从自己的 cron / watchdog / worker 循环触发. 这让 core 免于 // 运行时基础设施责任, 也让平台层自选调度粒度 (分钟级 cron / Agent // session 结束事件驱动 / admin HTTP 按需触发). type Opener interface { // Open creates a new shadow session tagged with sessionID. The // physical shadow table (opts.ShadowTable) MUST exist with a // session_id VARCHAR(64) NOT NULL column and a schema that // mirrors opts.SeedFromTable (if seeding is requested). // // Seeding: if opts.SeedFromTable is non-empty, Open executes // INSERT INTO shadow SELECT sessionID AS session_id, * FROM seed. // If empty, the session starts with an empty shadow (caller // writes from scratch). // // Returns ErrEmptySessionID if sessionID is "", ErrDuplicateSession // if sessionID is already open on this Opener, or // ErrInvalidIdentifier if ShadowTable / SeedFromTable fail // ValidateIdentifier. Any driver-level error is returned // verbatim, not wrapped in a shadowdb sentinel. // // Open 创建新的 shadow session 并以 sessionID 打标. 物理影子 // 表 (opts.ShadowTable) 必须预建, 带 session_id VARCHAR(64) // NOT NULL 列, 且 schema 与 opts.SeedFromTable 对齐 (如要种子). // // 种子: opts.SeedFromTable 非空时, Open 执行 INSERT INTO shadow // SELECT sessionID AS session_id, * FROM seed. 为空则 session // 从空 shadow 开始 (调用方自己写). // // sessionID 为空返回 ErrEmptySessionID, 已存在返回 // ErrDuplicateSession, ShadowTable / SeedFromTable 校验失败 // 返回 ErrInvalidIdentifier. driver 级错误原样返回, 不包 shadowdb // 哨兵. Open(ctx context.Context, sessionID string, opts Options) (*Session, error) // Reap finds sessions whose CreatedAt is older than now-olderThan // and closes them (DELETE WHERE session_id=? for each). Intended // for platform-layer cron to clean up crashed / abandoned // sessions that missed Close. A zero olderThan reaps everything // currently open; callers almost always want a positive duration. // // Returns ReapResult with per-session outcomes. Per-session // errors accumulate in Errors rather than aborting the sweep -- // the next session's cleanup is independent. // // Reap 找出 CreatedAt 比 now-olderThan 更早的 session 并关闭 // (对每个 DELETE WHERE session_id=?). 给平台层 cron 清理错过 // Close 的崩溃 / 弃置 session. olderThan 为 0 会回收所有当前 // 打开的 session, 调用方几乎总应传正值. // // 返回 ReapResult 汇总 per-session 结果. per-session 错误累积到 // Errors 而不中止扫描 -- 下一个 session 的清理独立进行. Reap(ctx context.Context, olderThan time.Duration) (ReapResult, error) } // Options configures Session construction at Open time. All fields // are validated synchronously; misconfiguration surfaces as an // error from Open, not as a deferred failure on first Exec. // // Options 在 Open 时配置 Session 构造. 所有字段同步校验, 配置错误 // 经 Open 即时暴露, 不推迟到首次 Exec 炸出. type Options struct { // ShadowTable is the physical table name that carries shadow // rows for this session. Required. Must pass ValidateIdentifier // (plain [a-zA-Z_]\w*, no quoting). The table MUST exist with // a session_id VARCHAR(64) NOT NULL column; core does not // CREATE TABLE for the caller (schema-agnostic contract). // // ShadowTable 是承载本 session 影子行的物理表名. 必填, 必须通过 // ValidateIdentifier (纯 [a-zA-Z_]\w*, 无引号). 表必须预建, 带 // session_id VARCHAR(64) NOT NULL 列; core 不替调用方 CREATE TABLE // (schema-agnostic 契约). ShadowTable string // SeedFromTable is the optional source table name for initial // population. If non-empty, Open issues INSERT INTO shadow // SELECT sessionID AS session_id, * FROM seed. If empty, the // session starts empty and the caller populates via subsequent // INSERTs. Must pass ValidateIdentifier when non-empty. // // The schema of SeedFromTable MUST match ShadowTable minus the // session_id column; otherwise the INSERT ... SELECT fails at // Open time with a driver error, surfacing the mismatch early. // // SeedFromTable 是可选的种子源表名. 非空时 Open 执行 INSERT // INTO shadow SELECT sessionID AS session_id, * FROM seed. // 为空则 session 从空开始, 调用方经后续 INSERT 自行填充. 非空 // 时必须通过 ValidateIdentifier. // // SeedFromTable 的 schema 必须与 ShadowTable 扣去 session_id // 列后对齐, 否则 INSERT ... SELECT 在 Open 阶段即以 driver // 错误失败, 早暴露 schema 不匹配. SeedFromTable string // OnClose is invoked with the underlying error if Close fails // (typically a driver-level error during DELETE). Intended // hook-up point for platform-layer alerting / VerdictSink / // orphan-count metrics. nil disables the callback. // // Not called on successful Close or on Reap-initiated cleanup // (Reap collects its own errors in ReapResult). Keeps the // callback semantics "only fire on anomalous single-session // close" so alerting rules stay crisp. // // OnClose 在 Close 失败时被调用, 参数为底层错误 (通常 DELETE 的 // driver 级错误). 给平台层告警 / VerdictSink / 孤儿数指标提供 // 钩子. nil 禁用回调. // // 成功 Close 不触发, Reap 主导的清理也不触发 (Reap 在 ReapResult // 自收错误). 保持回调语义为 "仅单 session 异常 Close 时触发", // 告警规则干净. OnClose func(error) } // ReapResult aggregates Reap outcomes across swept sessions. // Callers typically log (Swept, Errors) for observability and // alert when len(Errors) > 0 or Swept > some threshold. // // ReapResult 汇总 Reap 扫过所有 session 的结果. 调用方通常以 // (Swept, Errors) 打日志做观测, len(Errors) > 0 或 Swept 超阈值 // 时告警. type ReapResult struct { // Swept is the number of sessions this Reap call closed // successfully (Close returned nil). // // Swept 是本次 Reap 成功关闭的 session 数 (Close 返回 nil). Swept int // Errors holds the per-session errors encountered during the // sweep. Empty slice means clean sweep. Errors are wrapped with // the sessionID in %w context so callers can identify which // session failed without the Opener re-emitting logs. // // Errors 承载扫描中遇到的 per-session 错误. 空 slice 表示干净 // 扫描. 错误以 %w 附带 sessionID 上下文包装, 调用方可据此识别 // 哪个 session 失败, Opener 不必再打日志. Errors []error }