fix: reconcile worker restores by operation

This commit is contained in:
2026-09-26 22:35:43 +09:00
parent a9c7f79012
commit 343af9ea3c
3 changed files with 274 additions and 70 deletions
+14
View File
@@ -388,6 +388,13 @@ pub trait WorkerExecutionBackend: Send + Sync + 'static {
Ok(()) Ok(())
} }
fn reconcile_restore(
&self,
request: WorkerExecutionRestoreRequest,
) -> WorkerExecutionSpawnResult {
self.restore_worker(request)
}
fn restore_worker( fn restore_worker(
&self, &self,
_request: WorkerExecutionRestoreRequest, _request: WorkerExecutionRestoreRequest,
@@ -608,6 +615,13 @@ impl WorkerExecutionBackendRef {
self.backend.preflight_restore(request) self.backend.preflight_restore(request)
} }
pub(crate) fn reconcile_restore(
&self,
request: WorkerExecutionRestoreRequest,
) -> WorkerExecutionSpawnResult {
self.backend.reconcile_restore(request)
}
pub(crate) fn restore_worker( pub(crate) fn restore_worker(
&self, &self,
request: WorkerExecutionRestoreRequest, request: WorkerExecutionRestoreRequest,
+100 -50
View File
@@ -1668,19 +1668,25 @@ impl Runtime {
worker_ref: &WorkerRef, worker_ref: &WorkerRef,
mode: WorkerRestoreMode, mode: WorkerRestoreMode,
) -> Result<RuntimeWorkerRestoreResult, RuntimeError> { ) -> Result<RuntimeWorkerRestoreResult, RuntimeError> {
let operation_id = WorkerLifecycleOperationId::new(); let fresh_operation_id = WorkerLifecycleOperationId::new();
let (backend, request) = { let (backend, request, operation_id, operation_mode, reconcile) = {
let state = self.lock()?; let state = self.lock()?;
state.ensure_running()?; state.ensure_running()?;
let worker = state.worker(worker_ref)?; let worker = state.worker(worker_ref)?;
if worker.has_pending_lifecycle_operation() { if worker.pending_stop.is_some() {
return Ok(RuntimeWorkerRestoreResult::failed( return Ok(RuntimeWorkerRestoreResult::failed(
WorkerRestoreState::ReconciliationRequired, WorkerRestoreState::ReconciliationRequired,
"worker_restore_reconciliation_pending", "worker_stop_reconciliation_pending",
"Worker restore reconciliation is still pending", "Worker stop reconciliation is still pending",
)); ));
} }
if worker.execution_handle.is_some() { let pending_restore = worker.pending_restore;
let reconcile = pending_restore.is_some();
let operation_id = pending_restore
.map(|pending| pending.operation_id)
.unwrap_or(fresh_operation_id);
let operation_mode = pending_restore.map(|pending| pending.mode).unwrap_or(mode);
if !reconcile && worker.execution_handle.is_some() {
if worker.status.is_active() { if worker.status.is_active() {
return Ok(RuntimeWorkerRestoreResult::accepted(worker.detail())); return Ok(RuntimeWorkerRestoreResult::accepted(worker.detail()));
} }
@@ -1695,7 +1701,8 @@ impl Runtime {
// before Workspace Backend renewed operation-scoped Repository // before Workspace Backend renewed operation-scoped Repository
// access). Preserve its automatic intent so an explicit restore can // access). Preserve its automatic intent so an explicit restore can
// serve as the authorized retry for that state. // serve as the authorized retry for that state.
if mode == WorkerRestoreMode::Explicit if !reconcile
&& mode == WorkerRestoreMode::Explicit
&& worker.execution_metadata_available && worker.execution_metadata_available
&& worker.status != WorkerStatus::Stopped && worker.status != WorkerStatus::Stopped
&& worker.restore_intent != WorkerRestoreIntent::Automatic && worker.restore_intent != WorkerRestoreIntent::Automatic
@@ -1706,7 +1713,8 @@ impl Runtime {
"Worker restore requires a stopped Worker", "Worker restore requires a stopped Worker",
)); ));
} }
if mode == WorkerRestoreMode::Automatic if !reconcile
&& mode == WorkerRestoreMode::Automatic
&& (!worker.status.is_active() && (!worker.status.is_active()
|| worker.restore_intent != WorkerRestoreIntent::Automatic) || worker.restore_intent != WorkerRestoreIntent::Automatic)
{ {
@@ -1743,10 +1751,10 @@ impl Runtime {
workdir_attachments: BTreeMap::new(), workdir_attachments: BTreeMap::new(),
config_bundle: None, config_bundle: None,
}; };
(backend, request) (backend, request, operation_id, operation_mode, reconcile)
}; };
if let Err(_result) = backend.preflight_restore(&request) { if !reconcile && let Err(_result) = backend.preflight_restore(&request) {
return Ok(RuntimeWorkerRestoreResult::failed( return Ok(RuntimeWorkerRestoreResult::failed(
WorkerRestoreState::Rejected, WorkerRestoreState::Rejected,
"worker_restore_preflight_rejected", "worker_restore_preflight_rejected",
@@ -1754,7 +1762,10 @@ impl Runtime {
)); ));
} }
if let Err(error) = self.begin_restore_operation(worker_ref, operation_id, mode) { if !reconcile
&& let Err(error) =
self.begin_restore_operation(worker_ref, operation_id, operation_mode)
{
if matches!(error, RuntimeError::StoreCommitOutcomeUnknown { .. }) { if matches!(error, RuntimeError::StoreCommitOutcomeUnknown { .. }) {
return Ok(RuntimeWorkerRestoreResult::failed( return Ok(RuntimeWorkerRestoreResult::failed(
WorkerRestoreState::ReconciliationRequired, WorkerRestoreState::ReconciliationRequired,
@@ -1765,7 +1776,12 @@ impl Runtime {
return Err(error); return Err(error);
} }
match backend.restore_worker(request) { let restore_result = if reconcile {
backend.reconcile_restore(request)
} else {
backend.restore_worker(request)
};
match restore_result {
WorkerExecutionSpawnResult::Connected { WorkerExecutionSpawnResult::Connected {
handle, handle,
worker_state, worker_state,
@@ -1860,6 +1876,13 @@ impl Runtime {
} }
} }
WorkerExecutionSpawnResult::Rejected(_result) => { WorkerExecutionSpawnResult::Rejected(_result) => {
if reconcile {
return Ok(RuntimeWorkerRestoreResult::failed(
WorkerRestoreState::ReconciliationRequired,
"worker_restore_reconciliation_rejected",
"Worker restore reconciliation was not completed",
));
}
if let Err(error) = self.settle_restore_rollback(worker_ref) { if let Err(error) = self.settle_restore_rollback(worker_ref) {
tracing::error!( tracing::error!(
worker_id = %worker_ref.worker_id, worker_id = %worker_ref.worker_id,
@@ -2922,6 +2945,32 @@ impl Runtime {
} }
} }
let pending_restores = {
let state = self.lock()?;
state
.workers
.values()
.filter_map(|worker| {
worker
.pending_restore
.map(|pending| (worker.worker_ref.clone(), pending.mode))
})
.collect::<Vec<_>>()
};
for (worker_ref, mode) in pending_restores {
let operation_lock = self.worker_operation_lock(worker_ref.worker_id)?;
let _operation_guard = operation_lock
.lock()
.map_err(|_| RuntimeError::StatePoisoned)?;
if let Err(error) = self.restore_worker_under_lock(&worker_ref, mode) {
tracing::warn!(
worker_id = %worker_ref.worker_id,
error = %error,
"Worker restore reconciliation remains pending after Runtime startup"
);
}
}
let candidates = { let candidates = {
let state = self.lock()?; let state = self.lock()?;
if state.execution_backend.is_none() { if state.execution_backend.is_none() {
@@ -8806,18 +8855,20 @@ mod tests {
let retry = runtime let retry = runtime
.restore_worker_operation(&created.worker_ref) .restore_worker_operation(&created.worker_ref)
.unwrap(); .unwrap();
assert_eq!(retry.state, WorkerRestoreState::ReconciliationRequired); assert_eq!(retry.state, WorkerRestoreState::Accepted);
assert_eq!(*backend.restore_count.lock().unwrap(), 1); assert_eq!(*backend.restore_count.lock().unwrap(), 2);
assert_eq!( assert_eq!(
backend.restore_operation_ids.lock().unwrap().as_slice(),
&[operation_id, operation_id]
);
assert!(
runtime runtime
.lock() .lock()
.unwrap() .unwrap()
.worker(&created.worker_ref) .worker(&created.worker_ref)
.unwrap() .unwrap()
.pending_restore .pending_restore
.unwrap() .is_none()
.operation_id,
operation_id
); );
} }
@@ -9226,24 +9277,28 @@ mod tests {
let retry = restarted let retry = restarted
.restore_worker_operation(&worker.worker_ref) .restore_worker_operation(&worker.worker_ref)
.unwrap(); .unwrap();
assert_eq!(retry.state, WorkerRestoreState::ReconciliationRequired); assert_eq!(retry.state, WorkerRestoreState::Accepted);
assert_eq!(*restarted_backend.restore_count.lock().unwrap(), 0); assert_eq!(*restarted_backend.restore_count.lock().unwrap(), 1);
assert_eq!( assert_eq!(
restarted_backend
.restore_operation_ids
.lock()
.unwrap()
.as_slice(),
&[operation_id]
);
assert!(
restarted restarted
.lock() .lock()
.unwrap() .unwrap()
.worker(&worker.worker_ref) .worker(&worker.worker_ref)
.unwrap() .unwrap()
.pending_restore .pending_restore
.unwrap() .is_none()
.operation_id,
operation_id
); );
let delete_error = restarted.delete_worker(&worker.worker_ref).unwrap_err(); assert_eq!(
assert!( restarted.worker_detail(&worker.worker_ref).unwrap().status,
delete_error WorkerStatus::Idle
.to_string()
.contains("pending restore reconciliation")
); );
let _ = std::fs::remove_dir_all(root); let _ = std::fs::remove_dir_all(root);
@@ -9298,37 +9353,32 @@ mod tests {
let retry = restored let retry = restored
.restore_worker_operation(&worker.worker_ref) .restore_worker_operation(&worker.worker_ref)
.unwrap(); .unwrap();
assert_eq!(retry.state, WorkerRestoreState::ReconciliationRequired); assert_eq!(retry.state, WorkerRestoreState::Accepted);
assert_eq!(*restoring_backend.restore_count.lock().unwrap(), 1); assert_eq!(*restoring_backend.restore_count.lock().unwrap(), 2);
drop(restored);
let restarted_backend = Arc::new(TestExecutionBackend::default());
let restarted = Runtime::with_fs_store_and_execution_backend(
crate::fs_store::FsRuntimeStoreOptions {
root: root.clone(),
runtime_id: "test-runtime".to_string(),
display_name: None,
},
restarted_backend.clone(),
)
.unwrap();
assert_eq!(*restarted_backend.restore_count.lock().unwrap(), 0);
assert_eq!( assert_eq!(
restarted restoring_backend
.restore_operation_ids
.lock()
.unwrap()
.as_slice(),
&[operation_id, operation_id]
);
assert!(
restored
.lock() .lock()
.unwrap() .unwrap()
.worker(&worker.worker_ref) .worker(&worker.worker_ref)
.unwrap() .unwrap()
.pending_restore .pending_restore
.unwrap() .is_none()
.operation_id,
operation_id
); );
let retry = restarted restored
.restore_worker_operation(&worker.worker_ref) .send_input(
&worker.worker_ref,
WorkerInput::user("after restore reconciliation"),
)
.unwrap(); .unwrap();
assert_eq!(retry.state, WorkerRestoreState::ReconciliationRequired); restored.stop_worker(&worker.worker_ref, None).unwrap();
assert_eq!(*restarted_backend.restore_count.lock().unwrap(), 0);
let _ = std::fs::remove_dir_all(root); let _ = std::fs::remove_dir_all(root);
} }
+160 -20
View File
@@ -472,6 +472,17 @@ impl ProfileRuntimeWorkerFactory {
.join(uuid::Uuid::now_v7().to_string())) .join(uuid::Uuid::now_v7().to_string()))
} }
fn worker_restore_run_dir(
&self,
worker_ref: &WorkerRef,
operation_id: crate::execution::WorkerLifecycleOperationId,
) -> Result<PathBuf, String> {
Ok(self
.worker_aggregate_dir(worker_ref)?
.join("runs")
.join(format!("restore-{operation_id}")))
}
fn runtime_worker_name_for_ref(worker_ref: &crate::identity::WorkerRef) -> String { fn runtime_worker_name_for_ref(worker_ref: &crate::identity::WorkerRef) -> String {
format!("worker-runtime-{}", worker_ref.worker_id) format!("worker-runtime-{}", worker_ref.worker_id)
} }
@@ -1423,7 +1434,8 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
} }
let workspace_client = worker.workspace_client_handle(); let workspace_client = worker.workspace_client_handle();
let run_dir = self.worker_run_dir(&request.worker_ref)?; let run_dir =
self.worker_restore_run_dir(&request.worker_ref, request.operation_id)?;
let bash_output_dir = bash_output_dir_for_worker_id(&request.worker_ref.worker_id); let bash_output_dir = bash_output_dir_for_worker_id(&request.worker_ref.worker_id);
let started = PreparedWorker::new( let started = PreparedWorker::new(
worker, worker,
@@ -1567,6 +1579,8 @@ struct RuntimeWorkerExecution {
tasks: RuntimeExecutionTaskScope, tasks: RuntimeExecutionTaskScope,
worker_state: Arc<RwLock<protocol::WorkerStateSnapshot>>, worker_state: Arc<RwLock<protocol::WorkerStateSnapshot>>,
workspace_client: Option<Arc<dyn WorkspaceClient>>, workspace_client: Option<Arc<dyn WorkspaceClient>>,
restore_operation_id: Option<crate::execution::WorkerLifecycleOperationId>,
workdir_attachments: Vec<crate::catalog::WorkingDirectoryAttachmentStatus>,
} }
/// `worker-runtime` execution backend backed by real `worker` crate Workers. /// `worker-runtime` execution backend backed by real `worker` crate Workers.
@@ -1836,6 +1850,8 @@ where
tasks: RuntimeExecutionTaskScope, tasks: RuntimeExecutionTaskScope,
worker_state: Arc<RwLock<protocol::WorkerStateSnapshot>>, worker_state: Arc<RwLock<protocol::WorkerStateSnapshot>>,
workspace_client: Option<Arc<dyn WorkspaceClient>>, workspace_client: Option<Arc<dyn WorkspaceClient>>,
restore_operation_id: Option<crate::execution::WorkerLifecycleOperationId>,
workdir_attachments: Vec<crate::catalog::WorkingDirectoryAttachmentStatus>,
) -> Result<WorkerExecutionHandle, String> { ) -> Result<WorkerExecutionHandle, String> {
let mut workers = self let mut workers = self
.workers .workers
@@ -1853,6 +1869,8 @@ where
tasks, tasks,
worker_state, worker_state,
workspace_client, workspace_client,
restore_operation_id,
workdir_attachments,
}, },
); );
Ok(WorkerExecutionHandle::new( Ok(WorkerExecutionHandle::new(
@@ -1907,6 +1925,7 @@ where
handle: WorkerHandle, handle: WorkerHandle,
shutdown: Arc<tokio::sync::Mutex<Option<worker::ShutdownReceiver>>>, shutdown: Arc<tokio::sync::Mutex<Option<worker::ShutdownReceiver>>>,
controller_task: tokio::task::JoinHandle<()>, controller_task: tokio::task::JoinHandle<()>,
restore_operation_id: Option<crate::execution::WorkerLifecycleOperationId>,
workdir_attachments: BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>, workdir_attachments: BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>,
workspace_client: Option<Arc<dyn WorkspaceClient>>, workspace_client: Option<Arc<dyn WorkspaceClient>>,
) -> WorkerExecutionSpawnResult { ) -> WorkerExecutionSpawnResult {
@@ -1921,6 +1940,15 @@ where
let worker_state_snapshot = handle.shared_state.snapshot(); let worker_state_snapshot = handle.shared_state.snapshot();
let worker_state = Arc::new(RwLock::new(worker_state_snapshot)); let worker_state = Arc::new(RwLock::new(worker_state_snapshot));
let tasks = RuntimeExecutionTaskScope::new(controller_task); let tasks = RuntimeExecutionTaskScope::new(controller_task);
let workdir_attachment_statuses = workdir_attachments
.iter()
.map(
|(alias, binding)| crate::catalog::WorkingDirectoryAttachmentStatus {
alias: alias.clone(),
working_directory: binding.status(),
},
)
.collect::<Vec<_>>();
#[cfg(feature = "ws-server")] #[cfg(feature = "ws-server")]
{ {
let mut events = streams.events; let mut events = streams.events;
@@ -1987,13 +2015,8 @@ where
Err(_) => { Err(_) => {
let retained_state = let retained_state =
worker_state.read().ok().map(|state| state.clone()); worker_state.read().ok().map(|state| state.clone());
let retained_workdir_attachments = workdir_attachments let retained_workdir_attachments =
.iter() workdir_attachment_statuses.clone();
.map(|(alias, binding)| crate::catalog::WorkingDirectoryAttachmentStatus {
alias: alias.clone(),
working_directory: binding.status(),
})
.collect();
let retained_handle = self let retained_handle = self
.retain_uncertain_unconnected_controller( .retain_uncertain_unconnected_controller(
&worker_ref, &worker_ref,
@@ -2002,6 +2025,8 @@ where
tasks, tasks,
worker_state, worker_state,
workspace_client, workspace_client,
restore_operation_id,
retained_workdir_attachments.clone(),
) )
.ok(); .ok();
WorkerExecutionSpawnResult::ReconciliationRequired { WorkerExecutionSpawnResult::ReconciliationRequired {
@@ -2072,21 +2097,15 @@ where
tasks, tasks,
worker_state, worker_state,
workspace_client, workspace_client,
restore_operation_id,
workdir_attachments: workdir_attachment_statuses.clone(),
}, },
); );
WorkerExecutionSpawnResult::Connected { WorkerExecutionSpawnResult::Connected {
handle: WorkerExecutionHandle::new(worker_ref, self.backend_id()), handle: WorkerExecutionHandle::new(worker_ref, self.backend_id()),
worker_state: connected_worker_state, worker_state: connected_worker_state,
workdir_attachments: workdir_attachments workdir_attachments: workdir_attachment_statuses,
.into_iter()
.map(
|(alias, binding)| crate::catalog::WorkingDirectoryAttachmentStatus {
alias,
working_directory: binding.status(),
},
)
.collect(),
} }
} }
} }
@@ -2338,6 +2357,7 @@ where
controller.handle, controller.handle,
controller.shutdown, controller.shutdown,
controller.controller_task, controller.controller_task,
None,
BTreeMap::new(), BTreeMap::new(),
Some(controller.workspace_client), Some(controller.workspace_client),
); );
@@ -2465,6 +2485,7 @@ where
controller.handle, controller.handle,
controller.shutdown, controller.shutdown,
controller.controller_task, controller.controller_task,
None,
workdir_attachments, workdir_attachments,
Some(controller.workspace_client), Some(controller.workspace_client),
) )
@@ -2577,10 +2598,25 @@ where
.workers .workers
.lock() .lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()); .unwrap_or_else(|poisoned| poisoned.into_inner());
if workers.contains_key(&request.worker_ref) { if let Some(existing) = workers.get(&request.worker_ref) {
if existing.restore_operation_id == Some(request.operation_id) {
let worker_state = existing
.worker_state
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
return WorkerExecutionSpawnResult::Connected {
handle: WorkerExecutionHandle::new(
request.worker_ref.clone(),
self.backend_id(),
),
worker_state,
workdir_attachments: existing.workdir_attachments.clone(),
};
}
return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::busy( return WorkerExecutionSpawnResult::Rejected(WorkerExecutionResult::busy(
WorkerExecutionOperation::Restore, WorkerExecutionOperation::Restore,
"Worker is already connected to execution backend", "Worker is already connected to execution backend by another lifecycle operation",
)); ));
} }
drop(workers); drop(workers);
@@ -2671,6 +2707,7 @@ where
let factory = self.factory.clone(); let factory = self.factory.clone();
let bridge_context = request.context.clone(); let bridge_context = request.context.clone();
let worker_ref = request.worker_ref.clone(); let worker_ref = request.worker_ref.clone();
let operation_id = request.operation_id;
let restore_result = self let restore_result = self
.run_cancellable_on_adapter_runtime(self.spawn_restore_timeout, async move { .run_cancellable_on_adapter_runtime(self.spawn_restore_timeout, async move {
factory.restore_controller(request).await factory.restore_controller(request).await
@@ -2698,6 +2735,7 @@ where
controller.handle, controller.handle,
controller.shutdown, controller.shutdown,
controller.controller_task, controller.controller_task,
Some(operation_id),
workdir_attachments, workdir_attachments,
Some(controller.workspace_client), Some(controller.workspace_client),
) )
@@ -3555,7 +3593,13 @@ mod tests {
fn test_execution_context(worker_ref: WorkerRef) -> WorkerExecutionContext { fn test_execution_context(worker_ref: WorkerRef) -> WorkerExecutionContext {
WorkerExecutionContext::new( WorkerExecutionContext::new(
worker_ref, worker_ref,
Arc::new(|_, _| panic!("unused test event sink")), Arc::new(|worker_ref, payload| {
Ok(crate::observation::WorkerObservationEvent::new(
1,
worker_ref,
payload,
))
}),
) )
} }
@@ -4562,6 +4606,102 @@ mod tests {
drop(restored_backend); drop(restored_backend);
} }
#[test]
#[serial_test::serial(worker_allocation)]
fn restore_reconciliation_reuses_the_operation_controller() {
let client = MockClient::sequential(Vec::new());
let runtime_base = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
let observed_workspace_clients = Arc::new(Mutex::new(Vec::new()));
let factory = MockFactory {
client,
runtime_base: runtime_base.path().to_path_buf(),
cwd: cwd.path().to_path_buf(),
store_dir: store.path().join("sessions"),
worker_metadata_dir: store.path().join("workers"),
observed_cwds: Arc::new(Mutex::new(Vec::new())),
observed_workspace_clients: observed_workspace_clients.clone(),
};
let backend = Arc::new(WorkerRuntimeExecutionBackend::new(factory).unwrap());
let runtime = EmbeddedRuntime::with_execution_backend(
RuntimeOptions::default(),
backend.clone(),
)
.unwrap();
runtime.store_config_bundle(test_bundle()).unwrap();
let request = create_request("idempotent restore reconciliation");
let worker = runtime.create_worker(request.clone()).unwrap();
runtime.stop_worker(&worker.worker_ref, None).unwrap();
let operation_id = crate::execution::WorkerLifecycleOperationId::new();
let restore_request = WorkerExecutionRestoreRequest {
operation_id,
worker_ref: worker.worker_ref.clone(),
request,
workspace_scope: None,
context: test_execution_context(worker.worker_ref.clone()),
previous_workdir_attachments: Vec::new(),
logical_workdir_attachments: Vec::new(),
workdir_attachments: BTreeMap::new(),
config_bundle: None,
};
let first = backend.restore_worker(restore_request.clone());
let first_handle = match first {
WorkerExecutionSpawnResult::Connected { handle, .. } => handle,
other => panic!("initial restore was not connected: {other:?}"),
};
let factory_calls_after_restore = observed_workspace_clients.lock().unwrap().len();
let reconciled = backend.reconcile_restore(restore_request);
assert!(matches!(
reconciled,
WorkerExecutionSpawnResult::Connected { .. }
));
assert_eq!(
observed_workspace_clients.lock().unwrap().len(),
factory_calls_after_restore,
"reconciliation must not construct a second controller"
);
let workers = backend.workers.lock().unwrap();
let execution = workers.get(&worker.worker_ref).unwrap();
assert_eq!(execution.restore_operation_id, Some(operation_id));
assert_eq!(workers.len(), 1);
drop(workers);
assert!(backend.stop_worker(&first_handle).is_accepted());
}
#[test]
fn profile_restore_run_directory_is_operation_scoped() {
let root = tempfile::tempdir().unwrap();
let runtime_store_dir = root.path().join("runtime");
let factory = ProfileRuntimeWorkerFactory::new(root.path())
.with_runtime_store_dir(&runtime_store_dir);
let worker_ref = WorkerRef::new(WorkerId::now_v7());
let operation_id = crate::execution::WorkerLifecycleOperationId::new();
let first = factory
.worker_restore_run_dir(&worker_ref, operation_id)
.unwrap();
let retry = factory
.worker_restore_run_dir(&worker_ref, operation_id)
.unwrap();
let different = factory
.worker_restore_run_dir(
&worker_ref,
crate::execution::WorkerLifecycleOperationId::new(),
)
.unwrap();
assert_eq!(first, retry);
assert_ne!(first, different);
assert_eq!(
first.file_name().unwrap().to_string_lossy(),
format!("restore-{operation_id}")
);
}
#[test] #[test]
fn builtin_profile_selector_is_not_double_prefixed() { fn builtin_profile_selector_is_not_double_prefixed() {
assert_eq!( assert_eq!(