Skip to content

Add cache-aware placement affinity - #2867

Closed
ValentaTomas wants to merge 43 commits into
mainfrom
valenta/api-placement-affinity
Closed

ValentaTomas wants to merge 43 commits into
mainfrom
valenta/api-placement-affinity

Conversation

@ValentaTomas

@ValentaTomas ValentaTomas commented May 30, 2026 •

Copy link
Copy Markdown
Member

Add disabled-by-default cache-aware placement affinity using recent successful build placements. Redis state is bounded by TTL/top-N limits and is best-effort.

Record recent successful placements and use them as a small, gated best-of-K bias so warm nodes are preferred without bypassing placement filters.
@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
Touches the sandbox create placement path and can skew load toward hot nodes when the flag is on; mitigations include default-off, capped Redis state, short read timeouts, and best-effort writes.

Overview
Introduces optional, flag-gated placement affinity so new sandboxes for a given build tend to land on nodes that recently hosted that build, improving cache locality. When enabled, the API reads bounded Redis history per build, passes node bonuses into Best-of-K scoring (and can pull high-affinity nodes into the candidate set even if they were not randomly sampled), records successful placements asynchronously, and emits a cache_affinity_hit metric. Resume skips build affinity so it does not fight an explicit node pin. Separately, if a cluster has no connected nodes yet, create now triggers on-demand discovery (with a local orchestrator fallback) so the first sandbox after startup is less likely to fail placement.

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

@codecov

codecov Bot commented May 30, 2026 •

Copy link
Copy Markdown

❌ 5 Tests Failed:

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

Flake rate in main: 40.55% (Passed 865 times, Failed 590 times)

Stack Traces | 97.3s run time
=== RUN   TestSandboxListPaginationRunningLargerLimit
    sandbox_list_test.go:327: Created sandbox 1/12: in8272cdp4z8hrqaf8542
    sandbox_list_test.go:327: Created sandbox 2/12: iauwoz5cg4dzq7kflej9c
    sandbox_list_test.go:327: Created sandbox 3/12: iiwrs73lf8ak6quxnsne3
    sandbox_list_test.go:327: Created sandbox 4/12: isn60dmf7pyu918uelu8e
    sandbox_list_test.go:327: Created sandbox 5/12: im1m5x8hvkwuc5a8ld8lr
    sandbox_list_test.go:327: Created sandbox 6/12: i7shvkfgxigzrpee4ra23
    sandbox_list_test.go:327: Created sandbox 7/12: i1auwpuvdqf4xl0pnfwc3
    sandbox_list_test.go:327: Created sandbox 8/12: iyohomf2szhv2lt9qlyj1
    sandbox_list_test.go:327: Created sandbox 9/12: i5gbcvv0j9dxctu7fuitg
    sandbox_list_test.go:327: Created sandbox 10/12: ifab595ogxdl8dslxjw0x
    sandbox_list_test.go:327: Created sandbox 11/12: ipyx8bqg7jx7tsya57ufi
    sandbox_list_test.go:327: Created sandbox 12/12: i731mevcsjeawof2g2vg3
    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 (97.34s)
github.com/e2b-dev/infra/tests/integration/internal/tests/api/sandboxes::TestSandboxRapidSnapshotForkChain

Flake rate in main: 39.18% (Passed 846 times, Failed 545 times)

Stack Traces | 19.9s run time
=== RUN   TestSandboxRapidSnapshotForkChain
=== PAUSE TestSandboxRapidSnapshotForkChain
=== CONT  TestSandboxRapidSnapshotForkChain
    sandbox_rapid_pause_resume_test.go:43: 
        	Error Trace:	.../api/sandboxes/snapshot_template_test.go:37
        	            				.../api/sandboxes/sandbox_rapid_pause_resume_test.go:43
        	Error:      	Not equal: 
        	            	expected: 201
        	            	actual  : 500
        	Test:       	TestSandboxRapidSnapshotForkChain
--- FAIL: TestSandboxRapidSnapshotForkChain (19.90s)
github.com/e2b-dev/infra/tests/integration/internal/tests/orchestrator::TestSandboxMemoryIntegrity

Flake rate in main: 55.38% (Passed 858 times, Failed 1065 times)

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

Flake rate in main: 55.46% (Passed 848 times, Failed 1056 times)

