From 4f0bb6c0f8a86cf6f91a41c8ce232950485d9933 Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 29 Jul 2026 12:37:17 +0000 Subject: [PATCH 1/4] Use slider for automatic replica selection Amp-Thread-ID: https://ampcode.com/threads/T-019fadba-520e-71b8-9208-ff74549f8468 Co-authored-by: Arjun Komath --- .../service/details/replicas-section.tsx | 57 +++++++++--------- web/components/ui/slider.tsx | 60 +++++++++++++++++++ 2 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 web/components/ui/slider.tsx diff --git a/web/components/service/details/replicas-section.tsx b/web/components/service/details/replicas-section.tsx index 80cec112..987cd3d7 100644 --- a/web/components/service/details/replicas-section.tsx +++ b/web/components/service/details/replicas-section.tsx @@ -13,6 +13,7 @@ import { EmptyTitle, } from "@/components/ui/empty"; import { Input } from "@/components/ui/input"; +import { Slider } from "@/components/ui/slider"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { @@ -402,40 +403,42 @@ export const ReplicasSection = memo(function ReplicasSection({ {placementMode === "automatic" ? (
- +

Desired replicas

The control plane distributes replicas evenly across healthy {service.serverlessEnabled ? " proxy nodes" : " nodes"} and moves them after failures.

