Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/sandbox-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ sequenceDiagram

### Suspension

Core suspends an allocation after initialization completes, when no root or Subagent Turn is queued, in progress or waiting, no input or file operation is pending, and real activity has been idle for the policy's idle timeout. A Session need not have run a Turn. Capacity pressure can suspend an already idle node allocation earlier, after the inactivity grace in [Timeouts and limits](#timeouts-and-limits), when compatible waiting demand exists. A node with no free active slot but a free retained slot also keeps one active slot free: after the free-slot inactivity in [Timeouts and limits](#timeouts-and-limits), Core can suspend an idle allocation without waiting demand, so the next Session does not wait for a suspension. It does not while a Session placed on the node still waits for its sandbox, which the node would otherwise create only after that suspension. It never interrupts an active Turn or releases capacity before the ordinary quiesce, capture and source-stop proofs complete. If suspension cannot make capacity usable, the waiting request keeps its original deadline. When retained slots are full, pressure can [release an idle checkpoint](#cold-replacement-after-checkpoint-retention), and it suspends an allocation only if that allocation's checkpoint could then be released.
Core suspends an allocation after initialization completes, when no root or Subagent Turn is queued, in progress or waiting, no input or file operation is pending, and real activity has been idle for the policy's idle timeout. A Session need not have run a Turn. Capacity pressure can suspend an already idle node allocation earlier, after the inactivity grace in [Timeouts and limits](#timeouts-and-limits), when compatible waiting demand exists. A node with no free active slot but a free retained slot also keeps one active slot free: after the free-slot inactivity in [Timeouts and limits](#timeouts-and-limits), Core can suspend an idle allocation without waiting demand, so the next Session does not wait for a suspension. It does not while a Session placed on the node still waits for its sandbox or its initialization, because the node runs that Session's create and preparation only after the suspension. It never interrupts an active Turn or releases capacity before the ordinary quiesce, capture and source-stop proofs complete. If suspension cannot make capacity usable, the waiting request keeps its original deadline. When retained slots are full, pressure can [release an idle checkpoint](#cold-replacement-after-checkpoint-retention), and it suspends an allocation only if that allocation's checkpoint could then be released.

A Provider relies on these rules:

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/sandbox-provider.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "沙箱提供商协议"
source: docs/sandbox-provider.md
source_hash: 33567fa0e722db83edabdd553bbed29224f02a3f94959224f97daca836c6ca31
source_hash: 2f9163d7e65eb780ab057b590d6ceca0bb24b5cd2a2376a13f440a53e9dd64e2
---

沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。Docker、microsandbox 和 E2B 实现了该协议。
Expand Down Expand Up @@ -243,7 +243,7 @@ sequenceDiagram

### 暂停 {#suspension}

Core 在初始化完成、没有 root 或 Subagent Turn 处于排队、进行中或等待状态、没有待处理的 input 或文件操作,并且真实活动已空闲达到策略的空闲超时之后暂停 allocation。Session 不必运行过 Turn。存在兼容的等待需求时,容量压力可以在[超时与限制](#timeouts-and-limits)所列的无活动宽限期之后,提前暂停已经空闲的 node allocation。没有空闲 active slot、但有空闲 retained slot 的 node 还会保留一个空闲 active slot:达到[超时与限制](#timeouts-and-limits)所列的空闲 slot 无活动时间后,即使没有等待需求,Core 也可以暂停空闲的 allocation,让下一个 Session 不必等待暂停。如果已放置到该 node 的 Session 还在等待创建沙箱,则不会这样做,否则 node 要等这次暂停完成后才会创建那个沙箱。它从不打断活动 Turn,也不会在常规 quiesce、capture 和 source-stop 证明完成之前释放容量。如果暂停无法让容量可用,等待中的请求保留原有期限。retained slot 已满时,容量压力可以[释放空闲检查点](#cold-replacement-after-checkpoint-retention),并且只暂停其检查点随后可被释放的 allocation。
Core 在初始化完成、没有 root 或 Subagent Turn 处于排队、进行中或等待状态、没有待处理的 input 或文件操作,并且真实活动已空闲达到策略的空闲超时之后暂停 allocation。Session 不必运行过 Turn。存在兼容的等待需求时,容量压力可以在[超时与限制](#timeouts-and-limits)所列的无活动宽限期之后,提前暂停已经空闲的 node allocation。没有空闲 active slot、但有空闲 retained slot 的 node 还会保留一个空闲 active slot:达到[超时与限制](#timeouts-and-limits)所列的空闲 slot 无活动时间后,即使没有等待需求,Core 也可以暂停空闲的 allocation,让下一个 Session 不必等待暂停。如果已放置到该 node 的 Session 还在等待创建沙箱或完成初始化,则不会这样做,否则 node 要等这次暂停完成后才会为那个 Session 创建沙箱和执行准备。它从不打断活动 Turn,也不会在常规 quiesce、capture 和 source-stop 证明完成之前释放容量。如果暂停无法让容量可用,等待中的请求保留原有期限。retained slot 已满时,容量压力可以[释放空闲检查点](#cold-replacement-after-checkpoint-retention),并且只暂停其检查点随后可被释放的 allocation。

提供商依赖以下规则:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ WHERE a.node_id IS NOT NULL AND a.state <> 'released'
AND (a.compute_phase IN ('quiescing', 'suspending')
OR (a.compute_retained_until <= clock_timestamp() AND a.compute_phase <> 'disabled'));

-- name: ListRuntimeStartingNodes :many
-- A node is starting a Session while that Session's sandbox is not created or
-- its initialization has not finished.
SELECT DISTINCT p.node_id
FROM runtime_placements p JOIN environments e ON e.id=p.environment_id
LEFT JOIN runtime_allocations a ON a.environment_id=p.environment_id AND a.state <> 'released'
WHERE p.released_at IS NULL AND (a.id IS NULL OR e.initialization IN ('pending', 'running'));

-- name: ListWaitingCheckpointRestores :many
SELECT DISTINCT a.node_id, a.deployment_generation, (a.compute_state->'snapshot'->'Compatibility')::jsonb AS checkpoint
FROM runtime_allocations a JOIN environments e ON e.id=a.environment_id JOIN sessions s ON s.id=e.session_id
Expand Down
29 changes: 29 additions & 0 deletions services/core/internal/db/sqlc/runtime_suspension_pressure.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions services/core/internal/deployment/placement/placement.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,11 @@ type Node struct {
ID string
Online, ServingReady bool
// ReadyGeneration is nil until the node serves a generation.
ReadyGeneration *uint64
TargetState string
// Reserved counts placements still waiting for their sandbox; Active includes them.
Active, Retained, Reserved int64
MaxActive, MaxRetained int
CoreURL string
ReadyGeneration *uint64
TargetState string
Active, Retained int64
MaxActive, MaxRetained int
CoreURL string
}

// Placement is the node and generation a new Session's Environment reserves.
Expand Down
7 changes: 4 additions & 3 deletions services/core/internal/deployment/suspension_pressure.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type SuspensionDemand struct {
Deployment placement.Deployment
Nodes []placement.Node
InFlightNodes []string
StartingNodes []string
CheckpointRestores []CheckpointRestoreDemand
}

Expand Down Expand Up @@ -48,11 +49,11 @@ func fullNode(pressure SuspensionDemand, owner Allocation) *placement.Node {
// canKeepFreeSlot allows suspending an allocation that stayed idle for
// freeSlotIdle on a full node with no reclamation in flight. The node must
// also have a free retained slot, or the freed active slot admits no Session,
// and no placed Session waiting for its sandbox, which the node's lane would
// otherwise create only after this suspension.
// and no Session still starting on it: the node runs one operation at a time,
// so that Session's create and preparation would wait for this suspension.
func canKeepFreeSlot(pressure SuspensionDemand, owner Allocation) bool {
n := fullNode(pressure, owner)
return n != nil && n.Retained < int64(n.MaxRetained) && n.Reserved == 0 && !slices.Contains(pressure.InFlightNodes, owner.NodeID)
return n != nil && n.Retained < int64(n.MaxRetained) && !slices.Contains(pressure.InFlightNodes, owner.NodeID) && !slices.Contains(pressure.StartingNodes, owner.NodeID)
}

// canSuspendForDemand reports whether compatible waiting demand would place on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ func (t *allocationTx) LoadSuspensionDemand(current deployment.Allocation) (depl
for _, node := range inFlight {
result.InFlightNodes = append(result.InFlightNodes, uuidString(node))
}
starting, err := t.q.ListRuntimeStartingNodes(t.ctx)
if err != nil {
return result, err
}
for _, node := range starting {
result.StartingNodes = append(result.StartingNodes, uuidString(node))
}
result.Nodes, err = placementpg.LoadNodes(t.ctx, t.q)
if err != nil {
return result, err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,36 @@ func TestPressureSuspensionRequiresUsefulCapacity(t *testing.T) {
}
}

func TestFreeSlotWaitsForPlacedSession(t *testing.T) {
f := newFixture(t)
changes, _ := f.execution(t)
installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")})
node := f.enroll(t, view, deployment.Capacity{MaxActive: 2, MaxRetained: 3})
f.connect(t, node.NodeID)
owner := runningPressureOwner(t, f, changes, installation, node.NodeID, view.Generation, "3 minutes")
placed := hostedEnvironment(t, f.pool)
if _, err := f.pool.Exec(t.Context(), `INSERT INTO runtime_placements(environment_id,node_id,deployment_generation) VALUES($1,$2,$3)`, placed.EnvironmentID, node.NodeID, view.Generation); err != nil {
t.Fatal(err)
}
until := time.Now().Add(time.Hour)
if _, err := changes.SetCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until); !errors.Is(err, deployment.ErrNotIdle) {
t.Fatalf("quiesce: %v, want %v", err, deployment.ErrNotIdle)
func TestFreeSlotWaitsForStartingSession(t *testing.T) {
for _, created := range []bool{false, true} {
t.Run(map[bool]string{false: "waiting for its sandbox", true: "initializing"}[created], func(t *testing.T) {
f := newFixture(t)
changes, _ := f.execution(t)
installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")})
node := f.enroll(t, view, deployment.Capacity{MaxActive: 2, MaxRetained: 3})
f.connect(t, node.NodeID)
owner := runningPressureOwner(t, f, changes, installation, node.NodeID, view.Generation, "3 minutes")
placed := hostedEnvironment(t, f.pool)
if _, err := f.pool.Exec(t.Context(), `INSERT INTO runtime_placements(environment_id,node_id,deployment_generation) VALUES($1,$2,$3)`, placed.EnvironmentID, node.NodeID, view.Generation); err != nil {
t.Fatal(err)
}
if created {
starting, err := changes.ReserveAllocation(t.Context(), placed, installation, credentialHash())
if err != nil {
t.Fatal(err)
}
if _, err = f.pool.Exec(t.Context(), `UPDATE runtime_allocations SET state='running',create_settled=true WHERE id=$1`, starting.ID); err != nil {
t.Fatal(err)
}
if _, err = f.pool.Exec(t.Context(), `UPDATE environments SET initialization='running' WHERE id=$1`, placed.EnvironmentID); err != nil {
t.Fatal(err)
}
}
until := time.Now().Add(time.Hour)
if _, err := changes.SetCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until); !errors.Is(err, deployment.ErrNotIdle) {
t.Fatalf("quiesce: %v, want %v", err, deployment.ErrNotIdle)
}
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func ComputeBlocksAdmission(ctx context.Context, q *sqlc.Queries, session pgtype
func node(row sqlc.ListRuntimeNodesRow) placement.Node {
n := placement.Node{
ID: uuidString(row.ID), Online: row.Online, ServingReady: row.ServingReady, TargetState: row.TargetState,
Active: row.Active, Retained: row.Retained, Reserved: row.Reserved, MaxActive: int(row.MaxActive), MaxRetained: int(row.MaxRetained), CoreURL: row.CoreUrl,
Active: row.Active, Retained: row.Retained, MaxActive: int(row.MaxActive), MaxRetained: int(row.MaxRetained), CoreURL: row.CoreUrl,
}
if row.ReadyGeneration.Valid {
ready := uint64(row.ReadyGeneration.Int64)
Expand Down
Loading