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
3 changes: 0 additions & 3 deletions boot/init/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,16 +327,13 @@ mod tests {

#[test]
fn parse_ip_param_variants() {
// No DNS, zero gateway.
let p = parse_ip_param("10.0.0.2::0.0.0.0:255.255.254.0:vm:eth1:off").unwrap();
assert_eq!(p.prefix, 23);
assert_eq!(p.gateway, None);
assert!(p.dns.is_empty());
assert_eq!(p.device, "eth1");
// Zero DNS entries are filtered.
let p = parse_ip_param("10.0.0.2::10.0.0.1:255.255.255.0:vm:eth0:off:0.0.0.0").unwrap();
assert!(p.dns.is_empty());
// Shorthand and malformed forms are ignored.
assert_eq!(parse_ip_param("dhcp"), None);
assert_eq!(parse_ip_param("off"), None);
assert_eq!(parse_ip_param("10.0.0.2::gw:not-a-mask:vm:eth0:off"), None);
Expand Down
29 changes: 13 additions & 16 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,19 @@ snapshot paths.

## Claims-journal write path

Every claim/release/hibernate persists `claims.json` inside the pool mutex
(marshal + write + rename). Benchmark (`BenchmarkStoreSaveScaling`, APFS
SSD), per save:

| live claims | ns/op | B/op |
|---|---|---|
| 10 | ~181k (0.18ms) | 4.5k |
| 100 | ~240k (0.24ms) | 36k |
| 1000 | ~829k (0.83ms) | 385k |

At realistic scale the in-mutex write is sub-millisecond and dwarfed by the
provision path (~40ms clone, ~230ms cold); the mutex-held serialization
does not approach a 2× claim-latency regression. **Decision: keep the
simple in-mutex write; do not move to a store-owned sequential writer.**
The benchmark stays as a regression sentinel — revisit only if it crosses
into the milliseconds at the deployment's real claim count.
`claim`/`release`/`hibernate`/`wake` persist `claims.json`, but the write is
kept off the manager mutex — the lock every data-plane op contends. `snapshot()`
takes a cheap by-value copy of the claim map under the mutex; `commit()` marshals
it, writes, and renames off the mutex, serialized and coalescing by sequence so
an older snapshot never overwrites a newer one. Only the startup `Reconcile`
pass (pre-contention) still marshals and writes in one call under the lock.

`BenchmarkStorePersistContention` measures the ns a concurrent manager-mutex
acquire waits during a persist. Moving first the write (F1), then the marshal
(F1 part 2), off the lock cut that wait from tens of µs to tens of ns — the
marshal-off-lock split alone is a ~6× drop at 1000 live claims, a margin that
grows with claim count. `BenchmarkStoreSaveScaling` (the combined `save()`
Reconcile uses) stays as a regression sentinel.

## Measured and declined (decision data)

Expand Down
9 changes: 3 additions & 6 deletions sandboxd/engine/portconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ const (
portReadBuf = 64 << 10
)

// guestPortConn adapts silkd's framed port_forward channel to a plain
// net.Conn: the guest's TCP bytes ride inside newline-JSON `data` frames
// (base64 payloads), so writes wrap into `{"op":"data",...}` frames and
// reads unwrap `{"type":"data",...}` frames back to raw bytes. This is the
// server-side twin of the SDK's PortConn, so a reverse proxy can splice HTTP
// straight through it.
// guestPortConn adapts silkd's newline-JSON data-frame port_forward channel to
// a plain net.Conn (base64 payloads), the server-side twin of the SDK's
// PortConn, so a reverse proxy can splice HTTP straight through it.
type guestPortConn struct {
net.Conn
r *bufio.Reader
Expand Down
11 changes: 5 additions & 6 deletions sandboxd/pool/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@ type claimDTO struct {
// VMs are deliberately not persisted: they are cheap to rebuild and unsafe
// to trust after an unsupervised gap.
//
// A write is split so the manager mutex covers only the marshal (a consistent
// snapshot of the claim map), not the file syscalls: snapshot() runs under the
// manager mutex, commit() writes off it. commit is serialized and coalescing —
// a snapshot no newer than one already on disk is dropped, because sequence
// order matches the manager-mutex mutation order, so the newest write wins and
// an older one only ever describes a superseded state.
// The write is split to keep the manager mutex off both the marshal and the
// file syscalls: snapshot() takes a cheap field-copy under the mutex, commit()
// marshals and writes off it — serialized and coalescing so a snapshot no newer
// than one already on disk is dropped (sequence order matches mutation order,
// so an older write only ever describes a superseded state).
type claimStore struct {
path string

Expand Down
11 changes: 4 additions & 7 deletions sandboxd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,10 @@ func (s *Server) handleClaim(w http.ResponseWriter, r *http.Request) {
}
}

// redirectClaim answers a warm-miss claim with a redirect when a peer is the
// better node: one that reports warm sandboxes, or — when this node has no
// golden for the key — the owner of a promoted template gossip names, which
// provisions from it instead of us cold-booting a nonexistent image ref. A
// claim already redirected here carries no_redirect and must warm-or-provision
// locally, never bounce again — that avoids a two-node stale-view ping-pong
// when both just emptied their pools.
// redirectClaim redirects a warm-miss to a better peer — a warm holder, or the
// template owner when we lack a golden (so we don't cold-boot a nonexistent
// image ref). A no_redirect request must resolve locally, never bounce again,
// to avoid a two-node ping-pong.
func (s *Server) redirectClaim(ctx context.Context, w http.ResponseWriter, req types.ClaimRequest, key types.PoolKey, hash string) bool {
if s.placer == nil || req.NoRedirect {
return false
Expand Down
2 changes: 1 addition & 1 deletion sdk/openai/cocoonsandbox_openai/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ async def delete(self, session: SandboxSession) -> SandboxSession:
inner = session._inner
if not isinstance(inner, CocoonSandboxSession):
raise TypeError("CocoonSandboxClient.delete expects a CocoonSandboxSession")
await inner._shutdown_backend() # close any local port proxies
await inner._shutdown_backend()
await asyncio.to_thread(inner._sandbox().close) # 404-safe in the SDK
return session

Expand Down
Loading