- { - setIsEditing(true); - setDesiredReplicas( - Math.max( - 1, - Math.min(10, Math.floor(event.target.valueAsNumber || 1)), +
+ { + setIsEditing(true); + setDesiredReplicas(value); + }} + /> +
+ {Array.from({ length: 10 }, (_, index) => index + 1).map( + (value) => ( + + {value} + ), - ); - }} - className="w-24" - aria-describedby="automatic-replica-range" - /> -

- Choose between 1 and 10 replicas. -

+ )} +
+
{hasChanges ? (
{!manualTotalIsValid && (

- Manual placement requires 1 to 10 replicas in total. + Manual placement requires 1 to 32 replicas in total.

)} {hasChanges && ( diff --git a/web/lib/compose-parser.ts b/web/lib/compose-parser.ts index 5adc7345..2662baa2 100644 --- a/web/lib/compose-parser.ts +++ b/web/lib/compose-parser.ts @@ -557,7 +557,7 @@ export function parseComposeYaml(yamlContent: string): ComposeParseResult { let replicas = serviceConfig.deploy?.replicas ?? 1; if (replicas < 1) replicas = 1; - if (replicas > 10) replicas = 10; + if (replicas > 32) replicas = 32; if (stateful && replicas > 1) { warnings.push({ service: serviceName, diff --git a/web/lib/inngest/functions/rollout-helpers.ts b/web/lib/inngest/functions/rollout-helpers.ts index 79d30979..9b05ac58 100644 --- a/web/lib/inngest/functions/rollout-helpers.ts +++ b/web/lib/inngest/functions/rollout-helpers.ts @@ -41,8 +41,8 @@ export function distributeReplicas( ): Placement[] { const ids = [...new Set(serverIds)].sort((a, b) => a.localeCompare(b)); if (ids.length === 0) throw new Error("No eligible servers for deployment"); - if (!Number.isInteger(replicas) || replicas < 1 || replicas > 10) - throw new Error("Replica count must be between 1 and 10"); + if (!Number.isInteger(replicas) || replicas < 1 || replicas > 32) + throw new Error("Replica count must be between 1 and 32"); const counts = new Map(ids.map((id) => [id, 0])); for (let index = 0; index < replicas; index++) { const id = ids[index % ids.length]; @@ -114,8 +114,8 @@ export function calculateRevisionPlacements( if (totalReplicas < 1) { throw new Error("At least one replica is required"); } - if (totalReplicas > 10) { - throw new Error("Maximum 10 replicas allowed"); + if (totalReplicas > 32) { + throw new Error("Maximum 32 replicas allowed"); } if (specification.stateful) { diff --git a/web/lib/inngest/functions/rollout-workflow.ts b/web/lib/inngest/functions/rollout-workflow.ts index 7316bed3..3b723990 100644 --- a/web/lib/inngest/functions/rollout-workflow.ts +++ b/web/lib/inngest/functions/rollout-workflow.ts @@ -24,7 +24,7 @@ import { handleRolloutFailure } from "./rollout-utils"; const PREFLIGHT_FAILURE_MESSAGES = [ "At least one replica is required", - "Maximum 10 replicas allowed", + "Maximum 32 replicas allowed", "No servers selected for deployment", "Stateful services can only have exactly 1 replica", "Stateful services must be deployed to exactly one server", diff --git a/web/lib/public-api.ts b/web/lib/public-api.ts index 6bc13d87..75fbfae6 100644 --- a/web/lib/public-api.ts +++ b/web/lib/public-api.ts @@ -554,7 +554,7 @@ const hostnameSchema = z export const placementSchema = z.discriminatedUnion("mode", [ z.strictObject({ mode: z.literal("automatic"), - replicas: z.number().int().min(1).max(10), + replicas: z.number().int().min(1).max(32), }), z .strictObject({ @@ -563,7 +563,7 @@ export const placementSchema = z.discriminatedUnion("mode", [ .array( z.strictObject({ serverId: z.string().min(1), - count: z.number().int().min(1).max(10), + count: z.number().int().min(1).max(32), }), ) .min(1), @@ -578,10 +578,10 @@ export const placementSchema = z.discriminatedUnion("mode", [ message: "Server IDs must be unique", path: ["placements"], }); - if (value.placements.reduce((sum, item) => sum + item.count, 0) > 10) + if (value.placements.reduce((sum, item) => sum + item.count, 0) > 32) context.addIssue({ code: "custom", - message: "Total replicas must be between 1 and 10", + message: "Total replicas must be between 1 and 32", path: ["placements"], }); }), diff --git a/web/lib/service-revision-changes.ts b/web/lib/service-revision-changes.ts index cdb0448c..cdfa0b31 100644 --- a/web/lib/service-revision-changes.ts +++ b/web/lib/service-revision-changes.ts @@ -85,7 +85,7 @@ const serviceRevisionSpecSchema = z z.strictObject({ mode: z.literal("manual") }), z.strictObject({ mode: z.literal("automatic"), - replicas: z.number().int().min(1).max(10), + replicas: z.number().int().min(1).max(32), }), ]), ...serviceRevisionSpecFields, diff --git a/web/lib/service-revision-spec.ts b/web/lib/service-revision-spec.ts index 0bad7661..315784b1 100644 --- a/web/lib/service-revision-spec.ts +++ b/web/lib/service-revision-spec.ts @@ -238,8 +238,8 @@ function validateServiceRevisionSpec( if (totalReplicas < 1 && !allowNoPlacements) { throw new Error("At least one replica is required"); } - if (totalReplicas > 10) { - throw new Error("Maximum 10 replicas allowed"); + if (totalReplicas > 32) { + throw new Error("Maximum 32 replicas allowed"); } if ( specification.placement.mode === "automatic" && diff --git a/web/tests/autoplacement.test.ts b/web/tests/autoplacement.test.ts index eae08e90..8c46c676 100644 --- a/web/tests/autoplacement.test.ts +++ b/web/tests/autoplacement.test.ts @@ -41,12 +41,18 @@ describe("automatic placement distribution", () => { ).toEqual(counts); }); - it("stacks ten replicas deterministically on two servers", () => { - expect(distributeReplicas(["b", "a"], 10)).toEqual([ - { serverId: "a", replicas: 5 }, - { serverId: "b", replicas: 5 }, + it("stacks 32 replicas deterministically on two servers", () => { + expect(distributeReplicas(["b", "a"], 32)).toEqual([ + { serverId: "a", replicas: 16 }, + { serverId: "b", replicas: 16 }, ]); }); + + it("rejects more than 32 replicas", () => { + expect(() => distributeReplicas(["a"], 33)).toThrow( + "Replica count must be between 1 and 32", + ); + }); }); describe("automatic placement eligibility diagnostics", () => { diff --git a/web/tests/public-api-source.test.ts b/web/tests/public-api-source.test.ts index 7cfcb4c7..22f97aea 100644 --- a/web/tests/public-api-source.test.ts +++ b/web/tests/public-api-source.test.ts @@ -154,7 +154,9 @@ describe("public API placement schema", () => { }); it.each([ + { mode: "automatic", replicas: 32 }, { mode: "automatic", replicas: 3 }, + { mode: "manual", placements: [{ serverId: "server-1", count: 32 }] }, { mode: "manual", placements: [{ serverId: "server-1", count: 2 }] }, ])("accepts valid placement intent", (placement) => { expect( @@ -165,12 +167,13 @@ describe("public API placement schema", () => { it.each([ { mode: "automatic", replicas: 0 }, + { mode: "automatic", replicas: 33 }, { mode: "manual", placements: [] }, { mode: "manual", placements: [ - { serverId: "a", count: 6 }, - { serverId: "b", count: 5 }, + { serverId: "a", count: 17 }, + { serverId: "b", count: 16 }, ], }, { diff --git a/web/tests/service-revision-spec.test.ts b/web/tests/service-revision-spec.test.ts index 6bb02475..b8e1755c 100644 --- a/web/tests/service-revision-spec.test.ts +++ b/web/tests/service-revision-spec.test.ts @@ -250,14 +250,24 @@ describe("service revision specification", () => { it("snapshots automatic placement intent without resolved placements", () => { const input = draft({ volumes: [] }); input.service.placementMode = "automatic"; - input.service.replicas = 4; + input.service.replicas = 32; expect(buildServiceRevisionSpec(input)).toMatchObject({ - placement: { mode: "automatic", replicas: 4 }, + placement: { mode: "automatic", replicas: 32 }, placements: [], }); }); + it("rejects more than 32 automatic replicas", () => { + const input = draft({ volumes: [] }); + input.service.placementMode = "automatic"; + input.service.replicas = 33; + + expect(() => buildServiceRevisionSpec(input)).toThrow( + "Maximum 32 replicas allowed", + ); + }); + it("rejects automatic placement for stateful and volume-backed services", () => { const stateful = draft({ volumes: [] }); stateful.service.stateful = true; From a59d8ee0da91eae7b73ba890871d88410e033ba6 Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 29 Jul 2026 19:52:41 +0000 Subject: [PATCH 3/4] Constrain replica slider width Amp-Thread-ID: https://ampcode.com/threads/T-019fadba-520e-71b8-9208-ff74549f8468 Co-authored-by: Arjun Komath --- web/components/service/details/replicas-section.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/service/details/replicas-section.tsx b/web/components/service/details/replicas-section.tsx index 0944b74c..a114e831 100644 --- a/web/components/service/details/replicas-section.tsx +++ b/web/components/service/details/replicas-section.tsx @@ -402,7 +402,7 @@ export const ReplicasSection = memo(function ReplicasSection({ {placementMode === "automatic" ? (
-
+

Desired replicas

@@ -415,7 +415,7 @@ export const ReplicasSection = memo(function ReplicasSection({ {desiredReplicas}

-
+
Date: Wed, 29 Jul 2026 19:53:42 +0000 Subject: [PATCH 4/4] Increase replica slider control size Amp-Thread-ID: https://ampcode.com/threads/T-019fadba-520e-71b8-9208-ff74549f8468 Co-authored-by: Arjun Komath --- web/components/service/details/replicas-section.tsx | 4 ++-- web/components/ui/slider.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/components/service/details/replicas-section.tsx b/web/components/service/details/replicas-section.tsx index a114e831..be1632c8 100644 --- a/web/components/service/details/replicas-section.tsx +++ b/web/components/service/details/replicas-section.tsx @@ -428,8 +428,8 @@ export const ReplicasSection = memo(function ReplicasSection({ }} />
- 1 - 32 + 1 + 32
{hasChanges ? ( diff --git a/web/components/ui/slider.tsx b/web/components/ui/slider.tsx index 798d5b8a..ead1fcec 100644 --- a/web/components/ui/slider.tsx +++ b/web/components/ui/slider.tsx @@ -34,7 +34,7 @@ function Slider({ ({ index={index} // biome-ignore lint/suspicious/noArrayIndexKey: Slider thumbs are identified by their value index. key={index} - className="relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50" + className="relative block size-4 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50" /> ))}