Background
The Redis storage layer (packages/api/internal/sandbox/storage/redis) has
grown a solid foundation of expiration-index and publisher metrics over recent
PRs (#3604, #3606). However, several high-traffic paths — distributed locking,
state transitions, background scans, and PubSub delivery — still emit nothing
to OTel. The gaps below are ordered by operational impact.
Missing metrics
P0 — silent failures, no alert possible
① Lock wait latency + timeout counter (locker.go · Obtain)
Every sandbox state change (StartRemoving) must acquire a distributed lock.
Obtain can block until lockTimeout (1 min) before returning ctx.Err(),
but no metric records how long callers waited or how often the timeout was hit.
Lock contention is the most direct cause of sandbox API tail latency and is
currently invisible.
Proposed:
api.redis_storage.lock.wait_duration — histogram (ms), from Obtain entry to lock acquired
api.redis_storage.lock.timeout_total — counter, incremented on ctx.Err() / deadline exceeded
② Transition callback failure counter (state_change.go · createCallback)
The callback that finalises every state transition has three failure paths
(lock acquire failure, SET result key failure, DEL transition key failure),
each logging only Warn. A failing callback leaves waiters blocked until the
30 s result-key TTL expires. There is no metric to page on.
Proposed:
api.redis_storage.transition.callback_failures_total — counter with
reason=lock_failed|set_result_failed|del_transition_key_failed
③ forEachSandboxBatch skipped-team counter (scan.go · forEachSandboxBatch)
Both healExpirationIndex and AllRunningItems call forEachSandboxBatch.
Per-team scan errors are silently continued with a Warn log. A team that
fails every pass never gets its sandboxes healed and never appears in
AllRunningItems, yet there is no counter to alert on.
Proposed:
api.redis_storage.scan.teams_skipped_total — counter, incremented on each skipped team
P1 — critical path latency invisible
④ State transition end-to-end duration (state_change.go · StartRemoving)
The full transition — lock acquire → Lua write → callback invoked — is the
core SLI for sandbox kill/pause/resume. No histogram captures this latency.
A rising P99 is the earliest signal of Redis or lock pressure.
Proposed:
api.redis_storage.transition.duration — histogram (ms), from StartRemoving entry to callback invocation
⑤ Joined transition counter (state_change.go · handleExistingTransition)
joined.Mark is already called when a concurrent caller joins an in-flight
transition, but the call is not wired to an OTel counter. Join rate is a proxy
for hot-sandbox concurrency; a spike indicates the same sandbox is being
operated on by many callers simultaneously.
Proposed:
api.redis_storage.transition.joined_total — counter
⑥ WaitForTransition fallback poll counter (state_change.go · waitForTransition)
When a PubSub notification is missed, waitForTransition falls back to a 1 s
ticker. Each fallback tick means a waiter is blocked for an extra second. This
counter, correlated with publisher.dropped, gives the full picture of PubSub
reliability degradation.
Proposed:
api.redis_storage.transition.wait_fallback_polls_total — counter
P2 — data quality and race visibility
⑦ ErrExecutionMismatch counter (state_change.go · StartRemoving)
ErrExecutionMismatch is returned when a pinned removal finds the sandbox has
been resumed under a new execution ID. Currently error-logged only. A high rate
indicates client retry storms or rapid pause→resume cycling.
Proposed:
api.redis_storage.transition.execution_mismatch_total — counter
⑧ Corrupt sandbox record counter (scan.go · fetchSandboxBatch)
JSON unmarshal failures during SSCAN+MGET are Warn-logged and skipped. A
batch of corrupt records would flood logs and silently shrink heal/reconcile
coverage. A counter makes the signal alertable.
Proposed:
api.redis_storage.scan.corrupt_records_total — counter
Summary table
| # |
Metric |
Type |
File · Function |
Priority |
| ① |
lock.wait_duration |
histogram |
locker.go · Obtain |
P0 |
| ① |
lock.timeout_total |
counter |
locker.go · Obtain |
P0 |
| ② |
transition.callback_failures_total |
counter |
state_change.go · createCallback |
P0 |
| ③ |
scan.teams_skipped_total |
counter |
scan.go · forEachSandboxBatch |
P0 |
| ④ |
transition.duration |
histogram |
state_change.go · StartRemoving |
P1 |
| ⑤ |
transition.joined_total |
counter |
state_change.go · handleExistingTransition |
P1 |
| ⑥ |
transition.wait_fallback_polls_total |
counter |
state_change.go · waitForTransition |
P1 |
| ⑦ |
transition.execution_mismatch_total |
counter |
state_change.go · StartRemoving |
P2 |
| ⑧ |
scan.corrupt_records_total |
counter |
scan.go · fetchSandboxBatch |
P2 |
All metric names follow the existing api.redis_storage.* namespace established
in #3604 and #3606.
Related
/cc @jakubno @dobrac @ValentaTomas
Background
The Redis storage layer (
packages/api/internal/sandbox/storage/redis) hasgrown a solid foundation of expiration-index and publisher metrics over recent
PRs (#3604, #3606). However, several high-traffic paths — distributed locking,
state transitions, background scans, and PubSub delivery — still emit nothing
to OTel. The gaps below are ordered by operational impact.
Missing metrics
P0 — silent failures, no alert possible
① Lock wait latency + timeout counter (
locker.go · Obtain)Every sandbox state change (
StartRemoving) must acquire a distributed lock.Obtaincan block untillockTimeout(1 min) before returningctx.Err(),but no metric records how long callers waited or how often the timeout was hit.
Lock contention is the most direct cause of sandbox API tail latency and is
currently invisible.
Proposed:
api.redis_storage.lock.wait_duration— histogram (ms), fromObtainentry to lock acquiredapi.redis_storage.lock.timeout_total— counter, incremented onctx.Err()/ deadline exceeded② Transition callback failure counter (
state_change.go · createCallback)The callback that finalises every state transition has three failure paths
(lock acquire failure,
SETresult key failure,DELtransition key failure),each logging only
Warn. A failing callback leaves waiters blocked until the30 s result-key TTL expires. There is no metric to page on.
Proposed:
api.redis_storage.transition.callback_failures_total— counter withreason=lock_failed|set_result_failed|del_transition_key_failed③ forEachSandboxBatch skipped-team counter (
scan.go · forEachSandboxBatch)Both
healExpirationIndexandAllRunningItemscallforEachSandboxBatch.Per-team scan errors are silently
continued with aWarnlog. A team thatfails every pass never gets its sandboxes healed and never appears in
AllRunningItems, yet there is no counter to alert on.Proposed:
api.redis_storage.scan.teams_skipped_total— counter, incremented on each skipped teamP1 — critical path latency invisible
④ State transition end-to-end duration (
state_change.go · StartRemoving)The full transition — lock acquire → Lua write → callback invoked — is the
core SLI for sandbox kill/pause/resume. No histogram captures this latency.
A rising P99 is the earliest signal of Redis or lock pressure.
Proposed:
api.redis_storage.transition.duration— histogram (ms), fromStartRemovingentry to callback invocation⑤ Joined transition counter (
state_change.go · handleExistingTransition)joined.Markis already called when a concurrent caller joins an in-flighttransition, but the call is not wired to an OTel counter. Join rate is a proxy
for hot-sandbox concurrency; a spike indicates the same sandbox is being
operated on by many callers simultaneously.
Proposed:
api.redis_storage.transition.joined_total— counter⑥ WaitForTransition fallback poll counter (
state_change.go · waitForTransition)When a PubSub notification is missed,
waitForTransitionfalls back to a 1 sticker. Each fallback tick means a waiter is blocked for an extra second. This
counter, correlated with
publisher.dropped, gives the full picture of PubSubreliability degradation.
Proposed:
api.redis_storage.transition.wait_fallback_polls_total— counterP2 — data quality and race visibility
⑦ ErrExecutionMismatch counter (
state_change.go · StartRemoving)ErrExecutionMismatchis returned when a pinned removal finds the sandbox hasbeen resumed under a new execution ID. Currently error-logged only. A high rate
indicates client retry storms or rapid pause→resume cycling.
Proposed:
api.redis_storage.transition.execution_mismatch_total— counter⑧ Corrupt sandbox record counter (
scan.go · fetchSandboxBatch)JSON unmarshal failures during SSCAN+MGET are
Warn-logged and skipped. Abatch of corrupt records would flood logs and silently shrink heal/reconcile
coverage. A counter makes the signal alertable.
Proposed:
api.redis_storage.scan.corrupt_records_total— counterSummary table
lock.wait_durationlocker.go · Obtainlock.timeout_totallocker.go · Obtaintransition.callback_failures_totalstate_change.go · createCallbackscan.teams_skipped_totalscan.go · forEachSandboxBatchtransition.durationstate_change.go · StartRemovingtransition.joined_totalstate_change.go · handleExistingTransitiontransition.wait_fallback_polls_totalstate_change.go · waitForTransitiontransition.execution_mismatch_totalstate_change.go · StartRemovingscan.corrupt_records_totalscan.go · fetchSandboxBatchAll metric names follow the existing
api.redis_storage.*namespace establishedin #3604 and #3606.
Related
/cc @jakubno @dobrac @ValentaTomas