From f329e071585dff7da4b0f33f3614cad163998e37 Mon Sep 17 00:00:00 2001 From: Techulus Agent <291950465+techulus-agent@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:07:00 +1000 Subject: [PATCH] Remove configurable serverless min ready --- agent/internal/http/client.go | 1 - agent/internal/serverless/gateway.go | 15 ++------ agent/internal/serverless/gateway_test.go | 1 - docs/architecture.mdx | 7 ++-- docs/services/scaling.mdx | 12 +++--- web/actions/projects.ts | 19 ---------- .../service/details/serverless-section.tsx | 37 ++----------------- web/db/schema.ts | 3 -- web/lib/agent-status.ts | 14 +++---- web/lib/agent/expected-state.ts | 5 --- web/lib/inngest/functions/rollout-helpers.ts | 9 ++--- web/lib/service-config.ts | 18 --------- web/tests/expected-state.test.ts | 9 ----- web/tests/service-config.test.ts | 6 +-- 14 files changed, 26 insertions(+), 130 deletions(-) diff --git a/agent/internal/http/client.go b/agent/internal/http/client.go index 9d513fbc..8ec46747 100644 --- a/agent/internal/http/client.go +++ b/agent/internal/http/client.go @@ -109,7 +109,6 @@ type ServerlessRoute struct { Port int `json:"port"` SleepAfterSeconds int `json:"sleepAfterSeconds"` WakeTimeoutSeconds int `json:"wakeTimeoutSeconds"` - MinReadyReplicas int `json:"minReadyReplicas"` LocalDeploymentIDs []string `json:"localDeploymentIds"` Upstreams []ServerlessUpstream `json:"upstreams"` } diff --git a/agent/internal/serverless/gateway.go b/agent/internal/serverless/gateway.go index 4d8f8c18..c7002585 100644 --- a/agent/internal/serverless/gateway.go +++ b/agent/internal/serverless/gateway.go @@ -236,7 +236,7 @@ func (g *Gateway) resolveUpstreams(host string) ([]agenthttp.ServerlessUpstream, } wakeStartedAt := time.Now() - if len(ready) >= max(1, route.MinReadyReplicas) || (len(ready) > 0 && hasAlwaysOnUpstream(ready)) { + if len(ready) > 0 { log.Printf( "[serverless-gateway] wake requested host=%s deployments=%d ready_upstreams=%d mode=background", host, @@ -380,11 +380,11 @@ func (g *Gateway) waitForReadyUpstreams(route *agenthttp.ServerlessRoute, wakeTi for { state := g.runtime.ExpectedState() - ready, sleepingLocalIDs, err := g.readyUpstreams(route, state) + ready, _, err := g.readyUpstreams(route, state) if err != nil { return nil, err } - if len(ready) >= max(1, route.MinReadyReplicas) || (len(ready) > 0 && len(sleepingLocalIDs) == 0) { + if len(ready) > 0 { pendingIDs := pendingWakeDeploymentIDs(wokenDeploymentIDs, ready) if len(pendingIDs) > 0 { go g.waitForWokenDeployments(route, route.WakeTimeoutSeconds, startedAt, pendingIDs) @@ -909,15 +909,6 @@ func localUpstream(route *agenthttp.ServerlessRoute, expected agenthttp.Expected return agenthttp.ServerlessUpstream{}, false } -func hasAlwaysOnUpstream(upstreams []agenthttp.ServerlessUpstream) bool { - for _, upstream := range upstreams { - if upstream.AlwaysOn { - return true - } - } - return false -} - func hasRunningLocalDeployment(deploymentIDs []string, actualByDeploymentID map[string]container.Container) bool { for _, deploymentID := range deploymentIDs { if actual, ok := actualByDeploymentID[deploymentID]; ok && actual.State == "running" { diff --git a/agent/internal/serverless/gateway_test.go b/agent/internal/serverless/gateway_test.go index c39123bb..ad1e3dec 100644 --- a/agent/internal/serverless/gateway_test.go +++ b/agent/internal/serverless/gateway_test.go @@ -481,7 +481,6 @@ func testExpectedState(localDesiredState string) *agenthttp.ExpectedState { Port: 3000, SleepAfterSeconds: 300, WakeTimeoutSeconds: 5, - MinReadyReplicas: 1, LocalDeploymentIDs: []string{"dep_local"}, Upstreams: []agenthttp.ServerlessUpstream{ { diff --git a/docs/architecture.mdx b/docs/architecture.mdx index fa9729cb..90dd7a29 100644 --- a/docs/architecture.mdx +++ b/docs/architecture.mdx @@ -254,10 +254,9 @@ to proxy nodes that own a local proxy replica for that service. Cross-proxy wake coordination is intentionally out of scope. The wake gateway keeps the incoming request open while it starts local proxy -replicas, unless an always-on worker upstream is already ready. `Min Ready` -controls how many ready upstreams are preferred before releasing queued requests. -If fewer replicas become ready and no local wake is still in progress, the -gateway serves any available ready upstream instead of returning a 503. +replicas, unless an always-on worker upstream is already ready. Queued requests +resume when one upstream is ready. If no upstream becomes ready and no local wake +is still in progress, the gateway returns a 503. Sleep is driven by the proxy gateway's in-memory activity timer. The control plane does not scan request activity and does not enqueue sleep work. It accepts diff --git a/docs/services/scaling.mdx b/docs/services/scaling.mdx index 46aa96ec..387d0115 100644 --- a/docs/services/scaling.mdx +++ b/docs/services/scaling.mdx @@ -34,15 +34,13 @@ Serverless settings are configured per service: | Enable serverless | Off | Allows proxy-hosted HTTP deployments to sleep and wake on request | | Sleep after | `300s` | Idle period before running containers are stopped | | Wake timeout | `300s` | Maximum time the wake gateway waits for ready upstreams | -| Min Ready | `1` | Preferred number of ready deployments before queued requests resume | -`Min Ready` does not cap how many local proxy replicas are started. A cold wake -starts the sleeping local proxy replicas for that host. `Min Ready` only controls -when held requests can resume. If a worker upstream is already ready, the gateway -can serve it immediately while local proxy replicas wake in the background. +A cold wake starts the sleeping local proxy replicas for that host. Held +requests resume when one upstream is ready. If a worker upstream is already +ready, the gateway can serve it immediately while local proxy replicas wake in +the background. -Serverless services require at least one configured replica. `Min Ready` must be -between `1` and `10`, and cannot exceed the configured replica count. +Serverless services require at least one configured replica. ## Placement diff --git a/web/actions/projects.ts b/web/actions/projects.ts index 3e3865c8..1b6e9c82 100644 --- a/web/actions/projects.ts +++ b/web/actions/projects.ts @@ -1004,7 +1004,6 @@ const serverlessSettingsSchema = z.object({ enabled: z.boolean(), sleepAfterSeconds: z.number().int().min(60).max(86_400), wakeTimeoutSeconds: z.number().int().min(10).max(900), - minReadyReplicas: z.number().int().min(1).max(10), }); export async function updateServiceServerlessSettings( @@ -1013,7 +1012,6 @@ export async function updateServiceServerlessSettings( enabled: boolean; sleepAfterSeconds: number; wakeTimeoutSeconds: number; - minReadyReplicas: number; }, ) { await requireDeveloperRole(); @@ -1064,11 +1062,6 @@ export async function updateServiceServerlessSettings( if (totalConfiguredReplicas < 1) { throw new Error("Serverless services require at least one replica"); } - if (validated.minReadyReplicas > totalConfiguredReplicas) { - throw new Error( - "Minimum ready replicas cannot exceed configured replicas", - ); - } } await tx @@ -1077,7 +1070,6 @@ export async function updateServiceServerlessSettings( serverlessEnabled: validated.enabled, serverlessSleepAfterSeconds: validated.sleepAfterSeconds, serverlessWakeTimeoutSeconds: validated.wakeTimeoutSeconds, - serverlessMinReadyReplicas: validated.minReadyReplicas, }) .where(eq(services.id, serviceId)); }); @@ -1219,10 +1211,8 @@ export async function updateServiceConfig( .delete(serviceReplicas) .where(eq(serviceReplicas.serviceId, serviceId)); - let totalReplicas = 0; for (const replica of config.replicas) { if (replica.count > 0) { - totalReplicas += replica.count; await db.insert(serviceReplicas).values({ id: randomUUID(), serviceId, @@ -1231,15 +1221,6 @@ export async function updateServiceConfig( }); } } - - await db - .update(services) - .set({ - serverlessMinReadyReplicas: sql`LEAST(${services.serverlessMinReadyReplicas}, ${Math.max(1, totalReplicas)})`, - }) - .where( - and(eq(services.id, serviceId), eq(services.serverlessEnabled, true)), - ); } return { success: true }; diff --git a/web/components/service/details/serverless-section.tsx b/web/components/service/details/serverless-section.tsx index 4e2691ee..17664b35 100644 --- a/web/components/service/details/serverless-section.tsx +++ b/web/components/service/details/serverless-section.tsx @@ -25,9 +25,6 @@ export const ServerlessSection = memo(function ServerlessSection({ const [wakeTimeoutSeconds, setWakeTimeoutSeconds] = useState( String(service.serverlessWakeTimeoutSeconds ?? 300), ); - const [minReadyReplicas, setMinReadyReplicas] = useState( - String(service.serverlessMinReadyReplicas ?? 1), - ); const hasPublicHttpEndpoint = useMemo( () => service.ports.some( @@ -44,9 +41,8 @@ export const ServerlessSection = memo(function ServerlessSection({ () => ({ sleepAfterSeconds: Number.parseInt(sleepAfterSeconds, 10), wakeTimeoutSeconds: Number.parseInt(wakeTimeoutSeconds, 10), - minReadyReplicas: Number.parseInt(minReadyReplicas, 10), }), - [sleepAfterSeconds, wakeTimeoutSeconds, minReadyReplicas], + [sleepAfterSeconds, wakeTimeoutSeconds], ); const validationError = useMemo(() => { @@ -67,21 +63,13 @@ export const ServerlessSection = memo(function ServerlessSection({ ) { return "Wake timeout must be between 10 and 900 seconds"; } - if ( - !Number.isInteger(parsed.minReadyReplicas) || - parsed.minReadyReplicas < 1 || - parsed.minReadyReplicas > 10 - ) { - return "Minimum ready replicas must be between 1 and 10"; - } return null; }, [enabled, parsed, unavailableReason]); const hasChanges = enabled !== service.serverlessEnabled || parsed.sleepAfterSeconds !== service.serverlessSleepAfterSeconds || - parsed.wakeTimeoutSeconds !== service.serverlessWakeTimeoutSeconds || - parsed.minReadyReplicas !== service.serverlessMinReadyReplicas; + parsed.wakeTimeoutSeconds !== service.serverlessWakeTimeoutSeconds; const handleSave = async () => { setIsSaving(true); @@ -90,7 +78,6 @@ export const ServerlessSection = memo(function ServerlessSection({ enabled, sleepAfterSeconds: parsed.sleepAfterSeconds, wakeTimeoutSeconds: parsed.wakeTimeoutSeconds, - minReadyReplicas: parsed.minReadyReplicas, }); onUpdate(); toast.success("Serverless settings saved. Deploy to apply."); @@ -123,7 +110,7 @@ export const ServerlessSection = memo(function ServerlessSection({ />
diff --git a/web/db/schema.ts b/web/db/schema.ts index 23883407..2a9137a4 100644 --- a/web/db/schema.ts +++ b/web/db/schema.ts @@ -387,9 +387,6 @@ export const services = pgTable("services", { serverlessWakeTimeoutSeconds: integer("serverless_wake_timeout_seconds") .notNull() .default(300), - serverlessMinReadyReplicas: integer("serverless_min_ready_replicas") - .notNull() - .default(1), deployedConfig: text("deployed_config"), deploymentSchedule: text("deployment_schedule"), lastScheduledDeploymentRunAt: timestamp("last_scheduled_deployment_run_at", { diff --git a/web/lib/agent-status.ts b/web/lib/agent-status.ts index 78bd7d6e..dde83bc6 100644 --- a/web/lib/agent-status.ts +++ b/web/lib/agent-status.ts @@ -19,10 +19,10 @@ import { getSteadyStateRecreateDecision, } from "@/lib/autoheal-policy"; import { - type ObservedPhase, isObservedActiveContainer, isObservedReady, markDeploymentFailedRemoved, + type ObservedPhase, observedStartingPhases, runtimeExpectedStates, } from "@/lib/deployment-status"; @@ -149,7 +149,6 @@ async function applyDeploymentErrors( serverlessEnabled: services.serverlessEnabled, serverlessSleepAfterSeconds: services.serverlessSleepAfterSeconds, serverlessWakeTimeoutSeconds: services.serverlessWakeTimeoutSeconds, - serverlessMinReadyReplicas: services.serverlessMinReadyReplicas, stateful: services.stateful, deployedConfig: services.deployedConfig, rolloutStatus: rollouts.status, @@ -281,7 +280,6 @@ async function applyServerlessTransitions( serverlessEnabled: services.serverlessEnabled, serverlessSleepAfterSeconds: services.serverlessSleepAfterSeconds, serverlessWakeTimeoutSeconds: services.serverlessWakeTimeoutSeconds, - serverlessMinReadyReplicas: services.serverlessMinReadyReplicas, stateful: services.stateful, deployedConfig: services.deployedConfig, serverIsProxy: servers.isProxy, @@ -480,7 +478,9 @@ function getServerlessTransitionResultBase( function isServerlessTransitionType( value: unknown, ): value is ServerlessTransition["type"] { - return value === "sleep" || value === "wake_started" || value === "wake_failed"; + return ( + value === "sleep" || value === "wake_started" || value === "wake_failed" + ); } export function getSleepTransitionDeploymentIds( @@ -561,7 +561,6 @@ function getInvalidServerlessTransitionReason({ serverlessEnabled: boolean; serverlessSleepAfterSeconds: number; serverlessWakeTimeoutSeconds: number; - serverlessMinReadyReplicas: number; deployedConfig: string | null; serverIsProxy: boolean; } @@ -729,8 +728,9 @@ export async function applyStatusReport( serverId, serverlessTransitions, ); - const sleepTransitionDeploymentIds = - getSleepTransitionDeploymentIds(serverlessTransitions); + const sleepTransitionDeploymentIds = getSleepTransitionDeploymentIds( + serverlessTransitions, + ); const reportedDeploymentIds = report.containers .map((c) => c.deploymentId) diff --git a/web/lib/agent/expected-state.ts b/web/lib/agent/expected-state.ts index 72f1501b..6340a112 100644 --- a/web/lib/agent/expected-state.ts +++ b/web/lib/agent/expected-state.ts @@ -101,7 +101,6 @@ export type ServerlessRoute = { port: number; sleepAfterSeconds: number; wakeTimeoutSeconds: number; - minReadyReplicas: number; localDeploymentIds: string[]; upstreams: ServerlessRouteUpstream[]; }; @@ -462,10 +461,6 @@ export function buildServerlessRoutesFromRows({ getDeployedServerlessConfig(service).sleepAfterSeconds, wakeTimeoutSeconds: getDeployedServerlessConfig(service).wakeTimeoutSeconds, - minReadyReplicas: Math.max( - 1, - getDeployedServerlessConfig(service).minReadyReplicas, - ), localDeploymentIds, upstreams, }, diff --git a/web/lib/inngest/functions/rollout-helpers.ts b/web/lib/inngest/functions/rollout-helpers.ts index abe53c72..20273c2d 100644 --- a/web/lib/inngest/functions/rollout-helpers.ts +++ b/web/lib/inngest/functions/rollout-helpers.ts @@ -41,7 +41,6 @@ export function isActiveDeploymentForRollout( serverlessEnabled?: boolean | null; serverlessSleepAfterSeconds?: number | null; serverlessWakeTimeoutSeconds?: number | null; - serverlessMinReadyReplicas?: number | null; }, ) { void service; @@ -192,8 +191,8 @@ export async function prepareRollingUpdate( .from(deployments) .where(eq(deployments.serviceId, serviceId)); - const runningDeployments = existingDeployments.filter( - (d) => isActiveDeploymentForRollout(d, service), + const runningDeployments = existingDeployments.filter((d) => + isActiveDeploymentForRollout(d, service), ); return { deploymentIds: runningDeployments.map((d) => d.id) }; @@ -487,8 +486,8 @@ export async function checkForRollingUpdate( .from(deployments) .where(eq(deployments.serviceId, serviceId)); - const runningDeployments = existingDeployments.filter( - (d) => isActiveDeploymentForRollout(d, service), + const runningDeployments = existingDeployments.filter((d) => + isActiveDeploymentForRollout(d, service), ); return runningDeployments.length > 0; diff --git a/web/lib/service-config.ts b/web/lib/service-config.ts index 263e34c9..6cf876ff 100644 --- a/web/lib/service-config.ts +++ b/web/lib/service-config.ts @@ -48,7 +48,6 @@ export type ServerlessConfig = { enabled: boolean; sleepAfterSeconds: number; wakeTimeoutSeconds: number; - minReadyReplicas: number; }; export type DeployedConfig = { @@ -90,7 +89,6 @@ export function buildCurrentConfig( serverlessEnabled?: boolean | null; serverlessSleepAfterSeconds?: number | null; serverlessWakeTimeoutSeconds?: number | null; - serverlessMinReadyReplicas?: number | null; }, replicas: { serverId: string; serverName: string; count: number }[], ports: { @@ -148,7 +146,6 @@ export function buildCurrentConfig( enabled: service.serverlessEnabled ?? false, sleepAfterSeconds: service.serverlessSleepAfterSeconds ?? 300, wakeTimeoutSeconds: service.serverlessWakeTimeoutSeconds ?? 300, - minReadyReplicas: service.serverlessMinReadyReplicas ?? 1, }, secrets: (secrets ?? []).map((s) => ({ key: s.key, @@ -289,16 +286,6 @@ export function diffConfigs( to: `${currentServerless.wakeTimeoutSeconds}s`, }); } - if ( - deployedServerless.minReadyReplicas !== - currentServerless.minReadyReplicas - ) { - changes.push({ - field: "Serverless min ready", - from: String(deployedServerless.minReadyReplicas), - to: String(currentServerless.minReadyReplicas), - }); - } } if (deployed.source.image !== current.source.image) { @@ -568,7 +555,6 @@ export function normalizeServerlessConfig( enabled: config?.enabled ?? false, sleepAfterSeconds: config?.sleepAfterSeconds ?? 300, wakeTimeoutSeconds: config?.wakeTimeoutSeconds ?? 300, - minReadyReplicas: config?.minReadyReplicas ?? 1, }; } @@ -576,13 +562,11 @@ export function getCurrentServerlessConfig(service: { serverlessEnabled?: boolean | null; serverlessSleepAfterSeconds?: number | null; serverlessWakeTimeoutSeconds?: number | null; - serverlessMinReadyReplicas?: number | null; }): ServerlessConfig { return { enabled: service.serverlessEnabled ?? false, sleepAfterSeconds: service.serverlessSleepAfterSeconds ?? 300, wakeTimeoutSeconds: service.serverlessWakeTimeoutSeconds ?? 300, - minReadyReplicas: service.serverlessMinReadyReplicas ?? 1, }; } @@ -591,7 +575,6 @@ export function getDeployedServerlessConfig(service: { serverlessEnabled?: boolean | null; serverlessSleepAfterSeconds?: number | null; serverlessWakeTimeoutSeconds?: number | null; - serverlessMinReadyReplicas?: number | null; }): ServerlessConfig { const deployed = parseDeployedConfig(service.deployedConfig ?? null); return deployed?.serverless @@ -613,7 +596,6 @@ export function isDeployedServerlessService(service: { serverlessEnabled?: boolean | null; serverlessSleepAfterSeconds?: number | null; serverlessWakeTimeoutSeconds?: number | null; - serverlessMinReadyReplicas?: number | null; }) { return getDeployedServerlessConfig(service).enabled; } diff --git a/web/tests/expected-state.test.ts b/web/tests/expected-state.test.ts index 9685bf23..2b01748d 100644 --- a/web/tests/expected-state.test.ts +++ b/web/tests/expected-state.test.ts @@ -32,7 +32,6 @@ describe("expected-state pure builders", () => { enabled: true, sleepAfterSeconds: 300, wakeTimeoutSeconds: 120, - minReadyReplicas: 1, }, }); @@ -487,7 +486,6 @@ describe("expected-state pure builders", () => { stateful: false, serverlessSleepAfterSeconds: 300, serverlessWakeTimeoutSeconds: 120, - serverlessMinReadyReplicas: 1, }, ] as any, ports: [ @@ -547,7 +545,6 @@ describe("expected-state pure builders", () => { port: 3000, sleepAfterSeconds: 300, wakeTimeoutSeconds: 120, - minReadyReplicas: 1, localDeploymentIds: ["dep_proxy"], upstreams: [ { @@ -572,7 +569,6 @@ describe("expected-state pure builders", () => { stateful: true, serverlessSleepAfterSeconds: 300, serverlessWakeTimeoutSeconds: 120, - serverlessMinReadyReplicas: 1, }, ] as any, ports: [ @@ -612,7 +608,6 @@ describe("expected-state pure builders", () => { port: 3000, sleepAfterSeconds: 300, wakeTimeoutSeconds: 120, - minReadyReplicas: 1, localDeploymentIds: ["dep_stateful"], upstreams: [], }, @@ -629,7 +624,6 @@ describe("expected-state pure builders", () => { stateful: false, serverlessSleepAfterSeconds: 300, serverlessWakeTimeoutSeconds: 120, - serverlessMinReadyReplicas: 1, }, ] as any, ports: [ @@ -686,7 +680,6 @@ describe("expected-state pure builders", () => { stateful: false, serverlessSleepAfterSeconds: 60, serverlessWakeTimeoutSeconds: 60, - serverlessMinReadyReplicas: 1, deployedConfig: deployedServerlessConfig, }, ] as any, @@ -715,7 +708,6 @@ describe("expected-state pure builders", () => { port: 3000, sleepAfterSeconds: 300, wakeTimeoutSeconds: 120, - minReadyReplicas: 1, localDeploymentIds: ["dep_sleeping"], upstreams: [], }, @@ -762,7 +754,6 @@ describe("expected-state pure builders", () => { stateful: false, serverlessSleepAfterSeconds: 300, serverlessWakeTimeoutSeconds: 120, - serverlessMinReadyReplicas: 1, }, ] as any, ports: [ diff --git a/web/tests/service-config.test.ts b/web/tests/service-config.test.ts index 5564eabf..98a1c55b 100644 --- a/web/tests/service-config.test.ts +++ b/web/tests/service-config.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; import { + type DeployedConfig, diffConfigs, getDeployedServerlessConfig, isDeployedServerlessService, - type DeployedConfig, } from "@/lib/service-config"; function deployedConfig( @@ -19,7 +19,6 @@ function deployedConfig( enabled: true, sleepAfterSeconds: 300, wakeTimeoutSeconds: 120, - minReadyReplicas: 1, }, ...overrides, }; @@ -31,7 +30,6 @@ describe("service config", () => { serverlessEnabled: false, serverlessSleepAfterSeconds: 60, serverlessWakeTimeoutSeconds: 60, - serverlessMinReadyReplicas: 1, stateful: true, deployedConfig: JSON.stringify(deployedConfig()), }; @@ -50,7 +48,6 @@ describe("service config", () => { stateful: true, serverlessSleepAfterSeconds: 300, serverlessWakeTimeoutSeconds: 120, - serverlessMinReadyReplicas: 1, deployedConfig: JSON.stringify(deployedConfig({ stateful: true })), }; @@ -68,7 +65,6 @@ describe("service config", () => { enabled: false, sleepAfterSeconds: 300, wakeTimeoutSeconds: 120, - minReadyReplicas: 1, }, });