Skip to content

Expose internal scheduling metadata - #2873

Merged
jakubno merged 33 commits into
mainfrom
valenta/snapshot-scheduling-metadata
Jun 1, 2026
Merged

jakubno merged 33 commits into
mainfrom
valenta/snapshot-scheduling-metadata

Conversation

@ValentaTomas

@ValentaTomas ValentaTomas commented May 30, 2026 •

Copy link
Copy Markdown
Member

Expose compact scheduling metadata from orchestrator and template-manager without DB or public API changes.

Return compact build-chain metadata from snapshot creation so API placement can later use real build ancestry for resume affinity.
@cla-bot cla-bot Bot added the cla-signed label May 30, 2026
@cursor

cursor Bot commented May 30, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes the Pause gRPC response type (breaking for clients) and feeds scheduling placement with capped/truncated build chains and approximate memfile bytes on snapshot.

Overview
Introduces a shared SchedulingMetadata protobuf describing which template build layers a sandbox’s memfile and rootfs reference (base/current build IDs, per-layer build lists with byte counts, and drop counts when lists exceed 128 entries). The orchestrator computes it from storage headers via a new scheduling.FromHeaders helper (backed by Mapping.BytesByBuild) and returns it on sandbox Create, Pause, and Checkpoint; Pause now returns SandboxPauseResponse instead of empty. Snapshot/pause derives memfile metadata synchronously using a pre-dedup dirty-size upper bound so pause does not wait on async memfile-dedup headers. Loaded templates expose SchedulingMetadata from resolved devices, and completed template builds attach the same shape on TemplateBuildMetadata so callers can align scheduling signals without a separate format.

Reviewed by Cursor Bugbot for commit e46ed48. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread packages/orchestrator/pkg/server/sandboxes.go
Comment thread packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql Outdated
@codecov

codecov Bot commented May 30, 2026 •

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
2707 3 2704 5
View the full list of 3 ❄️ flaky test(s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestSandboxListPaginationRunningLargerLimit

Flake rate in main: 42.64% (Passed 764 times, Failed 568 times)

Stack Traces | 92.9s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit
    sandbox_list_test.go:327: Created sandbox 1/12: i8z05cld3v1llq9p5l9c5
    sandbox_list_test.go:327: Created sandbox 2/12: i500ywb932i4bwu9nzazb
    sandbox_list_test.go:327: Created sandbox 3/12: izjesm0m5098h4whtkzgk
    sandbox_list_test.go:327: Created sandbox 4/12: iqq15xl5pkjnirihmgopy
    sandbox_list_test.go:327: Created sandbox 5/12: i4gy5nuew4zzgqpxe4cdb
    sandbox_list_test.go:327: Created sandbox 6/12: iiapwc37bpkel1ltg2thx
    sandbox_list_test.go:327: Created sandbox 7/12: ivvocelvk035o7qgz4rdy
    sandbox_list_test.go:327: Created sandbox 8/12: iwqf97a6eug4gxa0cjgau
    sandbox_list_test.go:327: Created sandbox 9/12: imd2vryg01rd37uoifes5
    sandbox_list_test.go:327: Created sandbox 10/12: ididxzza6vp363v1jwiir
    sandbox_list_test.go:327: Created sandbox 11/12: ib8ltlaixe5p3xnziqqlh
    sandbox_list_test.go:327: Created sandbox 12/12: itvfnn0klskwjkf42v54i
    sandbox_list_test.go:330: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:340
        	            				.../hostedtoolcache/go/1.26.3.../src/runtime/asm_amd64.s:1771
        	Error:      	"[]" should have 12 item(s), but has 0
    sandbox_list_test.go:330: 
        	Error Trace:	.../api/sandboxes/sandbox_list_test.go:330
        	Error:      	Condition never satisfied
        	Test:       	TestSandboxListPaginationRunningLargerLimit
--- FAIL: TestSandboxListPaginationRunningLargerLimit (92.91s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 57.45% (Passed 757 times, Failed 1022 times)

Stack Traces | 65s run time
=== RUN   TestSandboxMemoryIntegrity
=== PAUSE TestSandboxMemoryIntegrity
=== CONT  TestSandboxMemoryIntegrity
    sandbox_memory_integrity_test.go:27: Build completed successfully
--- FAIL: TestSandboxMemoryIntegrity (64.99s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity/tmpfs_hash

Flake rate in main: 57.56% (Passed 747 times, Failed 1013 times)

Stack Traces | 189s run time
=== RUN   TestSandboxMemoryIntegrity/tmpfs_hash
=== PAUSE TestSandboxMemoryIntegrity/tmpfs_hash
=== CONT  TestSandboxMemoryIntegrity/tmpfs_hash
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{start:{pid:1259}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Total memory: 985 MB\nUsed memory before tmpfs mount: 189 MB\nFree memory before tmpfs mount: 795 MB\nMemory to use in integrity test (60% of free, min 64MB): 477 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"477+0 records in\n477+0 records out\n500170752 bytes (500 MB, 477 MiB) copied, 2.12333 s, 236 MB/s\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\t"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"C"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"o"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"a"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"d"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"b"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"g"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"t"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"d"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:":"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\""}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"dd"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"if=/dev/urandom"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"of=/mnt/testfile"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"bs=1M"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"count=477"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\""}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\t"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"U"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"r"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"t"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"i"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"("}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"c"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"o"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"d"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:")"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:":"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"0.00"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\t"}}
Executing command bash in sandbox i73p43x69qr9adnrjkdhg (user: root)
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"S"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"y"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"s"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"t"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"e"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"m"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:" "}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"time (seconds): 2.11\n\tPercent of CPU this job got: 99%\n\tElapsed (wall clock) time (h:mm:ss or m:ss): 0:02.12\n\tAverage shared text size (kbytes): 0\n\tAverage unshared data size (kbytes): 0\n\tAverage stack size (kbytes): 0\n\tAverage total size (kbytes): 0\n\tMaximum resident set size (kbytes): 2652\n\tAverage resident set size (kbytes): 0\n\tMajor (requiring I/O) page faults: 3\n\tMinor (reclaiming a frame) page faults: 343\n\tVoluntary context switches: 4\n\tInvoluntary context switches: 38\n\tSwaps: 0\n\tFile system inputs: 176\n\tFile system outputs: 0\n\tSocket messages sent: 0\n\tSocket messages received: 0\n\tSignals delivered: 0\n\tPage size (bytes): 4096\n\tExit status: 0\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Used memory after tmpfs mount and file fill: 671 MB\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:70: Command [bash] completed successfully in sandbox ikfjokn6evutyi649nxgl
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1276}}
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{data:{stdout:"65c79feeead7359f0ed04c78dd23d51d209721f0ecfd68344100bfa8c48c269b\n"}}
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{end:{exited:true  status:"exit status 0"}}
    sandbox_memory_integrity_test.go:80: Command [bash] completed successfully in sandbox ikfjokn6evutyi649nxgl
