@sema-agent/server 7.43.0 → 7.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,50 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BlobBackend — the content-addressed BYTE store for the durable FileSnapshotStore (E19 / 2c), DECOUPLED from the
|
|
3
|
-
* manifest. clay 2026-06-26: the conversation TRANSCRIPT stays DB-first; the ONLY thing that moves to an object store
|
|
4
|
-
* is the file-snapshot BLOBS (binary file content — a perf-suboptimal TiDB blob row; the tree bound is core's
|
|
5
|
-
* DEFAULT_SNAPSHOT_BOUNDS 256 MiB with NO per-file cap, hence the (a) SQL per-blob cap below). This seam
|
|
6
|
-
* lets the tidb/pg file-snapshot stores keep their MANIFEST in SQL (`snapshot_manifest`, the hot relPath→hash rows the
|
|
7
|
-
* transcript read NEVER touches) while routing the heavy bytes either to SQL (`snapshot_blob.bytes`, the DEFAULT — a
|
|
8
|
-
* MINIO-unset deployment is byte-identical to today) or to MinIO (the offload).
|
|
9
|
-
*
|
|
10
|
-
* THE SQL BLOB INDEX (clay 2026-06-26 design correction). EVEN under the MinIO backend, every blob keeps an INDEX row in
|
|
11
|
-
* `snapshot_blob` — `(blob_hash, byte_len, created_at)` with `bytes` NULL (the bytes live in MinIO). This is the load-
|
|
12
|
-
* bearing decision: ALL the reference-tracking (gcOrphanBlobs computes the orphan SET = `snapshot_blob` rows NOT IN
|
|
13
|
-
* `snapshot_manifest`) and the created_at GRACE WINDOW (delete-in-use protection) read that index, so they work
|
|
14
|
-
* IDENTICALLY for both backends. WITHOUT the index, a MinIO deployment would never GC its objects AND an E21 session
|
|
15
|
-
* purge would leave the file CONTENT in MinIO forever (a privacy/compliance breach). With it, a purged session's blobs
|
|
16
|
-
* become orphans (no manifest) and are EVENTUALLY collected by the async sweepOrphanBlobs reaper.
|
|
17
|
-
*
|
|
18
|
-
* ASYNC byte deletion (clay 2026-06-26): MinIO objects are NEVER deleted on the synchronous E21 purge / reap path (a
|
|
19
|
-
* synchronous external-store delete could block/fail the whole purge on a network blip). The purge deletes ONLY the SQL
|
|
20
|
-
* manifest rows → the blob becomes an orphan; the periodic `sweepOrphanBlobs` reaper (main.ts) deletes the MinIO objects
|
|
21
|
-
* + the index rows LATER, error-tolerantly (a single MinIO delete failure is skipped + retried next sweep) → bounded-
|
|
22
|
-
* window EVENTUAL CONSISTENCY. {@link BlobBackend.deleteBlobs} is therefore only ever called from the reaper.
|
|
23
|
-
*
|
|
24
|
-
* REUSE iron rule (core-service-boundary / no-stopgap): {@link SqlBlobBackend} EXTRACTS the `snapshot_blob` ops from the
|
|
25
|
-
* tidb/pg stores VERBATIM (the created_at-refreshing upsert, the SELECT, the grace-window orphan DELETE), so the SQL
|
|
26
|
-
* default path is byte-identical to before this refactor. {@link MinioBlobBackend} REUSES the existing zero-dependency
|
|
27
|
-
* {@link presignS3Url} SigV4 generator + `fetch` (the repo's no-SDK posture — OTLP + the k8s adapter are zero-dep too);
|
|
28
|
-
* it NEVER pulls in an S3 SDK, and it REUSES the SqlBlobBackend's index ops for the (blob_hash, byte_len, created_at)
|
|
29
|
-
* rows (the bytes-NULL index — no SQL drift between the two backends' reference-tracking).
|
|
30
|
-
*/
|
|
31
1
|
import { createHash } from "node:crypto";
|
|
32
2
|
import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
|
|
33
|
-
/** Orphan-blob GC grace (SHARED with the file-snapshot stores' BLOB_GC_GRACE_MS) — a blob is GC-eligible only if
|
|
34
|
-
* unreferenced AND untouched for this long; putBlob refreshes created_at on every touch (incl a dedup hit) so an
|
|
35
|
-
* in-flight snapshot's blobs are protected from delete-in-use. 1h ≫ any bounded snapshot. */
|
|
36
3
|
const BLOB_GC_GRACE_MS = 3_600_000;
|
|
37
|
-
/** (a)(clay 拍 a+c,2026-07-27)tidb 方言的默认 per-blob 帽 = **6 MiB,TiDB 自己的默认
|
|
38
|
-
* `txn-entry-size-limit`**。两堵墙真库实测(k3s8 tidb-test v7.5.1 默认配置,2026-07-27 探针):
|
|
39
|
-
* 7MiB=`entry too large`(entry 墙,默认 6291456)、31MiB=同、32MiB=`max_allowed_packet`(包墙,
|
|
40
|
-
* 64MiB ÷ ~2× 文本协议膨胀)——矮墙是 entry 的 6MiB,不是先前推测的包墙 ~31MiB。帽=诚实拒绝:
|
|
41
|
-
* 超限 blob 在 query 之前 typed 拒(消费面 HTTP 413 / capture warn 拿到可行动信息);**帽拒绝的
|
|
42
|
-
* 在默认配置下本来也落不进去 ⇒ 零回归**。调大过 entry 限/packet 的部署用 SNAPSHOT_BLOB_SQL_MAX_BYTES
|
|
43
|
-
* 放宽(两方言都生效)。**pg 方言默认不设帽**(扩展协议无这两堵墙,不人为降档)。D-1 附件战役同墙
|
|
44
|
-
* 教训:task-attachment-store.ts:19。 */
|
|
45
4
|
export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
|
|
46
|
-
/** typed 判别错:blob 超过 SQL 店的承载帽。判别码供店侧结果映射/HTTP 面/日志使用——绝不能与
|
|
47
|
-
* transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。 */
|
|
48
5
|
export class BlobTooLargeError extends Error {
|
|
49
6
|
code = "blob_too_large_for_sql";
|
|
50
7
|
constructor(byteLen, capBytes) {
|
|
@@ -55,11 +12,7 @@ export class BlobTooLargeError extends Error {
|
|
|
55
12
|
this.name = "BlobTooLargeError";
|
|
56
13
|
}
|
|
57
14
|
}
|
|
58
|
-
/** Max concurrent MinIO object ops (HEAD/DELETE) per reap/sweep batch — bound the open sockets/presigns so a sweep with
|
|
59
|
-
* tens of thousands of orphans can't exhaust FDs / throttle MinIO (finding 22). The full hash set is processed in
|
|
60
|
-
* windows of this size; a slow/failed object never blocks the others (each is independently error-tolerant). */
|
|
61
15
|
const MINIO_OP_CONCURRENCY = 16;
|
|
62
|
-
/** Run `fn` over `items` with at most `limit` in flight at once (a tiny bounded-concurrency pool — no dependency). */
|
|
63
16
|
async function mapBounded(items, limit, fn) {
|
|
64
17
|
let i = 0;
|
|
65
18
|
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
@@ -70,33 +23,15 @@ async function mapBounded(items, limit, fn) {
|
|
|
70
23
|
});
|
|
71
24
|
await Promise.all(workers);
|
|
72
25
|
}
|
|
73
|
-
/**
|
|
74
|
-
* SQL BlobBackend — the `snapshot_blob` table ops EXTRACTED VERBATIM from the tidb/pg file-snapshot stores (the
|
|
75
|
-
* created_at-refreshing upsert, the bytes SELECT, the grace-window orphan DELETE→deleteBlobs, a has via SELECT). Two
|
|
76
|
-
* dialect twins selected by `dialect`. Keeping the SQL byte-identical means the existing behaviour + the env-gated
|
|
77
|
-
* real-DB integration suite are unchanged when MinIO is unset (the DEFAULT backend the file-snapshot stores construct).
|
|
78
|
-
*
|
|
79
|
-
* NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the `NOT IN (SELECT … snapshot_manifest)` orphan
|
|
80
|
-
* filter (that reference check stays in the file-snapshot store, which computes the orphan hash SET from the manifest
|
|
81
|
-
* and hands it here). The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
|
|
82
|
-
* that a concurrent in-flight snapshot still needs is never byte-deleted (delete-in-use is data loss; under-delete is
|
|
83
|
-
* harmless — the next sweep collects it).
|
|
84
|
-
*/
|
|
85
26
|
export class SqlBlobBackend {
|
|
86
27
|
dialect;
|
|
87
28
|
pool;
|
|
88
|
-
/** Effective per-blob cap: explicit ctor value wins; else tidb=SQL_BLOB_DEFAULT_MAX_BYTES, pg=uncapped. */
|
|
89
29
|
capBytes;
|
|
90
30
|
constructor(dialect, pool, maxBytes) {
|
|
91
31
|
this.dialect = dialect;
|
|
92
32
|
this.pool = pool;
|
|
93
33
|
this.capBytes = maxBytes ?? (dialect === "tidb" ? SQL_BLOB_DEFAULT_MAX_BYTES : Number.POSITIVE_INFINITY);
|
|
94
34
|
}
|
|
95
|
-
/** The created_at-refreshing content-addressed upsert — VERBATIM the tidb/pg stores' `putBlobRow`. Refreshes
|
|
96
|
-
* created_at even on a dedup hit so a re-used/just-written blob is "touched=now" and GC-grace-protected.
|
|
97
|
-
* (a): an over-cap blob is REJECTED (typed {@link BlobTooLargeError}) BEFORE the driver ever sees the bytes —
|
|
98
|
-
* the alternative is an inscrutable packet-limit error deep in the driver, and on the HTTP face a 502 that
|
|
99
|
-
* invites the peer to retry a blob that can never land. */
|
|
100
35
|
async putBlob(hash, bytes) {
|
|
101
36
|
if (bytes.byteLength > this.capBytes)
|
|
102
37
|
throw new BlobTooLargeError(bytes.byteLength, this.capBytes);
|
|
@@ -107,8 +42,6 @@ export class SqlBlobBackend {
|
|
|
107
42
|
await this.pool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES ($1,$2,$3,$4) ON CONFLICT (blob_hash) DO UPDATE SET created_at = EXCLUDED.created_at", [hash, bytes.byteLength, Buffer.from(bytes), new Date()]);
|
|
108
43
|
}
|
|
109
44
|
}
|
|
110
|
-
/** The bytes for `hash` (undefined if absent) — VERBATIM the tidb/pg stores' getBlob SELECT. A pool/query error
|
|
111
|
-
* PROPAGATES (a transient DB fault is loud, not a silent "missing blob" — parity with the MinIO backend's throw). */
|
|
112
45
|
async getBlob(hash) {
|
|
113
46
|
if (this.dialect === "tidb") {
|
|
114
47
|
const [rows] = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = ?", [hash]);
|
|
@@ -117,22 +50,14 @@ export class SqlBlobBackend {
|
|
|
117
50
|
const b = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = $1", [hash]);
|
|
118
51
|
return b.rows[0]?.bytes != null ? new Uint8Array(b.rows[0].bytes) : undefined;
|
|
119
52
|
}
|
|
120
|
-
/** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */
|
|
121
53
|
async hasBlobs(hashes) {
|
|
122
54
|
return sqlHasBlobs(this.dialect, this.pool, hashes);
|
|
123
55
|
}
|
|
124
|
-
/** Byte-delete the given hashes (the orphan SET is computed by the caller from snapshot_manifest). Retains the
|
|
125
|
-
* grace-window guard so a just-written blob a concurrent in-flight snapshot needs is never delete-in-use — VERBATIM
|
|
126
|
-
* the tidb/pg stores' gcOrphanBlobs delete, minus the `NOT IN (SELECT … manifest)` clause (the caller already
|
|
127
|
-
* excluded referenced hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */
|
|
128
56
|
async deleteBlobs(hashes) {
|
|
129
57
|
const deleted = await sqlDeleteIndexRowsPastGrace(this.dialect, this.pool, hashes);
|
|
130
58
|
return deleted.length;
|
|
131
59
|
}
|
|
132
60
|
}
|
|
133
|
-
/** SHARED index op — the subset of `hashes` present as a `snapshot_blob` row (the SQL INDEX). Used by SqlBlobBackend
|
|
134
|
-
* AND MinioBlobBackend (the index is the single source of presence truth for both — cheaper + consistent than a
|
|
135
|
-
* HEAD-per-hash on MinIO; the bytes-NULL MinIO index row still counts as present). */
|
|
136
61
|
async function sqlHasBlobs(dialect, pool, hashes) {
|
|
137
62
|
if (hashes.length === 0)
|
|
138
63
|
return new Set();
|
|
@@ -146,16 +71,10 @@ async function sqlHasBlobs(dialect, pool, hashes) {
|
|
|
146
71
|
const r = await pool.query(`SELECT blob_hash FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
|
|
147
72
|
return new Set(r.rows.map((row) => row.blob_hash));
|
|
148
73
|
}
|
|
149
|
-
/** SHARED index op(workspace 浏览面 #3,[1894]②)—— per-blob 字节数,读 SQL INDEX 的 `byte_len`
|
|
150
|
-
* 列(sqlHasBlobs 同款单 IN 查询;MinIO 后端的 bytes-NULL 索引行同样带 byte_len,两后端同源)。
|
|
151
|
-
* tree/file 面的 size 投影与「超限先拒后拉」都靠它——绝不为拿大小去 getBlob 整块字节。 */
|
|
152
74
|
export async function sqlBlobSizes(dialect, pool, hashes) {
|
|
153
75
|
if (hashes.length === 0)
|
|
154
76
|
return new Map();
|
|
155
77
|
const distinct = [...new Set(hashes)];
|
|
156
|
-
// 🔴 复审 F7:分块。workspace list 腿对每个快照各查一次全 manifest 的 unique hash——按 core 上限
|
|
157
|
-
// (maxFiles 10000)一次就是 1 万元素的 IN,`?limit=100` 则是 100 次这样的串行往返。PG 的 65535
|
|
158
|
-
// 参数协议墙虽未触到,但 1 万元素 IN 本身对计划器/网络包就是坏形。1000/块是保守值。
|
|
159
78
|
const CHUNK = 1000;
|
|
160
79
|
const out = new Map();
|
|
161
80
|
for (let i = 0; i < distinct.length; i += CHUNK) {
|
|
@@ -175,10 +94,6 @@ export async function sqlBlobSizes(dialect, pool, hashes) {
|
|
|
175
94
|
}
|
|
176
95
|
return out;
|
|
177
96
|
}
|
|
178
|
-
/** SHARED index op — DELETE the index rows for `hashes` that have passed the grace window (created_at < cutoff). Returns
|
|
179
|
-
* the hashes ACTUALLY deleted (so the MinIO backend deletes exactly the objects whose index rows it just removed — and
|
|
180
|
-
* a within-grace young orphan is left for a later sweep). Best-effort: a DB error is swallowed (under-delete is
|
|
181
|
-
* harmless; the next sweep retries). The grace guard (delete-in-use protection) is IDENTICAL across both backends. */
|
|
182
97
|
async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
183
98
|
if (hashes.length === 0)
|
|
184
99
|
return [];
|
|
@@ -186,8 +101,6 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
|
186
101
|
const cutoff = new Date(Date.now() - BLOB_GC_GRACE_MS);
|
|
187
102
|
if (dialect === "tidb") {
|
|
188
103
|
const ph = distinct.map(() => "?").join(",");
|
|
189
|
-
// MySQL/TiDB DELETE can't RETURNING — SELECT the grace-passed subset first, then DELETE it (the SELECT+DELETE
|
|
190
|
-
// race is benign: a row that flips referenced between them just isn't deleted = under-delete = harmless).
|
|
191
104
|
const grace = await pool
|
|
192
105
|
.query(`SELECT blob_hash FROM snapshot_blob WHERE created_at < ? AND blob_hash IN (${ph})`, [cutoff, ...distinct])
|
|
193
106
|
.then(([rows]) => rows.map((r) => String(r.blob_hash)))
|
|
@@ -206,35 +119,11 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
|
|
|
206
119
|
.catch(() => undefined);
|
|
207
120
|
return (r?.rows ?? []).map((row) => row.blob_hash);
|
|
208
121
|
}
|
|
209
|
-
/**
|
|
210
|
-
* MinIO BlobBackend — content-addressed bytes in an S3-compatible object store, via the zero-dependency
|
|
211
|
-
* {@link presignS3Url} SigV4 generator + `fetch` (NO S3 SDK; the repo's no-SDK posture). The object key is
|
|
212
|
-
* `keyPrefix + hash` (content-addressed → idempotent PUT, dedup across snapshots is automatic).
|
|
213
|
-
*
|
|
214
|
-
* THE SQL INDEX: the bytes go to MinIO, but a `snapshot_blob` INDEX row `(blob_hash, byte_len, created_at)` with bytes
|
|
215
|
-
* NULL is kept in SQL (same pool/dialect). This is what makes the orphan-reference computation + the created_at grace
|
|
216
|
-
* window + the §10 presence pre-check work identically to the SQL backend (see the file header). hasBlobs reads the
|
|
217
|
-
* index; putBlob PUTs then upserts the index (created_at-refresh on conflict); deleteBlobs deletes the grace-passed
|
|
218
|
-
* index rows AND the matching MinIO objects (called ONLY from the async sweep reaper).
|
|
219
|
-
*
|
|
220
|
-
* INTEGRITY (security-critical): getBlob VERIFIES sha256(bytes) === hash before returning. A content-addressed store's
|
|
221
|
-
* whole safety rests on bytes-match-hash; a mismatch means corruption (network) or POISONING (a hostile object written
|
|
222
|
-
* under a hash it doesn't match) — treated as ABSENT (undefined), NEVER returning wrong bytes. This mirrors the
|
|
223
|
-
* importManifest content-address check the SQL stores already do.
|
|
224
|
-
*/
|
|
225
|
-
/** 鲁棒性批2 C2/C3/C6(2026-08-03):S3 兼容端点 IO 的统一 abort 上界(fleet-client C1 定常量形
|
|
226
|
-
* 先例)。半开端点此前各挂 undici 默认 ~300s headersTimeout——blob GET 钉死 restore/session-import
|
|
227
|
-
* 同步路径,SendUserFile 工具调用永不返回。120s 对慢链路 ~64MiB 单 PUT/GET 仍宽裕(presignTtlSec
|
|
228
|
-
* 注释同口径),远小于病形。send-user-file 与 leader/diffup 同 import 此常量。 */
|
|
229
122
|
export const OBJECT_STORE_IO_TIMEOUT_MS = 120_000;
|
|
230
123
|
export class MinioBlobBackend {
|
|
231
124
|
cfg;
|
|
232
125
|
keyPrefix;
|
|
233
126
|
ttl;
|
|
234
|
-
/** The SQL index pool+dialect, or undefined for an index-LESS backend (the migration script + the unit suite, which
|
|
235
|
-
* exercise ONLY the object I/O — put/get/has-by-HEAD/delete — with no DB). When undefined, hasBlobs falls back to a
|
|
236
|
-
* HEAD-per-hash probe and deleteBlobs ignores the grace window (the script is a stop-the-world copy, not the live GC
|
|
237
|
-
* path). The file-snapshot stores ALWAYS pass the pool, so the live reference-tracking always has the index. */
|
|
238
127
|
indexPool;
|
|
239
128
|
indexDialect;
|
|
240
129
|
constructor(cfg, index) {
|
|
@@ -250,8 +139,6 @@ export class MinioBlobBackend {
|
|
|
250
139
|
return `${this.keyPrefix}${hash}`;
|
|
251
140
|
}
|
|
252
141
|
presign(hash, method) {
|
|
253
|
-
// presignS3Url only declares GET|PUT in its param type, but the SigV4 canonicalization is identical for any verb
|
|
254
|
-
// (host-only SignedHeaders, UNSIGNED-PAYLOAD) — HEAD/DELETE sign correctly with the same string math. Cast the verb.
|
|
255
142
|
return presignS3Url({
|
|
256
143
|
endpoint: this.cfg.endpoint,
|
|
257
144
|
bucket: this.cfg.bucket,
|
|
@@ -263,26 +150,13 @@ export class MinioBlobBackend {
|
|
|
263
150
|
expiresSec: this.ttl,
|
|
264
151
|
});
|
|
265
152
|
}
|
|
266
|
-
/** PUT the bytes under the content-addressed key, THEN upsert the SQL INDEX row (bytes NULL, created_at refresh on
|
|
267
|
-
* conflict — the SAME grace discipline as SqlBlobBackend, so a just-PUT blob an in-flight snapshot needs is touched=
|
|
268
|
-
* now and protected). Idempotent (same hash ⇒ overwrite-with-identical-bytes + created_at bump). A non-2xx PUT throws
|
|
269
|
-
* (an upload failure must surface — a silent drop would later read_failed on restore). The PUT happens BEFORE the
|
|
270
|
-
* index upsert so an index row never claims a blob the object store doesn't hold (an index row with no object would
|
|
271
|
-
* let hasBlobs report a phantom present; the reverse — an object with no index — is harmless and pruned by a bucket
|
|
272
|
-
* lifecycle if it ever happens). NOTE no post-PUT read-back verify on the LIVE path (perf): content-addressing
|
|
273
|
-
* catches a corrupted PUT at the next getBlob sha-check (fail-closed); the one-time migration script DOES read-back. */
|
|
274
153
|
async putBlob(hash, bytes) {
|
|
275
154
|
const url = this.presign(hash, "PUT");
|
|
276
|
-
// C2(2026-08-03):走注入座(此前直用全局 fetch=测试桩接不进的站点)+ abort 上界(半开端点
|
|
277
|
-
// 此前挂 undici ~300s;120s 对慢链路大对象仍宽裕,presignTtlSec 注释同口径)。六站点同批。
|
|
278
155
|
const res = await (this.cfg.fetchImpl ?? fetch)(url, { method: "PUT", body: bytes, signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
279
156
|
if (!res.ok) {
|
|
280
157
|
const detail = await res.text().catch(() => "");
|
|
281
158
|
throw new Error(`MinioBlobBackend.putBlob ${hash}: HTTP ${res.status} ${detail.slice(0, 200)}`);
|
|
282
159
|
}
|
|
283
|
-
// Upsert the bytes-NULL INDEX row (byte_len + created_at) so the orphan/grace reference-tracking + presence checks
|
|
284
|
-
// see this blob. Same created_at-refresh-on-conflict as the SQL backend. (No index pool ⇒ object-only mode = the
|
|
285
|
-
// migration/unit path; the live stores always pass it.)
|
|
286
160
|
if (this.indexPool && this.indexDialect) {
|
|
287
161
|
if (this.indexDialect === "tidb") {
|
|
288
162
|
await this.indexPool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES (?,?,NULL,?) ON DUPLICATE KEY UPDATE created_at = VALUES(created_at)", [hash, bytes.byteLength, new Date()]);
|
|
@@ -292,12 +166,6 @@ export class MinioBlobBackend {
|
|
|
292
166
|
}
|
|
293
167
|
}
|
|
294
168
|
}
|
|
295
|
-
/** GET the bytes for `hash`, then VERIFY content-address (sha256(bytes)===hash). THREE branches (finding 11):
|
|
296
|
-
* - 404 → undefined (genuinely ABSENT — the file-snapshot store treats undefined as a missing blob).
|
|
297
|
-
* - sha256 MISMATCH → undefined (corruption/poisoning; treat corrupt-as-absent, fail-CLOSED, NEVER wrong bytes).
|
|
298
|
-
* - a NON-2xx-NON-404 (5xx/throttle) OR a network/fetch REJECT → THROW (a TRANSIENT fault must fail LOUD so the
|
|
299
|
-
* restore/import surfaces an error, NOT silently drop a file as "missing"; the SQL backend likewise propagates a
|
|
300
|
-
* pool error). The snapshot store's restore() wraps applyManifest so this throw becomes a {ok:false} restore_failed. */
|
|
301
169
|
async getBlob(hash) {
|
|
302
170
|
const url = this.presign(hash, "GET");
|
|
303
171
|
let res;
|
|
@@ -308,24 +176,19 @@ export class MinioBlobBackend {
|
|
|
308
176
|
throw new Error(`MinioBlobBackend.getBlob ${hash}: transient fetch failure: ${err.message}`);
|
|
309
177
|
}
|
|
310
178
|
if (res.status === 404)
|
|
311
|
-
return undefined;
|
|
179
|
+
return undefined;
|
|
312
180
|
if (!res.ok) {
|
|
313
181
|
const detail = await res.text().catch(() => "");
|
|
314
182
|
throw new Error(`MinioBlobBackend.getBlob ${hash}: transient HTTP ${res.status} ${detail.slice(0, 200)}`);
|
|
315
183
|
}
|
|
316
184
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
317
|
-
// content-address integrity: the bytes MUST hash to the key. A mismatch = corrupt/poisoned → undefined (absent), NOT wrong bytes.
|
|
318
185
|
if (createHash("sha256").update(buf).digest("hex") !== hash)
|
|
319
186
|
return undefined;
|
|
320
187
|
return buf;
|
|
321
188
|
}
|
|
322
|
-
/** The subset of `hashes` present — reads the SQL INDEX (cheaper than a HEAD-per-hash AND consistent with the SQL
|
|
323
|
-
* backend). Falls back to a presigned HEAD per hash ONLY in object-only mode (no index pool: the migration / unit
|
|
324
|
-
* path). The caller passes DISTINCT hashes (migration idempotency batch / reap orphan set / §10 presence pre-check). */
|
|
325
189
|
async hasBlobs(hashes) {
|
|
326
190
|
if (this.indexPool && this.indexDialect)
|
|
327
191
|
return sqlHasBlobs(this.indexDialect, this.indexPool, hashes);
|
|
328
|
-
// object-only mode (migration/unit): HEAD probe, bounded concurrency.
|
|
329
192
|
const distinct = [...new Set(hashes)];
|
|
330
193
|
const present = new Set();
|
|
331
194
|
await mapBounded(distinct, MINIO_OP_CONCURRENCY, async (hash) => {
|
|
@@ -335,22 +198,13 @@ export class MinioBlobBackend {
|
|
|
335
198
|
present.add(hash);
|
|
336
199
|
}
|
|
337
200
|
catch {
|
|
338
|
-
/* treat an unreachable HEAD as absent — a false-absent only re-PUTs (idempotent) or re-checks; never a false-present */
|
|
339
201
|
}
|
|
340
202
|
});
|
|
341
203
|
return present;
|
|
342
204
|
}
|
|
343
|
-
/** D-1 孤儿对象 GC(clay 拍 2026-07-28):列举本后端 keyPrefix 下的全部对象(ListObjectsV2 分页,
|
|
344
|
-
* 零依赖 XML 取 <Key>/<LastModified>/<NextContinuationToken>)。**server 端 sweep 专用**——presign 的
|
|
345
|
-
* bucket-GET 列举能力绝不外发。响应非 2xx 即 throw(sweep 调用方自 catch:列举失败=本轮跳过,
|
|
346
|
-
* 绝不把「列不出来」当「没有孤儿」)。 */
|
|
347
205
|
async *listObjects() {
|
|
348
206
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
349
207
|
let token;
|
|
350
|
-
// 🔴 复审 F3:分页上限。异常/代理后端回环或恒定的 continuation-token 会让本循环无界翻页
|
|
351
|
-
// (实测可翻 5001 页仍不停),叠加维护 tick 就是逐轮堆积。超限 fail-loud —— 与本函数既有的
|
|
352
|
-
// 「列不出来 ≠ 没有孤儿」口径一致(宁可整轮失败也不给出残缺的「孤儿全集」)。
|
|
353
|
-
// 1000 页 × 1000 键/页 = 100 万对象,远超任何真实附件语料;真撞上=后端异常而非规模问题。
|
|
354
208
|
const MAX_PAGES = 1000;
|
|
355
209
|
let pages = 0;
|
|
356
210
|
do {
|
|
@@ -371,15 +225,14 @@ export class MinioBlobBackend {
|
|
|
371
225
|
if (!res.ok)
|
|
372
226
|
throw new Error(`listObjects: MinIO ${res.status}`);
|
|
373
227
|
const xml = await res.text();
|
|
374
|
-
// <Contents><Key>k</Key>…<LastModified>ISO</LastModified>…</Contents> —— 逐块取,键序无假设。
|
|
375
228
|
for (const m of xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)) {
|
|
376
229
|
const block = m[1];
|
|
377
230
|
const key = /<Key>([^<]*)<\/Key>/.exec(block)?.[1];
|
|
378
231
|
const lm = /<LastModified>([^<]*)<\/LastModified>/.exec(block)?.[1];
|
|
379
232
|
if (key === undefined || !key.startsWith(this.keyPrefix))
|
|
380
|
-
continue;
|
|
233
|
+
continue;
|
|
381
234
|
const ms = lm ? Date.parse(lm) : NaN;
|
|
382
|
-
yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
235
|
+
yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
383
236
|
}
|
|
384
237
|
const truncated = /<IsTruncated>true<\/IsTruncated>/.test(xml);
|
|
385
238
|
token = truncated ? /<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(xml)?.[1] : undefined;
|
|
@@ -387,8 +240,6 @@ export class MinioBlobBackend {
|
|
|
387
240
|
throw new Error("listObjects: truncated response without NextContinuationToken");
|
|
388
241
|
} while (token !== undefined);
|
|
389
242
|
}
|
|
390
|
-
/** 单对象 HEAD 探针(复审 B-F1 的删前复查面):返回当前 LastModified(缺席=对象已不在)。
|
|
391
|
-
* put 会 PUT 同键 ⇒ 刷新 LastModified,所以「删前再 HEAD 一次」能看见「刚刚被重新引用」的对象。 */
|
|
392
243
|
async headObject(hash) {
|
|
393
244
|
const f = this.cfg.fetchImpl ?? fetch;
|
|
394
245
|
const res = await f(this.presign(hash, "HEAD"), { method: "HEAD", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
@@ -396,28 +247,17 @@ export class MinioBlobBackend {
|
|
|
396
247
|
return undefined;
|
|
397
248
|
const lm = res.headers.get("last-modified");
|
|
398
249
|
const ms = lm ? Date.parse(lm) : NaN;
|
|
399
|
-
return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
250
|
+
return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
|
|
400
251
|
}
|
|
401
|
-
/** Byte-delete the given hashes — called ONLY from the async sweepOrphanBlobs reaper (NEVER the synchronous purge
|
|
402
|
-
* path). Honors the SAME created_at grace window as the SQL backend: with an index, DELETE the grace-passed index
|
|
403
|
-
* rows first (the index's created_at is the authoritative age — an S3 object carries no service-controlled timestamp
|
|
404
|
-
* the backend reads), then delete the MinIO objects ONLY for those grace-passed hashes (so a just-PUT blob an
|
|
405
|
-
* in-flight snapshot needs is NOT delete-in-use). ERROR-TOLERANT + BOUNDED: each object delete is independent (a
|
|
406
|
-
* failure is skipped → retried next sweep), at most MINIO_OP_CONCURRENCY in flight (no FD/throttle storm). In
|
|
407
|
-
* object-only mode (no index pool: the migration/unit cleanup path) it deletes every given hash with no grace. */
|
|
408
252
|
async deleteBlobs(hashes) {
|
|
409
253
|
if (hashes.length === 0)
|
|
410
254
|
return 0;
|
|
411
|
-
// Determine which hashes are grace-passed AND remove their index rows (so the index no longer claims them present).
|
|
412
|
-
// Order: index-row DELETE FIRST, then the object DELETE — if the object delete fails, the orphan re-appears only as
|
|
413
|
-
// an object with no index, which a later sweep can't re-target; acceptable (under-delete is harmless, and a bucket
|
|
414
|
-
// lifecycle rule prunes index-less objects). Doing it index-first keeps hasBlobs honest immediately.
|
|
415
255
|
let toDelete;
|
|
416
256
|
if (this.indexPool && this.indexDialect) {
|
|
417
257
|
toDelete = await sqlDeleteIndexRowsPastGrace(this.indexDialect, this.indexPool, hashes);
|
|
418
258
|
}
|
|
419
259
|
else {
|
|
420
|
-
toDelete = [...new Set(hashes)];
|
|
260
|
+
toDelete = [...new Set(hashes)];
|
|
421
261
|
}
|
|
422
262
|
if (toDelete.length === 0)
|
|
423
263
|
return 0;
|
|
@@ -425,16 +265,13 @@ export class MinioBlobBackend {
|
|
|
425
265
|
await mapBounded(toDelete, MINIO_OP_CONCURRENCY, async (hash) => {
|
|
426
266
|
try {
|
|
427
267
|
const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
|
|
428
|
-
// 🔴 复审 F4:统计**真实**删除数——index-less(附件)分支此前恒返回入参长度,MinIO 完全不可写
|
|
429
|
-
// 时指标/日志照报「清理成功」。2xx/404(已不在)都算删成功;其余=失败,不计数。
|
|
430
268
|
if (res.ok || res.status === 404)
|
|
431
269
|
okCount++;
|
|
432
270
|
}
|
|
433
271
|
catch {
|
|
434
|
-
/* error-tolerant: a failed/timed-out object delete is skipped → retried next sweep (eventual consistency) */
|
|
435
272
|
}
|
|
436
273
|
});
|
|
437
|
-
return okCount;
|
|
274
|
+
return okCount;
|
|
438
275
|
}
|
|
439
276
|
}
|
|
440
277
|
//# sourceMappingURL=blob-backend.js.map
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
2
|
-
/** PG translation of the tidb-pool.ts `circuit_breaker` DDL (DATETIME(3)→TIMESTAMPTZ(3)). Disjoint from other
|
|
3
|
-
* stores' tables, so a self-contained ensureSchema is safe (no central-schema shadowing). TiDB's DDL lives
|
|
4
|
-
* centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
|
|
5
2
|
export const PG_BREAKER_STATE_SCHEMA = [
|
|
6
3
|
`CREATE TABLE IF NOT EXISTS circuit_breaker (
|
|
7
4
|
breaker_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -12,29 +9,21 @@ export const PG_BREAKER_STATE_SCHEMA = [
|
|
|
12
9
|
PRIMARY KEY (breaker_key)
|
|
13
10
|
)`,
|
|
14
11
|
];
|
|
15
|
-
/** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */
|
|
16
12
|
export async function ensureSchema(pool) {
|
|
17
13
|
for (const stmt of PG_BREAKER_STATE_SCHEMA)
|
|
18
14
|
await pool.query(stmt);
|
|
19
15
|
}
|
|
20
|
-
/** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */
|
|
21
16
|
export class SqlBreakerState {
|
|
22
17
|
db;
|
|
23
18
|
onWriteFail;
|
|
24
19
|
cache = new Map();
|
|
25
20
|
timer;
|
|
26
|
-
/** Keys with an in-flight write-through upsert. A concurrent `refresh()` must NOT overwrite or prune
|
|
27
|
-
* these — their newer local value may not be in the DB yet. Closes the set()-vs-refresh race (core
|
|
28
|
-
* 1.40.0 council finding #5). */
|
|
29
21
|
pending = new Set();
|
|
30
22
|
writeFailStreak = 0;
|
|
31
|
-
constructor(db,
|
|
32
|
-
/** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
|
|
33
|
-
onWriteFail) {
|
|
23
|
+
constructor(db, onWriteFail) {
|
|
34
24
|
this.db = db;
|
|
35
25
|
this.onWriteFail = onWriteFail;
|
|
36
26
|
}
|
|
37
|
-
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
38
27
|
q(tidb, pg) {
|
|
39
28
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
40
29
|
}
|
|
@@ -42,9 +31,9 @@ export class SqlBreakerState {
|
|
|
42
31
|
return this.cache.get(key);
|
|
43
32
|
}
|
|
44
33
|
set(key, snap) {
|
|
45
|
-
this.cache.set(key, snap);
|
|
46
|
-
this.pending.add(key);
|
|
47
|
-
void this.writeThrough(key, snap);
|
|
34
|
+
this.cache.set(key, snap);
|
|
35
|
+
this.pending.add(key);
|
|
36
|
+
void this.writeThrough(key, snap);
|
|
48
37
|
}
|
|
49
38
|
async writeThrough(key, snap) {
|
|
50
39
|
try {
|
|
@@ -57,41 +46,18 @@ export class SqlBreakerState {
|
|
|
57
46
|
try {
|
|
58
47
|
this.onWriteFail?.(0);
|
|
59
48
|
}
|
|
60
|
-
catch {
|
|
61
|
-
}
|
|
62
|
-
// KNOWN/ACCEPTED (audit B): two rapid set()s on the SAME key race their async writeThroughs — the
|
|
63
|
-
// older write can land last (no version column), so a later refresh() may briefly revert this
|
|
64
|
-
// replica to the older snapshot (e.g. re-close a just-opened breaker). Advisory state: the very
|
|
65
|
-
// next failure re-opens it locally (set() is synchronous on the hot path) and re-publishes. A
|
|
66
|
-
// monotonic guard would need versioned rows + CAS; not worth it for an advisory cache.
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
67
51
|
}
|
|
68
52
|
catch {
|
|
69
53
|
try {
|
|
70
54
|
this.onWriteFail?.(++this.writeFailStreak);
|
|
71
55
|
}
|
|
72
|
-
catch {
|
|
73
|
-
// Advisory state — swallow; a DB outage must not turn into a brain-call failure. Crucially we KEEP
|
|
74
|
-
// `pending` on failure: the DB still holds the OLD value, so releasing the guard here would let the
|
|
75
|
-
// next refresh() clobber this replica's just-set (correct) local value — silently resetting an open
|
|
76
|
-
// breaker toward a known-bad backend. The guard self-heals on the next successful set()/write.
|
|
56
|
+
catch { }
|
|
77
57
|
}
|
|
78
58
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Pull the shared table into the local cache so this replica learns peers' trips, and PRUNE keys that
|
|
81
|
-
* vanished from the DB — but never touch a locally-pending write (its upsert may not have landed yet),
|
|
82
|
-
* else a just-`set` trip would be clobbered/reverted before it reaches the DB. Eventually consistent:
|
|
83
|
-
* a replica is at most one refresh interval behind a peer, never wrong in a data-losing way.
|
|
84
|
-
* `halfOpenInFlight` is intentionally not shared (it is a per-replica probe counter).
|
|
85
|
-
*
|
|
86
|
-
* SELECT text is byte-identical on both dialects (no placeholders) — written once, not through `q()`
|
|
87
|
-
* (same precedent as image-bake-store-sql.ts's queued-bake lookup).
|
|
88
|
-
*/
|
|
89
59
|
async refresh() {
|
|
90
60
|
try {
|
|
91
|
-
// Snapshot the pending set BEFORE the read: a key whose write-through is in-flight when the SELECT starts may
|
|
92
|
-
// not be in the SELECT's snapshot yet, and its writeThrough can `pending.delete` DURING the await — so a
|
|
93
|
-
// check-after-read (`this.pending.has`) alone would let the stale row clobber the just-set local value. Skip a
|
|
94
|
-
// key pending NOW or at read-start (union) to actually close the set()-vs-refresh race (#5), not just narrow it.
|
|
95
61
|
const pendingAtStart = new Set(this.pending);
|
|
96
62
|
const guarded = (key) => this.pending.has(key) || pendingAtStart.has(key);
|
|
97
63
|
const { rows } = await this.db.query("SELECT breaker_key, phase, failures, opened_at FROM circuit_breaker");
|
|
@@ -100,7 +66,7 @@ export class SqlBreakerState {
|
|
|
100
66
|
const key = String(r.breaker_key);
|
|
101
67
|
seen.add(key);
|
|
102
68
|
if (guarded(key))
|
|
103
|
-
continue;
|
|
69
|
+
continue;
|
|
104
70
|
this.cache.set(key, {
|
|
105
71
|
phase: r.phase,
|
|
106
72
|
failures: Number(r.failures),
|
|
@@ -112,10 +78,8 @@ export class SqlBreakerState {
|
|
|
112
78
|
this.cache.delete(key);
|
|
113
79
|
}
|
|
114
80
|
catch {
|
|
115
|
-
// Keep the last-known cache on a transient read failure (fail static, don't blank the breaker).
|
|
116
81
|
}
|
|
117
82
|
}
|
|
118
|
-
/** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */
|
|
119
83
|
startRefresh(intervalMs = 5_000) {
|
|
120
84
|
this.timer ??= setInterval(() => void this.refresh(), intervalMs);
|
|
121
85
|
this.timer.unref?.();
|
|
@@ -127,13 +91,11 @@ export class SqlBreakerState {
|
|
|
127
91
|
this.timer = undefined;
|
|
128
92
|
}
|
|
129
93
|
}
|
|
130
|
-
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
131
94
|
export class TiDBBreakerState extends SqlBreakerState {
|
|
132
95
|
constructor(pool, onWriteFail) {
|
|
133
96
|
super(mysqlDriver(pool), onWriteFail);
|
|
134
97
|
}
|
|
135
98
|
}
|
|
136
|
-
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
137
99
|
export class PgBreakerState extends SqlBreakerState {
|
|
138
100
|
constructor(pool, onWriteFail) {
|
|
139
101
|
super(pgDriver(pool), onWriteFail);
|