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
+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();