Executing command bash in sandbox i73p43x69qr9adnrjkdhg (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1279}}
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
Executing command bash in sandbox ikfjokn6evutyi649nxgl (user: root)
    sandbox_memory_integrity_test.go:110: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:81
        	            				.../hostedtoolcache/go/1.26.3.../src/runtime/asm_amd64.s:1771
        	Error:      	Received unexpected error:
        	            	failed to execute command bash in sandbox ikfjokn6evutyi649nxgl: unavailable: HTTP status 502 Bad Gateway
    sandbox_memory_integrity_test.go:110: 
        	Error Trace:	.../tests/orchestrator/sandbox_memory_integrity_test.go:78
        	            				.../tests/orchestrator/sandbox_memory_integrity_test.go:110
        	Error:      	Condition never satisfied
        	Test:       	TestSandboxMemoryIntegrity/tmpfs_hash
--- FAIL: TestSandboxMemoryIntegrity/tmpfs_hash (189.00s)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

In update_snapshot_scheduling_metadata.sql, using to_jsonb on a text parameter stores it as an escaped string scalar rather than a JSONB object, so the parameter should be cast directly to jsonb. In sandboxes.go, accessing res.SchedulingMetadata will cause a compilation error because the snapshotResult struct lacks this field. Additionally, NewSnapshotSchedulingMetadata in snapshot.go is missing nil checks for the Mapping fields of the headers, which can lead to a nil pointer dereference panic.

Comment thread packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql Outdated
Comment thread packages/orchestrator/pkg/server/sandboxes.go
Comment thread packages/orchestrator/pkg/server/sandboxes.go
Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
Populate pause/checkpoint responses from snapshot results and store scheduling metadata as JSONB.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Pause fails after orchestrator pause
    • Added failSnapshotBuild call when updateSnapshotSchedulingMetadata fails after successful pause RPC to properly mark build status as failed.
  • ✅ Fixed: Metadata error finishes snapshot success
    • Added finish(err) call before returning when updateSnapshotSchedulingMetadata fails to prevent deferred finish(nil) from incorrectly marking snapshot as success.

Create PR

Or push these changes by commenting:

@cursor push caa2284235
Preview (caa2284235)
diff --git a/packages/api/internal/orchestrator/pause_instance.go b/packages/api/internal/orchestrator/pause_instance.go
--- a/packages/api/internal/orchestrator/pause_instance.go
+++ b/packages/api/internal/orchestrator/pause_instance.go
@@ -52,6 +52,8 @@
 	}
 	if schedulingMetadata != nil {
 		if err := o.updateSnapshotSchedulingMetadata(ctx, sbx.SandboxID, schedulingMetadata); err != nil {
+			o.failSnapshotBuild(ctx, result.BuildID, err)
+
 			return err
 		}
 	}

diff --git a/packages/api/internal/orchestrator/snapshot_template.go b/packages/api/internal/orchestrator/snapshot_template.go
--- a/packages/api/internal/orchestrator/snapshot_template.go
+++ b/packages/api/internal/orchestrator/snapshot_template.go
@@ -106,6 +106,8 @@
 		if err := o.updateSnapshotSchedulingMetadata(ctx, sbx.SandboxID, metadata); err != nil {
 			o.failSnapshotBuild(ctx, upsertResult.BuildID, err)
 
+			finish(err)
+
 			return SnapshotTemplateResult{}, err
 		}
 	}

You can send follow-ups to the cloud agent here.

Comment thread packages/api/internal/orchestrator/pause_instance.go Outdated
Comment thread packages/api/internal/orchestrator/snapshot_template.go Outdated
Do not fail a successful pause or checkpoint if persisting optional scheduling metadata fails.
@cursor

