sandbox cache: StartRemoving state transition not broadcast, all allocations see stale Running state

オープン 初心者向け
#3,595 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
86/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
go, redis
領域
backend, databases

調査の方向性

packages/api/internal/sandbox/storage/redis/state_change.go の StartRemoving から始め、operations.go の Update() にある publish パターンと遷移処理を比較します。subscription_manager.go の dispatch を追跡して、cache が sandbox の更新イベントを受け取ることを確認します。StartRemoving の後に cache が新しい遷移状態を反映し、既存の callback の動作を変更していなければ完了です。

索引モデルが issue の本文から書いたものです。

説明

Summary

When StartRemoving atomically writes a state transition to Redis (e.g. Running → Killing), it never calls publishSandboxEvent. The per-allocation in-process sandbox cache (introduced in #3593) therefore never learns about the new state, and every allocation continues to serve the old Running state from memory until the sandbox is eventually deleted and a remove event arrives.

Code Path

Step 1 — Lua script writes new state to Redis, but no event is published
// state_change.go
updated := sbx
updated.State = newState                       // e.g. Killing

written, err := startTransitionScript.Run(ctx, s.redisClient,
    []string{key, transitionKey, resultKey},
    newData, transitionID, ...)                // Redis now has State=Killing

// StartRemoving returns here — no publishSandboxEvent call
return updated, false, s.createCallback(...), nil
Step 2 — createCallback publishes only a routing key, not a sandboxEvent
// state_change.go — createCallback
s.publisher.Publish(cbCtx, getTransitionRoutingKey(teamID.String(), sandboxID, transitionID))
// payload: "lock:sandbox:storage:...:transition:<uuid>"
Step 3 — dispatch routes the payload to waiters, never to cache.apply
// subscription_manager.go
func (m *subscriptionManager) dispatch(payload string) {
    if isSandboxEvent(payload) {   // strings.HasPrefix(payload, "{") → false for routing keys
        m.cache.apply(evt)         // never reached
        return
    }
    // falls through to waiter fan-out
}

Impact

Between startTransitionScript.Run() and the final Remove() call (which does publish a remove event), every allocation returns State = Running from TeamItems for a sandbox that Redis already has as Killing or Pausing. For transient transitions, restoreToRunning calls Update() which publishes correctly, but the first half of the round-trip remains invisible to the cache.

Concretely:

  • TeamItems(states: [Running]) returns sandboxes that are mid-removal — callers may act on them incorrectly.
  • Metrics and user-facing sandbox list show inflated Running counts during high-eviction periods.

Fix

In StartRemoving, after startTransitionScript.Run() succeeds, broadcast the updated sandbox:

s.publisher.publishSandboxEvent(ctx, sandboxEvent{
    Op:      sandboxEventOpUpdate,
    Sandbox: &updated,
})

This mirrors exactly what Update() does in operations.go. dispatch() will call cache.apply({update, updated}), replacing the cached entry with the correct state.

The createCallback path does not need a separate publish because:

  • For permanent removals (TransitionExpires), the subsequent Remove() call already publishes a remove event.
  • For transient transitions, restoreToRunning calls Update() which already publishes an update event.

Related

  • #3593 — introduces the per-allocation cache that this change affects
  • packages/api/internal/sandbox/storage/redis/state_change.go
  • packages/api/internal/sandbox/storage/redis/operations.go (reference: correct publish pattern in Update())
主要言語
Go
スター
1.6k
フォーク
438
PR マージ指標
30日以内にマージされた PR はありません

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

e2b-dev/runtime のほかの issue

e2b-dev/runtime の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。