Stack Traces | 196s 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:1258}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stdout:"Total memory: 985 MB\nUsed memory before tmpfs mount: 191 MB\nFree memory before tmpfs mount: 792 MB\nMemory to use in integrity test (60% of free, min 64MB): 475 MB\n"}}
Executing command bash in sandbox ib1jykczsohlojluvchl7 (user: root)
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"475+0 records in\n475+0 records out\n498073600 bytes (498 MB, 475 MiB) copied, 2.29405 s, 217 MB/s\n"}}
    sandbox_memory_integrity_test.go:70: Command [bash] output: event:{data:{stderr:"\tCommand being timed: \"dd if=/dev/urandom of=/mnt/testfile bs=1M count=475\"\n\tUser time (seconds): 0.00\n\tSystem time (seconds): 2.28\n\tPercent of CPU this job got: 99%\n\tElapsed (wall clock) time (h:mm:ss or m:ss): 0:02.30\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): 2632\n\tAverage resident set size (kbytes): 0\n\tMajor (requiring I/O) page faults: 2\n\tMinor (reclaiming a frame) page faults: 345\n\tVoluntary context switches: 3\n\tInvoluntary context switches: 9\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: 672 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 im54daxp3fkx15eorq5rm
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1275}}
Executing command bash in sandbox im7kt3catbrlj6tdhrvfe (user: root)
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{data:{stdout:"081051a7f04ec5b5b521c42208c752cb4545974547ed33fe04ebf78b8e8d6557\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 im54daxp3fkx15eorq5rm
    sandbox_memory_integrity_test.go:80: Command [bash] output: event:{start:{pid:1278}}
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (user: root)
Executing command bash in sandbox im54daxp3fkx15eorq5rm (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 im54daxp3fkx15eorq5rm: 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 (196.30s)
github.com/e2b-dev/infra/tests/integration/internal/tests/proxies::TestSandboxAutoResumeViaProxy

Flake rate in main: 40.92% (Passed 852 times, Failed 590 times)

Stack Traces | 12.2s run time
=== RUN   TestSandboxAutoResumeViaProxy
=== PAUSE TestSandboxAutoResumeViaProxy
=== CONT  TestSandboxAutoResumeViaProxy
    auto_resume_test.go:116: 
        	Error Trace:	.../tests/proxies/auto_resume_test.go:116
        	Error:      	Received unexpected error:
        	            	Get "http://localhost:3002": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
        	Test:       	TestSandboxAutoResumeViaProxy
--- FAIL: TestSandboxAutoResumeViaProxy (12.15s)

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

Creating and executing a Redis pipeline inside the loop in record defeats the purpose of pipelining and results in multiple sequential round-trips. Similarly, performing sequential Redis queries inside the loop in scores on the critical path of sandbox creation introduces unnecessary latency; both should be batched using a single pipeline outside the loops to improve performance.

Comment thread packages/api/internal/orchestrator/placement_affinity.go Outdated
Comment thread packages/api/internal/orchestrator/placement_affinity.go Outdated
Use a single pipeline for affinity reads and writes to keep the placement path bounded when the flag is enabled.
Use error-aware Redis checks and apply the repository formatter to the new feature flag.
Move cache affinity rollout knobs into the existing flag so Redis TTL, scoring, limits, and timeouts can be adjusted without a deploy.
Keep resume origin-node affinity as the primary signal and skip per-snapshot build keys so the Redis placement cache stays bounded.
Avoid Redis affinity lookups when resume already has a ready origin node and skip disabled affinity dimensions.
Use team-scoped template and base-template affinity as a low-cardinality fallback for resumes when origin-node placement is unavailable.
Track team-level placement history as the weakest cache-affinity fallback alongside template and base-template signals.
Keep template/team dimensions available for experiments but disable them by default until base build metadata is available.
Remove proxy affinity dimensions so the first rollout only uses concrete build cache signals.

@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: Affinity node repeatedly selected despite ResourceExhausted failures
    • Added the ResourceExhausted node to nodesExcluded to prevent affinity augmentation from re-selecting it on retry.

Create PR

Or push these changes by commenting:

@cursor push bbadc5591a
Preview (bbadc5591a)
diff --git a/packages/api/internal/orchestrator/placement/placement.go b/packages/api/internal/orchestrator/placement/placement.go
--- a/packages/api/internal/orchestrator/placement/placement.go
+++ b/packages/api/internal/orchestrator/placement/placement.go
@@ -102,6 +102,7 @@
 
 		switch statusCode {
 		case codes.ResourceExhausted:
+			nodesExcluded[failedNode.ID] = struct{}{}
 			failedNode.PlacementMetrics.Skip(sbxRequest.GetSandbox().GetSandboxId())
 			logger.L().Warn(ctx, "Node exhausted, trying another node", logger.WithSandboxID(sbxRequest.GetSandbox().GetSandboxId()), logger.WithNodeID(failedNode.ID))
 		default:

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

Comment thread packages/api/internal/orchestrator/placement/placement_best_of_K.go
Exclude nodes that reject placement as exhausted so affinity retries can fall through to other candidates.
@ValentaTomas
ValentaTomas marked this pull request as ready for review May 30, 2026 21:58

@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: ResourceExhausted tight retry loop on last remaining node
    • Added explicit check to exit retry loop with 'no nodes available' error when the last remaining node returns ResourceExhausted, preventing infinite retries until context timeout.

Create PR

Or push these changes by commenting:

@cursor push 7334edb433
Preview (7334edb433)
diff --git a/packages/api/internal/orchestrator/placement/placement.go b/packages/api/internal/orchestrator/placement/placement.go
--- a/packages/api/internal/orchestrator/placement/placement.go
+++ b/packages/api/internal/orchestrator/placement/placement.go
@@ -104,6 +104,9 @@
 		case codes.ResourceExhausted:
 			if len(nodesExcluded)+1 < len(clusterNodes) {
 				nodesExcluded[failedNode.ID] = struct{}{}
+			} else {
+				// All nodes are exhausted; exit the retry loop.
+				return nil, errors.New("no nodes available")
 			}
 			failedNode.PlacementMetrics.Skip(sbxRequest.GetSandbox().GetSandboxId())
 			logger.L().Warn(ctx, "Node exhausted, trying another node", logger.WithSandboxID(sbxRequest.GetSandbox().GetSandboxId()), logger.WithNodeID(failedNode.ID))

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

Comment thread packages/api/internal/orchestrator/placement/placement.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: 38d8022e3b

ℹ️ 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/api/internal/orchestrator/create_instance.go Outdated
dobrac
dobrac previously requested changes May 30, 2026
Comment thread packages/api/internal/orchestrator/placement/placement_best_of_K.go
Comment thread packages/api/internal/orchestrator/placement/placement_best_of_K.go
Comment thread packages/api/internal/orchestrator/create_instance.go
Comment thread packages/api/internal/orchestrator/placement_affinity.go Outdated
Comment thread packages/api/internal/orchestrator/create_instance.go
Comment thread packages/api/internal/orchestrator/placement_affinity.go
Comment thread packages/api/internal/orchestrator/placement_affinity.go
Comment thread packages/api/internal/orchestrator/placement_affinity.go Outdated
Comment thread packages/api/internal/orchestrator/placement/placement_best_of_K.go
Comment thread packages/api/internal/orchestrator/client.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: 75d9d736d4

ℹ️ 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/api/internal/orchestrator/client.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.

Autofix Details

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

  • ✅ Fixed: Affinity bonus not normalized to placement score scale
    • Normalized the affinity bonus by dividing it by totalCapacity to ensure consistent relative impact across nodes of different sizes.

Create PR

Or push these changes by commenting:

@cursor push 878be92bc5
Preview (878be92bc5)
diff --git a/packages/api/internal/orchestrator/placement/placement_best_of_K.go b/packages/api/internal/orchestrator/placement/placement_best_of_K.go
--- a/packages/api/internal/orchestrator/placement/placement_best_of_K.go
+++ b/packages/api/internal/orchestrator/placement/placement_best_of_K.go
@@ -63,7 +63,7 @@
 
 	score := (cpuRequested + float64(reserved) + config.Alpha*usageAvg) / totalCapacity
 	if len(affinityScores) > 0 {
-		score -= affinityScores[0][node.ID]
+		score -= affinityScores[0][node.ID] / totalCapacity
 	}
 
 	return score

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

Comment thread packages/api/internal/orchestrator/placement/placement_best_of_K.go

@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: 53bfe748ae

ℹ️ 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/api/internal/orchestrator/placement/placement.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: 9277fc3cd9

ℹ️ 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/api/internal/orchestrator/placement/placement.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.

Autofix Details

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

  • ✅ Fixed: Exhausted retry counter unreachable due to attempt increment
    • Removed the attempt++ increment from the exhausted pool retry path so maxExhaustedRetries (100) can be reached before the outer loop terminates at maxRetries (3).

Create PR

Or push these changes by commenting:

@cursor push 9277fc3cd9

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

Comment thread packages/api/internal/orchestrator/placement/placement.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: 202bab4374

ℹ️ 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/api/internal/orchestrator/create_instance.go Outdated
@ValentaTomas
ValentaTomas requested a review from dobrac May 31, 2026 05:08
@ValentaTomas
ValentaTomas dismissed dobrac’s stale review May 31, 2026 05:08

Rerequested review.

@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: 3d362021fe

ℹ️ 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/api/internal/orchestrator/placement/placement_best_of_K.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.

Autofix Details

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

  • ✅ Fixed: Affinity bonus not normalized by totalCapacity in Score
    • Normalized the affinity bonus by dividing it by totalCapacity to match the base score normalization, ensuring consistent placement behavior across nodes with different CPU counts.

Create PR

Or push these changes by commenting:

@cursor push b7048af342
Preview (b7048af342)
diff --git a/packages/api/internal/orchestrator/placement/placement_best_of_K.go b/packages/api/internal/orchestrator/placement/placement_best_of_K.go
--- a/packages/api/internal/orchestrator/placement/placement_best_of_K.go
+++ b/packages/api/internal/orchestrator/placement/placement_best_of_K.go
@@ -63,7 +63,7 @@
 
 	score := (cpuRequested + float64(reserved) + config.Alpha*usageAvg) / totalCapacity
 	if len(affinityScores) > 0 {
-		score -= affinityScores[0][node.ID]
+		score -= affinityScores[0][node.ID] / totalCapacity
 	}
 
 	return score

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

Comment thread packages/api/internal/orchestrator/placement/placement_best_of_K.go
Comment thread packages/api/internal/orchestrator/create_instance.go
Comment thread packages/api/internal/orchestrator/create_instance.go Outdated
@ValentaTomas
ValentaTomas marked this pull request as draft June 1, 2026 02:27
Always pass affinity scores to PlaceSandbox (a nil map is handled
downstream) and document the on-demand node discovery fallback.

@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: Exhausted retry sleep ignores context cancellation
    • Replaced unconditional time.Sleep with context-aware select statement that exits early on context cancellation.

Create PR

Or push these changes by commenting:

@cursor push 1b3e4e5c91
Preview (1b3e4e5c91)
diff --git a/packages/api/internal/orchestrator/placement/placement.go b/packages/api/internal/orchestrator/placement/placement.go
--- a/packages/api/internal/orchestrator/placement/placement.go
+++ b/packages/api/internal/orchestrator/placement/placement.go
@@ -78,7 +78,14 @@
 						return nil, err
 					}
 					clear(nodesExhausted)
-					time.Sleep(100 * time.Millisecond)
+					
+					timer := time.NewTimer(100 * time.Millisecond)
+					select {
+					case <-ctx.Done():
+						timer.Stop()
+						return nil, fmt.Errorf("request timed out during exhausted retry %d", exhaustedRetries)
+					case <-timer.C:
+					}
 
 					continue
 				}

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

Comment thread packages/api/internal/orchestrator/placement/placement.go Outdated
ValentaTomas and others added 3 commits May 31, 2026 20:20
Replace the blocking time.Sleep between exhausted-pool retries with a
select on ctx.Done so placement bails out promptly when the client
disconnects instead of looping for up to maxExhaustedRetries.
Tag created-sandbox metrics with whether placement landed on a
cache-affinity-preferred node, so the feature's hit rate and load skew are
observable when it is enabled.
Remove exhausted-node retry/backoff changes from the affinity PR so the branch only carries scheduling affinity behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: Redis trim wipes affinity set
    • Changed ZRemRangeByRank end parameter from -cfg.topNodes-1 to -(cfg.topNodes+1) to prevent deletion of entire set when member count equals topNodes.

Create PR

Or push these changes by commenting:

@cursor push 2190fb6d68
Preview (2190fb6d68)
diff --git a/packages/api/internal/orchestrator/placement_affinity.go b/packages/api/internal/orchestrator/placement_affinity.go
--- a/packages/api/internal/orchestrator/placement_affinity.go
+++ b/packages/api/internal/orchestrator/placement_affinity.go
@@ -99,7 +99,7 @@
 	pipe := a.redis.Pipeline()
 	key := placementAffinityKey(clusterID, placementAffinityKindBuild, buildID)
 	pipe.ZIncrBy(ctx, key, 1, nodeID)
-	pipe.ZRemRangeByRank(ctx, key, 0, -cfg.topNodes-1)
+	pipe.ZRemRangeByRank(ctx, key, 0, -(cfg.topNodes + 1))
 	pipe.Expire(ctx, key, cfg.ttl)
 
 	_, err := pipe.Exec(ctx)

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

Reviewed by Cursor Bugbot for commit 531e633. Configure here.

pipe := a.redis.Pipeline()
key := placementAffinityKey(clusterID, placementAffinityKindBuild, buildID)
pipe.ZIncrBy(ctx, key, 1, nodeID)
pipe.ZRemRangeByRank(ctx, key, 0, -cfg.topNodes-1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redis trim wipes affinity set

Medium Severity

ZRemRangeByRank with end rank -topNodes-1 removes the whole sorted set when it already has exactly topNodes members (stop becomes -1, i.e. lowest through highest). Steady-state affinity for a build with ~topNodes distinct nodes is cleared on every record.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 531e633. Configure here.

@ValentaTomas

Copy link
Copy Markdown
Member Author

Temporarily closing.

@ValentaTomas
ValentaTomas deleted the valenta/api-placement-affinity branch June 11, 2026 00:31
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.

2 participants