fix: decouple workdir attachments from materialization

This commit is contained in:
2026-09-22 02:35:42 +09:00
parent 2cdc35f3e2
commit aa41250beb
9 changed files with 439 additions and 168 deletions
+12 -1
View File
@@ -345,6 +345,15 @@ pub struct WorkingDirectoryRequest {
pub materialization: Option<RepositoryMaterializationContext>,
}
/// Workspace-authoritative alias-to-Workdir mapping, independent of any
/// Runtime-local materialized binding.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct LogicalWorkdirAttachment {
/// Stable Worker-local routing key. This is not a Workdir id or display name.
pub alias: workdir::WorkdirAttachmentAlias,
pub working_directory_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct WorkingDirectoryAttachmentClaim {
/// Stable Worker-local routing key. This is not a Workdir id or display name.
@@ -356,7 +365,9 @@ pub struct WorkingDirectoryAttachmentClaim {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct WorkerWorkdirAttachmentsRequest {
pub workdir_attachments: Vec<WorkingDirectoryAttachmentClaim>,
/// Exact current logical attachment set. Runtime-local bindings are not
/// inferred from these identities.
pub logical_workdir_attachments: Vec<LogicalWorkdirAttachment>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+8
View File
@@ -198,6 +198,14 @@ pub struct RepositoryRefObservation {
pub observed_at_epoch_seconds: u64,
}
/// Workspace-authoritative alias-to-Workdir mapping. This carries no claim that
/// the Worker Runtime materialized or can bind the referenced Workdir locally.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogicalWorkdirAttachment {
pub alias: workdir::WorkdirAttachmentAlias,
pub working_directory_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkingDirectoryAttachmentClaim {
pub alias: workdir::WorkdirAttachmentAlias,
+1
View File
@@ -259,6 +259,7 @@ pub struct WorkerExecutionRestoreRequest {
pub workspace_scope: Option<crate::runtime::RuntimeWorkspaceScope>,
pub context: WorkerExecutionContext,
pub previous_workdir_attachments: Vec<crate::catalog::WorkingDirectoryAttachmentStatus>,
pub logical_workdir_attachments: Vec<crate::catalog::LogicalWorkdirAttachment>,
pub workdir_attachments: BTreeMap<workdir::WorkdirAttachmentAlias, WorkingDirectoryBinding>,
pub config_bundle: Option<ConfigBundle>,
}
+47 -3
View File
@@ -1,7 +1,7 @@
use crate::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerRestoreIntent, WorkerStatus,
WorkingDirectoryAttachmentClaim, WorkingDirectoryAttachmentRequest,
WorkingDirectoryAttachmentStatus, WorkingDirectoryRequest,
ConfigBundleRef, CreateWorkerRequest, LogicalWorkdirAttachment, ProfileSelector,
WorkerRestoreIntent, WorkerStatus, WorkingDirectoryAttachmentClaim,
WorkingDirectoryAttachmentRequest, WorkingDirectoryAttachmentStatus, WorkingDirectoryRequest,
};
use crate::config_bundle::ConfigBundle;
use crate::diagnostics::{DiagnosticSeverity, RuntimeDiagnostic};
@@ -444,6 +444,7 @@ pub(crate) struct PersistedWorkerRecord {
pub(crate) execution_state: PersistedWorkerExecutionState,
pub(crate) workspace_id: Option<String>,
pub(crate) workdir_attachments: Vec<WorkingDirectoryAttachmentStatus>,
pub(crate) logical_workdir_attachments: Vec<LogicalWorkdirAttachment>,
}
fn runtime_io_error(operation: &'static str, path: &Path, source: std::io::Error) -> RuntimeError {
@@ -1688,6 +1689,8 @@ struct WorkerIdentityRecord {
workspace_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
workdir_attachments: Vec<WorkingDirectoryAttachmentStatus>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
logical_workdir_attachments: Vec<LogicalWorkdirAttachment>,
/// Schema-v8 compatibility input. New records serialize only `workdir_attachments`.
#[serde(
default,
@@ -1787,6 +1790,7 @@ impl WorkerIdentityRecord {
status: worker.status,
workspace_id: worker.workspace_id.clone(),
workdir_attachments: worker.workdir_attachments.clone(),
logical_workdir_attachments: worker.logical_workdir_attachments.clone(),
legacy_working_directory: None,
}
}
@@ -1860,6 +1864,7 @@ impl WorkerIdentityRecord {
execution_state,
workspace_id: self.workspace_id,
workdir_attachments,
logical_workdir_attachments: self.logical_workdir_attachments,
}
}
}
@@ -2337,6 +2342,7 @@ mod tests {
}))
.unwrap();
let persisted = identity.into_persisted(PersistedWorkerExecutionState::Unavailable);
assert!(persisted.logical_workdir_attachments.is_empty());
assert_eq!(persisted.workdir_attachments.len(), 1);
assert_eq!(persisted.workdir_attachments[0].alias.as_str(), "workdir");
assert_eq!(
@@ -2369,6 +2375,44 @@ mod tests {
assert!(serialized["request"].get("working_directory").is_none());
}
#[test]
fn worker_identity_round_trips_logical_workdir_attachments() {
let worker_id = WorkerId::now_v7();
let identity: WorkerIdentityRecord = serde_json::from_value(serde_json::json!({
"schema_version": SCHEMA_VERSION,
"worker_ref": { "worker_id": worker_id },
"worker_id": worker_id,
"profile": { "kind": "builtin", "value": "builtin:coder" },
"display_name": null,
"profile_source": {
"id": "archive-1",
"digest": "sha256:archive",
"size_bytes": 0,
"source_graph": {
"source_count": 0,
"total_source_bytes": 0,
"entrypoints": {},
"import_count": 0
}
},
"config_bundle": null,
"status": "stopped",
"logical_workdir_attachments": [{
"alias": "checkout",
"working_directory_id": "remote-workdir"
}]
}))
.unwrap();
let persisted = identity.into_persisted(PersistedWorkerExecutionState::Unavailable);
assert_eq!(
persisted.logical_workdir_attachments,
[LogicalWorkdirAttachment {
alias: workdir::WorkdirAttachmentAlias::new("checkout").unwrap(),
working_directory_id: "remote-workdir".to_string(),
}]
);
}
#[test]
fn startup_recovers_staging_created_before_source_backup() {
let temp = tempfile::tempdir().unwrap();
@@ -428,7 +428,7 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
worker_id: String,
value: runtime_api::WorkerWorkdirAttachmentsRequest,
) -> Result<runtime_api::WorkerResponse, runtime_api::RuntimeApiError> {
let attachments = request(value.workdir_attachments)?;
let attachments = request(value.logical_workdir_attachments)?;
let worker_ref = worker_ref_for(&self.state.runtime, worker_id).map_err(api_error)?;
let worker = match auth_workspace_scope(&self.state, auth_extension().as_ref())
.map_err(api_error)?
+186 -86
View File
@@ -1,10 +1,9 @@
use crate::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource,
RepositoryRefObservation, RepositoryRefObservationRequest, WorkerDetail, WorkerLifecycleAck,
WorkerRestoreIntent, WorkerStatus, WorkerSummary, WorkingDirectoryAttachmentClaim,
WorkingDirectoryAttachmentStatus, WorkingDirectoryRepositoryAccessRequest,
WorkingDirectoryRequest, WorkingDirectoryStatus as CatalogWorkingDirectoryStatus,
WorkspaceApiRef,
ConfigBundleRef, CreateWorkerRequest, LogicalWorkdirAttachment, ProfileSelector,
ProfileSourceArchiveSource, RepositoryRefObservation, RepositoryRefObservationRequest,
WorkerDetail, WorkerLifecycleAck, WorkerRestoreIntent, WorkerStatus, WorkerSummary,
WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest,
WorkingDirectoryStatus as CatalogWorkingDirectoryStatus, WorkspaceApiRef,
};
use crate::config_bundle::{
ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle,
@@ -1061,6 +1060,7 @@ impl Runtime {
execution_bound: true,
restore_intent: WorkerRestoreIntent::Explicit,
workdir_attachments: Vec::new(),
logical_workdir_attachments: Vec::new(),
execution_handle: None,
internal_workers: InternalWorkerActivityProjection::default(),
};
@@ -1447,106 +1447,95 @@ impl Runtime {
&self,
scope: &RuntimeWorkspaceScope,
worker_ref: &WorkerRef,
attachments: Vec<WorkingDirectoryAttachmentClaim>,
attachments: Vec<LogicalWorkdirAttachment>,
) -> Result<WorkerDetail, RuntimeError> {
self.ensure_worker_in_workspace(scope, worker_ref)?;
self.replace_worker_workdir_attachments(worker_ref, attachments)
}
/// Persist the exact current attachment set used by live mutation and future restore.
/// Persist the Workspace-authoritative logical attachment set used by live
/// mutation and future restore. Runtime-local bindings remain a separate,
/// optional implementation detail and never authorize logical attachment.
pub fn replace_worker_workdir_attachments(
&self,
worker_ref: &WorkerRef,
mut attachments: Vec<WorkingDirectoryAttachmentClaim>,
mut attachments: Vec<LogicalWorkdirAttachment>,
) -> Result<WorkerDetail, RuntimeError> {
let operation_lock = self.worker_operation_lock(worker_ref.worker_id)?;
let _operation_guard = operation_lock
.lock()
.map_err(|_| RuntimeError::StatePoisoned)?;
let backend = {
let state = self.lock()?;
let worker = state.worker(worker_ref)?;
if worker.execution_handle.is_none() || !worker.status.is_active() {
return Err(RuntimeError::WorkerExecutionUnavailable {
worker_id: worker.worker_id,
message: "Workdir attachments can only be changed for a live Worker"
.to_string(),
});
}
let request = worker.request.as_ref().ok_or_else(|| {
RuntimeError::WorkerExecutionUnavailable {
worker_id: worker.worker_id,
message: "persisted Worker restore request is unavailable".to_string(),
}
})?;
for attachment in &mut attachments {
if attachment.relative_cwd.is_none()
&& let Some(previous) = request.workdir_attachments.iter().find(|previous| {
previous.alias == attachment.alias
&& previous.working_directory_id == attachment.working_directory_id
})
{
attachment.relative_cwd = previous.relative_cwd.clone();
}
}
let mut candidate = request.clone();
candidate.workdir_attachment_requests.clear();
candidate.workdir_attachments = attachments.clone();
validate_create_worker_request(&candidate)?;
state.execution_backend.clone().ok_or_else(|| {
RuntimeError::ExecutionBackendUnavailable {
message: "Workdir attachment mutation requires an execution backend"
.to_string(),
}
})?
};
let statuses = attachments
.iter()
.map(|attachment| {
backend
.working_directory(&attachment.working_directory_id)
.map(|working_directory| WorkingDirectoryAttachmentStatus {
alias: attachment.alias.clone(),
working_directory,
})
.map_err(RuntimeError::from)
})
.collect::<Result<Vec<_>, RuntimeError>>()?;
if let Some(inactive) = statuses.iter().find(|attachment| {
attachment.working_directory.summary.status
!= workdir::workspace::WorkingDirectoryStatusKind::Active
}) {
return Err(RuntimeError::InvalidRequest(format!(
"working directory {} is not active ({})",
inactive.working_directory.summary.working_directory_id,
inactive.working_directory.summary.status
)));
}
validate_logical_workdir_attachments(&attachments)?;
attachments.sort_by(|left, right| left.alias.cmp(&right.alias));
let mut state = self.lock()?;
for attachment in &attachments {
if let Some(owner_worker_id) =
state.worker_id_for_workdir(&attachment.working_directory_id)
&& owner_worker_id != worker_ref.worker_id
{
return Err(RuntimeError::InvalidRequest(format!(
"working directory {} is already assigned to worker {owner_worker_id}",
attachment.working_directory_id
)));
}
}
let previous = state.worker(worker_ref)?.clone();
{
let worker = state.worker_mut(worker_ref)?;
if !attachments.is_empty() && worker.workspace_id.is_none() {
return Err(RuntimeError::InvalidRequest(
"logical Workdir attachments require Workspace authority".to_string(),
));
}
if worker.request.is_none() {
return Err(RuntimeError::WorkerExecutionUnavailable {
worker_id: worker.worker_id,
message: "persisted Worker restore request is unavailable".to_string(),
});
}
let logical_by_alias = attachments
.iter()
.map(|attachment| {
(
attachment.alias.clone(),
attachment.working_directory_id.as_str(),
)
})
.collect::<BTreeMap<_, _>>();
for local in &worker.workdir_attachments {
let local_id = local
.working_directory
.summary
.working_directory_id
.as_str();
if let Some(logical_id) = logical_by_alias.get(&local.alias)
&& *logical_id != local_id
{
return Err(RuntimeError::InvalidRequest(format!(
"logical Workdir attachment alias `{}` conflicts with Runtime-local Workdir {}",
local.alias, local_id
)));
}
}
let retained_local = worker
.workdir_attachments
.iter()
.filter_map(|local| {
let local_id = local
.working_directory
.summary
.working_directory_id
.as_str();
(logical_by_alias.get(&local.alias).copied() == Some(local_id))
.then(|| (local.alias.clone(), local_id.to_string()))
})
.collect::<BTreeMap<_, _>>();
worker
.workdir_attachments
.retain(|local| retained_local.contains_key(&local.alias));
let request = worker
.request
.as_mut()
.expect("restore request checked above");
request.workdir_attachment_requests.clear();
request.workdir_attachments = attachments;
worker.workdir_attachments = statuses;
.expect("restore request existence checked above");
request
.workdir_attachment_requests
.retain(|local| retained_local.contains_key(&local.alias));
request.workdir_attachments.retain(|local| {
retained_local
.get(&local.alias)
.is_some_and(|workdir_id| workdir_id == &local.working_directory_id)
});
worker.logical_workdir_attachments = attachments;
}
if let Err(error) = state.persist_worker(&worker_ref.worker_id) {
state.workers.insert(worker_ref.worker_id, previous);
@@ -1702,6 +1691,7 @@ impl Runtime {
workspace_scope,
context: self.execution_context(worker_ref.clone()),
previous_workdir_attachments: worker.workdir_attachments.clone(),
logical_workdir_attachments: worker.logical_workdir_attachments.clone(),
workdir_attachments: BTreeMap::new(),
config_bundle: None,
};
@@ -2225,6 +2215,8 @@ impl Runtime {
let _ = worker.apply_worker_state(snapshot);
}
worker.restore_intent = restore_intent_for_status(worker.status);
worker.logical_workdir_attachments =
logical_workdir_attachments_from_statuses(&workdir_attachments);
worker.workdir_attachments = workdir_attachments;
worker.detail()
};
@@ -2875,6 +2867,8 @@ impl Runtime {
candidate.execution_bound = true;
candidate.status = WorkerStatus::Idle;
candidate.restore_intent = WorkerRestoreIntent::Explicit;
candidate.logical_workdir_attachments =
logical_workdir_attachments_from_statuses(&workdir_attachments);
candidate.workdir_attachments = workdir_attachments;
state.workers.insert(worker_ref.worker_id, candidate);
state.publish_worker_upsert(worker_ref.worker_id)?;
@@ -3218,6 +3212,11 @@ impl RuntimeState {
(None, false, false, restore_intent_for_status(worker.status))
}
};
let logical_workdir_attachments = if worker.logical_workdir_attachments.is_empty() {
logical_workdir_attachments_from_statuses(&worker.workdir_attachments)
} else {
worker.logical_workdir_attachments
};
workers.insert(
worker_id,
WorkerRecord {
@@ -3236,6 +3235,7 @@ impl RuntimeState {
execution_bound,
restore_intent,
workdir_attachments: worker.workdir_attachments,
logical_workdir_attachments,
execution_handle: None,
internal_workers: InternalWorkerActivityProjection::default(),
},
@@ -4132,6 +4132,7 @@ struct WorkerRecord {
execution_bound: bool,
restore_intent: WorkerRestoreIntent,
workdir_attachments: Vec<crate::catalog::WorkingDirectoryAttachmentStatus>,
logical_workdir_attachments: Vec<LogicalWorkdirAttachment>,
execution_handle: Option<WorkerExecutionHandle>,
internal_workers: InternalWorkerActivityProjection,
}
@@ -4205,6 +4206,7 @@ impl WorkerRecord {
execution_state,
workspace_id: self.workspace_id.clone(),
workdir_attachments: self.workdir_attachments.clone(),
logical_workdir_attachments: self.logical_workdir_attachments.clone(),
}
}
}
@@ -4279,6 +4281,50 @@ fn requested_workdir_ids(request: &CreateWorkerRequest) -> Vec<&str> {
.collect()
}
fn logical_workdir_attachments_from_statuses(
attachments: &[crate::catalog::WorkingDirectoryAttachmentStatus],
) -> Vec<LogicalWorkdirAttachment> {
attachments
.iter()
.map(|attachment| LogicalWorkdirAttachment {
alias: attachment.alias.clone(),
working_directory_id: attachment
.working_directory
.summary
.working_directory_id
.clone(),
})
.collect()
}
fn validate_logical_workdir_attachments(
attachments: &[LogicalWorkdirAttachment],
) -> Result<(), RuntimeError> {
let mut aliases = std::collections::BTreeSet::new();
let mut workdir_ids = std::collections::BTreeSet::new();
for attachment in attachments {
let workdir_id = attachment.working_directory_id.trim();
if workdir_id.is_empty() || workdir_id.chars().any(char::is_control) {
return Err(RuntimeError::InvalidRequest(
"logical Workdir attachment id must not be empty or contain control characters"
.to_string(),
));
}
if !aliases.insert(attachment.alias.as_str()) {
return Err(RuntimeError::InvalidRequest(format!(
"duplicate logical Workdir attachment alias `{}`",
attachment.alias
)));
}
if !workdir_ids.insert(workdir_id) {
return Err(RuntimeError::InvalidRequest(format!(
"logical working directory `{workdir_id}` cannot be attached more than once"
)));
}
}
Ok(())
}
fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), RuntimeError> {
if request.create_fingerprint.trim().is_empty() {
return Err(RuntimeError::InvalidRequest(
@@ -4615,7 +4661,8 @@ mod tests {
ConfigBundleRef, MaterializerKind, ProfileSelector, RepositoryMaterializationContext,
RepositorySshCredentialCandidate, RepositorySshMaterializationAccess, SensitiveString,
WorkingDirectoryAttachmentClaim, WorkingDirectoryAttachmentRequest,
WorkingDirectoryRepository, WorkingDirectoryRequest, WorkspaceApiRef,
WorkingDirectoryAttachmentStatus, WorkingDirectoryRepository, WorkingDirectoryRequest,
WorkspaceApiRef,
};
use crate::config_bundle::{
ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigDeclaration,
@@ -6168,6 +6215,59 @@ mod tests {
);
}
#[test]
fn logical_workdir_attachments_do_not_require_local_materialization() {
let runtime = runtime_with_backend();
let workspace_scope = scope("workspace-a", "server-a");
let worker = runtime
.create_worker_scoped(
&workspace_scope,
scoped_task_request("logical attachment", "workspace-a"),
)
.unwrap();
let attachment = LogicalWorkdirAttachment {
alias: workdir::WorkdirAttachmentAlias::new("checkout").unwrap(),
working_directory_id: "workdir-on-another-runtime".to_string(),
};
let detail = runtime
.replace_worker_workdir_attachments_scoped(
&workspace_scope,
&worker.worker_ref,
vec![attachment.clone()],
)
.unwrap();
assert!(detail.workdir_attachments.is_empty());
{
let state = runtime.lock().unwrap();
assert_eq!(
state
.worker(&worker.worker_ref)
.unwrap()
.logical_workdir_attachments,
[attachment.clone()]
);
}
runtime.stop_worker(&worker.worker_ref, None).unwrap();
runtime
.replace_worker_workdir_attachments_scoped(
&workspace_scope,
&worker.worker_ref,
vec![attachment.clone()],
)
.unwrap();
runtime.restore_worker(&worker.worker_ref).unwrap();
let state = runtime.lock().unwrap();
assert_eq!(
state
.worker(&worker.worker_ref)
.unwrap()
.logical_workdir_attachments,
[attachment]
);
}
#[test]
fn workspace_owner_binding_rejects_other_backend_and_forgets_after_last_worker_delete() {
let runtime = runtime_with_backend();
+73 -62
View File
@@ -115,6 +115,7 @@ use workdir::{
#[cfg(test)]
use worker::WorkerController;
use worker::feature::builtin::manage_workdir::WorkspaceAttachedWorkdirSession;
use worker::feature::builtin::{
CompositeWorkerObservationProvider, WorkerObservationError, WorkerObservationProvider,
WorkerObservationSubject, WorkerObservationSubjectRef, WorkerSessionCapture,
@@ -796,6 +797,42 @@ fn runtime_local_workdir_router(
Ok(router)
}
fn restored_workdir_router(
local_attachments: &BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>,
logical_attachments: &[crate::catalog::LogicalWorkdirAttachment],
scope: manifest::SharedScope,
workspace_client: Arc<dyn WorkspaceClient>,
) -> Result<Arc<WorkdirSessionRouter>, String> {
let router = runtime_local_workdir_router(local_attachments, scope)?;
for attachment in logical_attachments {
if let Some(local) = local_attachments.get(&attachment.alias) {
if local.working_directory.id != attachment.working_directory_id {
return Err(format!(
"logical Workdir attachment `{}` conflicts with Runtime-local Workdir {}",
attachment.alias, local.working_directory.id
));
}
continue;
}
router
.attach(
attachment.alias.clone(),
WorkspaceAttachedWorkdirSession::handle_for_workdir(
workspace_client.clone(),
attachment.alias.as_str(),
&attachment.working_directory_id,
),
)
.map_err(|error| {
format!(
"bind logical Workdir attachment `{}`: {error}",
attachment.alias
)
})?;
}
Ok(router)
}
fn bind_workspace_memory_settings(
manifest: &mut manifest::WorkerManifest,
request: &CreateWorkerRequest,
@@ -1259,10 +1296,13 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
};
validate_worker_memory_settings(worker.manifest(), &request.request)?;
let flow_transition_enabled = worker.manifest().feature.flow.enabled;
worker.bind_workdir_sessions(runtime_local_workdir_router(
let workdir_sessions = restored_workdir_router(
&request.workdir_attachments,
&request.logical_workdir_attachments,
worker.scope().clone(),
)?);
worker.workspace_client_handle(),
)?;
worker.bind_workdir_sessions(workdir_sessions);
if let (Some(runtime_id), Some(workspace_id)) =
(observation_runtime_id, observation_workspace_id.clone())
&& observation_enabled
@@ -2897,9 +2937,10 @@ mod tests {
use crate::Runtime as EmbeddedRuntime;
use crate::catalog::{
ConfigBundleRef, CreateWorkerRequest, MaterializerKind, ProfileSelector,
RepositorySelector, WorkingDirectoryAttachmentClaim, WorkingDirectoryAttachmentRequest,
WorkingDirectoryRepository, WorkingDirectoryRequest, WorkspaceApiRef,
ConfigBundleRef, CreateWorkerRequest, LogicalWorkdirAttachment, MaterializerKind,
ProfileSelector, RepositorySelector, WorkingDirectoryAttachmentClaim,
WorkingDirectoryAttachmentRequest, WorkingDirectoryRepository, WorkingDirectoryRequest,
WorkspaceApiRef,
};
use crate::execution::WorkerExecutionContext;
use crate::identity::WorkerId;
@@ -2915,6 +2956,31 @@ mod tests {
use manifest::{Scope, WorkerManifest};
use session_store::{LogEntry, WorkerMetadataStore};
#[test]
fn restored_router_uses_workspace_proxy_for_logical_only_attachment() {
let root = tempfile::tempdir().unwrap();
let scope = manifest::SharedScope::new(manifest::Scope::writable(root.path()).unwrap());
let client = WorkerWorkspaceContext::local_filesystem(Some(
WorkspaceId::new("workspace-a").unwrap(),
))
.client_handle();
let alias = WorkdirAttachmentAlias::new("checkout").unwrap();
let router = restored_workdir_router(
&BTreeMap::new(),
&[LogicalWorkdirAttachment {
alias: alias.clone(),
working_directory_id: "remote-workdir".to_string(),
}],
scope,
client,
)
.unwrap();
let session = router.session(&alias).unwrap();
assert_eq!(session.workdir().id().as_str(), "remote-workdir");
}
#[test]
fn profile_factory_routes_workspace_requests_by_workspace_id() {
let profiles = tempfile::tempdir().unwrap();
@@ -3990,6 +4056,7 @@ mod tests {
)),
context: test_execution_context(worker_ref),
previous_workdir_attachments: Vec::new(),
logical_workdir_attachments: Vec::new(),
workdir_attachments: BTreeMap::new(),
config_bundle: None,
})
@@ -4080,6 +4147,7 @@ mod tests {
workspace_scope: None,
context: test_execution_context(worker_ref),
previous_workdir_attachments: Vec::new(),
logical_workdir_attachments: Vec::new(),
workdir_attachments: BTreeMap::new(),
config_bundle: None,
})
@@ -4550,47 +4618,6 @@ mod tests {
&[("unavailable".to_string(), None, false)]
);
let workdir_id = detail.workdir_attachments[0]
.working_directory
.summary
.working_directory_id
.clone();
let second = runtime.create_worker(create_request("second")).unwrap();
let conflict = runtime
.replace_worker_workdir_attachments(
&second.worker_ref,
vec![WorkingDirectoryAttachmentClaim {
alias: workdir::WorkdirAttachmentAlias::new("conflict").unwrap(),
working_directory_id: workdir_id.clone(),
relative_cwd: None,
}],
)
.unwrap_err();
assert!(matches!(
conflict,
crate::error::RuntimeError::InvalidRequest(_)
));
let inactive = runtime
.create_working_directory(working_directory_request(repo.path()))
.unwrap();
let inactive_id = inactive.summary.working_directory_id;
runtime.cleanup_working_directory(&inactive_id).unwrap();
let inactive_error = runtime
.replace_worker_workdir_attachments(
&second.worker_ref,
vec![WorkingDirectoryAttachmentClaim {
alias: workdir::WorkdirAttachmentAlias::new("inactive").unwrap(),
working_directory_id: inactive_id,
relative_cwd: None,
}],
)
.unwrap_err();
assert!(matches!(
inactive_error,
crate::error::RuntimeError::InvalidRequest(_)
| crate::error::RuntimeError::WorkingDirectory(_)
));
let detached = runtime
.replace_worker_workdir_attachments(&detail.worker_ref, Vec::new())
.unwrap();
@@ -4598,22 +4625,6 @@ mod tests {
runtime.stop_worker(&detail.worker_ref, None).unwrap();
let restored_without_workdir = runtime.restore_worker(&detail.worker_ref).unwrap();
assert!(restored_without_workdir.workdir_attachments.is_empty());
let alias = workdir::WorkdirAttachmentAlias::new("restored").unwrap();
runtime
.replace_worker_workdir_attachments(
&detail.worker_ref,
vec![WorkingDirectoryAttachmentClaim {
alias: alias.clone(),
working_directory_id: workdir_id,
relative_cwd: None,
}],
)
.unwrap();
runtime.stop_worker(&detail.worker_ref, None).unwrap();
let stopped = runtime.worker_detail(&detail.worker_ref).unwrap();
assert_eq!(stopped.workdir_attachments.len(), 1);
assert_eq!(stopped.workdir_attachments[0].alias, alias);
}
#[test]
+7 -7
View File
@@ -29,8 +29,8 @@ use workdir::{
};
use worker_runtime::RuntimeWorkspaceScope;
use worker_runtime::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource,
RepositoryRefObservation, RepositoryRefObservationRequest,
ConfigBundleRef, CreateWorkerRequest, LogicalWorkdirAttachment, ProfileSelector,
ProfileSourceArchiveSource, RepositoryRefObservation, RepositoryRefObservationRequest,
WorkerDetail as EmbeddedWorkerDetail, WorkerStatus as EmbeddedWorkerStatus,
WorkingDirectoryAttachmentClaim, WorkingDirectoryAttachmentRequest,
WorkingDirectoryAttachmentStatus, WorkingDirectoryRepositoryAccessRequest,
@@ -914,7 +914,7 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
fn replace_worker_workdir_attachments(
&self,
worker_id: &str,
_attachments: Vec<WorkingDirectoryAttachmentClaim>,
_attachments: Vec<LogicalWorkdirAttachment>,
) -> WorkerWorkspaceApiResult {
WorkerWorkspaceApiResult {
state: WorkerOperationState::Unsupported,
@@ -1516,7 +1516,7 @@ impl RuntimeRegistry {
pub fn replace_worker_workdir_attachments(
&self,
worker: &RuntimeWorkerRef,
attachments: Vec<WorkingDirectoryAttachmentClaim>,
attachments: Vec<LogicalWorkdirAttachment>,
) -> Result<WorkerWorkspaceApiResult, RuntimeRegistryError> {
let runtime_id = worker.runtime_id.as_str();
let worker_id = worker.worker_id.as_str();
@@ -2469,7 +2469,7 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
fn replace_worker_workdir_attachments(
&self,
worker_id: &str,
attachments: Vec<WorkingDirectoryAttachmentClaim>,
attachments: Vec<LogicalWorkdirAttachment>,
) -> WorkerWorkspaceApiResult {
let Some(worker_ref) = self.worker_ref(worker_id) else {
return WorkerWorkspaceApiResult {
@@ -4453,7 +4453,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
fn replace_worker_workdir_attachments(
&self,
worker_id: &str,
attachments: Vec<WorkingDirectoryAttachmentClaim>,
attachments: Vec<LogicalWorkdirAttachment>,
) -> WorkerWorkspaceApiResult {
let workdir_attachments = match runtime_contract_convert(attachments) {
Ok(attachments) => attachments,
@@ -4466,7 +4466,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
}
};
let request = runtime_api::WorkerWorkdirAttachmentsRequest {
workdir_attachments,
logical_workdir_attachments: workdir_attachments,
};
let worker_id = worker_id.to_string();
match self
+104 -8
View File
@@ -192,12 +192,12 @@ use crate::workspace_signing_identity::{
};
use crate::{Error, Result};
use worker_runtime::catalog::{
ConfigBundleRef, ProfileSelector, RepositoryMaterializationContext, RepositoryRefObservation,
RepositoryRefObservationRequest, RepositorySelector as RuntimeRepositorySelector,
RepositorySshCredentialCandidate, RepositorySshMaterializationAccess, SensitiveString,
WorkingDirectoryAttachmentClaim, WorkingDirectoryAttachmentRequest,
WorkingDirectoryAttachmentStatus, WorkingDirectoryRepository, WorkingDirectoryRequest,
WorkspaceApiRef,
ConfigBundleRef, LogicalWorkdirAttachment, ProfileSelector, RepositoryMaterializationContext,
RepositoryRefObservation, RepositoryRefObservationRequest,
RepositorySelector as RuntimeRepositorySelector, RepositorySshCredentialCandidate,
RepositorySshMaterializationAccess, SensitiveString, WorkingDirectoryAttachmentClaim,
WorkingDirectoryAttachmentRequest, WorkingDirectoryAttachmentStatus,
WorkingDirectoryRepository, WorkingDirectoryRequest, WorkspaceApiRef,
};
use worker_runtime::config_bundle::ConfigBundle;
use worker_runtime::http_server::MAX_WORKER_FILE_UPLOAD_BYTES;
@@ -3064,6 +3064,7 @@ impl WorkspaceApi {
diagnostics: binding.diagnostics,
});
}
sync_runtime_worker_workdir_attachments(self, worker)?;
Ok(self
.runtime
.restore_worker(worker)
@@ -8601,11 +8602,10 @@ fn sync_runtime_worker_workdir_attachments(
.into_iter()
.filter(|link| link.unlinked_at.is_none())
.map(|link| {
Ok(WorkingDirectoryAttachmentClaim {
Ok(LogicalWorkdirAttachment {
alias: workdir::WorkdirAttachmentAlias::new(link.alias)
.map_err(|error| Error::InvalidInput(error.to_string()))?,
working_directory_id: link.workdir_id,
relative_cwd: None,
})
})
.collect::<Result<Vec<_>>>()?;
@@ -8678,6 +8678,18 @@ async fn scoped_attach_current_worker_workdir(
}
let session_lock = current_worker_session_lock(&api, &worker);
let _session_guard = session_lock.lock().await;
if !api
.store
.worker_workdir_link_history_exists(&api.config.workspace_id, &worker)?
{
let observed_worker = api
.runtime
.worker(&worker)
.map_err(|error| error.into_error())?;
if !observed_worker.workdir_attachments.is_empty() {
sync_worker_observation(&api, &observed_worker)?;
}
}
if let Some(existing) = api
.store
.list_worker_workdir_links(&api.config.workspace_id, &worker)?
@@ -21666,6 +21678,90 @@ mod tests {
assert!(matches!(error, Error::WorkerSourceIdentity(_)));
}
#[tokio::test]
async fn cross_runtime_attachment_sync_does_not_require_worker_runtime_materialization() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let Json(created) = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(CreateWorkspaceWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: "Cross-runtime Worker".to_string(),
profile: Some("builtin:coder".to_string()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
control_operation_id: None,
}),
)
.await
.unwrap();
let worker = RuntimeWorkerRef::new(&created.runtime_id, &created.worker_id);
let workdir_id = "cross-runtime-workdir";
api.store
.upsert_workdir_registry(&WorkdirRegistryRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
workdir_id: workdir_id.to_string(),
display_name: Some("Cross-runtime checkout".to_string()),
runtime_id: "arcadia".to_string(),
repository_id: test_repository_id(&api),
creation_selector: Some("HEAD".to_string()),
creation_ref: None,
creation_tree: None,
current_selector: Some("HEAD".to_string()),
current_ref: None,
current_tree: None,
observed_at_epoch_seconds: None,
materialization_status: "present".to_string(),
cleanliness: "clean".to_string(),
created_at: TEST_CREATED_AT.to_string(),
updated_at: TEST_CREATED_AT.to_string(),
})
.unwrap();
api.store
.attach_worker_workdir(&WorkerWorkdirLinkRecord {
workspace_id: TEST_WORKSPACE_ID.to_string(),
worker: worker.clone(),
workdir_id: workdir_id.to_string(),
alias: "checkout".to_string(),
linked_at: TEST_CREATED_AT.to_string(),
unlinked_at: None,
})
.unwrap();
sync_runtime_worker_workdir_attachments(&api, &worker).unwrap();
assert!(
api.runtime
.worker(&worker)
.unwrap()
.workdir_attachments
.is_empty(),
"logical attachment must not fabricate a Runtime-local binding"
);
api.runtime
.stop_worker(
&worker,
WorkerLifecycleRequest {
reason: Some("exercise logical attachment reconciliation".to_string()),
ticket_assignment: None,
},
)
.unwrap();
sync_runtime_worker_workdir_attachments(&api, &worker).unwrap();
assert_eq!(
api.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
.unwrap()
.into_iter()
.filter(|link| link.unlinked_at.is_none())
.count(),
1
);
}
#[tokio::test]
async fn merge_request_completion_authority_requires_current_online_orchestrator() {
let workspace = tempfile::tempdir().unwrap();