fix: separate worker state from runtime lifecycle
This commit is contained in:
@@ -19,7 +19,7 @@ command_id: number, expected_execution_generation: number, expected_worker_state
|
||||
|
||||
export type WorkerCommandKind = "resume" | "cancel" | "pause" | "compact" | "shutdown";
|
||||
|
||||
export type WorkerCommandDisposition = "accepted" | "stale_execution_generation" | "stale_worker_state_revision" | "stale_command_id" | "invalid_state";
|
||||
export type WorkerCommandDisposition = "accepted" | "stale_execution_generation" | "stale_worker_state_revision" | "stale_command_id" | "conflict" | "invalid_state";
|
||||
|
||||
export type WorkerCommandAcknowledgement = { command_id: number, command: WorkerCommandKind, disposition: WorkerCommandDisposition,
|
||||
/**
|
||||
@@ -233,7 +233,16 @@ resource_key?: string | null,
|
||||
/**
|
||||
* Producer-owned monotonic revision for this Worker subject.
|
||||
*/
|
||||
subject_revision: number, state: SubscriptionWorkerState, has_running_internal_workers: boolean, workspace_id?: string | null, display_name?: string | null, profile?: string | null,
|
||||
subject_revision: number,
|
||||
/**
|
||||
* Latest revisioned foreground state observed from the Worker. This remains
|
||||
* absent until an authoritative Worker snapshot/event has been applied.
|
||||
*/
|
||||
worker_state?: WorkerStateSnapshot | null,
|
||||
/**
|
||||
* Runtime catalog lifecycle compatibility projection; not foreground-state authority.
|
||||
*/
|
||||
state: SubscriptionWorkerState, has_running_internal_workers: boolean, workspace_id?: string | null, display_name?: string | null, profile?: string | null,
|
||||
/**
|
||||
* Workspace-facing Repository key. Runtime producers leave this unset and
|
||||
* Workspace Server projections replace `repository_id` with this field.
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
Event as PodProtocolEvent,
|
||||
Method as PodProtocolMethod,
|
||||
Segment as PodProtocolSegment,
|
||||
WorkerStateSnapshot,
|
||||
} from "$lib/generated/protocol";
|
||||
import type {
|
||||
GitCommitSummary as SharedGitCommitSummary,
|
||||
@@ -99,6 +100,7 @@ export type Worker = {
|
||||
tags: string[];
|
||||
workspace: { visibility: string; identity: string };
|
||||
state: string;
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
pinned?: boolean;
|
||||
retention_state?: string;
|
||||
last_seen_at?: string | null;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { WorkerStateSnapshot } from "$lib/generated/protocol";
|
||||
|
||||
export function liveWorkerState(worker: {
|
||||
state: string;
|
||||
worker_state?: WorkerStateSnapshot | null;
|
||||
}): string {
|
||||
const state = worker.worker_state?.state;
|
||||
if (!state) return worker.state === "stopped" ? "stopped" : "unknown";
|
||||
if (state.kind === "idle") return "idle";
|
||||
if (state.state.kind === "maintenance") return "running";
|
||||
return state.state.state === "paused" ? "paused" : "running";
|
||||
}
|
||||
@@ -5,6 +5,7 @@ function assertEquals(actual: unknown, expected: unknown): void {
|
||||
throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||
}
|
||||
}
|
||||
import { liveWorkerState } from './worker-state';
|
||||
import {
|
||||
applyWorkspaceWorkersFrame,
|
||||
createWorkspaceWorkersProjection,
|
||||
@@ -33,6 +34,22 @@ function worker(
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test('Worker list state uses the authoritative live snapshot separately from lifecycle', () => {
|
||||
const active = worker('runtime-a', 'worker-1', 1);
|
||||
active.worker_state = {
|
||||
execution_generation: 4,
|
||||
revision: 2,
|
||||
last_command_id: 1,
|
||||
state: { kind: 'busy', state: { kind: 'run', state: 'paused' } },
|
||||
};
|
||||
assertEquals(liveWorkerState(active), 'paused');
|
||||
|
||||
const unavailable = worker('runtime-a', 'worker-2', 1);
|
||||
assertEquals(liveWorkerState(unavailable), 'unknown');
|
||||
unavailable.state = 'stopped';
|
||||
assertEquals(liveWorkerState(unavailable), 'stopped');
|
||||
});
|
||||
|
||||
Deno.test('workspace Worker snapshot keeps equal local ids from different Runtimes', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
const frame: SubscriptionFrame = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
applyWorkspaceWorkersFrame,
|
||||
createWorkspaceWorkersProjection,
|
||||
} from './worker-subscription-model';
|
||||
import { liveWorkerState } from './worker-state';
|
||||
import { compareWorkersForSidebar } from './workers';
|
||||
import type { Worker } from './types';
|
||||
|
||||
@@ -86,7 +87,8 @@ function projectWorker(worker: SubscriptionWorker): SidebarWorker {
|
||||
profile: worker.profile ?? null,
|
||||
tags: [],
|
||||
workspace: { visibility: 'workspace', identity: 'runtime_subscription_worker' },
|
||||
state: worker.state,
|
||||
state: liveWorkerState(worker),
|
||||
worker_state: worker.worker_state,
|
||||
pinned: false,
|
||||
retention_state: 'transient',
|
||||
implementation: {
|
||||
|
||||
+16
-44
@@ -52,11 +52,7 @@
|
||||
import { pushWorkspaceAlert } from "$lib/workspace/alerts/store";
|
||||
import { workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import { workspaceMultiplexer, type WorkspaceMultiplexerSubscription } from "$lib/workspace/multiplexer";
|
||||
import type {
|
||||
Diagnostic,
|
||||
Worker,
|
||||
PodProtocolEvent,
|
||||
} from "$lib/workspace/sidebar/types";
|
||||
import type { Diagnostic, Worker } from "$lib/workspace/sidebar/types";
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
@@ -207,7 +203,6 @@
|
||||
);
|
||||
let pendingObservationEvents: ConsoleEventInput[] = [];
|
||||
let protocolEventSequence = 0;
|
||||
let pendingObservedStates: Array<string | null> = [];
|
||||
let pendingStreamDiagnostics: Diagnostic[] = [];
|
||||
let observationFlushHandle: number | null = null;
|
||||
let nextReloadToken = 0;
|
||||
@@ -249,7 +244,9 @@
|
||||
const diagnostics = $derived(
|
||||
mergeDiagnostics(worker?.diagnostics ?? [], streamDiagnostics),
|
||||
);
|
||||
const workerState = $derived(liveWorkerState ?? worker?.state ?? "loading");
|
||||
const workerState = $derived(
|
||||
liveWorkerState ?? (worker?.state === "stopped" ? "stopped" : "loading"),
|
||||
);
|
||||
const workerRunning = $derived(workerState === "running");
|
||||
const workerPaused = $derived(workerState === "paused");
|
||||
const composerEditable = $derived(protocolState === "open" && !sending);
|
||||
@@ -343,7 +340,6 @@
|
||||
observationFlushHandle = null;
|
||||
}
|
||||
pendingObservationEvents = [];
|
||||
pendingObservedStates = [];
|
||||
pendingStreamDiagnostics = [];
|
||||
}
|
||||
|
||||
@@ -359,18 +355,15 @@
|
||||
function flushObservationBatch() {
|
||||
observationFlushHandle = null;
|
||||
const eventBatch = pendingObservationEvents;
|
||||
const stateBatch = pendingObservedStates;
|
||||
const diagnosticBatch = pendingStreamDiagnostics;
|
||||
pendingObservationEvents = [];
|
||||
pendingObservedStates = [];
|
||||
pendingStreamDiagnostics = [];
|
||||
|
||||
if (eventBatch.length > 0) {
|
||||
const latestState = stateBatch.findLast((state) => state !== null);
|
||||
if (latestState) {
|
||||
liveWorkerState = latestState;
|
||||
}
|
||||
consoleProjection = consoleProjector.append(eventBatch);
|
||||
liveWorkerState = consoleProjection.status === "shutdown"
|
||||
? "shutdown"
|
||||
: workerStateFromSnapshot(consoleProjection.workerState);
|
||||
advanceEventObservedAtVersion();
|
||||
}
|
||||
|
||||
@@ -407,7 +400,6 @@
|
||||
event: payload,
|
||||
observedAtMs,
|
||||
});
|
||||
pendingObservedStates.push(workerStateFromProtocolEvent(payload));
|
||||
scheduleObservationFlush();
|
||||
}
|
||||
|
||||
@@ -925,36 +917,16 @@
|
||||
handleComposerSubmit();
|
||||
}
|
||||
|
||||
function workerStateFromProtocolEvent(
|
||||
event: PodProtocolEvent,
|
||||
function workerStateFromSnapshot(
|
||||
snapshot: ConsoleProjection["workerState"],
|
||||
): string | null {
|
||||
switch (event.event) {
|
||||
case "snapshot":
|
||||
return event.data.state.state.kind === "idle"
|
||||
? "idle"
|
||||
: event.data.state.state.state.kind === "run" &&
|
||||
event.data.state.state.state.state === "paused"
|
||||
? "paused"
|
||||
: "running";
|
||||
case "worker_state":
|
||||
return event.data.snapshot.state.kind === "idle"
|
||||
? "idle"
|
||||
: event.data.snapshot.state.state.kind === "run" &&
|
||||
event.data.snapshot.state.state.state === "paused"
|
||||
? "paused"
|
||||
: "running";
|
||||
case "command_acknowledged":
|
||||
return event.data.acknowledgement.state.state.kind === "idle"
|
||||
? "idle"
|
||||
: event.data.acknowledgement.state.state.state.kind === "run" &&
|
||||
event.data.acknowledgement.state.state.state.state === "paused"
|
||||
? "paused"
|
||||
: "running";
|
||||
case "shutdown":
|
||||
return "shutdown";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (!snapshot) return null;
|
||||
return snapshot.state.kind === "idle"
|
||||
? "idle"
|
||||
: snapshot.state.state.kind === "run" &&
|
||||
snapshot.state.state.state === "paused"
|
||||
? "paused"
|
||||
: "running";
|
||||
}
|
||||
|
||||
function connectProtocolTransport(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { workerHref } from '$lib/workspace/resource-links';
|
||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
|
||||
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
|
||||
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
@@ -136,7 +137,7 @@
|
||||
}
|
||||
|
||||
function workerStatus(worker: Worker): string {
|
||||
return worker.state;
|
||||
return liveWorkerState(worker);
|
||||
}
|
||||
|
||||
function workerProfile(worker: Worker): string {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
|
||||
import type { PageData } from './$types';
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
@@ -24,7 +25,7 @@
|
||||
>Open console</a>
|
||||
</header>
|
||||
<dl class="resource-meta">
|
||||
<dt>Status</dt><dd>{data.worker.state}</dd>
|
||||
<dt>Status</dt><dd>{liveWorkerState(data.worker)}</dd>
|
||||
<dt>Profile</dt><dd>{data.worker.profile}</dd>
|
||||
<dt>Internal ID</dt><dd><code>{data.worker.worker_id}</code></dd>
|
||||
</dl>
|
||||
|
||||
Reference in New Issue
Block a user