Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion agent/internal/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
15 changes: 3 additions & 12 deletions agent/internal/serverless/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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" {
Expand Down
1 change: 0 additions & 1 deletion agent/internal/serverless/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down
7 changes: 3 additions & 4 deletions docs/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 5 additions & 7 deletions docs/services/scaling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 0 additions & 19 deletions web/actions/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -1013,7 +1012,6 @@ export async function updateServiceServerlessSettings(
enabled: boolean;
sleepAfterSeconds: number;
wakeTimeoutSeconds: number;
minReadyReplicas: number;
},
) {
await requireDeveloperRole();
Expand Down Expand Up @@ -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
Expand All @@ -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));
});
Expand Down Expand Up @@ -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,
Expand All @@ -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 };
Expand Down
37 changes: 3 additions & 34 deletions web/components/service/details/serverless-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(() => {
Expand All @@ -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);
Expand All @@ -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.");
Expand Down Expand Up @@ -123,7 +110,7 @@ export const ServerlessSection = memo(function ServerlessSection({
/>
</Item>
<div className="space-y-4 p-4">
<div className="grid gap-3 md:grid-cols-3">
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1">
<label
htmlFor="serverless-sleep-after"
Expand Down Expand Up @@ -160,24 +147,6 @@ export const ServerlessSection = memo(function ServerlessSection({
onChange={(event) => setWakeTimeoutSeconds(event.target.value)}
/>
</div>
<div className="space-y-1">
<label
htmlFor="serverless-min-ready"
className="text-xs font-medium"
>
Min Ready
</label>
<Input
id="serverless-min-ready"
type="number"
min="1"
max="10"
step="1"
value={minReadyReplicas}
disabled={optionsDisabled}
onChange={(event) => setMinReadyReplicas(event.target.value)}
/>
</div>
</div>

<p className="text-sm text-muted-foreground">
Expand Down
3 changes: 0 additions & 3 deletions web/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
14 changes: 7 additions & 7 deletions web/lib/agent-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import {
getSteadyStateRecreateDecision,
} from "@/lib/autoheal-policy";
import {
type ObservedPhase,
isObservedActiveContainer,
isObservedReady,
markDeploymentFailedRemoved,
type ObservedPhase,
observedStartingPhases,
runtimeExpectedStates,
} from "@/lib/deployment-status";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -561,7 +561,6 @@ function getInvalidServerlessTransitionReason({
serverlessEnabled: boolean;
serverlessSleepAfterSeconds: number;
serverlessWakeTimeoutSeconds: number;
serverlessMinReadyReplicas: number;
deployedConfig: string | null;
serverIsProxy: boolean;
}
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 0 additions & 5 deletions web/lib/agent/expected-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export type ServerlessRoute = {
port: number;
sleepAfterSeconds: number;
wakeTimeoutSeconds: number;
minReadyReplicas: number;
localDeploymentIds: string[];
upstreams: ServerlessRouteUpstream[];
};
Expand Down Expand Up @@ -462,10 +461,6 @@ export function buildServerlessRoutesFromRows({
getDeployedServerlessConfig(service).sleepAfterSeconds,
wakeTimeoutSeconds:
getDeployedServerlessConfig(service).wakeTimeoutSeconds,
minReadyReplicas: Math.max(
1,
getDeployedServerlessConfig(service).minReadyReplicas,
),
localDeploymentIds,
upstreams,
},
Expand Down
9 changes: 4 additions & 5 deletions web/lib/inngest/functions/rollout-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function isActiveDeploymentForRollout(
serverlessEnabled?: boolean | null;
serverlessSleepAfterSeconds?: number | null;
serverlessWakeTimeoutSeconds?: number | null;
serverlessMinReadyReplicas?: number | null;
},
) {
void service;
Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading