workspace: use opaque numeric worker ids
This commit is contained in:
parent
664f8693a0
commit
c5aae4b234
0
.yoi/tickets/00001KX8D417N/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KX8D417N/artifacts/.gitkeep
Normal file
17
.yoi/tickets/00001KX8D417N/item.md
Normal file
17
.yoi/tickets/00001KX8D417N/item.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: 'Opaque Worker and Workdir ids'
|
||||||
|
state: 'closed'
|
||||||
|
created_at: '2026-07-11T10:56:08Z'
|
||||||
|
updated_at: '2026-07-11T12:05:24Z'
|
||||||
|
assignee: null
|
||||||
|
queued_by: 'yoi ticket'
|
||||||
|
queued_at: '2026-07-11T10:56:21Z'
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
## 受け入れ条件
|
||||||
|
|
||||||
|
- 未定
|
||||||
1
.yoi/tickets/00001KX8D417N/resolution.md
Normal file
1
.yoi/tickets/00001KX8D417N/resolution.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/tmp/yoi-id-implementation-report.md
|
||||||
116
.yoi/tickets/00001KX8D417N/thread.md
Normal file
116
.yoi/tickets/00001KX8D417N/thread.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-07-11T10:56:08Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: intake_summary author: hare at: 2026-07-11T10:56:21Z -->
|
||||||
|
|
||||||
|
## Intake summary
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-07-11T10:56:21Z from: planning to: ready reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Marked ready by `yoi ticket state`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-07-11T10:56:21Z from: ready to: queued reason: queued field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を `yoi ticket` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-07-11T10:56:21Z from: queued to: inprogress reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `inprogress`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: plan author: hare at: 2026-07-11T10:56:21Z -->
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
Replace meaningful concatenated Worker/Workdir ids with opaque runtime/local ids and composite backend registry keys. Preserve display labels separately, migrate legacy registry/link rows, and validate server/runtime/frontend builds.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-07-11T11:25:17Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implemented opaque Worker/Workdir ids and removed Backend synthetic Worker PKs.
|
||||||
|
|
||||||
|
- Runtime generated Worker ids are now opaque fixed-width hex counters without the `worker-` prefix.
|
||||||
|
- Runtime and Backend generated Workdir ids are now opaque timestamp+sequence hex ids without `backend-`, `alloc-`, repository id, or Runtime/Worker-derived components.
|
||||||
|
- Workspace Backend worker registry now uses `(workspace_id, runtime_id, runtime_worker_id)` as the primary key.
|
||||||
|
- Worker/Workdir links now reference the same composite worker key instead of `{runtime_id}/{worker_id}` synthetic strings.
|
||||||
|
- Added schema v5 migration from the legacy synthetic `worker_id` registry/link tables.
|
||||||
|
- Updated cleanup, list, sync, retention, and tests to use composite keys and keep public `WorkerSummary.worker_id` Runtime-local.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- cargo test -q -p yoi-workspace-server
|
||||||
|
- cargo test -q
|
||||||
|
- cd web/workspace && deno task check && deno task test
|
||||||
|
- git diff --check
|
||||||
|
- nix build .#yoi --no-link
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-07-11T11:25:17Z from: inprogress to: done reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `done`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-07-11T11:25:17Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-07-11T11:25:17Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
/tmp/yoi-id-implementation-report.md
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-07-11T12:05:24Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Follow-up: Worker ids are now numeric, not String-backed.
|
||||||
|
|
||||||
|
- `worker_runtime::WorkerId` is now a transparent `u64` newtype instead of `String`.
|
||||||
|
- Runtime HTTP JSON uses numeric Worker ids; URL path handling parses the path segment as `u64`.
|
||||||
|
- Workspace Backend stores `runtime_worker_id` as INTEGER in `worker_registry` and `worker_workdir_links` composite keys.
|
||||||
|
- Browser-facing Workspace `WorkerSummary.worker_id` remains a string route segment derived from the numeric id, so existing frontend route composition stays simple.
|
||||||
|
- Schema v5 migration converts legacy `worker-*` / numeric string worker ids into integer `runtime_worker_id` values before rebuilding registry/link tables.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -366,7 +366,7 @@ impl FsRuntimeStore {
|
||||||
fn worker_dir(&self, worker_id: &WorkerId) -> PathBuf {
|
fn worker_dir(&self, worker_id: &WorkerId) -> PathBuf {
|
||||||
self.runtime_dir
|
self.runtime_dir
|
||||||
.join(WORKERS_DIR)
|
.join(WORKERS_DIR)
|
||||||
.join(encoded_component(worker_id.as_str()))
|
.join(encoded_component(&worker_id.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
|
|
|
||||||
|
|
@ -692,11 +692,11 @@ async fn cancel_worker(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_ref_for(runtime: &Runtime, worker_id: String) -> Result<WorkerRef, RuntimeHttpRestError> {
|
fn worker_ref_for(runtime: &Runtime, worker_id: String) -> Result<WorkerRef, RuntimeHttpRestError> {
|
||||||
let worker_id = WorkerId::new(worker_id).ok_or_else(|| {
|
let worker_id = WorkerId::parse(&worker_id).ok_or_else(|| {
|
||||||
RuntimeHttpRestError::new(
|
RuntimeHttpRestError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"invalid_worker_id",
|
"invalid_worker_id",
|
||||||
"worker_id must not be empty",
|
"worker_id must be an unsigned integer",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let runtime_id = runtime
|
let runtime_id = runtime
|
||||||
|
|
@ -1107,12 +1107,12 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_errors_use_typed_rest_error_shape() {
|
async fn runtime_errors_use_typed_rest_error_shape() {
|
||||||
let app = runtime_http_router(Runtime::new_memory(), None);
|
let app = runtime_http_router(Runtime::new_memory(), None);
|
||||||
let response = empty_request(app, Method::GET, "/v1/workers/worker-missing").await;
|
let response = empty_request(app, Method::GET, "/v1/workers/999").await;
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
let error: RuntimeHttpErrorResponse = read_json(response).await;
|
let error: RuntimeHttpErrorResponse = read_json(response).await;
|
||||||
assert_eq!(error.error.code, "worker_not_found");
|
assert_eq!(error.error.code, "worker_not_found");
|
||||||
assert!(error.error.message.contains("worker-missing"));
|
assert!(error.error.message.contains("999"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,26 +36,25 @@ impl fmt::Display for RuntimeId {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runtime-local Worker identity.
|
/// Runtime-local Worker identity.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct WorkerId(String);
|
pub struct WorkerId(u64);
|
||||||
|
|
||||||
impl WorkerId {
|
impl WorkerId {
|
||||||
pub fn new(value: impl Into<String>) -> Option<Self> {
|
pub fn new(value: u64) -> Self {
|
||||||
let value = value.into();
|
Self(value)
|
||||||
if value.trim().is_empty() {
|
}
|
||||||
None
|
|
||||||
} else {
|
pub fn parse(value: &str) -> Option<Self> {
|
||||||
Some(Self(value))
|
value.parse::<u64>().ok().map(Self)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn generated(sequence: u64) -> Self {
|
pub(crate) fn generated(sequence: u64) -> Self {
|
||||||
Self(format!("worker-{sequence:08x}"))
|
Self(sequence)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_u64(&self) -> u64 {
|
||||||
&self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ pub fn build_profile_source_archive_fetch_request(
|
||||||
BackendResourceFetchRequest {
|
BackendResourceFetchRequest {
|
||||||
handle,
|
handle,
|
||||||
runtime_id: runtime_id.as_str().to_string(),
|
runtime_id: runtime_id.as_str().to_string(),
|
||||||
worker_id: worker_id.map(|id| id.as_str().to_string()),
|
worker_id: worker_id.map(|id| id.to_string()),
|
||||||
audit_correlation_id,
|
audit_correlation_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1122,13 +1122,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_worker_name_is_namespaced_by_runtime_id() {
|
fn runtime_worker_name_is_namespaced_by_runtime_id() {
|
||||||
let runtime_id = RuntimeId::new("arc:remote".to_string()).unwrap();
|
let runtime_id = RuntimeId::new("arc:remote".to_string()).unwrap();
|
||||||
let worker_ref = crate::identity::WorkerRef::new(
|
let worker_ref =
|
||||||
runtime_id,
|
crate::identity::WorkerRef::new(runtime_id, crate::identity::WorkerId::new(1));
|
||||||
crate::identity::WorkerId::new("worker-00000001".to_string()).unwrap(),
|
|
||||||
);
|
|
||||||
let request = WorkerExecutionSpawnRequest {
|
let request = WorkerExecutionSpawnRequest {
|
||||||
worker_ref: worker_ref.clone(),
|
worker_ref: worker_ref.clone(),
|
||||||
request: create_request("worker-00000001"),
|
request: create_request("1"),
|
||||||
context: WorkerExecutionContext::new(worker_ref, Arc::new(|_, _| panic!("unused"))),
|
context: WorkerExecutionContext::new(worker_ref, Arc::new(|_, _| panic!("unused"))),
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
config_bundle: None,
|
config_bundle: None,
|
||||||
|
|
@ -1136,11 +1134,11 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ProfileRuntimeWorkerFactory::runtime_worker_name(&request),
|
ProfileRuntimeWorkerFactory::runtime_worker_name(&request),
|
||||||
"runtime-arc-remote-worker-00000001"
|
"runtime-arc-remote-1"
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
ProfileRuntimeWorkerFactory::runtime_worker_name(&request),
|
ProfileRuntimeWorkerFactory::runtime_worker_name(&request),
|
||||||
"worker-00000001"
|
"00000001"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,13 +167,8 @@ impl LocalGitWorktreeMaterializer {
|
||||||
&self.runtime_root
|
&self.runtime_root
|
||||||
}
|
}
|
||||||
|
|
||||||
fn working_directory_id(worker_ref: &WorkerRef, repository_id: &str) -> String {
|
fn working_directory_id(_worker_ref: &WorkerRef, repository_id: &str) -> String {
|
||||||
format!(
|
next_working_directory_id(repository_id)
|
||||||
"{}-{}-{}",
|
|
||||||
sanitize_path_component(worker_ref.runtime_id.as_str()),
|
|
||||||
sanitize_path_component(worker_ref.worker_id.as_str()),
|
|
||||||
sanitize_path_component(repository_id)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn working_directory_root(&self, working_directory_id: &str) -> PathBuf {
|
fn working_directory_root(&self, working_directory_id: &str) -> PathBuf {
|
||||||
|
|
@ -609,16 +604,13 @@ fn sanitize_path_component(value: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_working_directory_id(repository_id: &str) -> String {
|
fn next_working_directory_id(_repository_id: &str) -> String {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|duration| duration.as_millis())
|
.map(|duration| duration.as_millis() as u64)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let sequence = NEXT_WORKING_DIRECTORY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
let sequence = NEXT_WORKING_DIRECTORY_SEQUENCE.fetch_add(1, Ordering::Relaxed) & 0x00ff_ffff;
|
||||||
format!(
|
format!("{now:013x}{sequence:06x}")
|
||||||
"alloc-{now}-{sequence}-{}",
|
|
||||||
sanitize_path_component(repository_id)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_working_directory_id(
|
fn validate_working_directory_id(
|
||||||
|
|
|
||||||
|
|
@ -1158,7 +1158,7 @@ impl EmbeddedWorkerRuntime {
|
||||||
fn worker_ref(&self, worker_id: &str) -> Option<EmbeddedWorkerRef> {
|
fn worker_ref(&self, worker_id: &str) -> Option<EmbeddedWorkerRef> {
|
||||||
Some(EmbeddedWorkerRef::new(
|
Some(EmbeddedWorkerRef::new(
|
||||||
EmbeddedRuntimeId::new(self.runtime_id.clone())?,
|
EmbeddedRuntimeId::new(self.runtime_id.clone())?,
|
||||||
EmbeddedWorkerId::new(worker_id.to_string())?,
|
EmbeddedWorkerId::parse(worker_id)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1181,9 +1181,9 @@ impl EmbeddedWorkerRuntime {
|
||||||
fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary {
|
fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary {
|
||||||
WorkerSummary {
|
WorkerSummary {
|
||||||
runtime_id: self.runtime_id.clone(),
|
runtime_id: self.runtime_id.clone(),
|
||||||
worker_id: summary.worker_ref.worker_id.as_str().to_string(),
|
worker_id: summary.worker_ref.worker_id.to_string(),
|
||||||
host_id: self.host_id.clone(),
|
host_id: self.host_id.clone(),
|
||||||
label: safe_display_hint(summary.worker_ref.worker_id.as_str()),
|
label: safe_display_hint(&summary.worker_ref.worker_id.to_string()),
|
||||||
role: embedded_profile_label(&summary.profile),
|
role: embedded_profile_label(&summary.profile),
|
||||||
profile: embedded_profile_label(&summary.profile),
|
profile: embedded_profile_label(&summary.profile),
|
||||||
workspace: WorkerWorkspaceSummary {
|
workspace: WorkerWorkspaceSummary {
|
||||||
|
|
@ -1217,9 +1217,9 @@ impl EmbeddedWorkerRuntime {
|
||||||
fn map_worker_detail(&self, detail: EmbeddedWorkerDetail) -> WorkerSummary {
|
fn map_worker_detail(&self, detail: EmbeddedWorkerDetail) -> WorkerSummary {
|
||||||
WorkerSummary {
|
WorkerSummary {
|
||||||
runtime_id: self.runtime_id.clone(),
|
runtime_id: self.runtime_id.clone(),
|
||||||
worker_id: detail.worker_id.as_str().to_string(),
|
worker_id: detail.worker_id.to_string(),
|
||||||
host_id: self.host_id.clone(),
|
host_id: self.host_id.clone(),
|
||||||
label: safe_display_hint(detail.worker_id.as_str()),
|
label: safe_display_hint(&detail.worker_id.to_string()),
|
||||||
role: embedded_profile_label(&detail.profile),
|
role: embedded_profile_label(&detail.profile),
|
||||||
profile: embedded_profile_label(&detail.profile),
|
profile: embedded_profile_label(&detail.profile),
|
||||||
workspace: WorkerWorkspaceSummary {
|
workspace: WorkerWorkspaceSummary {
|
||||||
|
|
@ -1919,9 +1919,9 @@ impl RemoteWorkerRuntime {
|
||||||
fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary {
|
fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary {
|
||||||
WorkerSummary {
|
WorkerSummary {
|
||||||
runtime_id: self.runtime_id.clone(),
|
runtime_id: self.runtime_id.clone(),
|
||||||
worker_id: summary.worker_ref.worker_id.as_str().to_string(),
|
worker_id: summary.worker_ref.worker_id.to_string(),
|
||||||
host_id: self.host_id.clone(),
|
host_id: self.host_id.clone(),
|
||||||
label: safe_display_hint(summary.worker_ref.worker_id.as_str()),
|
label: safe_display_hint(&summary.worker_ref.worker_id.to_string()),
|
||||||
role: None,
|
role: None,
|
||||||
profile: embedded_profile_label(&summary.profile),
|
profile: embedded_profile_label(&summary.profile),
|
||||||
workspace: WorkerWorkspaceSummary {
|
workspace: WorkerWorkspaceSummary {
|
||||||
|
|
@ -1954,9 +1954,9 @@ impl RemoteWorkerRuntime {
|
||||||
fn map_worker_detail(&self, detail: EmbeddedWorkerDetail) -> WorkerSummary {
|
fn map_worker_detail(&self, detail: EmbeddedWorkerDetail) -> WorkerSummary {
|
||||||
WorkerSummary {
|
WorkerSummary {
|
||||||
runtime_id: self.runtime_id.clone(),
|
runtime_id: self.runtime_id.clone(),
|
||||||
worker_id: detail.worker_id.as_str().to_string(),
|
worker_id: detail.worker_id.to_string(),
|
||||||
host_id: self.host_id.clone(),
|
host_id: self.host_id.clone(),
|
||||||
label: safe_display_hint(detail.worker_id.as_str()),
|
label: safe_display_hint(&detail.worker_id.to_string()),
|
||||||
role: None,
|
role: None,
|
||||||
profile: embedded_profile_label(&detail.profile),
|
profile: embedded_profile_label(&detail.profile),
|
||||||
workspace: WorkerWorkspaceSummary {
|
workspace: WorkerWorkspaceSummary {
|
||||||
|
|
@ -3562,18 +3562,18 @@ mod tests {
|
||||||
Error::UnknownRuntime(runtime_id) if runtime_id == "runtime-missing"
|
Error::UnknownRuntime(runtime_id) if runtime_id == "runtime-missing"
|
||||||
));
|
));
|
||||||
|
|
||||||
let unknown_worker = registry.worker("runtime-a", "worker-missing").unwrap_err();
|
let unknown_worker = registry.worker("runtime-a", "999").unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unknown_worker,
|
unknown_worker,
|
||||||
RuntimeRegistryError::UnknownWorker {
|
RuntimeRegistryError::UnknownWorker {
|
||||||
runtime_id: "runtime-a".to_string(),
|
runtime_id: "runtime-a".to_string(),
|
||||||
worker_id: "worker-missing".to_string(),
|
worker_id: "999".to_string(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
unknown_worker.into_error(),
|
unknown_worker.into_error(),
|
||||||
Error::UnknownWorker { runtime_id, worker_id }
|
Error::UnknownWorker { runtime_id, worker_id }
|
||||||
if runtime_id == "runtime-a" && worker_id == "worker-missing"
|
if runtime_id == "runtime-a" && worker_id == "999"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3881,7 +3881,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_runtime_registry_routes_commands_without_browser_secret_leaks() {
|
fn remote_runtime_registry_routes_commands_without_browser_secret_leaks() {
|
||||||
let worker_json = worker_json("remote:primary", "worker-remote-1");
|
let worker_json = worker_json("remote:primary", "1");
|
||||||
let (base_url, server) = serve_mock_http(vec![
|
let (base_url, server) = serve_mock_http(vec![
|
||||||
mock_response(
|
mock_response(
|
||||||
"GET",
|
"GET",
|
||||||
|
|
@ -3892,19 +3892,19 @@ mod tests {
|
||||||
),
|
),
|
||||||
mock_response(
|
mock_response(
|
||||||
"GET",
|
"GET",
|
||||||
"/v1/workers/worker-remote-1",
|
"/v1/workers/1",
|
||||||
true,
|
true,
|
||||||
200,
|
200,
|
||||||
json!({ "worker": worker_json.clone() }).to_string(),
|
json!({ "worker": worker_json.clone() }).to_string(),
|
||||||
),
|
),
|
||||||
mock_response(
|
mock_response(
|
||||||
"POST",
|
"POST",
|
||||||
"/v1/workers/worker-remote-1/input",
|
"/v1/workers/1/input",
|
||||||
true,
|
true,
|
||||||
200,
|
200,
|
||||||
json!({
|
json!({
|
||||||
"ack": {
|
"ack": {
|
||||||
"worker_ref": { "runtime_id": "remote:primary", "worker_id": "worker-remote-1" },
|
"worker_ref": { "runtime_id": "remote:primary", "worker_id": 1 },
|
||||||
"status": "running",
|
"status": "running",
|
||||||
"event_id": 8
|
"event_id": 8
|
||||||
}
|
}
|
||||||
|
|
@ -3929,24 +3929,20 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let observation = registry
|
let observation = registry
|
||||||
.observation_source("remote:primary", "worker-remote-1")
|
.observation_source("remote:primary", "1")
|
||||||
.expect("remote runtime exposes backend-owned WS observation source");
|
.expect("remote runtime exposes backend-owned WS observation source");
|
||||||
let crate::observation::RuntimeObservationSource::RemoteWs(observation) = observation
|
let crate::observation::RuntimeObservationSource::RemoteWs(observation) = observation
|
||||||
else {
|
else {
|
||||||
panic!("remote runtime should expose a remote WS observation source");
|
panic!("remote runtime should expose a remote WS observation source");
|
||||||
};
|
};
|
||||||
assert!(observation.endpoint.starts_with("ws://127.0.0.1:"));
|
assert!(observation.endpoint.starts_with("ws://127.0.0.1:"));
|
||||||
assert!(
|
assert!(observation.endpoint.ends_with("/v1/workers/1/events/ws"));
|
||||||
observation
|
|
||||||
.endpoint
|
|
||||||
.ends_with("/v1/workers/worker-remote-1/events/ws")
|
|
||||||
);
|
|
||||||
assert_eq!(observation.bearer_token.as_deref(), Some(secret.as_str()));
|
assert_eq!(observation.bearer_token.as_deref(), Some(secret.as_str()));
|
||||||
|
|
||||||
let workers = registry.list_workers(10);
|
let workers = registry.list_workers(10);
|
||||||
assert_eq!(workers.items.len(), 1);
|
assert_eq!(workers.items.len(), 1);
|
||||||
assert_eq!(workers.items[0].runtime_id, "remote:primary");
|
assert_eq!(workers.items[0].runtime_id, "remote:primary");
|
||||||
assert_eq!(workers.items[0].worker_id, "worker-remote-1");
|
assert_eq!(workers.items[0].worker_id, "1");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workers.items[0].implementation.kind,
|
workers.items[0].implementation.kind,
|
||||||
"remote_worker_runtime"
|
"remote_worker_runtime"
|
||||||
|
|
@ -3961,7 +3957,7 @@ mod tests {
|
||||||
let input = registry
|
let input = registry
|
||||||
.send_input(
|
.send_input(
|
||||||
"remote:primary",
|
"remote:primary",
|
||||||
"worker-remote-1",
|
"1",
|
||||||
WorkerInputRequest {
|
WorkerInputRequest {
|
||||||
kind: WorkerInputKind::User,
|
kind: WorkerInputKind::User,
|
||||||
content: "hello remote".to_string(),
|
content: "hello remote".to_string(),
|
||||||
|
|
@ -3997,28 +3993,28 @@ mod tests {
|
||||||
"workers": [
|
"workers": [
|
||||||
worker_json_with_execution(
|
worker_json_with_execution(
|
||||||
"remote:primary",
|
"remote:primary",
|
||||||
"worker-stale",
|
"1",
|
||||||
"stale",
|
"stale",
|
||||||
"unconnected",
|
"unconnected",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
worker_json_with_execution(
|
worker_json_with_execution(
|
||||||
"remote:primary",
|
"remote:primary",
|
||||||
"worker-unconnected",
|
"2",
|
||||||
"unconnected",
|
"unconnected",
|
||||||
"unconnected",
|
"unconnected",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
worker_json_with_execution(
|
worker_json_with_execution(
|
||||||
"remote:primary",
|
"remote:primary",
|
||||||
"worker-rejected",
|
"3",
|
||||||
"connected",
|
"connected",
|
||||||
"rejected",
|
"rejected",
|
||||||
Some("rejected"),
|
Some("rejected"),
|
||||||
),
|
),
|
||||||
worker_json_with_execution(
|
worker_json_with_execution(
|
||||||
"remote:primary",
|
"remote:primary",
|
||||||
"worker-errored",
|
"4",
|
||||||
"connected",
|
"connected",
|
||||||
"errored",
|
"errored",
|
||||||
Some("errored"),
|
Some("errored"),
|
||||||
|
|
@ -4029,13 +4025,13 @@ mod tests {
|
||||||
),
|
),
|
||||||
mock_response(
|
mock_response(
|
||||||
"GET",
|
"GET",
|
||||||
"/v1/workers/worker-stale",
|
"/v1/workers/1",
|
||||||
true,
|
true,
|
||||||
200,
|
200,
|
||||||
json!({
|
json!({
|
||||||
"worker": worker_json_with_execution(
|
"worker": worker_json_with_execution(
|
||||||
"remote:primary",
|
"remote:primary",
|
||||||
"worker-stale",
|
"1",
|
||||||
"stale",
|
"stale",
|
||||||
"unconnected",
|
"unconnected",
|
||||||
None,
|
None,
|
||||||
|
|
@ -4076,7 +4072,7 @@ mod tests {
|
||||||
assert_eq!(workers.items[2].state, "rejected");
|
assert_eq!(workers.items[2].state, "rejected");
|
||||||
assert_eq!(workers.items[3].state, "errored");
|
assert_eq!(workers.items[3].state, "errored");
|
||||||
|
|
||||||
let stale_detail = registry.worker("remote:primary", "worker-stale").unwrap();
|
let stale_detail = registry.worker("remote:primary", "1").unwrap();
|
||||||
assert!(!stale_detail.capabilities.can_accept_input);
|
assert!(!stale_detail.capabilities.can_accept_input);
|
||||||
assert!(!stale_detail.capabilities.can_stop);
|
assert!(!stale_detail.capabilities.can_stop);
|
||||||
assert_eq!(stale_detail.state, "stale");
|
assert_eq!(stale_detail.state, "stale");
|
||||||
|
|
@ -4162,7 +4158,7 @@ mod tests {
|
||||||
fn remote_runtime_auth_errors_map_to_typed_backend_error() {
|
fn remote_runtime_auth_errors_map_to_typed_backend_error() {
|
||||||
let (base_url, server) = serve_mock_http(vec![mock_response(
|
let (base_url, server) = serve_mock_http(vec![mock_response(
|
||||||
"GET",
|
"GET",
|
||||||
"/v1/workers/worker-missing",
|
"/v1/workers/999",
|
||||||
true,
|
true,
|
||||||
401,
|
401,
|
||||||
json!({ "error": { "code": "unauthorized", "message": "bad token" } }).to_string(),
|
json!({ "error": { "code": "unauthorized", "message": "bad token" } }).to_string(),
|
||||||
|
|
@ -4183,7 +4179,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let error = registry
|
let error = registry
|
||||||
.worker("remote:primary", "worker-missing")
|
.worker("remote:primary", "999")
|
||||||
.expect_err("auth failure is a backend operation error");
|
.expect_err("auth failure is a backend operation error");
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
error,
|
error,
|
||||||
|
|
@ -4278,6 +4274,7 @@ mod tests {
|
||||||
"message": format!("{outcome} result")
|
"message": format!("{outcome} result")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
let worker_id = worker_id.parse::<u64>().unwrap();
|
||||||
json!({
|
json!({
|
||||||
"worker_ref": { "runtime_id": runtime_id, "worker_id": worker_id },
|
"worker_ref": { "runtime_id": runtime_id, "worker_id": worker_id },
|
||||||
"runtime_id": runtime_id,
|
"runtime_id": runtime_id,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ impl BackendResourceBroker {
|
||||||
workspace_id: workspace_id.clone(),
|
workspace_id: workspace_id.clone(),
|
||||||
scope_id: Some("workspace-profile-source".to_string()),
|
scope_id: Some("workspace-profile-source".to_string()),
|
||||||
runtime_id: runtime_id.map(|id| id.as_str().to_string()),
|
runtime_id: runtime_id.map(|id| id.as_str().to_string()),
|
||||||
worker_id: worker_id.map(|id| id.as_str().to_string()),
|
worker_id: worker_id.map(|id| id.to_string()),
|
||||||
resource_id: archive.reference.id.clone(),
|
resource_id: archive.reference.id.clone(),
|
||||||
digest: archive.reference.digest.clone(),
|
digest: archive.reference.digest.clone(),
|
||||||
operation: BackendResourceOperation::FetchArchive,
|
operation: BackendResourceOperation::FetchArchive,
|
||||||
|
|
@ -58,7 +58,7 @@ impl BackendResourceBroker {
|
||||||
};
|
};
|
||||||
let stored = StoredResource {
|
let stored = StoredResource {
|
||||||
runtime_id: runtime_id.map(|id| id.as_str().to_string()),
|
runtime_id: runtime_id.map(|id| id.as_str().to_string()),
|
||||||
worker_id: worker_id.map(|id| id.as_str().to_string()),
|
worker_id: worker_id.map(|id| id.to_string()),
|
||||||
handle: handle.clone(),
|
handle: handle.clone(),
|
||||||
archive,
|
archive,
|
||||||
};
|
};
|
||||||
|
|
@ -221,7 +221,7 @@ mod tests {
|
||||||
audit_correlation_id: handle.audit_correlation_id.clone(),
|
audit_correlation_id: handle.audit_correlation_id.clone(),
|
||||||
handle,
|
handle,
|
||||||
runtime_id: runtime_id.as_str().to_string(),
|
runtime_id: runtime_id.as_str().to_string(),
|
||||||
worker_id: worker_id.map(|id| id.as_str().to_string()),
|
worker_id: worker_id.map(|id| id.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,8 +271,8 @@ mod tests {
|
||||||
fn broker_rejects_worker_mismatch() {
|
fn broker_rejects_worker_mismatch() {
|
||||||
let broker = BackendResourceBroker::default();
|
let broker = BackendResourceBroker::default();
|
||||||
let runtime_id = RuntimeId::new("runtime-test").unwrap();
|
let runtime_id = RuntimeId::new("runtime-test").unwrap();
|
||||||
let worker_a = WorkerId::new("worker-a").unwrap();
|
let worker_a = WorkerId::new(1);
|
||||||
let worker_b = WorkerId::new("worker-b").unwrap();
|
let worker_b = WorkerId::new(2);
|
||||||
let handle = broker.issue_profile_source_archive_handle(
|
let handle = broker.issue_profile_source_archive_handle(
|
||||||
"workspace-test",
|
"workspace-test",
|
||||||
Some(&runtime_id),
|
Some(&runtime_id),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
||||||
use axum::extract::{Path as AxumPath, Query, State};
|
use axum::extract::{Path as AxumPath, Query, State};
|
||||||
|
|
@ -1539,10 +1540,14 @@ async fn set_worker_retention(
|
||||||
runtime_worker_id: String,
|
runtime_worker_id: String,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
) -> ApiResult<Json<WorkerRetentionResponse>> {
|
) -> ApiResult<Json<WorkerRetentionResponse>> {
|
||||||
let worker_id = backend_worker_id(runtime_id.as_str(), runtime_worker_id.as_str());
|
let runtime_worker_registry_id = parse_runtime_worker_id_for_registry(&runtime_worker_id)?;
|
||||||
if api
|
if api
|
||||||
.store
|
.store
|
||||||
.get_worker_registry(&api.config.workspace_id, worker_id.as_str())?
|
.get_worker_registry(
|
||||||
|
&api.config.workspace_id,
|
||||||
|
runtime_id.as_str(),
|
||||||
|
runtime_worker_registry_id,
|
||||||
|
)?
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
if let Ok(worker) = api
|
if let Ok(worker) = api
|
||||||
|
|
@ -1555,7 +1560,8 @@ async fn set_worker_retention(
|
||||||
let retention_state = if pinned { "pinned" } else { "normal" };
|
let retention_state = if pinned { "pinned" } else { "normal" };
|
||||||
let changed = api.store.update_worker_retention(
|
let changed = api.store.update_worker_retention(
|
||||||
&api.config.workspace_id,
|
&api.config.workspace_id,
|
||||||
worker_id.as_str(),
|
runtime_id.as_str(),
|
||||||
|
runtime_worker_registry_id,
|
||||||
retention_state,
|
retention_state,
|
||||||
now_registry_timestamp().as_str(),
|
now_registry_timestamp().as_str(),
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -1580,11 +1586,15 @@ fn build_runtime_cleanup_plan(
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
) -> ApiResult<RuntimeCleanupPlanResponse> {
|
) -> ApiResult<RuntimeCleanupPlanResponse> {
|
||||||
let workers = workers_response(api.clone())?;
|
let workers = workers_response(api.clone())?;
|
||||||
let live_running_worker_ids: HashSet<String> = workers
|
let live_running_worker_ids: HashSet<(String, u64)> = workers
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|worker| worker.state == "running")
|
.filter(|worker| worker.state == "running")
|
||||||
.map(|worker| backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str()))
|
.filter_map(|worker| {
|
||||||
|
parse_runtime_worker_id_for_registry(worker.worker_id.as_str())
|
||||||
|
.ok()
|
||||||
|
.map(|worker_id| (worker.runtime_id.clone(), worker_id))
|
||||||
|
})
|
||||||
.collect();
|
.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) {
|
||||||
|
|
@ -1609,7 +1619,12 @@ fn build_runtime_cleanup_plan(
|
||||||
.list_worker_registry(&api.config.workspace_id, 500)?;
|
.list_worker_registry(&api.config.workspace_id, 500)?;
|
||||||
let worker_by_id: HashMap<_, _> = worker_records
|
let worker_by_id: HashMap<_, _> = worker_records
|
||||||
.iter()
|
.iter()
|
||||||
.map(|record| (record.worker_id.clone(), record.clone()))
|
.map(|record| {
|
||||||
|
(
|
||||||
|
(record.runtime_id.clone(), record.runtime_worker_id.clone()),
|
||||||
|
record.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let observed_workdirs: HashMap<_, _> = workdir_summaries
|
let observed_workdirs: HashMap<_, _> = workdir_summaries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -1621,10 +1636,13 @@ fn build_runtime_cleanup_plan(
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|record| record.runtime_id == runtime_id)
|
.filter(|record| record.runtime_id == runtime_id)
|
||||||
{
|
{
|
||||||
let links = api
|
let links = api.store.list_worker_workdir_links(
|
||||||
.store
|
&api.config.workspace_id,
|
||||||
.list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?;
|
record.runtime_id.as_str(),
|
||||||
let is_running = live_running_worker_ids.contains(&record.worker_id);
|
record.runtime_worker_id,
|
||||||
|
)?;
|
||||||
|
let is_running = live_running_worker_ids
|
||||||
|
.contains(&(record.runtime_id.clone(), record.runtime_worker_id.clone()));
|
||||||
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())
|
||||||
|
|
@ -1634,10 +1652,14 @@ fn build_runtime_cleanup_plan(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
worker_candidates.push(CleanupWorkerCandidate {
|
worker_candidates.push(CleanupWorkerCandidate {
|
||||||
target_id: format!("worker:{}", record.worker_id),
|
target_id: format!(
|
||||||
|
"worker:{}:{}",
|
||||||
|
encode_path_segment(record.runtime_id.as_str()),
|
||||||
|
encode_path_segment(&record.runtime_worker_id.to_string())
|
||||||
|
),
|
||||||
action: CleanupTargetKind::WorkerDelete,
|
action: CleanupTargetKind::WorkerDelete,
|
||||||
worker_id: record.worker_id.clone(),
|
worker_id: record.runtime_worker_id.to_string(),
|
||||||
runtime_worker_id: record.runtime_worker_id.clone(),
|
runtime_worker_id: record.runtime_worker_id.to_string(),
|
||||||
runtime_id: record.runtime_id.clone(),
|
runtime_id: record.runtime_id.clone(),
|
||||||
reason: if blocking_reason.is_some() {
|
reason: if blocking_reason.is_some() {
|
||||||
"Worker registry row cannot be deleted until blocking conditions are cleared"
|
"Worker registry row cannot be deleted until blocking conditions are cleared"
|
||||||
|
|
@ -1664,16 +1686,21 @@ fn build_runtime_cleanup_plan(
|
||||||
.list_workdir_worker_links(&api.config.workspace_id, record.workdir_id.as_str())?;
|
.list_workdir_worker_links(&api.config.workspace_id, record.workdir_id.as_str())?;
|
||||||
let linked_workers = links
|
let linked_workers = links
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|link| worker_by_id.get(link.worker_id.as_str()))
|
.filter_map(|link| {
|
||||||
|
worker_by_id.get(&(link.runtime_id.clone(), link.runtime_worker_id.clone()))
|
||||||
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let linked_worker_ids = links
|
let linked_worker_ids = links
|
||||||
.iter()
|
.iter()
|
||||||
.map(|link| link.worker_id.clone())
|
.map(|link| link.runtime_worker_id.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let linked_running_worker_ids = linked_workers
|
let linked_running_worker_ids = linked_workers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|worker| live_running_worker_ids.contains(&worker.worker_id))
|
.filter(|worker| {
|
||||||
.map(|worker| worker.worker_id.clone())
|
live_running_worker_ids
|
||||||
|
.contains(&(worker.runtime_id.clone(), worker.runtime_worker_id.clone()))
|
||||||
|
})
|
||||||
|
.map(|worker| worker.runtime_worker_id.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let pinned_linked = linked_workers
|
let pinned_linked = linked_workers
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1812,8 +1839,12 @@ fn execute_runtime_cleanup(
|
||||||
"pinned Worker/history cannot be deleted",
|
"pinned Worker/history cannot be deleted",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
api.store
|
let runtime_worker_id = parse_runtime_worker_id_for_registry(&candidate.runtime_worker_id)?;
|
||||||
.delete_worker_registry(&api.config.workspace_id, candidate.worker_id.as_str())?;
|
api.store.delete_worker_registry(
|
||||||
|
&api.config.workspace_id,
|
||||||
|
candidate.runtime_id.as_str(),
|
||||||
|
runtime_worker_id,
|
||||||
|
)?;
|
||||||
results.push(RuntimeCleanupExecutionResult {
|
results.push(RuntimeCleanupExecutionResult {
|
||||||
target_id: candidate.target_id.clone(),
|
target_id: candidate.target_id.clone(),
|
||||||
action: candidate.action.clone(),
|
action: candidate.action.clone(),
|
||||||
|
|
@ -2697,7 +2728,11 @@ async fn create_workspace_worker(
|
||||||
management_kind.as_str(),
|
management_kind.as_str(),
|
||||||
);
|
);
|
||||||
api.store.upsert_workdir_registry(&workdir_record)?;
|
api.store.upsert_workdir_registry(&workdir_record)?;
|
||||||
link_worker_to_workdir(&api, &worker_record, &working_directory.working_directory_id)?;
|
link_worker_to_workdir(
|
||||||
|
&api,
|
||||||
|
&worker_record,
|
||||||
|
&working_directory.working_directory_id,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
if let Some(workdir_id) = selected_working_directory_id.as_deref() {
|
if let Some(workdir_id) = selected_working_directory_id.as_deref() {
|
||||||
if api
|
if api
|
||||||
|
|
@ -2813,9 +2848,11 @@ async fn get_runtime_worker(
|
||||||
.worker(&runtime_id, &worker_id)
|
.worker(&runtime_id, &worker_id)
|
||||||
.map_err(|err| err.into_error())?;
|
.map_err(|err| err.into_error())?;
|
||||||
let record = sync_worker_observation(&api, &worker)?;
|
let record = sync_worker_observation(&api, &worker)?;
|
||||||
let links = api
|
let links = api.store.list_worker_workdir_links(
|
||||||
.store
|
&api.config.workspace_id,
|
||||||
.list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?;
|
record.runtime_id.as_str(),
|
||||||
|
record.runtime_worker_id,
|
||||||
|
)?;
|
||||||
let workdirs = api
|
let workdirs = api
|
||||||
.store
|
.store
|
||||||
.list_workdir_registry(&api.config.workspace_id, 500)?;
|
.list_workdir_registry(&api.config.workspace_id, 500)?;
|
||||||
|
|
@ -2981,10 +3018,10 @@ async fn stop_runtime_worker(
|
||||||
.runtime
|
.runtime
|
||||||
.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 runtime_worker_id = parse_runtime_worker_id_for_registry(&worker_id)?;
|
||||||
if let Some(record) = api
|
if let Some(record) =
|
||||||
.store
|
api.store
|
||||||
.get_worker_registry(&api.config.workspace_id, backend_id.as_str())?
|
.get_worker_registry(&api.config.workspace_id, &runtime_id, runtime_worker_id)?
|
||||||
{
|
{
|
||||||
sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?;
|
sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?;
|
||||||
}
|
}
|
||||||
|
|
@ -3137,10 +3174,14 @@ fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSu
|
||||||
let mut observed = std::collections::BTreeMap::new();
|
let mut observed = std::collections::BTreeMap::new();
|
||||||
for worker in &runtime_workers.items {
|
for worker in &runtime_workers.items {
|
||||||
let _ = sync_worker_observation(&api, worker);
|
let _ = sync_worker_observation(&api, worker);
|
||||||
observed.insert(
|
if let Ok(runtime_worker_id) =
|
||||||
backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str()),
|
parse_runtime_worker_id_for_registry(worker.worker_id.as_str())
|
||||||
worker.clone(),
|
{
|
||||||
);
|
observed.insert(
|
||||||
|
(worker.runtime_id.clone(), runtime_worker_id),
|
||||||
|
worker.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let worker_records = api
|
let worker_records = api
|
||||||
.store
|
.store
|
||||||
|
|
@ -3150,11 +3191,13 @@ fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSu
|
||||||
.list_workdir_registry(&api.config.workspace_id, 500)?;
|
.list_workdir_registry(&api.config.workspace_id, 500)?;
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
for record in worker_records {
|
for record in worker_records {
|
||||||
let links = api
|
let links = api.store.list_worker_workdir_links(
|
||||||
.store
|
&api.config.workspace_id,
|
||||||
.list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?;
|
record.runtime_id.as_str(),
|
||||||
|
record.runtime_worker_id,
|
||||||
|
)?;
|
||||||
items.push(merge_worker_registry_projection(
|
items.push(merge_worker_registry_projection(
|
||||||
observed.get(record.worker_id.as_str()),
|
observed.get(&(record.runtime_id.clone(), record.runtime_worker_id.clone())),
|
||||||
&record,
|
&record,
|
||||||
links,
|
links,
|
||||||
&workdir_records,
|
&workdir_records,
|
||||||
|
|
@ -3518,7 +3561,7 @@ async fn test_remote_runtime_config(
|
||||||
if let Some(worker) = workers.as_ref().and_then(|workers| workers.workers.first()) {
|
if let Some(worker) = workers.as_ref().and_then(|workers| workers.workers.first()) {
|
||||||
let path = format!(
|
let path = format!(
|
||||||
"/v1/workers/{}",
|
"/v1/workers/{}",
|
||||||
encode_path_segment(worker.worker_id.as_str())
|
encode_path_segment(&worker.worker_id.to_string())
|
||||||
);
|
);
|
||||||
match remote_probe_url(remote, &path) {
|
match remote_probe_url(remote, &path) {
|
||||||
Ok(url) => match probe_remote_json(&client, url, "workers.detail", "Worker detail").await {
|
Ok(url) => match probe_remote_json(&client, url, "workers.detail", "Worker detail").await {
|
||||||
|
|
@ -3890,9 +3933,7 @@ fn runtime_working_directory_summaries(
|
||||||
Ok((items, diagnostics))
|
Ok((items, diagnostics))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn backend_worker_id(runtime_id: &str, runtime_worker_id: &str) -> String {
|
static BACKEND_WORKDIR_ID_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
format!("{runtime_id}/{runtime_worker_id}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn now_registry_timestamp() -> String {
|
fn now_registry_timestamp() -> String {
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
|
|
@ -3901,31 +3942,13 @@ fn now_registry_timestamp() -> String {
|
||||||
.unwrap_or_else(|_| "0".to_string())
|
.unwrap_or_else(|_| "0".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registry_safe_id_component(value: &str) -> String {
|
fn next_backend_workdir_id(_repository_id: &str) -> String {
|
||||||
let sanitized: String = value
|
let timestamp_ms = std::time::SystemTime::now()
|
||||||
.chars()
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|ch| {
|
.map(|duration| duration.as_millis() as u64)
|
||||||
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
|
.unwrap_or(0);
|
||||||
ch
|
let sequence = BACKEND_WORKDIR_ID_SEQUENCE.fetch_add(1, Ordering::Relaxed) & 0x00ff_ffff;
|
||||||
} else {
|
format!("{timestamp_ms:013x}{sequence:06x}")
|
||||||
'-'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
sanitized.trim_matches('-').chars().take(48).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_backend_workdir_id(repository_id: &str) -> String {
|
|
||||||
let repository = registry_safe_id_component(repository_id);
|
|
||||||
format!(
|
|
||||||
"backend-{}-{}",
|
|
||||||
now_registry_timestamp(),
|
|
||||||
if repository.is_empty() {
|
|
||||||
"workdir"
|
|
||||||
} else {
|
|
||||||
&repository
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -3942,10 +3965,12 @@ fn record_worker_summary(
|
||||||
display_name_policy: WorkerRegistryDisplayNamePolicy,
|
display_name_policy: WorkerRegistryDisplayNamePolicy,
|
||||||
) -> ApiResult<WorkerRegistryRecord> {
|
) -> ApiResult<WorkerRegistryRecord> {
|
||||||
let timestamp = now_registry_timestamp();
|
let timestamp = now_registry_timestamp();
|
||||||
let worker_id = backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str());
|
let runtime_worker_id = parse_runtime_worker_id_for_registry(worker.worker_id.as_str())?;
|
||||||
let existing = api
|
let existing = api.store.get_worker_registry(
|
||||||
.store
|
&api.config.workspace_id,
|
||||||
.get_worker_registry(&api.config.workspace_id, worker_id.as_str())?;
|
worker.runtime_id.as_str(),
|
||||||
|
runtime_worker_id,
|
||||||
|
)?;
|
||||||
let display_name = match (display_name_policy, existing.as_ref()) {
|
let display_name = match (display_name_policy, existing.as_ref()) {
|
||||||
(WorkerRegistryDisplayNamePolicy::PreserveExisting, Some(record)) => {
|
(WorkerRegistryDisplayNamePolicy::PreserveExisting, Some(record)) => {
|
||||||
record.display_name.clone()
|
record.display_name.clone()
|
||||||
|
|
@ -3954,9 +3979,8 @@ fn record_worker_summary(
|
||||||
};
|
};
|
||||||
let record = WorkerRegistryRecord {
|
let record = WorkerRegistryRecord {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
worker_id: worker_id.clone(),
|
|
||||||
runtime_id: worker.runtime_id.as_str().to_string(),
|
runtime_id: worker.runtime_id.as_str().to_string(),
|
||||||
runtime_worker_id: worker.worker_id.as_str().to_string(),
|
runtime_worker_id,
|
||||||
display_name,
|
display_name,
|
||||||
profile,
|
profile,
|
||||||
retention_state: existing
|
retention_state: existing
|
||||||
|
|
@ -3977,13 +4001,17 @@ fn record_worker_summary(
|
||||||
api.store.upsert_worker_registry(&record)?;
|
api.store.upsert_worker_registry(&record)?;
|
||||||
Ok(api
|
Ok(api
|
||||||
.store
|
.store
|
||||||
.get_worker_registry(&api.config.workspace_id, worker_id.as_str())?
|
.get_worker_registry(
|
||||||
|
&api.config.workspace_id,
|
||||||
|
worker.runtime_id.as_str(),
|
||||||
|
runtime_worker_id,
|
||||||
|
)?
|
||||||
.unwrap_or(record))
|
.unwrap_or(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary {
|
fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary {
|
||||||
WorkerSummary {
|
WorkerSummary {
|
||||||
worker_id: record.runtime_worker_id.clone(),
|
worker_id: record.runtime_worker_id.to_string(),
|
||||||
runtime_id: record.runtime_id.clone(),
|
runtime_id: record.runtime_id.clone(),
|
||||||
host_id: "backend-registry".to_string(),
|
host_id: "backend-registry".to_string(),
|
||||||
role: None,
|
role: None,
|
||||||
|
|
@ -4159,9 +4187,11 @@ fn sync_linked_workdir_after_worker_stop(
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
worker_record: &WorkerRegistryRecord,
|
worker_record: &WorkerRegistryRecord,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
let links = api
|
let links = api.store.list_worker_workdir_links(
|
||||||
.store
|
&api.config.workspace_id,
|
||||||
.list_worker_workdir_links(&api.config.workspace_id, worker_record.worker_id.as_str())?;
|
worker_record.runtime_id.as_str(),
|
||||||
|
worker_record.runtime_worker_id,
|
||||||
|
)?;
|
||||||
for link in links {
|
for link in links {
|
||||||
let result = api
|
let result = api
|
||||||
.runtime
|
.runtime
|
||||||
|
|
@ -4253,7 +4283,8 @@ fn link_worker_to_workdir(
|
||||||
api.store
|
api.store
|
||||||
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
|
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
worker_id: worker_record.worker_id.clone(),
|
runtime_id: worker_record.runtime_id.clone(),
|
||||||
|
runtime_worker_id: worker_record.runtime_worker_id.clone(),
|
||||||
workdir_id: workdir_id.to_string(),
|
workdir_id: workdir_id.to_string(),
|
||||||
role: "primary_cwd".to_string(),
|
role: "primary_cwd".to_string(),
|
||||||
linked_at: timestamp,
|
linked_at: timestamp,
|
||||||
|
|
@ -4380,6 +4411,15 @@ fn profile_selector_for_candidate_with_root(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_runtime_worker_id_for_registry(worker_id: &str) -> ApiResult<u64> {
|
||||||
|
worker_id.parse::<u64>().map_err(|_| {
|
||||||
|
settings_bad_request(
|
||||||
|
"workspace_worker_id_invalid",
|
||||||
|
"Runtime Worker id must be an unsigned integer",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn sanitize_worker_display_name(value: &str) -> Option<String> {
|
fn sanitize_worker_display_name(value: &str) -> Option<String> {
|
||||||
let display_name = value.trim();
|
let display_name = value.trim();
|
||||||
if display_name.chars().any(char::is_control) {
|
if display_name.chars().any(char::is_control) {
|
||||||
|
|
@ -4837,9 +4877,8 @@ mod tests {
|
||||||
fn backend_worker_projection_preserves_archive_rows_links_and_redacts_paths() {
|
fn backend_worker_projection_preserves_archive_rows_links_and_redacts_paths() {
|
||||||
let worker = WorkerRegistryRecord {
|
let worker = WorkerRegistryRecord {
|
||||||
workspace_id: "workspace-1".to_string(),
|
workspace_id: "workspace-1".to_string(),
|
||||||
worker_id: "embedded/worker-1".to_string(),
|
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
runtime_worker_id: "worker-1".to_string(),
|
runtime_worker_id: 1,
|
||||||
display_name: "Archived Worker".to_string(),
|
display_name: "Archived Worker".to_string(),
|
||||||
profile: Some("builtin:coder".to_string()),
|
profile: Some("builtin:coder".to_string()),
|
||||||
retention_state: "pinned".to_string(),
|
retention_state: "pinned".to_string(),
|
||||||
|
|
@ -4852,7 +4891,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
let workdir = WorkdirRegistryRecord {
|
let workdir = WorkdirRegistryRecord {
|
||||||
workspace_id: "workspace-1".to_string(),
|
workspace_id: "workspace-1".to_string(),
|
||||||
workdir_id: "backend-1-repo".to_string(),
|
workdir_id: "0000019a00000000000".to_string(),
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: Some("develop".to_string()),
|
selector: Some("develop".to_string()),
|
||||||
|
|
@ -4865,7 +4904,8 @@ mod tests {
|
||||||
};
|
};
|
||||||
let link = WorkerWorkdirLinkRecord {
|
let link = WorkerWorkdirLinkRecord {
|
||||||
workspace_id: "workspace-1".to_string(),
|
workspace_id: "workspace-1".to_string(),
|
||||||
worker_id: worker.worker_id.clone(),
|
runtime_id: worker.runtime_id.clone(),
|
||||||
|
runtime_worker_id: worker.runtime_worker_id.clone(),
|
||||||
workdir_id: workdir.workdir_id.clone(),
|
workdir_id: workdir.workdir_id.clone(),
|
||||||
role: "primary_cwd".to_string(),
|
role: "primary_cwd".to_string(),
|
||||||
linked_at: "4".to_string(),
|
linked_at: "4".to_string(),
|
||||||
|
|
@ -5423,17 +5463,15 @@ mod tests {
|
||||||
|
|
||||||
fn seed_cleanup_worker(
|
fn seed_cleanup_worker(
|
||||||
api: &WorkspaceApi,
|
api: &WorkspaceApi,
|
||||||
runtime_worker_id: &str,
|
runtime_worker_id: u64,
|
||||||
retention_state: &str,
|
retention_state: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let worker_id = backend_worker_id("runtime-test", runtime_worker_id);
|
|
||||||
let now = now_registry_timestamp();
|
let now = now_registry_timestamp();
|
||||||
api.store
|
api.store
|
||||||
.upsert_worker_registry(&WorkerRegistryRecord {
|
.upsert_worker_registry(&WorkerRegistryRecord {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
worker_id: worker_id.clone(),
|
|
||||||
runtime_id: "runtime-test".to_string(),
|
runtime_id: "runtime-test".to_string(),
|
||||||
runtime_worker_id: runtime_worker_id.to_string(),
|
runtime_worker_id,
|
||||||
display_name: runtime_worker_id.to_string(),
|
display_name: runtime_worker_id.to_string(),
|
||||||
profile: None,
|
profile: None,
|
||||||
retention_state: retention_state.to_string(),
|
retention_state: retention_state.to_string(),
|
||||||
|
|
@ -5445,7 +5483,7 @@ mod tests {
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
worker_id
|
runtime_worker_id.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seed_cleanup_workdir(api: &WorkspaceApi, workdir_id: &str, status: &str, cleanliness: &str) {
|
fn seed_cleanup_workdir(api: &WorkspaceApi, workdir_id: &str, status: &str, cleanliness: &str) {
|
||||||
|
|
@ -5467,11 +5505,13 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seed_cleanup_link(api: &WorkspaceApi, worker_id: &str, workdir_id: &str) {
|
fn seed_cleanup_link(api: &WorkspaceApi, runtime_worker_id: &str, workdir_id: &str) {
|
||||||
|
let runtime_worker_id = runtime_worker_id.parse::<u64>().unwrap();
|
||||||
api.store
|
api.store
|
||||||
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
|
.upsert_worker_workdir_link(&WorkerWorkdirLinkRecord {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
worker_id: worker_id.to_string(),
|
runtime_id: "runtime-test".to_string(),
|
||||||
|
runtime_worker_id,
|
||||||
workdir_id: workdir_id.to_string(),
|
workdir_id: workdir_id.to_string(),
|
||||||
role: "primary".to_string(),
|
role: "primary".to_string(),
|
||||||
linked_at: now_registry_timestamp(),
|
linked_at: now_registry_timestamp(),
|
||||||
|
|
@ -5606,8 +5646,8 @@ 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", "pinned");
|
let pinned = seed_cleanup_worker(&api, 1, "pinned");
|
||||||
let unobserved = seed_cleanup_worker(&api, "worker-unobserved", "normal");
|
let unobserved = seed_cleanup_worker(&api, 2, "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");
|
||||||
|
|
@ -5652,7 +5692,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", "pinned");
|
let worker = seed_cleanup_worker(&api, 1, "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
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||||
name: "remove durable worker lifecycle state",
|
name: "remove durable worker lifecycle state",
|
||||||
apply: remove_worker_registry_legacy_live_state_column,
|
apply: remove_worker_registry_legacy_live_state_column,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 5,
|
||||||
|
name: "use composite worker registry keys",
|
||||||
|
apply: use_composite_worker_registry_keys,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
struct Migration {
|
struct Migration {
|
||||||
|
|
@ -57,10 +62,8 @@ pub struct WorkspaceRecord {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct WorkerRegistryRecord {
|
pub struct WorkerRegistryRecord {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
/// Backend-owned archival Worker id. In v0 it is derived from runtime_id + runtime_worker_id.
|
|
||||||
pub worker_id: String,
|
|
||||||
pub runtime_id: String,
|
pub runtime_id: String,
|
||||||
pub runtime_worker_id: String,
|
pub runtime_worker_id: u64,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub profile: Option<String>,
|
pub profile: Option<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.
|
||||||
|
|
@ -92,7 +95,8 @@ pub struct WorkdirRegistryRecord {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct WorkerWorkdirLinkRecord {
|
pub struct WorkerWorkdirLinkRecord {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub worker_id: String,
|
pub runtime_id: String,
|
||||||
|
pub runtime_worker_id: u64,
|
||||||
pub workdir_id: String,
|
pub workdir_id: String,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub linked_at: String,
|
pub linked_at: String,
|
||||||
|
|
@ -107,15 +111,10 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
|
|
||||||
fn upsert_worker_registry(&self, record: &WorkerRegistryRecord) -> Result<()>;
|
fn upsert_worker_registry(&self, record: &WorkerRegistryRecord) -> Result<()>;
|
||||||
fn get_worker_registry(
|
fn get_worker_registry(
|
||||||
&self,
|
|
||||||
workspace_id: &str,
|
|
||||||
worker_id: &str,
|
|
||||||
) -> Result<Option<WorkerRegistryRecord>>;
|
|
||||||
fn get_worker_registry_by_runtime(
|
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
runtime_worker_id: &str,
|
runtime_worker_id: u64,
|
||||||
) -> Result<Option<WorkerRegistryRecord>>;
|
) -> Result<Option<WorkerRegistryRecord>>;
|
||||||
fn list_worker_registry(
|
fn list_worker_registry(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -125,11 +124,17 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
fn update_worker_retention(
|
fn update_worker_retention(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
worker_id: &str,
|
runtime_id: &str,
|
||||||
|
runtime_worker_id: u64,
|
||||||
retention_state: &str,
|
retention_state: &str,
|
||||||
updated_at: &str,
|
updated_at: &str,
|
||||||
) -> Result<bool>;
|
) -> Result<bool>;
|
||||||
fn delete_worker_registry(&self, workspace_id: &str, worker_id: &str) -> Result<bool>;
|
fn delete_worker_registry(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
runtime_worker_id: u64,
|
||||||
|
) -> Result<bool>;
|
||||||
|
|
||||||
fn upsert_workdir_registry(&self, record: &WorkdirRegistryRecord) -> Result<()>;
|
fn upsert_workdir_registry(&self, record: &WorkdirRegistryRecord) -> Result<()>;
|
||||||
fn get_workdir_registry(
|
fn get_workdir_registry(
|
||||||
|
|
@ -153,7 +158,8 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
fn list_worker_workdir_links(
|
fn list_worker_workdir_links(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
worker_id: &str,
|
runtime_id: &str,
|
||||||
|
runtime_worker_id: u64,
|
||||||
) -> Result<Vec<WorkerWorkdirLinkRecord>>;
|
) -> Result<Vec<WorkerWorkdirLinkRecord>>;
|
||||||
fn list_workdir_worker_links(
|
fn list_workdir_worker_links(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -247,13 +253,11 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
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, runtime_id, runtime_worker_id, display_name, profile,
|
||||||
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)
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
||||||
ON CONFLICT(workspace_id, worker_id) DO UPDATE SET
|
ON CONFLICT(workspace_id, runtime_id, runtime_worker_id) DO UPDATE SET
|
||||||
runtime_id = excluded.runtime_id,
|
|
||||||
runtime_worker_id = excluded.runtime_worker_id,
|
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
profile = excluded.profile,
|
profile = excluded.profile,
|
||||||
retention_state = CASE
|
retention_state = CASE
|
||||||
|
|
@ -268,7 +272,6 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
updated_at = excluded.updated_at"#,
|
updated_at = excluded.updated_at"#,
|
||||||
params![
|
params![
|
||||||
record.workspace_id,
|
record.workspace_id,
|
||||||
record.worker_id,
|
|
||||||
record.runtime_id,
|
record.runtime_id,
|
||||||
record.runtime_worker_id,
|
record.runtime_worker_id,
|
||||||
record.display_name,
|
record.display_name,
|
||||||
|
|
@ -287,26 +290,10 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_worker_registry(
|
fn get_worker_registry(
|
||||||
&self,
|
|
||||||
workspace_id: &str,
|
|
||||||
worker_id: &str,
|
|
||||||
) -> Result<Option<WorkerRegistryRecord>> {
|
|
||||||
self.with_conn(|conn| {
|
|
||||||
conn.query_row(
|
|
||||||
worker_registry_select_sql("WHERE workspace_id = ?1 AND worker_id = ?2").as_str(),
|
|
||||||
params![workspace_id, worker_id],
|
|
||||||
read_worker_registry_record,
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
.map_err(Error::from)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_worker_registry_by_runtime(
|
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
runtime_worker_id: &str,
|
runtime_worker_id: u64,
|
||||||
) -> Result<Option<WorkerRegistryRecord>> {
|
) -> Result<Option<WorkerRegistryRecord>> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.query_row(
|
conn.query_row(
|
||||||
|
|
@ -344,26 +331,38 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
fn update_worker_retention(
|
fn update_worker_retention(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
worker_id: &str,
|
runtime_id: &str,
|
||||||
|
runtime_worker_id: u64,
|
||||||
retention_state: &str,
|
retention_state: &str,
|
||||||
updated_at: &str,
|
updated_at: &str,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
let changed = conn.execute(
|
let changed = conn.execute(
|
||||||
r#"UPDATE worker_registry
|
r#"UPDATE worker_registry
|
||||||
SET retention_state = ?3, updated_at = ?4
|
SET retention_state = ?4, updated_at = ?5
|
||||||
WHERE workspace_id = ?1 AND worker_id = ?2"#,
|
WHERE workspace_id = ?1 AND runtime_id = ?2 AND runtime_worker_id = ?3"#,
|
||||||
params![workspace_id, worker_id, retention_state, updated_at],
|
params![
|
||||||
|
workspace_id,
|
||||||
|
runtime_id,
|
||||||
|
runtime_worker_id,
|
||||||
|
retention_state,
|
||||||
|
updated_at
|
||||||
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(changed > 0)
|
Ok(changed > 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_worker_registry(&self, workspace_id: &str, worker_id: &str) -> Result<bool> {
|
fn delete_worker_registry(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
runtime_worker_id: u64,
|
||||||
|
) -> Result<bool> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
let changed = conn.execute(
|
let changed = conn.execute(
|
||||||
"DELETE FROM worker_registry WHERE workspace_id = ?1 AND worker_id = ?2",
|
"DELETE FROM worker_registry WHERE workspace_id = ?1 AND runtime_id = ?2 AND runtime_worker_id = ?3",
|
||||||
params![workspace_id, worker_id],
|
params![workspace_id, runtime_id, runtime_worker_id],
|
||||||
)?;
|
)?;
|
||||||
Ok(changed > 0)
|
Ok(changed > 0)
|
||||||
})
|
})
|
||||||
|
|
@ -468,14 +467,15 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"INSERT INTO worker_workdir_links (
|
r#"INSERT INTO worker_workdir_links (
|
||||||
workspace_id, worker_id, workdir_id, role, linked_at, unlinked_at
|
workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
ON CONFLICT(workspace_id, worker_id, workdir_id, role) DO UPDATE SET
|
ON CONFLICT(workspace_id, runtime_id, runtime_worker_id, workdir_id, role) DO UPDATE SET
|
||||||
linked_at = excluded.linked_at,
|
linked_at = excluded.linked_at,
|
||||||
unlinked_at = excluded.unlinked_at"#,
|
unlinked_at = excluded.unlinked_at"#,
|
||||||
params![
|
params![
|
||||||
record.workspace_id,
|
record.workspace_id,
|
||||||
record.worker_id,
|
record.runtime_id,
|
||||||
|
record.runtime_worker_id,
|
||||||
record.workdir_id,
|
record.workdir_id,
|
||||||
record.role,
|
record.role,
|
||||||
record.linked_at,
|
record.linked_at,
|
||||||
|
|
@ -489,17 +489,18 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
fn list_worker_workdir_links(
|
fn list_worker_workdir_links(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
worker_id: &str,
|
runtime_id: &str,
|
||||||
|
runtime_worker_id: u64,
|
||||||
) -> Result<Vec<WorkerWorkdirLinkRecord>> {
|
) -> Result<Vec<WorkerWorkdirLinkRecord>> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r#"SELECT workspace_id, worker_id, workdir_id, role, linked_at, unlinked_at
|
r#"SELECT workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at
|
||||||
FROM worker_workdir_links
|
FROM worker_workdir_links
|
||||||
WHERE workspace_id = ?1 AND worker_id = ?2 AND unlinked_at IS NULL
|
WHERE workspace_id = ?1 AND runtime_id = ?2 AND runtime_worker_id = ?3 AND unlinked_at IS NULL
|
||||||
ORDER BY linked_at DESC"#,
|
ORDER BY linked_at DESC"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(
|
let rows = stmt.query_map(
|
||||||
params![workspace_id, worker_id],
|
params![workspace_id, runtime_id, runtime_worker_id],
|
||||||
read_worker_workdir_link_record,
|
read_worker_workdir_link_record,
|
||||||
)?;
|
)?;
|
||||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
|
@ -514,7 +515,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
) -> Result<Vec<WorkerWorkdirLinkRecord>> {
|
) -> Result<Vec<WorkerWorkdirLinkRecord>> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r#"SELECT workspace_id, worker_id, workdir_id, role, linked_at, unlinked_at
|
r#"SELECT workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at
|
||||||
FROM worker_workdir_links
|
FROM worker_workdir_links
|
||||||
WHERE workspace_id = ?1 AND workdir_id = ?2 AND unlinked_at IS NULL
|
WHERE workspace_id = ?1 AND workdir_id = ?2 AND unlinked_at IS NULL
|
||||||
ORDER BY linked_at DESC"#,
|
ORDER BY linked_at DESC"#,
|
||||||
|
|
@ -534,17 +535,18 @@ fn read_worker_workdir_link_record(
|
||||||
) -> rusqlite::Result<WorkerWorkdirLinkRecord> {
|
) -> rusqlite::Result<WorkerWorkdirLinkRecord> {
|
||||||
Ok(WorkerWorkdirLinkRecord {
|
Ok(WorkerWorkdirLinkRecord {
|
||||||
workspace_id: row.get(0)?,
|
workspace_id: row.get(0)?,
|
||||||
worker_id: row.get(1)?,
|
runtime_id: row.get(1)?,
|
||||||
workdir_id: row.get(2)?,
|
runtime_worker_id: row.get(2)?,
|
||||||
role: row.get(3)?,
|
workdir_id: row.get(3)?,
|
||||||
linked_at: row.get(4)?,
|
role: row.get(4)?,
|
||||||
unlinked_at: row.get(5)?,
|
linked_at: row.get(5)?,
|
||||||
|
unlinked_at: row.get(6)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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, runtime_id, runtime_worker_id, display_name, profile, \
|
||||||
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}"
|
||||||
)
|
)
|
||||||
|
|
@ -553,18 +555,17 @@ fn worker_registry_select_sql(where_clause: &str) -> String {
|
||||||
fn read_worker_registry_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<WorkerRegistryRecord> {
|
fn read_worker_registry_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<WorkerRegistryRecord> {
|
||||||
Ok(WorkerRegistryRecord {
|
Ok(WorkerRegistryRecord {
|
||||||
workspace_id: row.get(0)?,
|
workspace_id: row.get(0)?,
|
||||||
worker_id: row.get(1)?,
|
runtime_id: row.get(1)?,
|
||||||
runtime_id: row.get(2)?,
|
runtime_worker_id: row.get(2)?,
|
||||||
runtime_worker_id: row.get(3)?,
|
display_name: row.get(3)?,
|
||||||
display_name: row.get(4)?,
|
profile: row.get(4)?,
|
||||||
profile: row.get(5)?,
|
retention_state: row.get(5)?,
|
||||||
retention_state: row.get(6)?,
|
transcript_ref: row.get(6)?,
|
||||||
transcript_ref: row.get(7)?,
|
session_ref: row.get(7)?,
|
||||||
session_ref: row.get(8)?,
|
summary_ref: row.get(8)?,
|
||||||
summary_ref: row.get(9)?,
|
diagnostics_ref: row.get(9)?,
|
||||||
diagnostics_ref: row.get(10)?,
|
created_at: row.get(10)?,
|
||||||
created_at: row.get(11)?,
|
updated_at: row.get(11)?,
|
||||||
updated_at: row.get(12)?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -599,9 +600,8 @@ fn create_worker_workdir_registry_tables(conn: &Connection) -> Result<()> {
|
||||||
r#"
|
r#"
|
||||||
CREATE TABLE IF NOT EXISTS worker_registry (
|
CREATE TABLE IF NOT EXISTS worker_registry (
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
worker_id TEXT NOT NULL,
|
|
||||||
runtime_id TEXT NOT NULL,
|
runtime_id TEXT NOT NULL,
|
||||||
runtime_worker_id TEXT NOT NULL,
|
runtime_worker_id INTEGER NOT NULL,
|
||||||
display_name TEXT NOT NULL,
|
display_name TEXT NOT NULL,
|
||||||
profile TEXT,
|
profile TEXT,
|
||||||
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
||||||
|
|
@ -611,8 +611,7 @@ CREATE TABLE IF NOT EXISTS worker_registry (
|
||||||
diagnostics_ref TEXT,
|
diagnostics_ref TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
PRIMARY KEY (workspace_id, worker_id),
|
PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id),
|
||||||
UNIQUE (workspace_id, runtime_id, runtime_worker_id),
|
|
||||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -634,13 +633,14 @@ CREATE TABLE IF NOT EXISTS workdir_registry (
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS worker_workdir_links (
|
CREATE TABLE IF NOT EXISTS worker_workdir_links (
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
worker_id TEXT NOT NULL,
|
runtime_id TEXT NOT NULL,
|
||||||
|
runtime_worker_id INTEGER NOT NULL,
|
||||||
workdir_id TEXT NOT NULL,
|
workdir_id TEXT NOT NULL,
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL,
|
||||||
linked_at TEXT NOT NULL,
|
linked_at TEXT NOT NULL,
|
||||||
unlinked_at TEXT,
|
unlinked_at TEXT,
|
||||||
PRIMARY KEY (workspace_id, worker_id, workdir_id, role),
|
PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id, workdir_id, role),
|
||||||
FOREIGN KEY (workspace_id, worker_id) REFERENCES worker_registry(workspace_id, worker_id) ON DELETE CASCADE,
|
FOREIGN KEY (workspace_id, runtime_id, runtime_worker_id) REFERENCES worker_registry(workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry(workspace_id, workdir_id) ON DELETE CASCADE
|
FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry(workspace_id, workdir_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -649,7 +649,7 @@ CREATE INDEX IF NOT EXISTS idx_worker_registry_workspace_updated
|
||||||
CREATE INDEX IF NOT EXISTS idx_workdir_registry_workspace_updated
|
CREATE INDEX IF NOT EXISTS idx_workdir_registry_workspace_updated
|
||||||
ON workdir_registry(workspace_id, updated_at DESC);
|
ON workdir_registry(workspace_id, updated_at DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_worker_workdir_links_worker
|
CREATE INDEX IF NOT EXISTS idx_worker_workdir_links_worker
|
||||||
ON worker_workdir_links(workspace_id, worker_id, linked_at DESC);
|
ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id, linked_at DESC);
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -802,7 +802,7 @@ fn remove_worker_registry_legacy_live_state_column(conn: &Connection) -> Result<
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
worker_id TEXT NOT NULL,
|
worker_id TEXT NOT NULL,
|
||||||
runtime_id TEXT NOT NULL,
|
runtime_id TEXT NOT NULL,
|
||||||
runtime_worker_id TEXT NOT NULL,
|
runtime_worker_id INTEGER NOT NULL,
|
||||||
display_name TEXT NOT NULL,
|
display_name TEXT NOT NULL,
|
||||||
profile TEXT,
|
profile TEXT,
|
||||||
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')),
|
||||||
|
|
@ -835,6 +835,98 @@ fn remove_worker_registry_legacy_live_state_column(conn: &Connection) -> Result<
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn use_composite_worker_registry_keys(conn: &Connection) -> Result<()> {
|
||||||
|
if !table_exists(conn, "worker_registry")?
|
||||||
|
|| !table_columns(conn, "worker_registry")?
|
||||||
|
.iter()
|
||||||
|
.any(|column| column == "worker_id")
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE worker_registry_v5 (
|
||||||
|
workspace_id TEXT NOT NULL,
|
||||||
|
runtime_id TEXT NOT NULL,
|
||||||
|
runtime_worker_id INTEGER 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, runtime_id, runtime_worker_id),
|
||||||
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT OR REPLACE INTO worker_registry_v5 (
|
||||||
|
workspace_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,
|
||||||
|
runtime_id,
|
||||||
|
CASE
|
||||||
|
WHEN typeof(runtime_worker_id) = 'integer' THEN runtime_worker_id
|
||||||
|
WHEN runtime_worker_id GLOB 'worker-[0-9]*' THEN CAST(substr(runtime_worker_id, 8) AS INTEGER)
|
||||||
|
WHEN runtime_worker_id GLOB '[0-9]*' THEN CAST(runtime_worker_id AS INTEGER)
|
||||||
|
ELSE rowid
|
||||||
|
END,
|
||||||
|
display_name, profile,
|
||||||
|
retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM worker_registry;
|
||||||
|
|
||||||
|
CREATE TABLE worker_workdir_links_v5 (
|
||||||
|
workspace_id TEXT NOT NULL,
|
||||||
|
runtime_id TEXT NOT NULL,
|
||||||
|
runtime_worker_id INTEGER NOT NULL,
|
||||||
|
workdir_id TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
linked_at TEXT NOT NULL,
|
||||||
|
unlinked_at TEXT,
|
||||||
|
PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id, workdir_id, role),
|
||||||
|
FOREIGN KEY (workspace_id, runtime_id, runtime_worker_id) REFERENCES worker_registry_v5(workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry(workspace_id, workdir_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT OR REPLACE INTO worker_workdir_links_v5 (
|
||||||
|
workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
links.workspace_id,
|
||||||
|
registry.runtime_id,
|
||||||
|
CASE
|
||||||
|
WHEN typeof(registry.runtime_worker_id) = 'integer' THEN registry.runtime_worker_id
|
||||||
|
WHEN registry.runtime_worker_id GLOB 'worker-[0-9]*' THEN CAST(substr(registry.runtime_worker_id, 8) AS INTEGER)
|
||||||
|
WHEN registry.runtime_worker_id GLOB '[0-9]*' THEN CAST(registry.runtime_worker_id AS INTEGER)
|
||||||
|
ELSE registry.rowid
|
||||||
|
END,
|
||||||
|
links.workdir_id,
|
||||||
|
links.role,
|
||||||
|
links.linked_at,
|
||||||
|
links.unlinked_at
|
||||||
|
FROM worker_workdir_links AS links
|
||||||
|
JOIN worker_registry AS registry
|
||||||
|
ON registry.workspace_id = links.workspace_id
|
||||||
|
AND registry.worker_id = links.worker_id;
|
||||||
|
|
||||||
|
DROP TABLE worker_workdir_links;
|
||||||
|
DROP TABLE worker_registry;
|
||||||
|
ALTER TABLE worker_registry_v5 RENAME TO worker_registry;
|
||||||
|
ALTER TABLE worker_workdir_links_v5 RENAME TO worker_workdir_links;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_worker_registry_workspace_updated
|
||||||
|
ON worker_registry(workspace_id, updated_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_worker_workdir_links_worker
|
||||||
|
ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id, linked_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#"
|
||||||
|
|
@ -1058,7 +1150,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(), 4);
|
assert_eq!(store.schema_version().await.unwrap(), 5);
|
||||||
|
|
||||||
let record = WorkspaceRecord {
|
let record = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
|
|
@ -1070,7 +1162,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(), 4);
|
assert_eq!(reopened.schema_version().await.unwrap(), 5);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reopened.get_workspace("local-dev").await.unwrap(),
|
reopened.get_workspace("local-dev").await.unwrap(),
|
||||||
Some(record)
|
Some(record)
|
||||||
|
|
@ -1190,7 +1282,6 @@ mod tests {
|
||||||
"worker_registry",
|
"worker_registry",
|
||||||
[
|
[
|
||||||
"workspace_id",
|
"workspace_id",
|
||||||
"worker_id",
|
|
||||||
"runtime_id",
|
"runtime_id",
|
||||||
"runtime_worker_id",
|
"runtime_worker_id",
|
||||||
"display_name",
|
"display_name",
|
||||||
|
|
@ -1280,7 +1371,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(), 4);
|
assert_eq!(store.schema_version().await.unwrap(), 5);
|
||||||
|
|
||||||
store
|
store
|
||||||
.with_conn(|conn| {
|
.with_conn(|conn| {
|
||||||
|
|
@ -1381,13 +1472,12 @@ mod tests {
|
||||||
|
|
||||||
let worker = WorkerRegistryRecord {
|
let worker = WorkerRegistryRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
worker_id: "embedded/browser-1".to_string(),
|
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
runtime_worker_id: "browser-1".to_string(),
|
runtime_worker_id: 1,
|
||||||
display_name: "Browser 1".to_string(),
|
display_name: "Browser 1".to_string(),
|
||||||
profile: Some("builtin:companion".to_string()),
|
profile: Some("builtin:companion".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/00000001/transcript".to_string()),
|
||||||
session_ref: None,
|
session_ref: None,
|
||||||
summary_ref: None,
|
summary_ref: None,
|
||||||
diagnostics_ref: None,
|
diagnostics_ref: None,
|
||||||
|
|
@ -1404,7 +1494,7 @@ mod tests {
|
||||||
|
|
||||||
let workdir = WorkdirRegistryRecord {
|
let workdir = WorkdirRegistryRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
workdir_id: "backend-2-repo".to_string(),
|
workdir_id: "0000019a00000000001".to_string(),
|
||||||
runtime_id: "embedded".to_string(),
|
runtime_id: "embedded".to_string(),
|
||||||
repository_id: "repo".to_string(),
|
repository_id: "repo".to_string(),
|
||||||
selector: Some("develop".to_string()),
|
selector: Some("develop".to_string()),
|
||||||
|
|
@ -1433,7 +1523,8 @@ mod tests {
|
||||||
|
|
||||||
let link = WorkerWorkdirLinkRecord {
|
let link = WorkerWorkdirLinkRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
worker_id: worker.worker_id.clone(),
|
runtime_id: worker.runtime_id.clone(),
|
||||||
|
runtime_worker_id: worker.runtime_worker_id.clone(),
|
||||||
workdir_id: workdir.workdir_id.clone(),
|
workdir_id: workdir.workdir_id.clone(),
|
||||||
role: "primary_cwd".to_string(),
|
role: "primary_cwd".to_string(),
|
||||||
linked_at: "4".to_string(),
|
linked_at: "4".to_string(),
|
||||||
|
|
@ -1443,13 +1534,13 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store
|
store
|
||||||
.get_worker_registry_by_runtime("local-dev", "embedded", "browser-1")
|
.get_worker_registry("local-dev", "embedded", 1)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Some(expected_worker.clone())
|
Some(expected_worker.clone())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store
|
store
|
||||||
.get_workdir_registry("local-dev", "backend-2-repo")
|
.get_workdir_registry("local-dev", "0000019a00000000001")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Some(workdir.clone())
|
Some(workdir.clone())
|
||||||
);
|
);
|
||||||
|
|
@ -1465,7 +1556,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store
|
store
|
||||||
.list_worker_workdir_links("local-dev", "embedded/browser-1")
|
.list_worker_workdir_links("local-dev", "embedded", 1)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
vec![link]
|
vec![link]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user