workspace: remove worker lifecycle state
This commit is contained in:
parent
13f9e0fab8
commit
b877c92975
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Remove durable worker lifecycle_state'
|
title: 'Remove durable worker lifecycle_state'
|
||||||
state: 'inprogress'
|
state: 'done'
|
||||||
created_at: '2026-07-11T04:08:12Z'
|
created_at: '2026-07-11T04:08:12Z'
|
||||||
updated_at: '2026-07-11T04:09:06Z'
|
updated_at: '2026-07-11T04:33:35Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'yoi ticket'
|
queued_by: 'yoi ticket'
|
||||||
queued_at: '2026-07-11T04:09:06Z'
|
queued_at: '2026-07-11T04:09:06Z'
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,38 @@ Ticket を `yoi ticket` が queued にしました。
|
||||||
State changed to `inprogress`.
|
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 blocking_reason: Option<String>,
|
||||||
pub pinned: bool,
|
pub pinned: bool,
|
||||||
pub retention_state: String,
|
pub retention_state: String,
|
||||||
pub lifecycle_state: String,
|
|
||||||
pub linked_workdir_ids: Vec<String>,
|
pub linked_workdir_ids: Vec<String>,
|
||||||
pub running_linked: bool,
|
pub running_linked: bool,
|
||||||
pub estimated_reclaim_bytes: Option<u64>,
|
pub estimated_reclaim_bytes: Option<u64>,
|
||||||
|
|
@ -1579,7 +1578,13 @@ fn build_runtime_cleanup_plan(
|
||||||
api: &WorkspaceApi,
|
api: &WorkspaceApi,
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
) -> ApiResult<RuntimeCleanupPlanResponse> {
|
) -> 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) =
|
let (workdir_summaries, mut diagnostics) =
|
||||||
match runtime_working_directory_summaries(api, runtime_id) {
|
match runtime_working_directory_summaries(api, runtime_id) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
|
|
@ -1618,7 +1623,7 @@ fn build_runtime_cleanup_plan(
|
||||||
let links = api
|
let links = api
|
||||||
.store
|
.store
|
||||||
.list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?;
|
.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 pinned = record.retention_state == "pinned";
|
||||||
let blocking_reason = if pinned {
|
let blocking_reason = if pinned {
|
||||||
Some("worker is pinned".to_string())
|
Some("worker is pinned".to_string())
|
||||||
|
|
@ -1642,7 +1647,6 @@ fn build_runtime_cleanup_plan(
|
||||||
blocking_reason,
|
blocking_reason,
|
||||||
pinned,
|
pinned,
|
||||||
retention_state: record.retention_state.clone(),
|
retention_state: record.retention_state.clone(),
|
||||||
lifecycle_state: record.lifecycle_state.clone(),
|
|
||||||
linked_workdir_ids: links.iter().map(|link| link.workdir_id.clone()).collect(),
|
linked_workdir_ids: links.iter().map(|link| link.workdir_id.clone()).collect(),
|
||||||
running_linked: is_running,
|
running_linked: is_running,
|
||||||
estimated_reclaim_bytes: None,
|
estimated_reclaim_bytes: None,
|
||||||
|
|
@ -1667,7 +1671,7 @@ fn build_runtime_cleanup_plan(
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let linked_running_worker_ids = linked_workers
|
let linked_running_worker_ids = linked_workers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|worker| worker.lifecycle_state == "running")
|
.filter(|worker| live_running_worker_ids.contains(&worker.worker_id))
|
||||||
.map(|worker| worker.worker_id.clone())
|
.map(|worker| worker.worker_id.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let pinned_linked = linked_workers
|
let pinned_linked = linked_workers
|
||||||
|
|
@ -2913,13 +2917,10 @@ async fn stop_runtime_worker(
|
||||||
.stop_worker(&runtime_id, &worker_id, request)
|
.stop_worker(&runtime_id, &worker_id, request)
|
||||||
.map_err(|err| err.into_error())?;
|
.map_err(|err| err.into_error())?;
|
||||||
let backend_id = backend_worker_id(&runtime_id, &worker_id);
|
let backend_id = backend_worker_id(&runtime_id, &worker_id);
|
||||||
if let Some(mut record) = api
|
if let Some(record) = api
|
||||||
.store
|
.store
|
||||||
.get_worker_registry(&api.config.workspace_id, backend_id.as_str())?
|
.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)?;
|
sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?;
|
||||||
}
|
}
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
|
|
@ -3879,7 +3880,6 @@ fn record_worker_summary(
|
||||||
runtime_worker_id: worker.worker_id.as_str().to_string(),
|
runtime_worker_id: worker.worker_id.as_str().to_string(),
|
||||||
display_name: display_name.to_string(),
|
display_name: display_name.to_string(),
|
||||||
profile,
|
profile,
|
||||||
lifecycle_state: worker.status.clone(),
|
|
||||||
retention_state: existing
|
retention_state: existing
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|record| record.retention_state.clone())
|
.map(|record| record.retention_state.clone())
|
||||||
|
|
@ -3909,8 +3909,8 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary
|
||||||
host_id: "backend-registry".to_string(),
|
host_id: "backend-registry".to_string(),
|
||||||
role: None,
|
role: None,
|
||||||
label: record.display_name.clone(),
|
label: record.display_name.clone(),
|
||||||
status: record.lifecycle_state.clone(),
|
status: "archived".to_string(),
|
||||||
state: record.lifecycle_state.clone(),
|
state: "archived".to_string(),
|
||||||
last_seen_at: Some(record.updated_at.clone()),
|
last_seen_at: Some(record.updated_at.clone()),
|
||||||
pinned: record.retention_state == "pinned",
|
pinned: record.retention_state == "pinned",
|
||||||
retention_state: record.retention_state.clone(),
|
retention_state: record.retention_state.clone(),
|
||||||
|
|
@ -3949,8 +3949,6 @@ fn merge_worker_registry_projection(
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| worker_summary_from_registry(record));
|
.unwrap_or_else(|| worker_summary_from_registry(record));
|
||||||
summary.label = record.display_name.clone();
|
summary.label = record.display_name.clone();
|
||||||
summary.status = record.lifecycle_state.clone();
|
|
||||||
summary.state = record.lifecycle_state.clone();
|
|
||||||
summary.profile = record.profile.clone();
|
summary.profile = record.profile.clone();
|
||||||
summary.pinned = record.retention_state == "pinned";
|
summary.pinned = record.retention_state == "pinned";
|
||||||
summary.retention_state = record.retention_state.clone();
|
summary.retention_state = record.retention_state.clone();
|
||||||
|
|
@ -4759,7 +4757,6 @@ mod tests {
|
||||||
runtime_worker_id: "worker-1".to_string(),
|
runtime_worker_id: "worker-1".to_string(),
|
||||||
display_name: "Archived Worker".to_string(),
|
display_name: "Archived Worker".to_string(),
|
||||||
profile: Some("builtin:coder".to_string()),
|
profile: Some("builtin:coder".to_string()),
|
||||||
lifecycle_state: "stopped".to_string(),
|
|
||||||
retention_state: "pinned".to_string(),
|
retention_state: "pinned".to_string(),
|
||||||
transcript_ref: Some("runtime://embedded/workers/worker-1/transcript".to_string()),
|
transcript_ref: Some("runtime://embedded/workers/worker-1/transcript".to_string()),
|
||||||
session_ref: None,
|
session_ref: None,
|
||||||
|
|
@ -4792,7 +4789,8 @@ mod tests {
|
||||||
|
|
||||||
let projected = merge_worker_registry_projection(None, &worker, vec![link], &[workdir]);
|
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!(
|
assert_eq!(
|
||||||
projected.working_directory.as_ref().unwrap().status,
|
projected.working_directory.as_ref().unwrap().status,
|
||||||
WorkingDirectoryStatusKind::Removed
|
WorkingDirectoryStatusKind::Removed
|
||||||
|
|
@ -5341,7 +5339,6 @@ mod tests {
|
||||||
fn seed_cleanup_worker(
|
fn seed_cleanup_worker(
|
||||||
api: &WorkspaceApi,
|
api: &WorkspaceApi,
|
||||||
runtime_worker_id: &str,
|
runtime_worker_id: &str,
|
||||||
lifecycle_state: &str,
|
|
||||||
retention_state: &str,
|
retention_state: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let worker_id = backend_worker_id("runtime-test", runtime_worker_id);
|
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(),
|
runtime_worker_id: runtime_worker_id.to_string(),
|
||||||
display_name: runtime_worker_id.to_string(),
|
display_name: runtime_worker_id.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
lifecycle_state: lifecycle_state.to_string(),
|
|
||||||
retention_state: retention_state.to_string(),
|
retention_state: retention_state.to_string(),
|
||||||
transcript_ref: None,
|
transcript_ref: None,
|
||||||
session_ref: None,
|
session_ref: None,
|
||||||
|
|
@ -5525,12 +5521,12 @@ mod tests {
|
||||||
let workspace = tempfile::tempdir().unwrap();
|
let workspace = tempfile::tempdir().unwrap();
|
||||||
init_clean_git_workspace(workspace.path());
|
init_clean_git_workspace(workspace.path());
|
||||||
let api = test_api(workspace.path()).await;
|
let api = test_api(workspace.path()).await;
|
||||||
let pinned = seed_cleanup_worker(&api, "worker-pinned", "stopped", "pinned");
|
let pinned = seed_cleanup_worker(&api, "worker-pinned", "pinned");
|
||||||
let running = seed_cleanup_worker(&api, "worker-running", "running", "normal");
|
let unobserved = seed_cleanup_worker(&api, "worker-unobserved", "normal");
|
||||||
seed_cleanup_workdir(&api, "workdir-dirty", "present", "dirty");
|
seed_cleanup_workdir(&api, "workdir-dirty", "present", "dirty");
|
||||||
seed_cleanup_workdir(&api, "workdir-removed", "removed", "clean");
|
seed_cleanup_workdir(&api, "workdir-removed", "removed", "clean");
|
||||||
seed_cleanup_link(&api, pinned.as_str(), "workdir-dirty");
|
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")
|
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
|
||||||
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
|
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
|
||||||
|
|
@ -5553,11 +5549,8 @@ mod tests {
|
||||||
running_linked_workdir.action,
|
running_linked_workdir.action,
|
||||||
CleanupTargetKind::WorkdirRecordDelete
|
CleanupTargetKind::WorkdirRecordDelete
|
||||||
);
|
);
|
||||||
assert!(running_linked_workdir.running_linked);
|
assert!(!running_linked_workdir.running_linked);
|
||||||
assert_eq!(
|
assert_eq!(running_linked_workdir.blocking_reason.as_deref(), None);
|
||||||
running_linked_workdir.blocking_reason.as_deref(),
|
|
||||||
Some("workdir is linked to a running Worker")
|
|
||||||
);
|
|
||||||
let dirty_workdir = plan
|
let dirty_workdir = plan
|
||||||
.workdirs
|
.workdirs
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -5574,7 +5567,7 @@ mod tests {
|
||||||
let workspace = tempfile::tempdir().unwrap();
|
let workspace = tempfile::tempdir().unwrap();
|
||||||
init_clean_git_workspace(workspace.path());
|
init_clean_git_workspace(workspace.path());
|
||||||
let api = test_api(workspace.path()).await;
|
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")
|
let plan = build_runtime_cleanup_plan(&api, "runtime-test")
|
||||||
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
|
.unwrap_or_else(|err| panic!("cleanup plan: {}", err.error));
|
||||||
let target = plan
|
let target = plan
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||||
name: "backend worker workdir registry schema",
|
name: "backend worker workdir registry schema",
|
||||||
apply: create_worker_workdir_registry_tables,
|
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 {
|
struct Migration {
|
||||||
|
|
@ -58,7 +63,6 @@ pub struct WorkerRegistryRecord {
|
||||||
pub runtime_worker_id: String,
|
pub runtime_worker_id: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
pub lifecycle_state: String,
|
|
||||||
/// Retention state is explicit so `pinned` can be represented before prune exists.
|
/// Retention state is explicit so `pinned` can be represented before prune exists.
|
||||||
pub retention_state: String,
|
pub retention_state: String,
|
||||||
pub transcript_ref: Option<String>,
|
pub transcript_ref: Option<String>,
|
||||||
|
|
@ -244,15 +248,14 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"INSERT INTO worker_registry (
|
r#"INSERT INTO worker_registry (
|
||||||
workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile,
|
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
|
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
|
ON CONFLICT(workspace_id, worker_id) DO UPDATE SET
|
||||||
runtime_id = excluded.runtime_id,
|
runtime_id = excluded.runtime_id,
|
||||||
runtime_worker_id = excluded.runtime_worker_id,
|
runtime_worker_id = excluded.runtime_worker_id,
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
profile = excluded.profile,
|
profile = excluded.profile,
|
||||||
lifecycle_state = excluded.lifecycle_state,
|
|
||||||
retention_state = CASE
|
retention_state = CASE
|
||||||
WHEN worker_registry.retention_state = 'pinned' AND excluded.retention_state = 'normal'
|
WHEN worker_registry.retention_state = 'pinned' AND excluded.retention_state = 'normal'
|
||||||
THEN worker_registry.retention_state
|
THEN worker_registry.retention_state
|
||||||
|
|
@ -270,7 +273,6 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
record.runtime_worker_id,
|
record.runtime_worker_id,
|
||||||
record.display_name,
|
record.display_name,
|
||||||
record.profile,
|
record.profile,
|
||||||
record.lifecycle_state,
|
|
||||||
record.retention_state,
|
record.retention_state,
|
||||||
record.transcript_ref,
|
record.transcript_ref,
|
||||||
record.session_ref,
|
record.session_ref,
|
||||||
|
|
@ -543,7 +545,7 @@ fn read_worker_workdir_link_record(
|
||||||
fn worker_registry_select_sql(where_clause: &str) -> String {
|
fn worker_registry_select_sql(where_clause: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"SELECT workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile, \
|
"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}"
|
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)?,
|
runtime_worker_id: row.get(3)?,
|
||||||
display_name: row.get(4)?,
|
display_name: row.get(4)?,
|
||||||
profile: row.get(5)?,
|
profile: row.get(5)?,
|
||||||
lifecycle_state: row.get(6)?,
|
retention_state: row.get(6)?,
|
||||||
retention_state: row.get(7)?,
|
transcript_ref: row.get(7)?,
|
||||||
transcript_ref: row.get(8)?,
|
session_ref: row.get(8)?,
|
||||||
session_ref: row.get(9)?,
|
summary_ref: row.get(9)?,
|
||||||
summary_ref: row.get(10)?,
|
diagnostics_ref: row.get(10)?,
|
||||||
diagnostics_ref: row.get(11)?,
|
created_at: row.get(11)?,
|
||||||
created_at: row.get(12)?,
|
updated_at: row.get(12)?,
|
||||||
updated_at: row.get(13)?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -603,7 +604,6 @@ CREATE TABLE IF NOT EXISTS worker_registry (
|
||||||
runtime_worker_id TEXT NOT NULL,
|
runtime_worker_id TEXT NOT NULL,
|
||||||
display_name TEXT NOT NULL,
|
display_name TEXT NOT NULL,
|
||||||
profile TEXT,
|
profile TEXT,
|
||||||
lifecycle_state TEXT NOT NULL,
|
|
||||||
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
||||||
transcript_ref TEXT,
|
transcript_ref TEXT,
|
||||||
session_ref TEXT,
|
session_ref TEXT,
|
||||||
|
|
@ -787,6 +787,54 @@ fn rename_legacy_table(conn: &Connection, table_name: &str, legacy_name: &str) -
|
||||||
Ok(())
|
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<()> {
|
fn create_schema_v0_tables(conn: &Connection) -> Result<()> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -1010,7 +1058,7 @@ mod tests {
|
||||||
let db = dir.path().join("control-plane.sqlite");
|
let db = dir.path().join("control-plane.sqlite");
|
||||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
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 {
|
let record = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
|
|
@ -1022,7 +1070,7 @@ mod tests {
|
||||||
store.upsert_workspace(&record).await.unwrap();
|
store.upsert_workspace(&record).await.unwrap();
|
||||||
|
|
||||||
let reopened = SqliteWorkspaceStore::open(&db).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!(
|
assert_eq!(
|
||||||
reopened.get_workspace("local-dev").await.unwrap(),
|
reopened.get_workspace("local-dev").await.unwrap(),
|
||||||
Some(record)
|
Some(record)
|
||||||
|
|
@ -1137,6 +1185,25 @@ mod tests {
|
||||||
"caused_by_event_id",
|
"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(
|
assert_columns(
|
||||||
&conn,
|
&conn,
|
||||||
"artifacts",
|
"artifacts",
|
||||||
|
|
@ -1213,7 +1280,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let store = SqliteWorkspaceStore::from_connection(conn).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
|
store
|
||||||
.with_conn(|conn| {
|
.with_conn(|conn| {
|
||||||
|
|
@ -1319,7 +1386,6 @@ mod tests {
|
||||||
runtime_worker_id: "browser-1".to_string(),
|
runtime_worker_id: "browser-1".to_string(),
|
||||||
display_name: "Browser 1".to_string(),
|
display_name: "Browser 1".to_string(),
|
||||||
profile: Some("builtin:companion".to_string()),
|
profile: Some("builtin:companion".to_string()),
|
||||||
lifecycle_state: "idle".to_string(),
|
|
||||||
retention_state: "pinned".to_string(),
|
retention_state: "pinned".to_string(),
|
||||||
transcript_ref: Some("runtime://embedded/workers/browser-1/transcript".to_string()),
|
transcript_ref: Some("runtime://embedded/workers/browser-1/transcript".to_string()),
|
||||||
session_ref: None,
|
session_ref: None,
|
||||||
|
|
@ -1330,12 +1396,10 @@ mod tests {
|
||||||
};
|
};
|
||||||
store.upsert_worker_registry(&worker).unwrap();
|
store.upsert_worker_registry(&worker).unwrap();
|
||||||
let mut runtime_sync_worker = worker.clone();
|
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.retention_state = "normal".to_string();
|
||||||
runtime_sync_worker.updated_at = "5".to_string();
|
runtime_sync_worker.updated_at = "5".to_string();
|
||||||
store.upsert_worker_registry(&runtime_sync_worker).unwrap();
|
store.upsert_worker_registry(&runtime_sync_worker).unwrap();
|
||||||
let mut expected_worker = worker.clone();
|
let mut expected_worker = worker.clone();
|
||||||
expected_worker.lifecycle_state = "running".to_string();
|
|
||||||
expected_worker.updated_at = "5".to_string();
|
expected_worker.updated_at = "5".to_string();
|
||||||
|
|
||||||
let workdir = WorkdirRegistryRecord {
|
let workdir = WorkdirRegistryRecord {
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,6 @@ export type CleanupWorkerCandidate = {
|
||||||
blocking_reason?: string | null;
|
blocking_reason?: string | null;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
retention_state: string;
|
retention_state: string;
|
||||||
lifecycle_state: string;
|
|
||||||
linked_workdir_ids: string[];
|
linked_workdir_ids: string[];
|
||||||
running_linked: boolean;
|
running_linked: boolean;
|
||||||
estimated_reclaim_bytes?: number | null;
|
estimated_reclaim_bytes?: number | null;
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<strong>Delete Worker registry row:</strong> <code>{candidate.runtime_worker_id}</code>
|
<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}
|
{#if candidate.blocking_reason}<small class="error">Blocked: {candidate.blocking_reason}</small>{/if}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user