This comment has been minimized.

Use slices.SortFunc and remove an unnecessary conversion in metadata generation.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Limit one drops parent build
    • Added condition to prevent base build from overwriting parent build when chain limit is 1 and last element is the parent.

Create PR

Or push these changes by commenting:

@cursor push 527fa53cf5
Preview (527fa53cf5)
diff --git a/packages/orchestrator/pkg/sandbox/snapshot.go b/packages/orchestrator/pkg/sandbox/snapshot.go
--- a/packages/orchestrator/pkg/sandbox/snapshot.go
+++ b/packages/orchestrator/pkg/sandbox/snapshot.go
@@ -139,7 +139,7 @@
 					break
 				}
 			}
-			if !hasBase {
+			if !hasBase && chain[len(chain)-1].buildID != parentBuildID {
 				if c, ok := contributions[baseBuildID]; ok {
 					chain[len(chain)-1] = *c
 				} else {

You can send follow-ups to the cloud agent here.

Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
Avoid replacing the only chain entry with the base build because base build is already stored separately.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Nested metadata breaks JSONBStringMap scans
    • Changed update_snapshot_scheduling_metadata.sql to use to_jsonb(@metadata::text) instead of (@metadata::text)::jsonb, storing the scheduling metadata as a JSON string value that JSONBStringMap can unmarshal correctly.
  • ✅ Fixed: Upsert clears scheduling metadata key
    • Changed create_new_snapshot.sql conflict handler to use snapshots.metadata || excluded.metadata instead of excluded.metadata, preserving existing metadata keys including snapshot_scheduling_metadata during upsert.

Create PR

Or push these changes by commenting:

@cursor push c71543e849
Preview (c71543e849)
diff --git a/packages/db/queries/create_new_snapshot.sql.go b/packages/db/queries/create_new_snapshot.sql.go
--- a/packages/db/queries/create_new_snapshot.sql.go
+++ b/packages/db/queries/create_new_snapshot.sql.go
@@ -55,7 +55,7 @@
             now()
    )
     ON CONFLICT (sandbox_id) DO UPDATE SET
-        metadata = excluded.metadata,
+        metadata = snapshots.metadata || excluded.metadata,
         sandbox_started_at = excluded.sandbox_started_at,
         allow_internet_access = COALESCE(excluded.allow_internet_access, snapshots.allow_internet_access),
         origin_node_id = excluded.origin_node_id,

diff --git a/packages/db/queries/snapshots/create_new_snapshot.sql b/packages/db/queries/snapshots/create_new_snapshot.sql
--- a/packages/db/queries/snapshots/create_new_snapshot.sql
+++ b/packages/db/queries/snapshots/create_new_snapshot.sql
@@ -41,7 +41,7 @@
             now()
    )
     ON CONFLICT (sandbox_id) DO UPDATE SET
-        metadata = excluded.metadata,
+        metadata = snapshots.metadata || excluded.metadata,
         sandbox_started_at = excluded.sandbox_started_at,
         allow_internet_access = COALESCE(excluded.allow_internet_access, snapshots.allow_internet_access),
         origin_node_id = excluded.origin_node_id,

diff --git a/packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql b/packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql
--- a/packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql
+++ b/packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql
@@ -3,7 +3,7 @@
 SET metadata = jsonb_set(
     metadata,
     '{snapshot_scheduling_metadata}',
-    (@metadata::text)::jsonb,
+    to_jsonb(@metadata::text),
     true
 )
 WHERE sandbox_id = @sandbox_id;

diff --git a/packages/db/queries/update_snapshot_scheduling_metadata.sql.go b/packages/db/queries/update_snapshot_scheduling_metadata.sql.go
--- a/packages/db/queries/update_snapshot_scheduling_metadata.sql.go
+++ b/packages/db/queries/update_snapshot_scheduling_metadata.sql.go
@@ -1,6 +1,6 @@
 // Code generated by sqlc. DO NOT EDIT.
 // versions:
-//   sqlc v1.31.1
+//   sqlc v1.29.0
 // source: update_snapshot_scheduling_metadata.sql
 
 package queries
@@ -14,7 +14,7 @@
 SET metadata = jsonb_set(
     metadata,
     '{snapshot_scheduling_metadata}',
-    ($1::text)::jsonb,
+    to_jsonb($1::text),
     true
 )
 WHERE sandbox_id = $2

You can send follow-ups to the cloud agent here.

Comment thread packages/db/queries/snapshots/update_snapshot_scheduling_metadata.sql Outdated
Comment thread packages/api/internal/orchestrator/pause_instance.go Outdated
Store scheduling metadata as a string value in the existing metadata map and keep it across snapshot upserts until refreshed.
@ValentaTomas
ValentaTomas marked this pull request as ready for review May 30, 2026 21:58
dobrac
dobrac previously requested changes May 30, 2026
Comment thread packages/api/internal/orchestrator/pause_instance.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
Comment on lines +99 to +102
baseBuildID := memfileHeader.Metadata.BaseBuildId
if baseBuildID == uuid.Nil {
baseBuildID = rootfsHeader.Metadata.BaseBuildId
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain this please? Should we have two different baseBuildIDs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one scheduling base in the persisted metadata. We prefer the memfile base and fall back to rootfs only if the memfile base is nil; this avoids adding two base IDs to the scheduling contract in this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we have two baseBuildIDs if effectively there are two?

Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
@ValentaTomas ValentaTomas changed the title Persist snapshot scheduling metadata Expose internal scheduling metadata May 31, 2026
Comment thread packages/orchestrator/pkg/sandbox/sandbox.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/template/storage_template.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 055b4e0d2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/orchestrator/pkg/sandbox/sandbox.go Outdated
Comment thread packages/orchestrator/pkg/scheduling/metadata.go Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Generation incremented twice
    • Removed the duplicate generation increment from FromHeaders and moved it to only apply in NewSnapshotSchedulingMetadata for pre-snapshot headers.

Create PR

Or push these changes by commenting:

@cursor push 9d1cfd4f72
Preview (9d1cfd4f72)
diff --git a/packages/orchestrator/pkg/sandbox/snapshot.go b/packages/orchestrator/pkg/sandbox/snapshot.go
--- a/packages/orchestrator/pkg/sandbox/snapshot.go
+++ b/packages/orchestrator/pkg/sandbox/snapshot.go
@@ -58,5 +58,9 @@
 }
 
 func NewSnapshotSchedulingMetadata(memfileHeader, rootfsHeader *header.Header) *orchestrator.SchedulingMetadata {
-	return scheduling.FromHeaders(memfileHeader, rootfsHeader)
+	metadata := scheduling.FromHeaders(memfileHeader, rootfsHeader)
+	if metadata != nil {
+		metadata.Generation++
+	}
+	return metadata
 }

diff --git a/packages/orchestrator/pkg/scheduling/metadata.go b/packages/orchestrator/pkg/scheduling/metadata.go
--- a/packages/orchestrator/pkg/scheduling/metadata.go
+++ b/packages/orchestrator/pkg/scheduling/metadata.go
@@ -93,7 +93,7 @@
 
 	res := &orchestrator.SchedulingMetadata{
 		BaseBuildId:          baseBuildID.String(),
-		Generation:           max(memfileHeader.Metadata.Generation, rootfsHeader.Metadata.Generation) + 1,
+		Generation:           max(memfileHeader.Metadata.Generation, rootfsHeader.Metadata.Generation),
 		ChainBuildIds:        make([]string, 0, len(chain)),
 		MemfileLogicalBytes:  make([]uint64, 0, len(chain)),
 		MemfileChunkCounts:   make([]uint32, 0, len(chain)),

You can send follow-ups to the cloud agent here.

Comment thread packages/orchestrator/pkg/scheduling/metadata.go Outdated
@cursor

cursor Bot commented May 31, 2026

Copy link
Copy Markdown

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Pause metadata omits snapshot layer
    • Changed NewSnapshotSchedulingMetadata to compute from diff headers (memfileDiffHeader, rootfsDiffHeader) instead of original parent headers, ensuring the new snapshot buildID and its storage contributions are included in the scheduling metadata.
  • ✅ Fixed: Cached metadata skips pending headers
    • Changed SchedulingMetadata() to use WaitWithContext instead of Result, allowing it to wait for asynchronously-resolving headers rather than returning nil when headers are still pending.

Create PR

Or push these changes by commenting:

@cursor push 526c21400a
Preview (526c21400a)
diff --git a/packages/orchestrator/pkg/sandbox/sandbox.go b/packages/orchestrator/pkg/sandbox/sandbox.go
--- a/packages/orchestrator/pkg/sandbox/sandbox.go
+++ b/packages/orchestrator/pkg/sandbox/sandbox.go
@@ -1185,7 +1185,7 @@
 	}
 	cleanup.AddNoContext(ctx, rootfsDiff.Close)
 
-	schedulingMetadata := NewSnapshotSchedulingMetadata(originalMemfile.Header(), originalRootfs.Header())
+	schedulingMetadata := NewSnapshotSchedulingMetadata(ctx, memfileDiffHeader, NewResolvedDiffHeader(rootfsHeader))
 
 	metadataFileLink := template.NewLocalFileLink(cachePaths.CacheMetadata())
 	cleanup.AddNoContext(ctx, metadataFileLink.Close)

diff --git a/packages/orchestrator/pkg/sandbox/snapshot.go b/packages/orchestrator/pkg/sandbox/snapshot.go
--- a/packages/orchestrator/pkg/sandbox/snapshot.go
+++ b/packages/orchestrator/pkg/sandbox/snapshot.go
@@ -57,6 +57,12 @@
 	return nil
 }
 
