workspace: remove worker lifecycle state
This commit is contained in:
parent
13f9e0fab8
commit
b877c92975
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Remove durable worker lifecycle_state'
|
||||
state: 'inprogress'
|
||||
state: 'done'
|
||||
created_at: '2026-07-11T04:08:12Z'
|
||||
updated_at: '2026-07-11T04:09:06Z'
|
||||
updated_at: '2026-07-11T04:33:35Z'
|
||||
assignee: null
|
||||
queued_by: 'yoi ticket'
|
||||
queued_at: '2026-07-11T04:09:06Z'
|
||||
|
|
|
|||
|
|
@ -39,4 +39,38 @@ Ticket を `yoi ticket` が queued にしました。
|
|||
State changed to `inprogress`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-07-11T04:33:35Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Removed durable worker lifecycle state from the Backend worker registry.
|
||||
|
||||
- `WorkerRegistryRecord` no longer has `lifecycle_state`.
|
||||
- Active worker_registry CREATE/INSERT/SELECT/read paths no longer include lifecycle state.
|
||||
- Added schema migration v4 to rebuild legacy worker_registry tables without the old lifecycle column.
|
||||
- Registry-only Worker projection now reports `archived` instead of stale `idle`/`running`/`stopped` values.
|
||||
- Live Worker merge no longer lets registry data overwrite Runtime status/state.
|
||||
- Cleanup planning uses live Runtime workers for running-linked detection; registry records only provide retention/link metadata.
|
||||
- Removed lifecycle_state from frontend cleanup candidate types/display.
|
||||
|
||||
Validation:
|
||||
- cargo test -q -p yoi-workspace-server
|
||||
- cd web/workspace && deno task check
|
||||
- cd web/workspace && deno task test
|
||||
- cargo test -q
|
||||
- git diff --check
|
||||
- nix build .#yoi --no-link
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: "yoi ticket" at: 2026-07-11T04:33:35Z from: inprogress to: done reason: cli_state field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
State changed to `done`.
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -601,7 +601,6 @@ pub struct CleanupWorkerCandidate {
|
|||
pub blocking_reason: Option<String>,
|
||||
pub pinned: bool,
|
||||
pub retention_state: String,
|
||||
pub lifecycle_state: String,
|
||||
pub linked_workdir_ids: Vec<String>,
|
||||
pub running_linked: bool,
|
||||
pub estimated_reclaim_bytes: Option<u64>,
|
||||
|
|
@ -1579,7 +1578,13 @@ fn build_runtime_cleanup_plan(
|
|||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
) -> ApiResult<RuntimeCleanupPlanResponse> {
|
||||
let _ = workers_response(api.clone());
|
||||
let workers = workers_response(api.clone())?;
|
||||
let live_running_worker_ids: HashSet<String> = workers
|
||||
.items
|
||||
.iter()
|
||||
.filter(|worker| worker.status == "running" || worker.state == "running")
|
||||
.map(|worker| backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str()))
|
||||
.collect();
|
||||
let (workdir_summaries, mut diagnostics) =
|
||||
match runtime_working_directory_summaries(api, runtime_id) {
|
||||
Ok(result) => result,
|
||||
|
|
@ -1618,7 +1623,7 @@ fn build_runtime_cleanup_plan(
|
|||
let links = api
|
||||
.store
|
||||
.list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?;
|
||||
let is_running = record.lifecycle_state == "running";
|
||||
let is_running = live_running_worker_ids.contains(&record.worker_id);
|
||||
let pinned = record.retention_state == "pinned";
|
||||
let blocking_reason = if pinned {
|
||||
Some("worker is pinned".to_string())
|
||||
|
|
@ -1642,7 +1647,6 @@ fn build_runtime_cleanup_plan(
|
|||
blocking_reason,
|
||||
pinned,
|
||||
retention_state: record.retention_state.clone(),
|
||||
lifecycle_state: record.lifecycle_state.clone(),
|
||||
linked_workdir_ids: links.iter().map(|link| link.workdir_id.clone()).collect(),
|
||||
running_linked: is_running,
|
||||
estimated_reclaim_bytes: None,
|
||||
|
|
@ -1667,7 +1671,7 @@ fn build_runtime_cleanup_plan(
|
|||
.collect::<Vec<_>>();
|
||||
let linked_running_worker_ids = linked_workers
|
||||
.iter()
|
||||
.filter(|worker| worker.lifecycle_state == "running")
|
||||
.filter(|worker| live_running_worker_ids.contains(&worker.worker_id))
|
||||
.map(|worker| worker.worker_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let pinned_linked = linked_workers
|
||||
|
|
@ -2913,13 +2917,10 @@ async fn stop_runtime_worker(
|
|||
.stop_worker(&runtime_id, &worker_id, request)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let backend_id = backend_worker_id(&runtime_id, &worker_id);
|
||||
if let Some(mut record) = api
|
||||
if let Some(record) = api
|
||||
.store
|
||||
.get_worker_registry(&api.config.workspace_id, backend_id.as_str())?
|
||||
{
|
||||
record.lifecycle_state = "stopped".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_worker_registry(&record)?;
|
||||
sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?;
|
||||
}
|
||||
Ok(Json(result))
|
||||
|
|
@ -3879,7 +3880,6 @@ fn record_worker_summary(
|
|||
runtime_worker_id: worker.worker_id.as_str().to_string(),
|
||||
display_name: display_name.to_string(),
|
||||
profile,
|
||||
lifecycle_state: worker.status.clone(),
|
||||
retention_state: existing
|
||||
.as_ref()
|
||||
.map(|record| record.retention_state.clone())
|
||||
|
|
@ -3909,8 +3909,8 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary
|
|||
host_id: "backend-registry".to_string(),
|
||||
role: None,
|
||||
label: record.display_name.clone(),
|
||||
status: record.lifecycle_state.clone(),
|
||||
state: record.lifecycle_state.clone(),
|
||||
status: "archived".to_string(),
|
||||
state: "archived".to_string(),
|
||||
last_seen_at: Some(record.updated_at.clone()),
|
||||
pinned: record.retention_state == "pinned",
|
||||
retention_state: record.retention_state.clone(),
|
||||
|
|
@ -3949,8 +3949,6 @@ fn merge_worker_registry_projection(
|
|||
.cloned()
|
||||
.unwrap_or_else(|| worker_summary_from_registry(record));
|
||||
summary.label = record.display_name.clone();
|
||||
summary.status = record.lifecycle_state.clone();
|
||||
summary.state = record.lifecycle_state.clone();
|
||||
summary.profile = record.profile.clone();
|
||||
summary.pinned = record.retention_state == "pinned";
|
||||
summary.retention_state = record.retention_state.clone();
|
||||
|
|
@ -4759,7 +4757,6 @@ mod tests {
|
|||
runtime_worker_id: "worker-1".to_string(),
|
||||
display_name: "Archived Worker".to_string(),
|
||||
profile: Some("builtin:coder".to_string()),
|
||||
lifecycle_state: "stopped".to_string(),
|
||||
retention_state: "pinned".to_string(),
|
||||
transcript_ref: Some("runtime://embedded/workers/worker-1/transcript".to_string()),
|
||||
session_ref: None,
|
||||
|
|
@ -4792,7 +4789,8 @@ mod tests {
|
|||
|
||||
let projected = merge_worker_registry_projection(None, &worker, vec![link], &[workdir]);
|
||||
|
||||
assert_eq!(projected.status, "stopped");
|
||||
assert_eq!(projected.status, "archived");
|
||||
assert_eq!(projected.state, "archived");
|
||||
assert_eq!(
|
||||
projected.working_directory.as_ref().unwrap().status,
|
||||
WorkingDirectoryStatusKind::Removed
|
||||
|
|
@ -5341,7 +5339,6 @@ mod tests {
|
|||
fn seed_cleanup_worker(
|
||||
api: &WorkspaceApi,
|
||||
runtime_worker_id: &str,
|
||||
lifecycle_state: &str,
|
||||
retention_state: &str,
|
||||
) -> String {
|
||||
let worker_id = backend_worker_id("runtime-test", runtime_worker_id);
|
||||
|
|
@ -5354,7 +5351,6 @@ mod tests {
|
|||
runtime_worker_id: runtime_worker_id.to_string(),
|
||||
display_name: runtime_worker_id.to_string(),
|
||||
profile: None,
|
||||
lifecycle_state: lifecycle_state.to_string(),
|
||||
retention_state: retention_state.to_string(),
|
||||
transcript_ref: None,
|
||||
session_ref: None,
|
||||
|
|
@ -5525,12 +5521,12 @@ mod tests {
|
|||
let workspace = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(workspace.path());
|
||||
let api = test_api(workspace.path()).await;
|
||||
let pinned = seed_cleanup_worker(&api, "worker-pinned", "stopped", "pinned");
|
||||
let running = seed_cleanup_worker(&api, "worker-running", "running", "normal");
|
||||
let pinned = seed_cleanup_worker(&api, "worker-pinned", "pinned");
|
||||
let unobserved = seed_cleanup_worker(&api, "worker-unobserved", "normal");
|
||||
seed_cleanup_workdir(&api, "workdir-dirty", "present", "dirty");
|
||||
seed_cleanup_workdir(&api, "workdir-removed", "removed", "clean");
|
||||
seed_cleanup_link(&api, pinned.as_str(), "workdir-dirty");
|
||||
seed_cleanup_link(&api, running.as_str(), "workdir-removed");
|
||||
seed_cleanup_link(&api, unobserved.as_str(), "workdir-removed");
|
||||
|
||||
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
|
||||
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
|
||||
|
|
@ -5553,11 +5549,8 @@ mod tests {
|
|||
running_linked_workdir.action,
|
||||
CleanupTargetKind::WorkdirRecordDelete
|
||||
);
|
||||
assert!(running_linked_workdir.running_linked);
|
||||
assert_eq!(
|
||||
running_linked_workdir.blocking_reason.as_deref(),
|
||||
Some("workdir is linked to a running Worker")
|
||||
);
|
||||
assert!(!running_linked_workdir.running_linked);
|
||||
assert_eq!(running_linked_workdir.blocking_reason.as_deref(), None);
|
||||
let dirty_workdir = plan
|
||||
.workdirs
|
||||
.iter()
|
||||
|
|
@ -5574,7 +5567,7 @@ mod tests {
|
|||
let workspace = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(workspace.path());
|
||||
let api = test_api(workspace.path()).await;
|
||||
let worker = seed_cleanup_worker(&api, "worker-pinned", "stopped", "pinned");
|
||||
let worker = seed_cleanup_worker(&api, "worker-pinned", "pinned");
|
||||
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
|
||||
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
|
||||
let target = plan
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ const MIGRATIONS: &[Migration] = &[
|
|||
name: "backend worker workdir registry schema",
|
||||
apply: create_worker_workdir_registry_tables,
|
||||
},
|
||||
Migration {
|
||||
version: 4,
|
||||
name: "remove durable worker lifecycle state",
|
||||
apply: remove_worker_registry_legacy_live_state_column,
|
||||
},
|
||||
];
|
||||
|
||||
struct Migration {
|
||||
|
|
@ -58,7 +63,6 @@ pub struct WorkerRegistryRecord {
|
|||
pub runtime_worker_id: String,
|
||||
pub display_name: String,
|
||||
pub profile: Option<String>,
|
||||
pub lifecycle_state: String,
|
||||
/// Retention state is explicit so `pinned` can be represented before prune exists.
|
||||
pub retention_state: String,
|
||||
pub transcript_ref: Option<String>,
|
||||
|
|
@ -244,15 +248,14 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||
conn.execute(
|
||||
r#"INSERT INTO worker_registry (
|
||||
workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile,
|
||||
lifecycle_state, retention_state, transcript_ref, session_ref, summary_ref,
|
||||
retention_state, transcript_ref, session_ref, summary_ref,
|
||||
diagnostics_ref, created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
|
||||
ON CONFLICT(workspace_id, worker_id) DO UPDATE SET
|
||||
runtime_id = excluded.runtime_id,
|
||||
runtime_worker_id = excluded.runtime_worker_id,
|
||||
display_name = excluded.display_name,
|
||||
profile = excluded.profile,
|
||||
lifecycle_state = excluded.lifecycle_state,
|
||||
retention_state = CASE
|
||||
WHEN worker_registry.retention_state = 'pinned' AND excluded.retention_state = 'normal'
|
||||
THEN worker_registry.retention_state
|
||||
|
|
@ -270,7 +273,6 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||
record.runtime_worker_id,
|
||||
record.display_name,
|
||||
record.profile,
|
||||
record.lifecycle_state,
|
||||
record.retention_state,
|
||||
record.transcript_ref,
|
||||
record.session_ref,
|
||||
|
|
@ -543,7 +545,7 @@ fn read_worker_workdir_link_record(
|
|||
fn worker_registry_select_sql(where_clause: &str) -> String {
|
||||
format!(
|
||||
"SELECT workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile, \
|
||||
lifecycle_state, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, \
|
||||
retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, \
|
||||
created_at, updated_at FROM worker_registry {where_clause}"
|
||||
)
|
||||
}
|
||||
|
|
@ -556,14 +558,13 @@ fn read_worker_registry_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<Work
|
|||
runtime_worker_id: row.get(3)?,
|
||||
display_name: row.get(4)?,
|
||||
profile: row.get(5)?,
|
||||
lifecycle_state: row.get(6)?,
|
||||
retention_state: row.get(7)?,
|
||||
transcript_ref: row.get(8)?,
|
||||
session_ref: row.get(9)?,
|
||||
summary_ref: row.get(10)?,
|
||||
diagnostics_ref: row.get(11)?,
|
||||
created_at: row.get(12)?,
|
||||
updated_at: row.get(13)?,
|
||||
retention_state: row.get(6)?,
|
||||
transcript_ref: row.get(7)?,
|
||||
session_ref: row.get(8)?,
|
||||
summary_ref: row.get(9)?,
|
||||
diagnostics_ref: row.get(10)?,
|
||||
created_at: row.get(11)?,
|
||||
updated_at: row.get(12)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -603,7 +604,6 @@ CREATE TABLE IF NOT EXISTS worker_registry (
|
|||
runtime_worker_id TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
profile TEXT,
|
||||
lifecycle_state TEXT NOT NULL,
|
||||
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
||||
transcript_ref TEXT,
|
||||
session_ref TEXT,
|
||||
|
|
@ -787,6 +787,54 @@ fn rename_legacy_table(conn: &Connection, table_name: &str, legacy_name: &str) -
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_worker_registry_legacy_live_state_column(conn: &Connection) -> Result<()> {
|
||||
if !table_exists(conn, "worker_registry")?
|
||||
|| !table_columns(conn, "worker_registry")?
|
||||
.iter()
|
||||
.any(|column| column == "lifecycle_state")
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE TABLE worker_registry_v4 (
|
||||
workspace_id TEXT NOT NULL,
|
||||
worker_id TEXT NOT NULL,
|
||||
runtime_id TEXT NOT NULL,
|
||||
runtime_worker_id TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
profile TEXT,
|
||||
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
||||
transcript_ref TEXT,
|
||||
session_ref TEXT,
|
||||
summary_ref TEXT,
|
||||
diagnostics_ref TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worker_id),
|
||||
UNIQUE (workspace_id, runtime_id, runtime_worker_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO worker_registry_v4 (
|
||||
workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile,
|
||||
retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile,
|
||||
retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref,
|
||||
created_at, updated_at
|
||||
FROM worker_registry;
|
||||
DROP TABLE worker_registry;
|
||||
ALTER TABLE worker_registry_v4 RENAME TO worker_registry;
|
||||
CREATE INDEX IF NOT EXISTS idx_worker_registry_workspace_updated
|
||||
ON worker_registry(workspace_id, updated_at DESC);
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_schema_v0_tables(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
|
|
@ -1010,7 +1058,7 @@ mod tests {
|
|||
let db = dir.path().join("control-plane.sqlite");
|
||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
||||
|
||||
assert_eq!(store.schema_version().await.unwrap(), 3);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 4);
|
||||
|
||||
let record = WorkspaceRecord {
|
||||
workspace_id: "local-dev".to_string(),
|
||||
|
|
@ -1022,7 +1070,7 @@ mod tests {
|
|||
store.upsert_workspace(&record).await.unwrap();
|
||||
|
||||
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
||||
assert_eq!(reopened.schema_version().await.unwrap(), 3);
|
||||
assert_eq!(reopened.schema_version().await.unwrap(), 4);
|
||||
assert_eq!(
|
||||
reopened.get_workspace("local-dev").await.unwrap(),
|
||||
Some(record)
|
||||
|
|
@ -1137,6 +1185,25 @@ mod tests {
|
|||
"caused_by_event_id",
|
||||
],
|
||||
);
|
||||
assert_columns(
|
||||
&conn,
|
||||
"worker_registry",
|
||||
[
|
||||
"workspace_id",
|
||||
"worker_id",
|
||||
"runtime_id",
|
||||
"runtime_worker_id",
|
||||
"display_name",
|
||||
"profile",
|
||||
"retention_state",
|
||||
"transcript_ref",
|
||||
"session_ref",
|
||||
"summary_ref",
|
||||
"diagnostics_ref",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
);
|
||||
assert_columns(
|
||||
&conn,
|
||||
"artifacts",
|
||||
|
|
@ -1213,7 +1280,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
||||
assert_eq!(store.schema_version().await.unwrap(), 3);
|
||||
assert_eq!(store.schema_version().await.unwrap(), 4);
|
||||
|
||||
store
|
||||
.with_conn(|conn| {
|
||||
|
|
@ -1319,7 +1386,6 @@ mod tests {
|
|||
runtime_worker_id: "browser-1".to_string(),
|
||||
display_name: "Browser 1".to_string(),
|
||||
profile: Some("builtin:companion".to_string()),
|
||||
lifecycle_state: "idle".to_string(),
|
||||
retention_state: "pinned".to_string(),
|
||||
transcript_ref: Some("runtime://embedded/workers/browser-1/transcript".to_string()),
|
||||
session_ref: None,
|
||||
|
|
@ -1330,12 +1396,10 @@ mod tests {
|
|||
};
|
||||
store.upsert_worker_registry(&worker).unwrap();
|
||||
let mut runtime_sync_worker = worker.clone();
|
||||
runtime_sync_worker.lifecycle_state = "running".to_string();
|
||||
runtime_sync_worker.retention_state = "normal".to_string();
|
||||
runtime_sync_worker.updated_at = "5".to_string();
|
||||
store.upsert_worker_registry(&runtime_sync_worker).unwrap();
|
||||
let mut expected_worker = worker.clone();
|
||||
expected_worker.lifecycle_state = "running".to_string();
|
||||
expected_worker.updated_at = "5".to_string();
|
||||
|
||||
let workdir = WorkdirRegistryRecord {
|
||||
|
|
|
|||
|
|
@ -163,7 +163,6 @@ export type CleanupWorkerCandidate = {
|
|||
blocking_reason?: string | null;
|
||||
pinned: boolean;
|
||||
retention_state: string;
|
||||
lifecycle_state: string;
|
||||
linked_workdir_ids: string[];
|
||||
running_linked: boolean;
|
||||
estimated_reclaim_bytes?: number | null;
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@
|
|||
/>
|
||||
<span>
|
||||
<strong>Delete Worker registry row:</strong> <code>{candidate.runtime_worker_id}</code>
|
||||
<small>{candidate.lifecycle_state}; {candidate.retention_state}; linked Workdirs {candidate.linked_workdir_ids.length}</small>
|
||||
<small>{candidate.retention_state}; linked Workdirs {candidate.linked_workdir_ids.length}</small>
|
||||
{#if candidate.blocking_reason}<small class="error">Blocked: {candidate.blocking_reason}</small>{/if}
|
||||
</span>
|
||||
</label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user