-func NewSnapshotSchedulingMetadata(memfileHeader, rootfsHeader *header.Header) *orchestrator.SchedulingMetadata {
+func NewSnapshotSchedulingMetadata(ctx context.Context, memfileDiffHeader, rootfsDiffHeader *DiffHeader) *orchestrator.SchedulingMetadata {
+	memfileHeader, memfileErr := memfileDiffHeader.WaitWithContext(ctx)
+	rootfsHeader, rootfsErr := rootfsDiffHeader.WaitWithContext(ctx)
+	if memfileErr != nil || rootfsErr != nil {
+		return nil
+	}
+
 	return scheduling.FromHeaders(memfileHeader, rootfsHeader)
 }

diff --git a/packages/orchestrator/pkg/sandbox/template/storage_template.go b/packages/orchestrator/pkg/sandbox/template/storage_template.go
--- a/packages/orchestrator/pkg/sandbox/template/storage_template.go
+++ b/packages/orchestrator/pkg/sandbox/template/storage_template.go
@@ -272,9 +272,9 @@
 	return t.paths
 }
 
-func (t *storageTemplate) SchedulingMetadata() *orchestrator.SchedulingMetadata {
-	memfileHeader, memfileErr := t.memfileHeader.Result()
-	rootfsHeader, rootfsErr := t.rootfsHeader.Result()
+func (t *storageTemplate) SchedulingMetadata(ctx context.Context) *orchestrator.SchedulingMetadata {
+	memfileHeader, memfileErr := t.memfileHeader.WaitWithContext(ctx)
+	rootfsHeader, rootfsErr := t.rootfsHeader.WaitWithContext(ctx)
 	if memfileErr != nil || rootfsErr != nil {
 		return nil
 	}

diff --git a/packages/orchestrator/pkg/server/template_cache.go b/packages/orchestrator/pkg/server/template_cache.go
--- a/packages/orchestrator/pkg/server/template_cache.go
+++ b/packages/orchestrator/pkg/server/template_cache.go
@@ -20,9 +20,9 @@
 	for key, item := range s.templateCache.Items() {
 		var metadata *orchestrator.SchedulingMetadata
 		if provider, ok := item.Value().(interface {
-			SchedulingMetadata() *orchestrator.SchedulingMetadata
+			SchedulingMetadata(context.Context) *orchestrator.SchedulingMetadata
 		}); ok {
-			metadata = provider.SchedulingMetadata()
+			metadata = provider.SchedulingMetadata(ctx)
 		}
 		builds = append(builds, &orchestrator.CachedBuildInfo{
 			BuildId:            key,

diff --git a/packages/orchestrator/pkg/template/build/builder.go b/packages/orchestrator/pkg/template/build/builder.go
--- a/packages/orchestrator/pkg/template/build/builder.go
+++ b/packages/orchestrator/pkg/template/build/builder.go
@@ -390,23 +390,23 @@
 		KernelVersion:      bc.Config.KernelVersion,
 		FirecrackerVersion: bc.Config.FirecrackerVersion,
 		RootfsSizeMB:       units.BytesToMB(int64(rootfsSize)),
-		SchedulingMetadata: templateSchedulingMetadata(builder.templateCache, lastLayerResult.Metadata.Template.BuildID),
+		SchedulingMetadata: templateSchedulingMetadata(ctx, builder.templateCache, lastLayerResult.Metadata.Template.BuildID),
 	}, nil
 }
 
-func templateSchedulingMetadata(cache *sbxtemplate.Cache, buildID string) *templatemanager.TemplateSchedulingMetadata {
+func templateSchedulingMetadata(ctx context.Context, cache *sbxtemplate.Cache, buildID string) *templatemanager.TemplateSchedulingMetadata {
 	t, ok := cache.GetCachedTemplate(buildID)
 	if !ok {
 		return nil
 	}
 	provider, ok := t.(interface {
-		SchedulingMetadata() *orchestratorgrpc.SchedulingMetadata
+		SchedulingMetadata(context.Context) *orchestratorgrpc.SchedulingMetadata
 	})
 	if !ok {
 		return nil
 	}
 
-	return toTemplateSchedulingMetadata(provider.SchedulingMetadata())
+	return toTemplateSchedulingMetadata(provider.SchedulingMetadata(ctx))
 }
 
 func toTemplateSchedulingMetadata(m *orchestratorgrpc.SchedulingMetadata) *templatemanager.TemplateSchedulingMetadata {

You can send follow-ups to the cloud agent here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d21605098a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/orchestrator/pkg/sandbox/template/storage_template.go Outdated
@ValentaTomas
ValentaTomas marked this pull request as draft June 1, 2026 02:32
Derive scheduling metadata from resolved device/diff headers so pause,
create, cached-build listing and template builds report the actual build,
its own diff contributions, and the on-disk generation. Report logical vs
on-disk byte stats and merge template cache entries into ListCachedBuilds.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Uninitialized storage diff stats panic
    • Added nil check in StorageDiff.FileSize to return error instead of dereferencing nil chunker when called on uninitialized diffs.
  • ✅ Fixed: Pause hides header wait failures
    • Changed NewSnapshotSchedulingMetadata to return error when header wait fails, causing pause/checkpoint operations to fail instead of silently omitting metadata.

Create PR

Or push these changes by commenting:

@cursor push 5691fa6c77
Preview (5691fa6c77)
diff --git a/packages/orchestrator/pkg/sandbox/build/storage_diff.go b/packages/orchestrator/pkg/sandbox/build/storage_diff.go
--- a/packages/orchestrator/pkg/sandbox/build/storage_diff.go
+++ b/packages/orchestrator/pkg/sandbox/build/storage_diff.go
@@ -128,6 +128,10 @@
 }
 
 func (b *StorageDiff) FileSize(ctx context.Context) (int64, error) {
+	if b.chunker == nil {
+		return 0, fmt.Errorf("chunker not initialized")
+	}
+
 	return b.chunker.FileSize(ctx)
 }
 

diff --git a/packages/orchestrator/pkg/sandbox/sandbox.go b/packages/orchestrator/pkg/sandbox/sandbox.go
--- a/packages/orchestrator/pkg/sandbox/sandbox.go
+++ b/packages/orchestrator/pkg/sandbox/sandbox.go
@@ -1186,7 +1186,10 @@
 	cleanup.AddNoContext(ctx, rootfsDiff.Close)
 
 	rootfsDiffHeader := NewResolvedDiffHeader(rootfsHeader)
-	schedulingMetadata := NewSnapshotSchedulingMetadata(ctx, memfileDiffHeader, rootfsDiffHeader)
+	schedulingMetadata, err := NewSnapshotSchedulingMetadata(ctx, memfileDiffHeader, rootfsDiffHeader)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve scheduling metadata: %w", err)
+	}
 
 	metadataFileLink := template.NewLocalFileLink(cachePaths.CacheMetadata())
 	cleanup.AddNoContext(ctx, metadataFileLink.Close)

diff --git a/packages/orchestrator/pkg/sandbox/snapshot.go b/packages/orchestrator/pkg/sandbox/snapshot.go
--- a/packages/orchestrator/pkg/sandbox/snapshot.go
+++ b/packages/orchestrator/pkg/sandbox/snapshot.go
@@ -59,12 +59,16 @@
 
 // NewSnapshotSchedulingMetadata derives the metadata from the new snapshot's
 // diff headers so the freshly created build and its contributions are included.
-func NewSnapshotSchedulingMetadata(ctx context.Context, memfileDiffHeader, rootfsDiffHeader *DiffHeader) *orchestrator.SchedulingMetadata {
+func NewSnapshotSchedulingMetadata(ctx context.Context, memfileDiffHeader, rootfsDiffHeader *DiffHeader) (*orchestrator.SchedulingMetadata, error) {
 	memfileHeader, memfileErr := memfileDiffHeader.WaitWithContext(ctx)
+	if memfileErr != nil {
+		return nil, fmt.Errorf("memfile header wait failed: %w", memfileErr)
+	}
+
 	rootfsHeader, rootfsErr := rootfsDiffHeader.WaitWithContext(ctx)
-	if memfileErr != nil || rootfsErr != nil {
-		return nil
+	if rootfsErr != nil {
+		return nil, fmt.Errorf("rootfs header wait failed: %w", rootfsErr)
 	}
 
-	return scheduling.FromHeaders(memfileHeader, rootfsHeader)
+	return scheduling.FromHeaders(memfileHeader, rootfsHeader), nil
 }

You can send follow-ups to the cloud agent here.

Comment thread packages/orchestrator/pkg/sandbox/build/cache.go Outdated
Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
…adata

FileSize panicked on diffs cached before Init (nil chunker), matching the
nil guards already on Size/IsCached/Close. NewSnapshotSchedulingMetadata now
warns when a diff header fails to resolve instead of silently returning nil.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Pause waits on memfile header
    • Moved scheduling metadata computation from synchronous snapshot creation to checkpoint handler, allowing Pause to return before memfile-dedup header resolution completes.
  • ✅ Fixed: Chain cap is thirty two
    • Increased chainLimit constant from 32 to 128 to match the intended hard maximum for build chain metadata truncation.

Create PR

Or push these changes by commenting:

@cursor push 7d7801b4ce
Preview (7d7801b4ce)
diff --git a/packages/orchestrator/pkg/sandbox/sandbox.go b/packages/orchestrator/pkg/sandbox/sandbox.go
--- a/packages/orchestrator/pkg/sandbox/sandbox.go
+++ b/packages/orchestrator/pkg/sandbox/sandbox.go
@@ -1186,7 +1186,6 @@
 	cleanup.AddNoContext(ctx, rootfsDiff.Close)
 
 	rootfsDiffHeader := NewResolvedDiffHeader(rootfsHeader)
-	schedulingMetadata := NewSnapshotSchedulingMetadata(ctx, memfileDiffHeader, rootfsDiffHeader)
 
 	metadataFileLink := template.NewLocalFileLink(cachePaths.CacheMetadata())
 	cleanup.AddNoContext(ctx, metadataFileLink.Close)
@@ -1203,7 +1202,7 @@
 		MemfileDiffHeader:  memfileDiffHeader,
 		RootfsDiff:         rootfsDiff,
 		RootfsDiffHeader:   rootfsDiffHeader,
-		SchedulingMetadata: schedulingMetadata,
+		SchedulingMetadata: nil,
 		MemfileBlockSize:   originalMemfile.Header().Metadata.BlockSize,
 		RootfsBlockSize:    originalRootfs.Header().Metadata.BlockSize,
 

diff --git a/packages/orchestrator/pkg/scheduling/metadata.go b/packages/orchestrator/pkg/scheduling/metadata.go
--- a/packages/orchestrator/pkg/scheduling/metadata.go
+++ b/packages/orchestrator/pkg/scheduling/metadata.go
@@ -10,7 +10,7 @@
 	"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
 )
 
-const chainLimit = 32
+const chainLimit = 128
 
 type buildContribution struct {
 	buildID             uuid.UUID

diff --git a/packages/orchestrator/pkg/server/sandboxes.go b/packages/orchestrator/pkg/server/sandboxes.go
--- a/packages/orchestrator/pkg/server/sandboxes.go
+++ b/packages/orchestrator/pkg/server/sandboxes.go
@@ -873,9 +873,14 @@
 		}
 	}
 
+	// Compute scheduling metadata after snapshot creation so Pause returns
+	// without waiting on memfile-dedup header resolution. The background
+	// goroutine in pauseProcessMemory will have had time to finish by now.
+	schedulingMetadata := sandbox.NewSnapshotSchedulingMetadata(ctx, snapshot.MemfileDiffHeader, snapshot.RootfsDiffHeader)
+
 	return &snapshotResult{
 		meta:               meta,
-		schedulingMetadata: snapshot.SchedulingMetadata,
+		schedulingMetadata: schedulingMetadata,
 		upload:             upload,
 		completeUpload:     completeUpload,
 	}, nil

You can send follow-ups to the cloud agent here.

Comment thread packages/orchestrator/pkg/sandbox/snapshot.go Outdated
Comment thread packages/orchestrator/pkg/scheduling/metadata.go
Drop per-build byte/mapping-count arrays and overall sizes; report only
base build, generation, and the deduplicated union of referenced build
IDs (the resume-affinity signal). Compute it synchronously on pause from
the resolved parent memfile header plus the new rootfs header, so pause no
longer blocks on the async memfile-dedup header. Generation is max() of the
two headers; chain cap raised to 64.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Chain cap drops scheduling build IDs
    • Removed the arbitrary 64-entry cap that silently truncated build IDs, allowing all referenced builds to be included in scheduling metadata.

Create PR

Or push these changes by commenting:

@cursor push ebf40fc783
Preview (ebf40fc783)
diff --git a/packages/orchestrator/pkg/scheduling/metadata.go b/packages/orchestrator/pkg/scheduling/metadata.go
--- a/packages/orchestrator/pkg/scheduling/metadata.go
+++ b/packages/orchestrator/pkg/scheduling/metadata.go
@@ -9,9 +9,6 @@
 	"github.com/e2b-dev/infra/packages/shared/pkg/storage/header"
 )
 
-// chainLimit caps how many build IDs are reported in scheduling metadata.
-const chainLimit = 64
-
 // FromHeaders builds scheduling metadata for resume affinity. ChainBuildIds is
 // the deduplicated union of build IDs referenced by the memfile and rootfs
 // mappings, sorted for determinism (a membership set, not lineage order).
@@ -35,9 +32,6 @@
 		chain = append(chain, id.String())
 	}
 	slices.Sort(chain)
-	if len(chain) > chainLimit {
-		chain = chain[:chainLimit]
-	}
 
 	baseBuildID := memfileHeader.Metadata.BaseBuildId
 	if baseBuildID == uuid.Nil {

You can send follow-ups to the cloud agent here.

Comment thread packages/orchestrator/pkg/scheduling/metadata.go
github-actions Bot and others added 2 commits June 1, 2026 04:07
Report per-artifact (memfile, rootfs) referenced build IDs instead of a
single union chain, and drop base/generation. Save paths inject the new
build (last layer) and derive the rest from resolved parent headers, so
pause stays synchronous even if the rootfs copy later becomes async. Sets
are sorted; order is not significant for affinity matching.
Comment thread packages/orchestrator/pkg/server/template_cache.go Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Base build omitted from ID lists
    • Added h.Metadata.BaseBuildId to the buildIDs function to ensure base build IDs appear in the repeated lists as required by the proto contract.

Create PR

Or push these changes by commenting:

@cursor push 759e604362
Preview (759e604362)
diff --git a/packages/orchestrator/pkg/scheduling/metadata.go b/packages/orchestrator/pkg/scheduling/metadata.go
--- a/packages/orchestrator/pkg/scheduling/metadata.go
+++ b/packages/orchestrator/pkg/scheduling/metadata.go
@@ -38,6 +38,9 @@
 
 func buildIDs(h *header.Header, extra uuid.UUID) []string {
 	seen := make(map[uuid.UUID]struct{})
+	if h.Metadata.BaseBuildId != uuid.Nil {
+		seen[h.Metadata.BaseBuildId] = struct{}{}
+	}
 	for _, m := range h.Mapping.All() {
 		if m.BuildId != uuid.Nil {
 			seen[m.BuildId] = struct{}{}

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 31c1d2b. Configure here.

Comment thread packages/orchestrator/pkg/scheduling/metadata.go
github-actions Bot and others added 10 commits June 1, 2026 04:28
Import the orchestrator SchedulingMetadata into template-manager instead of
duplicating it as TemplateSchedulingMetadata, so the template build result and
the create/resume/list responses use the identical type and the API can match
them directly. Drops the duplicate message and its mapper.
The per-build uncompressed size is already carried in the header
(Builds[id].Size) and captured into StorageDiff.uncompressedSize, so return
that directly and drop the redundant Chunker.Size accessor.
Add Mapping.BytesByBuild which scans the length/buildIdx columns directly
instead of materializing a BuildMap and hashing a uuid per entry, and use it
for the scheduling metadata per-layer byte sums.
ListCachedBuilds reported per-build cached/total bytes from the diff store via
FileSize, which is the wrong source: per-layer referenced bytes already come
from the mapping in SchedulingMetadata. Remove the CachedBuildStats machinery
and the CachedBuildInfo byte fields; ListCachedBuilds keeps build id,
expiration, and scheduling metadata. Reverts StorageDiff.Size to FileSize.
@ValentaTomas
ValentaTomas marked this pull request as ready for review June 1, 2026 05:13
Comment on lines 19 to 30
for key, item := range s.templateCache.Items() {
builds = append(builds, &orchestrator.CachedBuildInfo{
info := &orchestrator.CachedBuildInfo{
BuildId: key,
ExpirationTime: timestamppb.New(item.ExpiresAt()),
})
}
if provider, ok := item.Value().(interface {
SchedulingMetadata(ctx context.Context) *orchestrator.SchedulingMetadata
}); ok {
info.SchedulingMetadata = provider.SchedulingMetadata(ctx)
}
builds = append(builds, info)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 ListCachedBuilds now calls SchedulingMetadata(ctx) for every cached template, which blocks on memfile/rootfs WaitWithContext until the background Fetch finishes (cache.go:340-344 inserts entries before Fetch resolves the SetOnce headers). This converts a previously O(1)-per-entry RPC into one bounded by the slowest in-progress GCS fetch, multiplied across every still-loading cache entry because the loop is serial. Suggested fix: use a non-blocking IsSet check or a short per-call timeout, returning nil scheduling_metadata when the headers aren't yet resolved.

Extended reasoning...

Bug: ListCachedBuilds at packages/orchestrator/pkg/server/template_cache.go:19-30 iterates every cached template and synchronously calls provider.SchedulingMetadata(ctx). The storageTemplate.SchedulingMetadata implementation at packages/orchestrator/pkg/sandbox/template/storage_template.go:278-282 blocks on t.memfile.WaitWithContext(ctx) and t.rootfs.WaitWithContext(ctx). Per packages/shared/pkg/utils/set_once.go:104-113, WaitWithContext blocks until SetValue/SetError is called or ctx is cancelled.\n\nHow entries get into the cache before resolving: getTemplateWithFetch at packages/orchestrator/pkg/sandbox/template/cache.go:340-344 inserts a fresh storageTemplate into the TTL cache via c.cache.GetOrSet(key, tmpl, ...) and then launches go tmpl.Fetch(context.WithoutCancel(ctx), c.buildStore) in a background goroutine. Fetch is what eventually calls SetValue on t.memfile and t.rootfs (storage_template.go:209, 246), but only after NewStorage downloads diff headers from GCS. During that window the entry is enumerated by s.templateCache.Items() with unresolved SetOnces.\n\nWhy existing code does not prevent it: Items() returns all live cache entries, not just resolved ones, and the new code path has no IsSet / Result fast path — every entry is awaited. The loop is serial, so a single slow fetch stalls every subsequent entry too. Pre-PR the same loop only read item.ExpiresAt() (an O(1) field read) and returned in microseconds.\n\nImpact: ListCachedBuilds becomes O(slowest-in-flight-fetch * N) in the worst case, bounded only by the gRPC client deadline. It will not deadlock the orchestrator (WaitWithContext respects ctx.Done()), but the RPC's purpose — surfacing locally cached builds for placement decisions — is defeated when callers time out instead of getting fast partial answers. The Cursor bot raised this exact regression in inline-comment 3331665823 ("ListCachedBuilds waits on fetch", Medium Severity) on commit 936aef4; the comment was marked resolved on GitHub but the code at HEAD still calls WaitWithContext.\n\nStep-by-step proof:\n1. Orchestrator boots; templateCache is empty.\n2. SandboxCreate is called for two templates whose builds are not yet local. Each getTemplateWithFetch (cache.go:340) inserts a new storageTemplate into the cache and then runs go tmpl.Fetch(...). Both Fetch goroutines block on GCS for, say, 4 s each.\n3. ~50 ms later, an operator calls ListCachedBuilds. The handler ranges over s.templateCache.Items() (template_cache.go:19) and hits entry A first.\n4. provider.SchedulingMetadata(ctx) calls t.memfile.WaitWithContext(ctx) (storage_template.go:279). t.memfile is unset because A's Fetch hasn't finished, so the call blocks for the remaining ~4 s of A's GCS download.\n5. The loop moves to entry B and repeats: WaitWithContext again blocks for B's remaining download time.\n6. The RPC returns after ~8 s instead of microseconds, even though the question — which builds are cached and when do they expire — does not require resolved headers.\n\nSuggested fix: prefer a non-blocking variant — check memfile.IsSet() / rootfs.IsSet() (or use Result() which returns immediately) and return scheduling_metadata: nil when either is not yet resolved. This keeps the RPC O(1) per entry and matches the author's stated "best-effort enrichment" intent for scheduling metadata. Alternatively, dispatch the providers concurrently with a small per-call timeout (e.g. via errgroup + context.WithTimeout).

🔬 also observed by verify-runtime

Keep scheduling metadata only on lifecycle responses (create/resume,
pause, checkpoint, build) and stop exposing it on the cached-builds
listing/polling path.
@jakubno
jakubno dismissed dobrac’s stale review June 1, 2026 13:15

looks like not high prio, let's do a follow up if needed

@jakubno
jakubno merged commit b48f213 into main Jun 1, 2026
51 checks passed
@jakubno
jakubno deleted the valenta/snapshot-scheduling-metadata branch June 1, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants