fix: make WorkerId the standalone primary identity
This commit is contained in:
Generated
+1
@@ -3508,6 +3508,7 @@ dependencies = [
|
|||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2 0.11.0",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ pub use backend_workspace::{
|
|||||||
};
|
};
|
||||||
pub use client::{Client, ClientError};
|
pub use client::{Client, ClientError};
|
||||||
pub use target::{
|
pub use target::{
|
||||||
BackendTarget, Dashboard, ResolvedTarget, StandaloneSessionListIntent,
|
BackendTarget, Dashboard, ResolvedTarget, StandaloneTarget, StandaloneWorkerListIntent,
|
||||||
StandaloneSessionResumeIntent, StandaloneTarget, Target, TargetError, TargetKind,
|
StandaloneWorkerResumeIntent, Target, TargetError, TargetKind, WorkerConnection,
|
||||||
WorkerConnection, WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerSpawn,
|
WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerSpawn,
|
||||||
};
|
};
|
||||||
pub use workspace_api::{ObjectiveDetail, ObjectiveSummary};
|
pub use workspace_api::{ObjectiveDetail, ObjectiveSummary};
|
||||||
pub use workspace_product::BackendWorkspaceProductClient;
|
pub use workspace_product::BackendWorkspaceProductClient;
|
||||||
|
|||||||
+24
-24
@@ -105,16 +105,16 @@ pub struct WorkerSpawn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct StandaloneSessionListIntent {
|
pub struct StandaloneWorkerListIntent {
|
||||||
pub state_dir: PathBuf,
|
pub state_dir: PathBuf,
|
||||||
pub cwd: PathBuf,
|
pub cwd: PathBuf,
|
||||||
pub include_all: bool,
|
pub include_all: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct StandaloneSessionResumeIntent {
|
pub struct StandaloneWorkerResumeIntent {
|
||||||
pub state_dir: PathBuf,
|
pub state_dir: PathBuf,
|
||||||
pub session_id: String,
|
pub worker_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -175,22 +175,22 @@ pub trait Target: fmt::Debug + Send + Sync {
|
|||||||
Err(TargetError::unsupported("Worker spawn", self.kind()))
|
Err(TargetError::unsupported("Worker spawn", self.kind()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn standalone_session_list(
|
fn standalone_worker_list(
|
||||||
&self,
|
&self,
|
||||||
_include_all: bool,
|
_include_all: bool,
|
||||||
) -> Result<StandaloneSessionListIntent, TargetError> {
|
) -> Result<StandaloneWorkerListIntent, TargetError> {
|
||||||
Err(TargetError::unsupported(
|
Err(TargetError::unsupported(
|
||||||
"standalone session listing",
|
"standalone Worker listing",
|
||||||
self.kind(),
|
self.kind(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn standalone_session_resume(
|
fn standalone_worker_resume(
|
||||||
&self,
|
&self,
|
||||||
_session_id: String,
|
_worker_id: String,
|
||||||
) -> Result<StandaloneSessionResumeIntent, TargetError> {
|
) -> Result<StandaloneWorkerResumeIntent, TargetError> {
|
||||||
Err(TargetError::unsupported(
|
Err(TargetError::unsupported(
|
||||||
"standalone session restore",
|
"standalone Worker restore",
|
||||||
self.kind(),
|
self.kind(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -243,26 +243,26 @@ impl Target for StandaloneTarget {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn standalone_session_list(
|
fn standalone_worker_list(
|
||||||
&self,
|
&self,
|
||||||
include_all: bool,
|
include_all: bool,
|
||||||
) -> Result<StandaloneSessionListIntent, TargetError> {
|
) -> Result<StandaloneWorkerListIntent, TargetError> {
|
||||||
let cwd = std::env::current_dir()
|
let cwd = std::env::current_dir()
|
||||||
.map_err(|error| TargetError::invalid(self.kind(), error.to_string()))?;
|
.map_err(|error| TargetError::invalid(self.kind(), error.to_string()))?;
|
||||||
Ok(StandaloneSessionListIntent {
|
Ok(StandaloneWorkerListIntent {
|
||||||
state_dir: self.state_dir.clone(),
|
state_dir: self.state_dir.clone(),
|
||||||
cwd,
|
cwd,
|
||||||
include_all,
|
include_all,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn standalone_session_resume(
|
fn standalone_worker_resume(
|
||||||
&self,
|
&self,
|
||||||
session_id: String,
|
worker_id: String,
|
||||||
) -> Result<StandaloneSessionResumeIntent, TargetError> {
|
) -> Result<StandaloneWorkerResumeIntent, TargetError> {
|
||||||
Ok(StandaloneSessionResumeIntent {
|
Ok(StandaloneWorkerResumeIntent {
|
||||||
state_dir: self.state_dir.clone(),
|
state_dir: self.state_dir.clone(),
|
||||||
session_id,
|
worker_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,17 +437,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn standalone_target_builds_explicit_session_intents() {
|
fn standalone_target_builds_explicit_worker_intents() {
|
||||||
let target = StandaloneTarget::new("/tmp/yoi-client-sessions");
|
let target = StandaloneTarget::new("/tmp/yoi-client-workers");
|
||||||
let list = target.standalone_session_list(true).unwrap();
|
let list = target.standalone_worker_list(true).unwrap();
|
||||||
assert_eq!(list.state_dir, PathBuf::from("/tmp/yoi-client-sessions"));
|
assert_eq!(list.state_dir, PathBuf::from("/tmp/yoi-client-workers"));
|
||||||
assert!(list.include_all);
|
assert!(list.include_all);
|
||||||
assert!(list.cwd.is_absolute());
|
assert!(list.cwd.is_absolute());
|
||||||
|
|
||||||
let resume = target
|
let resume = target
|
||||||
.standalone_session_resume("019d1234-0000-7000-8000-000000000000".to_string())
|
.standalone_worker_resume("019d1234-0000-7000-8000-000000000000".to_string())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resume.state_dir, list.state_dir);
|
assert_eq!(resume.state_dir, list.state_dir);
|
||||||
assert_eq!(resume.session_id, "019d1234-0000-7000-8000-000000000000");
|
assert_eq!(resume.worker_id, "019d1234-0000-7000-8000-000000000000");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ json-schema = ["dep:schemars"]
|
|||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
sha2.workspace = true
|
||||||
tokio = { workspace = true, features = ["io-util"], optional = true }
|
tokio = { workspace = true, features = ["io-util"], optional = true }
|
||||||
ts-rs = { version = "12.0.1", optional = true }
|
ts-rs = { version = "12.0.1", optional = true }
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::{Uuid, Version};
|
||||||
|
|
||||||
|
/// Stable Worker identity independent of its current Runtime placement or
|
||||||
|
/// conversation Session.
|
||||||
|
///
|
||||||
|
/// Workspace authority allocates this ID for managed Workers. A standalone
|
||||||
|
/// Worker store allocates it locally when no Workspace authority is present.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct WorkerId(Uuid);
|
||||||
|
|
||||||
|
impl WorkerId {
|
||||||
|
pub fn now_v7() -> Self {
|
||||||
|
Self(Uuid::now_v7())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a legacy Runtime-local numeric id into a syntactically valid
|
||||||
|
/// migration-only UUIDv7 value. New Worker allocation must use `now_v7`.
|
||||||
|
pub fn from_legacy_u64(value: u64) -> Self {
|
||||||
|
let mut bytes = [0_u8; 16];
|
||||||
|
bytes[8..].copy_from_slice(&value.to_be_bytes());
|
||||||
|
bytes[6] = 0x70;
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
|
Self(Uuid::from_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_legacy_binding(workspace_id: &str, runtime_id: &str, value: u64) -> Self {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(b"yoi.workspace-worker-id.v1\0");
|
||||||
|
hasher.update(workspace_id.as_bytes());
|
||||||
|
hasher.update([0]);
|
||||||
|
hasher.update(runtime_id.as_bytes());
|
||||||
|
hasher.update([0]);
|
||||||
|
hasher.update(value.to_be_bytes());
|
||||||
|
let digest = hasher.finalize();
|
||||||
|
let mut bytes = [0_u8; 16];
|
||||||
|
bytes.copy_from_slice(&digest[..16]);
|
||||||
|
// Migrated ids sort before normally allocated UUIDv7 values while retaining
|
||||||
|
// deterministic collision-resistant payload bits.
|
||||||
|
bytes[..6].fill(0);
|
||||||
|
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
|
Self(Uuid::from_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(value: &str) -> Option<Self> {
|
||||||
|
let value = Uuid::parse_str(value).ok()?;
|
||||||
|
(value.get_version() == Some(Version::SortRand)).then_some(Self(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_uuid(self) -> Uuid {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn short(self) -> String {
|
||||||
|
let simple = self.0.simple().to_string();
|
||||||
|
simple[simple.len() - 12..].to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for WorkerId {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(formatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for WorkerId {
|
||||||
|
type Err = WorkerIdParseError;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::parse(value).ok_or(WorkerIdParseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for WorkerId {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for WorkerId {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = String::deserialize(deserializer)?;
|
||||||
|
Self::parse(&value).ok_or_else(|| de::Error::custom("Worker id must be a UUIDv7"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct WorkerIdParseError;
|
||||||
|
|
||||||
|
impl fmt::Display for WorkerIdParseError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str("Worker id must be a UUIDv7")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for WorkerIdParseError {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_id_accepts_only_uuid_v7() {
|
||||||
|
let worker_id = WorkerId::now_v7();
|
||||||
|
assert_eq!(WorkerId::parse(&worker_id.to_string()), Some(worker_id));
|
||||||
|
assert!(WorkerId::parse("30").is_none());
|
||||||
|
assert!(WorkerId::parse(&Uuid::nil().to_string()).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_worker_id_mapping_is_stable() {
|
||||||
|
assert_eq!(
|
||||||
|
WorkerId::from_legacy_binding("workspace", "runtime", 42),
|
||||||
|
WorkerId::from_legacy_binding("workspace", "runtime", 42)
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
WorkerId::from_legacy_binding("workspace", "runtime", 42),
|
||||||
|
WorkerId::from_legacy_binding("workspace", "runtime", 43)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod identity;
|
||||||
#[cfg(feature = "stream")]
|
#[cfg(feature = "stream")]
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
@@ -8,6 +9,8 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use identity::{WorkerId, WorkerIdParseError};
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use agen::llm_client::client::LlmClient;
|
|||||||
use client::Client;
|
use client::Client;
|
||||||
use client::transport::in_process::{Peer as InProcessPeer, Socket as InProcessSocket};
|
use client::transport::in_process::{Peer as InProcessPeer, Socket as InProcessSocket};
|
||||||
use protocol::stream::{decode_method, encode_event};
|
use protocol::stream::{decode_method, encode_event};
|
||||||
use protocol::{Event, Method};
|
use protocol::{Event, Method, WorkerId};
|
||||||
use session_store::{
|
use session_store::{
|
||||||
CombinedStore, FsStore, FsWorkerStore, WorkerActiveSegmentRef, WorkerMetadataStore,
|
CombinedStore, FsStore, FsWorkerStore, WorkerActiveSegmentRef, WorkerMetadataStore,
|
||||||
};
|
};
|
||||||
@@ -20,14 +20,14 @@ use worker::{BootstrappedWorker, WorkerError, WorkerFilesystemAuthority, WorkerW
|
|||||||
|
|
||||||
use crate::launch::ResolvedStandaloneLaunch;
|
use crate::launch::ResolvedStandaloneLaunch;
|
||||||
use crate::store::{
|
use crate::store::{
|
||||||
StaleLeasePolicy, StandaloneSessionId, StandaloneSessionLease, StandaloneSessionRecord,
|
StaleLeasePolicy, StandaloneShutdownReason, StandaloneStoreError, StandaloneWorkerLease,
|
||||||
StandaloneSessionStore, StandaloneShutdownReason, StandaloneStoreError,
|
StandaloneWorkerRecord, StandaloneWorkerStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
|
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
type StandaloneBackingStore = CombinedStore<FsStore, FsWorkerStore>;
|
type StandaloneBackingStore = CombinedStore<FsStore, FsWorkerStore>;
|
||||||
|
|
||||||
/// One client-owned top-level Worker and its standalone session authority.
|
/// One client-owned top-level Worker and its standalone Worker authority.
|
||||||
///
|
///
|
||||||
/// The host deliberately exposes the existing typed Worker protocol rather than owning an
|
/// The host deliberately exposes the existing typed Worker protocol rather than owning an
|
||||||
/// HTTP/WebSocket server or creating Runtime/Workspace/Ticket/Workdir domain records.
|
/// HTTP/WebSocket server or creating Runtime/Workspace/Ticket/Workdir domain records.
|
||||||
@@ -35,21 +35,21 @@ pub struct StandaloneHost {
|
|||||||
handle: worker::WorkerHandle,
|
handle: worker::WorkerHandle,
|
||||||
shutdown: Option<worker::controller::ShutdownReceiver>,
|
shutdown: Option<worker::controller::ShutdownReceiver>,
|
||||||
shutdown_timeout: Duration,
|
shutdown_timeout: Duration,
|
||||||
store: StandaloneSessionStore,
|
store: StandaloneWorkerStore,
|
||||||
worker_store: FsWorkerStore,
|
worker_store: FsWorkerStore,
|
||||||
record: StandaloneSessionRecord,
|
record: StandaloneWorkerRecord,
|
||||||
lease: Option<StandaloneSessionLease>,
|
lease: Option<StandaloneWorkerLease>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
pub enum StandaloneStartupError {
|
pub enum StandaloneStartupError {
|
||||||
#[error("the standalone state store could not be opened or validated")]
|
#[error("the standalone state store could not be opened or validated")]
|
||||||
StateStore,
|
StateStore,
|
||||||
#[error("the standalone session is already active")]
|
#[error("the standalone Worker is already active")]
|
||||||
SessionActive,
|
WorkerActive,
|
||||||
#[error("the standalone session lease cannot be observed safely; recovery is rejected")]
|
#[error("the standalone Worker lease cannot be observed safely; recovery is rejected")]
|
||||||
LeaseLivenessUnknown,
|
LeaseLivenessUnknown,
|
||||||
#[error("the standalone session working directory is unavailable or changed")]
|
#[error("the standalone Worker working directory is unavailable or changed")]
|
||||||
WorkingDirectoryUnavailable,
|
WorkingDirectoryUnavailable,
|
||||||
#[error("the resolved Worker configuration or persisted history is invalid")]
|
#[error("the resolved Worker configuration or persisted history is invalid")]
|
||||||
WorkerConfiguration,
|
WorkerConfiguration,
|
||||||
@@ -67,7 +67,7 @@ pub enum StandaloneShutdownError {
|
|||||||
DeadlineExceeded,
|
DeadlineExceeded,
|
||||||
#[error("the standalone Worker shutdown confirmation was lost")]
|
#[error("the standalone Worker shutdown confirmation was lost")]
|
||||||
ConfirmationLost,
|
ConfirmationLost,
|
||||||
#[error("the standalone session final state could not be committed")]
|
#[error("the standalone Worker final state could not be committed")]
|
||||||
StateStore,
|
StateStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,22 +87,24 @@ impl StandaloneHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn start_with_optional_model_client(
|
async fn start_with_optional_model_client(
|
||||||
mut launch: ResolvedStandaloneLaunch,
|
launch: ResolvedStandaloneLaunch,
|
||||||
model_client: Option<Box<dyn LlmClient>>,
|
model_client: Option<Box<dyn LlmClient>>,
|
||||||
) -> Result<Self, StandaloneStartupError> {
|
) -> Result<Self, StandaloneStartupError> {
|
||||||
let store = StandaloneSessionStore::open(&launch.state_dir)
|
let store =
|
||||||
.map_err(classify_store_startup_error)?;
|
StandaloneWorkerStore::open(&launch.state_dir).map_err(classify_store_startup_error)?;
|
||||||
let allocation = store
|
let allocation = store
|
||||||
.allocate(&launch.cwd, StaleLeasePolicy::Reject)
|
.allocate(&launch.cwd, StaleLeasePolicy::Reject)
|
||||||
.map_err(classify_store_startup_error)?;
|
.map_err(classify_store_startup_error)?;
|
||||||
let id = allocation.id();
|
let worker_id = allocation.worker_id();
|
||||||
|
|
||||||
// The standalone session ID is the local identity. A unique internal Worker name avoids
|
// WorkerId is the stable identity. The current Worker store remains
|
||||||
// process-global allocation collisions without creating a Runtime/Workspace Worker ID.
|
// name-keyed, so keep its derived storage key separate from the
|
||||||
launch.profile.manifest.worker.name = format!("standalone-{id}");
|
// user-facing profile name.
|
||||||
let manifest = launch.profile.manifest.clone();
|
let manifest = launch.profile.manifest.clone();
|
||||||
let worker_name = manifest.worker.name.clone();
|
let storage_key = format!("standalone-{worker_id}");
|
||||||
let (backing_store, worker_store) = match backing_store(&store, id) {
|
let mut bootstrap_manifest = manifest.clone();
|
||||||
|
bootstrap_manifest.worker.name = storage_key.clone();
|
||||||
|
let (backing_store, worker_store) = match backing_store(&store, worker_id) {
|
||||||
Ok(stores) => stores,
|
Ok(stores) => stores,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let _ = store.abandon_allocation(allocation);
|
let _ = store.abandon_allocation(allocation);
|
||||||
@@ -112,10 +114,10 @@ impl StandaloneHost {
|
|||||||
let filesystem_authority =
|
let filesystem_authority =
|
||||||
WorkerFilesystemAuthority::local(launch.cwd.clone(), launch.cwd.clone());
|
WorkerFilesystemAuthority::local(launch.cwd.clone(), launch.cwd.clone());
|
||||||
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||||
let runtime_base = store.runtime_dir(id);
|
let runtime_base = store.runtime_dir(worker_id);
|
||||||
|
|
||||||
let mut bootstrap = WorkerBootstrap::new(
|
let mut bootstrap = WorkerBootstrap::new(
|
||||||
manifest.clone(),
|
bootstrap_manifest,
|
||||||
backing_store,
|
backing_store,
|
||||||
launch.prompt_catalog,
|
launch.prompt_catalog,
|
||||||
workspace_context,
|
workspace_context,
|
||||||
@@ -133,7 +135,7 @@ impl StandaloneHost {
|
|||||||
return Err(classify_startup_error(error));
|
return Err(classify_startup_error(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let active = match active_pointer(&worker_store, &worker_name) {
|
let active = match active_pointer(&worker_store, &storage_key) {
|
||||||
Ok(active) => active,
|
Ok(active) => active,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
stop_started_worker(started).await;
|
stop_started_worker(started).await;
|
||||||
@@ -141,16 +143,20 @@ impl StandaloneHost {
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let record =
|
let record = match store.commit_created(
|
||||||
match store.commit_created(&allocation, manifest, active.session_id, active.segment_id)
|
&allocation,
|
||||||
{
|
manifest,
|
||||||
Ok(record) => record,
|
storage_key,
|
||||||
Err(_) => {
|
active.session_id,
|
||||||
stop_started_worker(started).await;
|
active.segment_id,
|
||||||
let _ = store.abandon_allocation(allocation);
|
) {
|
||||||
return Err(StandaloneStartupError::StateStore);
|
Ok(record) => record,
|
||||||
}
|
Err(_) => {
|
||||||
};
|
stop_started_worker(started).await;
|
||||||
|
let _ = store.abandon_allocation(allocation);
|
||||||
|
return Err(StandaloneStartupError::StateStore);
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(Self::from_started(
|
Ok(Self::from_started(
|
||||||
started,
|
started,
|
||||||
store,
|
store,
|
||||||
@@ -162,50 +168,46 @@ impl StandaloneHost {
|
|||||||
|
|
||||||
pub async fn restore(
|
pub async fn restore(
|
||||||
state_dir: PathBuf,
|
state_dir: PathBuf,
|
||||||
session_id: StandaloneSessionId,
|
worker_id: WorkerId,
|
||||||
) -> Result<Self, StandaloneStartupError> {
|
) -> Result<Self, StandaloneStartupError> {
|
||||||
Self::restore_with_optional_model_client(state_dir, session_id, None).await
|
Self::restore_with_optional_model_client(state_dir, worker_id, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restore_with_model_client<C>(
|
pub async fn restore_with_model_client<C>(
|
||||||
state_dir: PathBuf,
|
state_dir: PathBuf,
|
||||||
session_id: StandaloneSessionId,
|
worker_id: WorkerId,
|
||||||
model_client: C,
|
model_client: C,
|
||||||
) -> Result<Self, StandaloneStartupError>
|
) -> Result<Self, StandaloneStartupError>
|
||||||
where
|
where
|
||||||
C: LlmClient + 'static,
|
C: LlmClient + 'static,
|
||||||
{
|
{
|
||||||
Self::restore_with_optional_model_client(
|
Self::restore_with_optional_model_client(state_dir, worker_id, Some(Box::new(model_client)))
|
||||||
state_dir,
|
.await
|
||||||
session_id,
|
|
||||||
Some(Box::new(model_client)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_with_optional_model_client(
|
async fn restore_with_optional_model_client(
|
||||||
state_dir: PathBuf,
|
state_dir: PathBuf,
|
||||||
session_id: StandaloneSessionId,
|
worker_id: WorkerId,
|
||||||
model_client: Option<Box<dyn LlmClient>>,
|
model_client: Option<Box<dyn LlmClient>>,
|
||||||
) -> Result<Self, StandaloneStartupError> {
|
) -> Result<Self, StandaloneStartupError> {
|
||||||
let store =
|
let store = StandaloneWorkerStore::open(state_dir).map_err(classify_store_startup_error)?;
|
||||||
StandaloneSessionStore::open(state_dir).map_err(classify_store_startup_error)?;
|
|
||||||
let record = store
|
let record = store
|
||||||
.load(session_id)
|
.load(worker_id)
|
||||||
.map_err(classify_store_startup_error)?;
|
.map_err(classify_store_startup_error)?;
|
||||||
record.cwd.verify().map_err(classify_store_startup_error)?;
|
record.cwd.verify().map_err(classify_store_startup_error)?;
|
||||||
let lease = store
|
let lease = store
|
||||||
.acquire_lease(session_id, StaleLeasePolicy::Recover)
|
.acquire_lease(worker_id, StaleLeasePolicy::Recover)
|
||||||
.map_err(classify_store_startup_error)?;
|
.map_err(classify_store_startup_error)?;
|
||||||
let (backing_store, worker_store) = backing_store(&store, session_id)?;
|
let (backing_store, worker_store) = backing_store(&store, worker_id)?;
|
||||||
let worker_name = record.worker_name.clone();
|
let storage_key = record.storage_key.clone();
|
||||||
let manifest = record.manifest.clone();
|
let mut manifest = record.manifest.clone();
|
||||||
|
manifest.worker.name = storage_key.clone();
|
||||||
let filesystem_authority = WorkerFilesystemAuthority::local(
|
let filesystem_authority = WorkerFilesystemAuthority::local(
|
||||||
record.cwd.canonical_path.clone(),
|
record.cwd.canonical_path.clone(),
|
||||||
record.cwd.canonical_path.clone(),
|
record.cwd.canonical_path.clone(),
|
||||||
);
|
);
|
||||||
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
let workspace_context = WorkerWorkspaceContext::local_filesystem(None);
|
||||||
let runtime_base = store.runtime_dir(session_id);
|
let runtime_base = store.runtime_dir(worker_id);
|
||||||
|
|
||||||
let mut bootstrap = WorkerBootstrap::new(
|
let mut bootstrap = WorkerBootstrap::new(
|
||||||
manifest,
|
manifest,
|
||||||
@@ -220,11 +222,11 @@ impl StandaloneHost {
|
|||||||
bootstrap = bootstrap.with_model_client(model_client);
|
bootstrap = bootstrap.with_model_client(model_client);
|
||||||
}
|
}
|
||||||
let prepared = bootstrap
|
let prepared = bootstrap
|
||||||
.prepare_restored(&worker_name)
|
.prepare_restored(&storage_key)
|
||||||
.await
|
.await
|
||||||
.map_err(classify_startup_error)?;
|
.map_err(classify_startup_error)?;
|
||||||
let started = prepared.start().await.map_err(classify_startup_error)?;
|
let started = prepared.start().await.map_err(classify_startup_error)?;
|
||||||
let active = match active_pointer(&worker_store, &worker_name) {
|
let active = match active_pointer(&worker_store, &storage_key) {
|
||||||
Ok(active) => active,
|
Ok(active) => active,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
stop_started_worker(started).await;
|
stop_started_worker(started).await;
|
||||||
@@ -251,10 +253,10 @@ impl StandaloneHost {
|
|||||||
|
|
||||||
fn from_started(
|
fn from_started(
|
||||||
started: BootstrappedWorker,
|
started: BootstrappedWorker,
|
||||||
store: StandaloneSessionStore,
|
store: StandaloneWorkerStore,
|
||||||
worker_store: FsWorkerStore,
|
worker_store: FsWorkerStore,
|
||||||
record: StandaloneSessionRecord,
|
record: StandaloneWorkerRecord,
|
||||||
lease: StandaloneSessionLease,
|
lease: StandaloneWorkerLease,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
handle: started.handle,
|
handle: started.handle,
|
||||||
@@ -268,12 +270,12 @@ impl StandaloneHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn session_id(&self) -> StandaloneSessionId {
|
pub fn worker_id(&self) -> WorkerId {
|
||||||
self.record.session_id
|
self.record.worker_id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn record(&self) -> &StandaloneSessionRecord {
|
pub fn record(&self) -> &StandaloneWorkerRecord {
|
||||||
&self.record
|
&self.record
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +312,7 @@ impl StandaloneHost {
|
|||||||
return Err(StandaloneShutdownError::DeadlineExceeded);
|
return Err(StandaloneShutdownError::DeadlineExceeded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let active = match active_pointer(&self.worker_store, &self.record.worker_name) {
|
let active = match active_pointer(&self.worker_store, &self.record.storage_key) {
|
||||||
Ok(active) => active,
|
Ok(active) => active,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.retain_lease();
|
self.retain_lease();
|
||||||
@@ -451,12 +453,12 @@ async fn send_protocol_event(peer: &InProcessPeer, event: Event) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn backing_store(
|
fn backing_store(
|
||||||
store: &StandaloneSessionStore,
|
store: &StandaloneWorkerStore,
|
||||||
id: StandaloneSessionId,
|
worker_id: WorkerId,
|
||||||
) -> Result<(StandaloneBackingStore, FsWorkerStore), StandaloneStartupError> {
|
) -> Result<(StandaloneBackingStore, FsWorkerStore), StandaloneStartupError> {
|
||||||
let session_store =
|
let session_store = FsStore::new(store.sessions_dir(worker_id))
|
||||||
FsStore::new(store.session_log_dir(id)).map_err(|_| StandaloneStartupError::StateStore)?;
|
.map_err(|_| StandaloneStartupError::StateStore)?;
|
||||||
let worker_store = FsWorkerStore::new(store.worker_metadata_dir(id))
|
let worker_store = FsWorkerStore::new(store.worker_metadata_dir(worker_id))
|
||||||
.map_err(|_| StandaloneStartupError::StateStore)?;
|
.map_err(|_| StandaloneStartupError::StateStore)?;
|
||||||
Ok((
|
Ok((
|
||||||
CombinedStore::new(session_store, worker_store.clone()),
|
CombinedStore::new(session_store, worker_store.clone()),
|
||||||
@@ -466,10 +468,10 @@ fn backing_store(
|
|||||||
|
|
||||||
fn active_pointer(
|
fn active_pointer(
|
||||||
worker_store: &FsWorkerStore,
|
worker_store: &FsWorkerStore,
|
||||||
worker_name: &str,
|
storage_key: &str,
|
||||||
) -> Result<WorkerActiveSegmentRef, StandaloneStartupError> {
|
) -> Result<WorkerActiveSegmentRef, StandaloneStartupError> {
|
||||||
worker_store
|
worker_store
|
||||||
.read_by_name(worker_name)
|
.read_by_name(storage_key)
|
||||||
.map_err(|_| StandaloneStartupError::StateStore)?
|
.map_err(|_| StandaloneStartupError::StateStore)?
|
||||||
.and_then(|metadata| metadata.active)
|
.and_then(|metadata| metadata.active)
|
||||||
.ok_or(StandaloneStartupError::StateStore)
|
.ok_or(StandaloneStartupError::StateStore)
|
||||||
@@ -482,7 +484,7 @@ async fn stop_started_worker(started: BootstrappedWorker) {
|
|||||||
|
|
||||||
fn classify_store_startup_error(error: StandaloneStoreError) -> StandaloneStartupError {
|
fn classify_store_startup_error(error: StandaloneStoreError) -> StandaloneStartupError {
|
||||||
match error {
|
match error {
|
||||||
StandaloneStoreError::SessionLeased(_) => StandaloneStartupError::SessionActive,
|
StandaloneStoreError::WorkerLeased(_) => StandaloneStartupError::WorkerActive,
|
||||||
StandaloneStoreError::LeaseLivenessUnknown(_) => {
|
StandaloneStoreError::LeaseLivenessUnknown(_) => {
|
||||||
StandaloneStartupError::LeaseLivenessUnknown
|
StandaloneStartupError::LeaseLivenessUnknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ pub mod store;
|
|||||||
|
|
||||||
pub use host::{StandaloneHost, StandaloneShutdownError, StandaloneStartupError};
|
pub use host::{StandaloneHost, StandaloneShutdownError, StandaloneStartupError};
|
||||||
pub use launch::{ResolvedStandaloneLaunch, StandaloneLaunchConfig, StandaloneLaunchError};
|
pub use launch::{ResolvedStandaloneLaunch, StandaloneLaunchConfig, StandaloneLaunchError};
|
||||||
|
pub use protocol::WorkerId;
|
||||||
pub use store::{
|
pub use store::{
|
||||||
StaleLeasePolicy, StandaloneCwdIdentity, StandaloneListScope, StandaloneSessionId,
|
StaleLeasePolicy, StandaloneCwdIdentity, StandaloneListScope, StandaloneShutdownReason,
|
||||||
StandaloneSessionRecord, StandaloneSessionStatus, StandaloneSessionStore,
|
StandaloneStoreError, StandaloneWorkerRecord, StandaloneWorkerStatus, StandaloneWorkerStore,
|
||||||
StandaloneShutdownReason, StandaloneStoreError,
|
|
||||||
};
|
};
|
||||||
|
|||||||
+106
-143
@@ -1,12 +1,11 @@
|
|||||||
use std::fmt;
|
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File, OpenOptions};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use fs4::fs_std::FileExt;
|
use fs4::fs_std::FileExt;
|
||||||
use manifest::WorkerManifest;
|
use manifest::WorkerManifest;
|
||||||
|
use protocol::WorkerId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use session_store::{SegmentId, SessionId};
|
use session_store::{SegmentId, SessionId};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -16,47 +15,10 @@ const RECORD_FILE: &str = "record.json";
|
|||||||
const COMMIT_MARKER: &str = "commit.pending";
|
const COMMIT_MARKER: &str = "commit.pending";
|
||||||
const LEASE_FILE: &str = "lease.json";
|
const LEASE_FILE: &str = "lease.json";
|
||||||
const LEASE_LOCK_FILE: &str = "lease.lock";
|
const LEASE_LOCK_FILE: &str = "lease.lock";
|
||||||
const SESSION_DIR: &str = "session";
|
const SESSIONS_DIR: &str = "sessions";
|
||||||
const WORKER_DIR: &str = "worker";
|
const WORKER_DIR: &str = "worker";
|
||||||
const SCHEMA_VERSION: u32 = 1;
|
const SCHEMA_VERSION: u32 = 1;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct StandaloneSessionId(Uuid);
|
|
||||||
|
|
||||||
impl StandaloneSessionId {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(Uuid::now_v7())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn short(self) -> String {
|
|
||||||
let simple = self.0.simple().to_string();
|
|
||||||
simple[simple.len() - 12..].to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for StandaloneSessionId {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for StandaloneSessionId {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(formatter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for StandaloneSessionId {
|
|
||||||
type Err = uuid::Error;
|
|
||||||
|
|
||||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
||||||
Uuid::parse_str(value).map(Self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct StandaloneCwdIdentity {
|
pub struct StandaloneCwdIdentity {
|
||||||
pub canonical_path: PathBuf,
|
pub canonical_path: PathBuf,
|
||||||
@@ -100,7 +62,7 @@ impl StandaloneCwdIdentity {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum StandaloneSessionStatus {
|
pub enum StandaloneWorkerStatus {
|
||||||
Active,
|
Active,
|
||||||
Stopped,
|
Stopped,
|
||||||
}
|
}
|
||||||
@@ -115,17 +77,20 @@ pub enum StandaloneShutdownReason {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StandaloneSessionRecord {
|
pub struct StandaloneWorkerRecord {
|
||||||
pub schema_version: u32,
|
pub schema_version: u32,
|
||||||
pub revision: u64,
|
pub revision: u64,
|
||||||
pub session_id: StandaloneSessionId,
|
pub worker_id: WorkerId,
|
||||||
|
/// User-facing Worker name resolved from the profile.
|
||||||
pub worker_name: String,
|
pub worker_name: String,
|
||||||
|
/// Internal key used by the current name-keyed Worker store.
|
||||||
|
pub storage_key: String,
|
||||||
pub cwd: StandaloneCwdIdentity,
|
pub cwd: StandaloneCwdIdentity,
|
||||||
pub manifest: WorkerManifest,
|
pub manifest: WorkerManifest,
|
||||||
pub active_session_id: SessionId,
|
pub active_session_id: SessionId,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub active_segment_id: Option<SegmentId>,
|
pub active_segment_id: Option<SegmentId>,
|
||||||
pub status: StandaloneSessionStatus,
|
pub status: StandaloneWorkerStatus,
|
||||||
pub created_at_unix_ms: u64,
|
pub created_at_unix_ms: u64,
|
||||||
pub updated_at_unix_ms: u64,
|
pub updated_at_unix_ms: u64,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -145,11 +110,11 @@ pub enum StaleLeasePolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StandaloneSessionStore {
|
pub struct StandaloneWorkerStore {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StandaloneSessionStore {
|
impl StandaloneWorkerStore {
|
||||||
pub fn open(root: impl Into<PathBuf>) -> Result<Self, StandaloneStoreError> {
|
pub fn open(root: impl Into<PathBuf>) -> Result<Self, StandaloneStoreError> {
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
fs::create_dir_all(&root).map_err(StandaloneStoreError::Io)?;
|
fs::create_dir_all(&root).map_err(StandaloneStoreError::Io)?;
|
||||||
@@ -171,35 +136,41 @@ impl StandaloneSessionStore {
|
|||||||
&self,
|
&self,
|
||||||
cwd: impl AsRef<Path>,
|
cwd: impl AsRef<Path>,
|
||||||
policy: StaleLeasePolicy,
|
policy: StaleLeasePolicy,
|
||||||
) -> Result<StandaloneSessionAllocation, StandaloneStoreError> {
|
) -> Result<StandaloneWorkerAllocation, StandaloneStoreError> {
|
||||||
let id = StandaloneSessionId::new();
|
let worker_id = WorkerId::now_v7();
|
||||||
let cwd = StandaloneCwdIdentity::capture(cwd)?;
|
let cwd = StandaloneCwdIdentity::capture(cwd)?;
|
||||||
let dir = self.session_dir(id);
|
let dir = self.worker_dir(worker_id);
|
||||||
fs::create_dir(&dir).map_err(StandaloneStoreError::Io)?;
|
fs::create_dir(&dir).map_err(StandaloneStoreError::Io)?;
|
||||||
fs::create_dir(dir.join(SESSION_DIR)).map_err(StandaloneStoreError::Io)?;
|
fs::create_dir(dir.join(SESSIONS_DIR)).map_err(StandaloneStoreError::Io)?;
|
||||||
fs::create_dir(dir.join(WORKER_DIR)).map_err(StandaloneStoreError::Io)?;
|
fs::create_dir(dir.join(WORKER_DIR)).map_err(StandaloneStoreError::Io)?;
|
||||||
let lease = self.acquire_lease(id, policy)?;
|
let lease = self.acquire_lease(worker_id, policy)?;
|
||||||
Ok(StandaloneSessionAllocation { id, cwd, lease })
|
Ok(StandaloneWorkerAllocation {
|
||||||
|
worker_id,
|
||||||
|
cwd,
|
||||||
|
lease,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_created(
|
pub fn commit_created(
|
||||||
&self,
|
&self,
|
||||||
allocation: &StandaloneSessionAllocation,
|
allocation: &StandaloneWorkerAllocation,
|
||||||
manifest: WorkerManifest,
|
manifest: WorkerManifest,
|
||||||
|
storage_key: String,
|
||||||
active_session_id: SessionId,
|
active_session_id: SessionId,
|
||||||
active_segment_id: Option<SegmentId>,
|
active_segment_id: Option<SegmentId>,
|
||||||
) -> Result<StandaloneSessionRecord, StandaloneStoreError> {
|
) -> Result<StandaloneWorkerRecord, StandaloneStoreError> {
|
||||||
let now = now_unix_ms()?;
|
let now = now_unix_ms()?;
|
||||||
let record = StandaloneSessionRecord {
|
let record = StandaloneWorkerRecord {
|
||||||
schema_version: SCHEMA_VERSION,
|
schema_version: SCHEMA_VERSION,
|
||||||
revision: 1,
|
revision: 1,
|
||||||
session_id: allocation.id,
|
worker_id: allocation.worker_id,
|
||||||
worker_name: manifest.worker.name.clone(),
|
worker_name: manifest.worker.name.clone(),
|
||||||
|
storage_key,
|
||||||
cwd: allocation.cwd.clone(),
|
cwd: allocation.cwd.clone(),
|
||||||
manifest,
|
manifest,
|
||||||
active_session_id,
|
active_session_id,
|
||||||
active_segment_id,
|
active_segment_id,
|
||||||
status: StandaloneSessionStatus::Active,
|
status: StandaloneWorkerStatus::Active,
|
||||||
created_at_unix_ms: now,
|
created_at_unix_ms: now,
|
||||||
updated_at_unix_ms: now,
|
updated_at_unix_ms: now,
|
||||||
shutdown_reason: None,
|
shutdown_reason: None,
|
||||||
@@ -208,22 +179,19 @@ impl StandaloneSessionStore {
|
|||||||
Ok(record)
|
Ok(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(
|
pub fn load(&self, id: WorkerId) -> Result<StandaloneWorkerRecord, StandaloneStoreError> {
|
||||||
&self,
|
let dir = self.worker_dir(id);
|
||||||
id: StandaloneSessionId,
|
|
||||||
) -> Result<StandaloneSessionRecord, StandaloneStoreError> {
|
|
||||||
let dir = self.session_dir(id);
|
|
||||||
if dir.join(COMMIT_MARKER).exists() {
|
if dir.join(COMMIT_MARKER).exists() {
|
||||||
return Err(StandaloneStoreError::IncompleteCommit(id));
|
return Err(StandaloneStoreError::IncompleteCommit(id));
|
||||||
}
|
}
|
||||||
let bytes = fs::read(dir.join(RECORD_FILE)).map_err(|error| {
|
let bytes = fs::read(dir.join(RECORD_FILE)).map_err(|error| {
|
||||||
if error.kind() == io::ErrorKind::NotFound {
|
if error.kind() == io::ErrorKind::NotFound {
|
||||||
StandaloneStoreError::SessionNotFound(id)
|
StandaloneStoreError::WorkerNotFound(id)
|
||||||
} else {
|
} else {
|
||||||
StandaloneStoreError::Io(error)
|
StandaloneStoreError::Io(error)
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
let record: StandaloneSessionRecord = serde_json::from_slice(&bytes)
|
let record: StandaloneWorkerRecord = serde_json::from_slice(&bytes)
|
||||||
.map_err(|source| StandaloneStoreError::CorruptRecord { id, source })?;
|
.map_err(|source| StandaloneStoreError::CorruptRecord { id, source })?;
|
||||||
if record.schema_version > SCHEMA_VERSION {
|
if record.schema_version > SCHEMA_VERSION {
|
||||||
return Err(StandaloneStoreError::NewerSchema {
|
return Err(StandaloneStoreError::NewerSchema {
|
||||||
@@ -232,7 +200,7 @@ impl StandaloneSessionStore {
|
|||||||
supported: SCHEMA_VERSION,
|
supported: SCHEMA_VERSION,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if record.schema_version != SCHEMA_VERSION || record.session_id != id {
|
if record.schema_version != SCHEMA_VERSION || record.worker_id != id {
|
||||||
return Err(StandaloneStoreError::InvalidRecord(id));
|
return Err(StandaloneStoreError::InvalidRecord(id));
|
||||||
}
|
}
|
||||||
Ok(record)
|
Ok(record)
|
||||||
@@ -243,7 +211,7 @@ impl StandaloneSessionStore {
|
|||||||
cwd: impl AsRef<Path>,
|
cwd: impl AsRef<Path>,
|
||||||
scope: StandaloneListScope,
|
scope: StandaloneListScope,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StandaloneSessionRecord>, StandaloneStoreError> {
|
) -> Result<Vec<StandaloneWorkerRecord>, StandaloneStoreError> {
|
||||||
let current_cwd = (scope == StandaloneListScope::CurrentCwd)
|
let current_cwd = (scope == StandaloneListScope::CurrentCwd)
|
||||||
.then(|| StandaloneCwdIdentity::capture(cwd))
|
.then(|| StandaloneCwdIdentity::capture(cwd))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
@@ -269,12 +237,7 @@ impl StandaloneSessionStore {
|
|||||||
right
|
right
|
||||||
.updated_at_unix_ms
|
.updated_at_unix_ms
|
||||||
.cmp(&left.updated_at_unix_ms)
|
.cmp(&left.updated_at_unix_ms)
|
||||||
.then_with(|| {
|
.then_with(|| right.worker_id.to_string().cmp(&left.worker_id.to_string()))
|
||||||
right
|
|
||||||
.session_id
|
|
||||||
.to_string()
|
|
||||||
.cmp(&left.session_id.to_string())
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
records.truncate(limit);
|
records.truncate(limit);
|
||||||
Ok(records)
|
Ok(records)
|
||||||
@@ -282,10 +245,10 @@ impl StandaloneSessionStore {
|
|||||||
|
|
||||||
pub fn acquire_lease(
|
pub fn acquire_lease(
|
||||||
&self,
|
&self,
|
||||||
id: StandaloneSessionId,
|
id: WorkerId,
|
||||||
policy: StaleLeasePolicy,
|
policy: StaleLeasePolicy,
|
||||||
) -> Result<StandaloneSessionLease, StandaloneStoreError> {
|
) -> Result<StandaloneWorkerLease, StandaloneStoreError> {
|
||||||
let dir = self.session_dir(id);
|
let dir = self.worker_dir(id);
|
||||||
let path = dir.join(LEASE_FILE);
|
let path = dir.join(LEASE_FILE);
|
||||||
let _guard = LeaseMutationGuard::acquire(&dir)?;
|
let _guard = LeaseMutationGuard::acquire(&dir)?;
|
||||||
let lease = LeaseRecord::current()?;
|
let lease = LeaseRecord::current()?;
|
||||||
@@ -296,7 +259,7 @@ impl StandaloneSessionStore {
|
|||||||
file.write_all(b"\n").map_err(StandaloneStoreError::Io)?;
|
file.write_all(b"\n").map_err(StandaloneStoreError::Io)?;
|
||||||
file.sync_all().map_err(StandaloneStoreError::Io)?;
|
file.sync_all().map_err(StandaloneStoreError::Io)?;
|
||||||
sync_directory(&dir)?;
|
sync_directory(&dir)?;
|
||||||
return Ok(StandaloneSessionLease {
|
return Ok(StandaloneWorkerLease {
|
||||||
path,
|
path,
|
||||||
lease_id: lease.lease_id,
|
lease_id: lease.lease_id,
|
||||||
released: false,
|
released: false,
|
||||||
@@ -306,7 +269,7 @@ impl StandaloneSessionStore {
|
|||||||
let existing = read_lease(&path, id)?;
|
let existing = read_lease(&path, id)?;
|
||||||
match existing.liveness() {
|
match existing.liveness() {
|
||||||
LeaseLiveness::Live => {
|
LeaseLiveness::Live => {
|
||||||
return Err(StandaloneStoreError::SessionLeased(id));
|
return Err(StandaloneStoreError::WorkerLeased(id));
|
||||||
}
|
}
|
||||||
LeaseLiveness::Unknown => {
|
LeaseLiveness::Unknown => {
|
||||||
return Err(StandaloneStoreError::LeaseLivenessUnknown(id));
|
return Err(StandaloneStoreError::LeaseLivenessUnknown(id));
|
||||||
@@ -326,16 +289,16 @@ impl StandaloneSessionStore {
|
|||||||
|
|
||||||
pub fn update_active_pointer(
|
pub fn update_active_pointer(
|
||||||
&self,
|
&self,
|
||||||
record: &StandaloneSessionRecord,
|
record: &StandaloneWorkerRecord,
|
||||||
active_session_id: SessionId,
|
active_session_id: SessionId,
|
||||||
active_segment_id: Option<SegmentId>,
|
active_segment_id: Option<SegmentId>,
|
||||||
) -> Result<StandaloneSessionRecord, StandaloneStoreError> {
|
) -> Result<StandaloneWorkerRecord, StandaloneStoreError> {
|
||||||
let mut next = record.clone();
|
let mut next = record.clone();
|
||||||
next.revision = next.revision.saturating_add(1);
|
next.revision = next.revision.saturating_add(1);
|
||||||
next.updated_at_unix_ms = now_unix_ms()?;
|
next.updated_at_unix_ms = now_unix_ms()?;
|
||||||
next.active_session_id = active_session_id;
|
next.active_session_id = active_session_id;
|
||||||
next.active_segment_id = active_segment_id;
|
next.active_segment_id = active_segment_id;
|
||||||
next.status = StandaloneSessionStatus::Active;
|
next.status = StandaloneWorkerStatus::Active;
|
||||||
next.shutdown_reason = None;
|
next.shutdown_reason = None;
|
||||||
self.commit_record(Some(record.revision), &next)?;
|
self.commit_record(Some(record.revision), &next)?;
|
||||||
Ok(next)
|
Ok(next)
|
||||||
@@ -343,73 +306,73 @@ impl StandaloneSessionStore {
|
|||||||
|
|
||||||
pub fn mark_stopped(
|
pub fn mark_stopped(
|
||||||
&self,
|
&self,
|
||||||
record: &StandaloneSessionRecord,
|
record: &StandaloneWorkerRecord,
|
||||||
active_session_id: SessionId,
|
active_session_id: SessionId,
|
||||||
active_segment_id: Option<SegmentId>,
|
active_segment_id: Option<SegmentId>,
|
||||||
reason: StandaloneShutdownReason,
|
reason: StandaloneShutdownReason,
|
||||||
) -> Result<StandaloneSessionRecord, StandaloneStoreError> {
|
) -> Result<StandaloneWorkerRecord, StandaloneStoreError> {
|
||||||
let mut next = record.clone();
|
let mut next = record.clone();
|
||||||
next.revision = next.revision.saturating_add(1);
|
next.revision = next.revision.saturating_add(1);
|
||||||
next.updated_at_unix_ms = now_unix_ms()?;
|
next.updated_at_unix_ms = now_unix_ms()?;
|
||||||
next.active_session_id = active_session_id;
|
next.active_session_id = active_session_id;
|
||||||
next.active_segment_id = active_segment_id;
|
next.active_segment_id = active_segment_id;
|
||||||
next.status = StandaloneSessionStatus::Stopped;
|
next.status = StandaloneWorkerStatus::Stopped;
|
||||||
next.shutdown_reason = Some(reason);
|
next.shutdown_reason = Some(reason);
|
||||||
self.commit_record(Some(record.revision), &next)?;
|
self.commit_record(Some(record.revision), &next)?;
|
||||||
Ok(next)
|
Ok(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, id: StandaloneSessionId) -> Result<(), StandaloneStoreError> {
|
pub fn delete(&self, id: WorkerId) -> Result<(), StandaloneStoreError> {
|
||||||
let record = self.load(id)?;
|
let record = self.load(id)?;
|
||||||
if record.status != StandaloneSessionStatus::Stopped {
|
if record.status != StandaloneWorkerStatus::Stopped {
|
||||||
return Err(StandaloneStoreError::DeleteActive(id));
|
return Err(StandaloneStoreError::DeleteActive(id));
|
||||||
}
|
}
|
||||||
let session_dir = self.session_dir(id);
|
let worker_dir = self.worker_dir(id);
|
||||||
let _guard = LeaseMutationGuard::acquire(&session_dir)?;
|
let _guard = LeaseMutationGuard::acquire(&worker_dir)?;
|
||||||
let lease_path = session_dir.join(LEASE_FILE);
|
let lease_path = worker_dir.join(LEASE_FILE);
|
||||||
if lease_path.exists() {
|
if lease_path.exists() {
|
||||||
let lease = read_lease(&lease_path, id)?;
|
let lease = read_lease(&lease_path, id)?;
|
||||||
return Err(match lease.liveness() {
|
return Err(match lease.liveness() {
|
||||||
LeaseLiveness::Live => StandaloneStoreError::SessionLeased(id),
|
LeaseLiveness::Live => StandaloneStoreError::WorkerLeased(id),
|
||||||
LeaseLiveness::Stale => StandaloneStoreError::StaleLease(id),
|
LeaseLiveness::Stale => StandaloneStoreError::StaleLease(id),
|
||||||
LeaseLiveness::Unknown => StandaloneStoreError::LeaseLivenessUnknown(id),
|
LeaseLiveness::Unknown => StandaloneStoreError::LeaseLivenessUnknown(id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
fs::remove_dir_all(self.session_dir(id)).map_err(StandaloneStoreError::Io)?;
|
fs::remove_dir_all(self.worker_dir(id)).map_err(StandaloneStoreError::Io)?;
|
||||||
sync_directory(&self.root)
|
sync_directory(&self.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn session_log_dir(&self, id: StandaloneSessionId) -> PathBuf {
|
pub fn sessions_dir(&self, id: WorkerId) -> PathBuf {
|
||||||
self.session_dir(id).join(SESSION_DIR)
|
self.worker_dir(id).join(SESSIONS_DIR)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn worker_metadata_dir(&self, id: StandaloneSessionId) -> PathBuf {
|
pub fn worker_metadata_dir(&self, id: WorkerId) -> PathBuf {
|
||||||
self.session_dir(id).join(WORKER_DIR)
|
self.worker_dir(id).join(WORKER_DIR)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn runtime_dir(&self, id: StandaloneSessionId) -> PathBuf {
|
pub(crate) fn runtime_dir(&self, id: WorkerId) -> PathBuf {
|
||||||
self.session_dir(id).join("runtime")
|
self.worker_dir(id).join("runtime")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn abandon_allocation(
|
pub(crate) fn abandon_allocation(
|
||||||
&self,
|
&self,
|
||||||
allocation: StandaloneSessionAllocation,
|
allocation: StandaloneWorkerAllocation,
|
||||||
) -> Result<(), StandaloneStoreError> {
|
) -> Result<(), StandaloneStoreError> {
|
||||||
let id = allocation.id;
|
let worker_id = allocation.worker_id;
|
||||||
allocation.lease.release()?;
|
allocation.lease.release()?;
|
||||||
fs::remove_dir_all(self.session_dir(id)).map_err(StandaloneStoreError::Io)?;
|
fs::remove_dir_all(self.worker_dir(worker_id)).map_err(StandaloneStoreError::Io)?;
|
||||||
sync_directory(&self.root)
|
sync_directory(&self.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_record(
|
fn commit_record(
|
||||||
&self,
|
&self,
|
||||||
expected_revision: Option<u64>,
|
expected_revision: Option<u64>,
|
||||||
next: &StandaloneSessionRecord,
|
next: &StandaloneWorkerRecord,
|
||||||
) -> Result<(), StandaloneStoreError> {
|
) -> Result<(), StandaloneStoreError> {
|
||||||
let dir = self.session_dir(next.session_id);
|
let dir = self.worker_dir(next.worker_id);
|
||||||
let marker = dir.join(COMMIT_MARKER);
|
let marker = dir.join(COMMIT_MARKER);
|
||||||
let mut marker_file = OpenOptions::new()
|
let mut marker_file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
@@ -417,7 +380,7 @@ impl StandaloneSessionStore {
|
|||||||
.open(&marker)
|
.open(&marker)
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
if error.kind() == io::ErrorKind::AlreadyExists {
|
if error.kind() == io::ErrorKind::AlreadyExists {
|
||||||
StandaloneStoreError::IncompleteCommit(next.session_id)
|
StandaloneStoreError::IncompleteCommit(next.worker_id)
|
||||||
} else {
|
} else {
|
||||||
StandaloneStoreError::Io(error)
|
StandaloneStoreError::Io(error)
|
||||||
}
|
}
|
||||||
@@ -427,11 +390,11 @@ impl StandaloneSessionStore {
|
|||||||
sync_directory(&dir)?;
|
sync_directory(&dir)?;
|
||||||
|
|
||||||
if let Some(expected) = expected_revision {
|
if let Some(expected) = expected_revision {
|
||||||
let current = self.load_record_while_committing(next.session_id)?;
|
let current = self.load_record_while_committing(next.worker_id)?;
|
||||||
if current.revision != expected {
|
if current.revision != expected {
|
||||||
let _ = fs::remove_file(&marker);
|
let _ = fs::remove_file(&marker);
|
||||||
return Err(StandaloneStoreError::RevisionConflict {
|
return Err(StandaloneStoreError::RevisionConflict {
|
||||||
id: next.session_id,
|
id: next.worker_id,
|
||||||
expected,
|
expected,
|
||||||
found: current.revision,
|
found: current.revision,
|
||||||
});
|
});
|
||||||
@@ -461,30 +424,30 @@ impl StandaloneSessionStore {
|
|||||||
|
|
||||||
fn load_record_while_committing(
|
fn load_record_while_committing(
|
||||||
&self,
|
&self,
|
||||||
id: StandaloneSessionId,
|
id: WorkerId,
|
||||||
) -> Result<StandaloneSessionRecord, StandaloneStoreError> {
|
) -> Result<StandaloneWorkerRecord, StandaloneStoreError> {
|
||||||
let bytes =
|
let bytes =
|
||||||
fs::read(self.session_dir(id).join(RECORD_FILE)).map_err(StandaloneStoreError::Io)?;
|
fs::read(self.worker_dir(id).join(RECORD_FILE)).map_err(StandaloneStoreError::Io)?;
|
||||||
serde_json::from_slice(&bytes)
|
serde_json::from_slice(&bytes)
|
||||||
.map_err(|source| StandaloneStoreError::CorruptRecord { id, source })
|
.map_err(|source| StandaloneStoreError::CorruptRecord { id, source })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_dir(&self, id: StandaloneSessionId) -> PathBuf {
|
fn worker_dir(&self, id: WorkerId) -> PathBuf {
|
||||||
self.root.join(id.to_string())
|
self.root.join(id.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StandaloneSessionAllocation {
|
pub struct StandaloneWorkerAllocation {
|
||||||
id: StandaloneSessionId,
|
worker_id: WorkerId,
|
||||||
cwd: StandaloneCwdIdentity,
|
cwd: StandaloneCwdIdentity,
|
||||||
lease: StandaloneSessionLease,
|
lease: StandaloneWorkerLease,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StandaloneSessionAllocation {
|
impl StandaloneWorkerAllocation {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn id(&self) -> StandaloneSessionId {
|
pub fn worker_id(&self) -> WorkerId {
|
||||||
self.id
|
self.worker_id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -492,19 +455,19 @@ impl StandaloneSessionAllocation {
|
|||||||
&self.cwd
|
&self.cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_lease(self) -> StandaloneSessionLease {
|
pub fn into_lease(self) -> StandaloneWorkerLease {
|
||||||
self.lease
|
self.lease
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StandaloneSessionLease {
|
pub struct StandaloneWorkerLease {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
lease_id: Uuid,
|
lease_id: Uuid,
|
||||||
released: bool,
|
released: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StandaloneSessionLease {
|
impl StandaloneWorkerLease {
|
||||||
pub fn release(mut self) -> Result<(), StandaloneStoreError> {
|
pub fn release(mut self) -> Result<(), StandaloneStoreError> {
|
||||||
self.release_inner()
|
self.release_inner()
|
||||||
}
|
}
|
||||||
@@ -534,7 +497,7 @@ impl StandaloneSessionLease {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for StandaloneSessionLease {
|
impl Drop for StandaloneWorkerLease {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = self.release_inner();
|
let _ = self.release_inner();
|
||||||
}
|
}
|
||||||
@@ -624,7 +587,7 @@ fn classify_lease_liveness(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_lease(path: &Path, id: StandaloneSessionId) -> Result<LeaseRecord, StandaloneStoreError> {
|
fn read_lease(path: &Path, id: WorkerId) -> Result<LeaseRecord, StandaloneStoreError> {
|
||||||
let bytes = fs::read(path).map_err(StandaloneStoreError::Io)?;
|
let bytes = fs::read(path).map_err(StandaloneStoreError::Io)?;
|
||||||
serde_json::from_slice(&bytes)
|
serde_json::from_slice(&bytes)
|
||||||
.map_err(|source| StandaloneStoreError::CorruptLease { id, source })
|
.map_err(|source| StandaloneStoreError::CorruptLease { id, source })
|
||||||
@@ -692,47 +655,47 @@ pub enum StandaloneStoreError {
|
|||||||
CwdUnavailable(#[source] io::Error),
|
CwdUnavailable(#[source] io::Error),
|
||||||
#[error("standalone cwd is not a directory")]
|
#[error("standalone cwd is not a directory")]
|
||||||
CwdNotDirectory,
|
CwdNotDirectory,
|
||||||
#[error("standalone cwd identity no longer matches the persisted session")]
|
#[error("standalone cwd identity no longer matches the persisted Worker")]
|
||||||
CwdIdentityMismatch,
|
CwdIdentityMismatch,
|
||||||
#[error("standalone session {0} was not found")]
|
#[error("standalone Worker {0} was not found")]
|
||||||
SessionNotFound(StandaloneSessionId),
|
WorkerNotFound(WorkerId),
|
||||||
#[error("standalone session {0} has an incomplete metadata commit")]
|
#[error("standalone Worker {0} has an incomplete metadata commit")]
|
||||||
IncompleteCommit(StandaloneSessionId),
|
IncompleteCommit(WorkerId),
|
||||||
#[error("standalone session {0} has invalid metadata")]
|
#[error("standalone Worker {0} has invalid metadata")]
|
||||||
InvalidRecord(StandaloneSessionId),
|
InvalidRecord(WorkerId),
|
||||||
#[error("standalone session {id} metadata is corrupt")]
|
#[error("standalone Worker {id} metadata is corrupt")]
|
||||||
CorruptRecord {
|
CorruptRecord {
|
||||||
id: StandaloneSessionId,
|
id: WorkerId,
|
||||||
#[source]
|
#[source]
|
||||||
source: serde_json::Error,
|
source: serde_json::Error,
|
||||||
},
|
},
|
||||||
#[error("standalone session {id} lease is corrupt")]
|
#[error("standalone Worker {id} lease is corrupt")]
|
||||||
CorruptLease {
|
CorruptLease {
|
||||||
id: StandaloneSessionId,
|
id: WorkerId,
|
||||||
#[source]
|
#[source]
|
||||||
source: serde_json::Error,
|
source: serde_json::Error,
|
||||||
},
|
},
|
||||||
#[error("standalone session {id} uses schema {found}, newer than supported schema {supported}")]
|
#[error("standalone Worker {id} uses schema {found}, newer than supported schema {supported}")]
|
||||||
NewerSchema {
|
NewerSchema {
|
||||||
id: StandaloneSessionId,
|
id: WorkerId,
|
||||||
found: u32,
|
found: u32,
|
||||||
supported: u32,
|
supported: u32,
|
||||||
},
|
},
|
||||||
#[error("standalone session {0} is already active")]
|
#[error("standalone Worker {0} is already active")]
|
||||||
SessionLeased(StandaloneSessionId),
|
WorkerLeased(WorkerId),
|
||||||
#[error("standalone session {0} lease liveness cannot be proven; recovery is rejected")]
|
#[error("standalone Worker {0} lease liveness cannot be proven; recovery is rejected")]
|
||||||
LeaseLivenessUnknown(StandaloneSessionId),
|
LeaseLivenessUnknown(WorkerId),
|
||||||
#[error("standalone session {0} has a stale lease; explicit recovery is required")]
|
#[error("standalone Worker {0} has a stale lease; explicit recovery is required")]
|
||||||
StaleLease(StandaloneSessionId),
|
StaleLease(WorkerId),
|
||||||
#[error("standalone session lease ownership changed")]
|
#[error("standalone Worker lease ownership changed")]
|
||||||
LeaseOwnershipLost,
|
LeaseOwnershipLost,
|
||||||
#[error("standalone session {0} must be stopped before deletion")]
|
#[error("standalone Worker {0} must be stopped before deletion")]
|
||||||
DeleteActive(StandaloneSessionId),
|
DeleteActive(WorkerId),
|
||||||
#[error(
|
#[error(
|
||||||
"standalone session {id} metadata revision changed (expected {expected}, found {found})"
|
"standalone Worker {id} metadata revision changed (expected {expected}, found {found})"
|
||||||
)]
|
)]
|
||||||
RevisionConflict {
|
RevisionConflict {
|
||||||
id: StandaloneSessionId,
|
id: WorkerId,
|
||||||
expected: u64,
|
expected: u64,
|
||||||
found: u64,
|
found: u64,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use futures::{Stream, stream};
|
|||||||
use protocol::{Event, Method};
|
use protocol::{Event, Method};
|
||||||
use standalone::{
|
use standalone::{
|
||||||
StaleLeasePolicy, StandaloneHost, StandaloneLaunchConfig, StandaloneListScope,
|
StaleLeasePolicy, StandaloneHost, StandaloneLaunchConfig, StandaloneListScope,
|
||||||
StandaloneSessionStatus, StandaloneSessionStore, StandaloneStartupError, StandaloneStoreError,
|
StandaloneStartupError, StandaloneStoreError, StandaloneWorkerStatus, StandaloneWorkerStore,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -90,6 +90,12 @@ async fn in_process_host_runs_text_and_read_tool_then_shuts_down() {
|
|||||||
let host = StandaloneHost::start_with_model_client(launch, client)
|
let host = StandaloneHost::start_with_model_client(launch, client)
|
||||||
.await
|
.await
|
||||||
.expect("start in-process host");
|
.expect("start in-process host");
|
||||||
|
assert_eq!(host.record().worker_name, worker_name);
|
||||||
|
assert_eq!(host.record().manifest.worker.name, worker_name);
|
||||||
|
assert_eq!(
|
||||||
|
host.record().storage_key,
|
||||||
|
format!("standalone-{}", host.worker_id())
|
||||||
|
);
|
||||||
let mut protocol_client = host.connect();
|
let mut protocol_client = host.connect();
|
||||||
|
|
||||||
protocol_client
|
protocol_client
|
||||||
@@ -238,7 +244,7 @@ type TestResult = Result<(), Box<dyn std::error::Error>>;
|
|||||||
async fn standalone_restore_preserves_history_tasks_notifications_and_cwd_scope() -> TestResult {
|
async fn standalone_restore_preserves_history_tasks_notifications_and_cwd_scope() -> TestResult {
|
||||||
let temp = tempfile::tempdir()?;
|
let temp = tempfile::tempdir()?;
|
||||||
let cwd = temp.path().join("project");
|
let cwd = temp.path().join("project");
|
||||||
let state_dir = temp.path().join("client").join("standalone-sessions");
|
let state_dir = temp.path().join("client").join("standalone-workers");
|
||||||
std::fs::create_dir_all(&cwd)?;
|
std::fs::create_dir_all(&cwd)?;
|
||||||
let launch = StandaloneLaunchConfig::new(
|
let launch = StandaloneLaunchConfig::new(
|
||||||
&cwd,
|
&cwd,
|
||||||
@@ -268,7 +274,7 @@ async fn standalone_restore_preserves_history_tasks_notifications_and_cwd_scope(
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
let host = StandaloneHost::start_with_model_client(launch, first_client).await?;
|
let host = StandaloneHost::start_with_model_client(launch, first_client).await?;
|
||||||
let session_id = host.session_id();
|
let worker_id = host.worker_id();
|
||||||
let mut protocol_client = host.connect();
|
let mut protocol_client = host.connect();
|
||||||
protocol_client
|
protocol_client
|
||||||
.send(&Method::run_text("first request"))
|
.send(&Method::run_text("first request"))
|
||||||
@@ -283,11 +289,11 @@ async fn standalone_restore_preserves_history_tasks_notifications_and_cwd_scope(
|
|||||||
wait_for_run_end(&mut protocol_client).await?;
|
wait_for_run_end(&mut protocol_client).await?;
|
||||||
host.shutdown().await?;
|
host.shutdown().await?;
|
||||||
|
|
||||||
let store = StandaloneSessionStore::open(&state_dir)?;
|
let store = StandaloneWorkerStore::open(&state_dir)?;
|
||||||
let current = store.list(&cwd, StandaloneListScope::CurrentCwd, 100)?;
|
let current = store.list(&cwd, StandaloneListScope::CurrentCwd, 100)?;
|
||||||
assert_eq!(current.len(), 1);
|
assert_eq!(current.len(), 1);
|
||||||
assert_eq!(current[0].session_id, session_id);
|
assert_eq!(current[0].worker_id, worker_id);
|
||||||
assert_eq!(current[0].status, StandaloneSessionStatus::Stopped);
|
assert_eq!(current[0].status, StandaloneWorkerStatus::Stopped);
|
||||||
let other_cwd = temp.path().join("other");
|
let other_cwd = temp.path().join("other");
|
||||||
std::fs::create_dir(&other_cwd)?;
|
std::fs::create_dir(&other_cwd)?;
|
||||||
assert!(
|
assert!(
|
||||||
@@ -307,8 +313,13 @@ async fn standalone_restore_preserves_history_tasks_notifications_and_cwd_scope(
|
|||||||
]]);
|
]]);
|
||||||
let second_inspection = second_client.clone();
|
let second_inspection = second_client.clone();
|
||||||
let host =
|
let host =
|
||||||
StandaloneHost::restore_with_model_client(state_dir.clone(), session_id, second_client)
|
StandaloneHost::restore_with_model_client(state_dir.clone(), worker_id, second_client)
|
||||||
.await?;
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
host.record().worker_name,
|
||||||
|
"display-name-is-not-session-identity"
|
||||||
|
);
|
||||||
|
assert_eq!(host.record().storage_key, format!("standalone-{worker_id}"));
|
||||||
let mut protocol_client = host.connect();
|
let mut protocol_client = host.connect();
|
||||||
let snapshot = format!(
|
let snapshot = format!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
@@ -338,11 +349,11 @@ async fn standalone_restore_preserves_history_tasks_notifications_and_cwd_scope(
|
|||||||
assert!(projected.contains("persisted task"), "{projected}");
|
assert!(projected.contains("persisted task"), "{projected}");
|
||||||
host.shutdown().await?;
|
host.shutdown().await?;
|
||||||
|
|
||||||
store.delete(session_id)?;
|
store.delete(worker_id)?;
|
||||||
assert!(cwd.exists(), "deleting session state must not mutate cwd");
|
assert!(cwd.exists(), "deleting session state must not mutate cwd");
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.load(session_id),
|
store.load(worker_id),
|
||||||
Err(StandaloneStoreError::SessionNotFound(_))
|
Err(StandaloneStoreError::WorkerNotFound(_))
|
||||||
));
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -363,28 +374,25 @@ async fn standalone_restore_rejects_concurrent_lease_and_missing_cwd() -> TestRe
|
|||||||
.resolve()?;
|
.resolve()?;
|
||||||
let host =
|
let host =
|
||||||
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
||||||
let session_id = host.session_id();
|
let worker_id = host.worker_id();
|
||||||
let store = StandaloneSessionStore::open(&state_dir)?;
|
let store = StandaloneWorkerStore::open(&state_dir)?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.acquire_lease(session_id, StaleLeasePolicy::Recover),
|
store.acquire_lease(worker_id, StaleLeasePolicy::Recover),
|
||||||
Err(StandaloneStoreError::SessionLeased(id)) if id == session_id
|
Err(StandaloneStoreError::WorkerLeased(id)) if id == worker_id
|
||||||
));
|
));
|
||||||
let restore = StandaloneHost::restore_with_model_client(
|
let restore = StandaloneHost::restore_with_model_client(
|
||||||
state_dir.clone(),
|
state_dir.clone(),
|
||||||
session_id,
|
worker_id,
|
||||||
ScriptedClient::new(Vec::new()),
|
ScriptedClient::new(Vec::new()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(matches!(
|
assert!(matches!(restore, Err(StandaloneStartupError::WorkerActive)));
|
||||||
restore,
|
|
||||||
Err(StandaloneStartupError::SessionActive)
|
|
||||||
));
|
|
||||||
host.shutdown().await?;
|
host.shutdown().await?;
|
||||||
|
|
||||||
std::fs::rename(&cwd, &moved)?;
|
std::fs::rename(&cwd, &moved)?;
|
||||||
let restore = StandaloneHost::restore_with_model_client(
|
let restore = StandaloneHost::restore_with_model_client(
|
||||||
state_dir,
|
state_dir,
|
||||||
session_id,
|
worker_id,
|
||||||
ScriptedClient::new(Vec::new()),
|
ScriptedClient::new(Vec::new()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -421,11 +429,11 @@ async fn standalone_restore_recovers_only_a_proven_stale_lease() -> TestResult {
|
|||||||
});
|
});
|
||||||
let host =
|
let host =
|
||||||
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
||||||
let session_id = host.session_id();
|
let worker_id = host.worker_id();
|
||||||
host.shutdown().await?;
|
host.shutdown().await?;
|
||||||
let store = StandaloneSessionStore::open(&state_dir)?;
|
let store = StandaloneWorkerStore::open(&state_dir)?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.load(session_id)?.manifest.profile,
|
store.load(worker_id)?.manifest.profile,
|
||||||
Some(manifest::ProfileManifestSnapshot {
|
Some(manifest::ProfileManifestSnapshot {
|
||||||
source: manifest::ProfileSource::Registry {
|
source: manifest::ProfileSource::Registry {
|
||||||
source: manifest::ProfileRegistrySource::User,
|
source: manifest::ProfileRegistrySource::User,
|
||||||
@@ -434,9 +442,9 @@ async fn standalone_restore_recovers_only_a_proven_stale_lease() -> TestResult {
|
|||||||
..
|
..
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
let session_dir = state_dir.join(session_id.to_string());
|
let worker_dir = state_dir.join(worker_id.to_string());
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
session_dir.join("lease.json"),
|
worker_dir.join("lease.json"),
|
||||||
serde_json::to_vec(&serde_json::json!({
|
serde_json::to_vec(&serde_json::json!({
|
||||||
"lease_id": uuid::Uuid::now_v7(),
|
"lease_id": uuid::Uuid::now_v7(),
|
||||||
"pid": u32::MAX,
|
"pid": u32::MAX,
|
||||||
@@ -447,7 +455,7 @@ async fn standalone_restore_recovers_only_a_proven_stale_lease() -> TestResult {
|
|||||||
|
|
||||||
let host = StandaloneHost::restore_with_model_client(
|
let host = StandaloneHost::restore_with_model_client(
|
||||||
state_dir,
|
state_dir,
|
||||||
session_id,
|
worker_id,
|
||||||
ScriptedClient::new(Vec::new()),
|
ScriptedClient::new(Vec::new()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -468,11 +476,11 @@ async fn standalone_restore_rejects_lease_with_missing_start_marker() -> TestRes
|
|||||||
.resolve()?;
|
.resolve()?;
|
||||||
let host =
|
let host =
|
||||||
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
||||||
let session_id = host.session_id();
|
let worker_id = host.worker_id();
|
||||||
host.shutdown().await?;
|
host.shutdown().await?;
|
||||||
let session_dir = state_dir.join(session_id.to_string());
|
let worker_dir = state_dir.join(worker_id.to_string());
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
session_dir.join("lease.json"),
|
worker_dir.join("lease.json"),
|
||||||
serde_json::to_vec(&serde_json::json!({
|
serde_json::to_vec(&serde_json::json!({
|
||||||
"lease_id": uuid::Uuid::now_v7(),
|
"lease_id": uuid::Uuid::now_v7(),
|
||||||
"pid": std::process::id(),
|
"pid": std::process::id(),
|
||||||
@@ -480,14 +488,14 @@ async fn standalone_restore_rejects_lease_with_missing_start_marker() -> TestRes
|
|||||||
}))?,
|
}))?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let store = StandaloneSessionStore::open(&state_dir)?;
|
let store = StandaloneWorkerStore::open(&state_dir)?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.acquire_lease(session_id, StaleLeasePolicy::Recover),
|
store.acquire_lease(worker_id, StaleLeasePolicy::Recover),
|
||||||
Err(StandaloneStoreError::LeaseLivenessUnknown(id)) if id == session_id
|
Err(StandaloneStoreError::LeaseLivenessUnknown(id)) if id == worker_id
|
||||||
));
|
));
|
||||||
let restore = StandaloneHost::restore_with_model_client(
|
let restore = StandaloneHost::restore_with_model_client(
|
||||||
state_dir,
|
state_dir,
|
||||||
session_id,
|
worker_id,
|
||||||
ScriptedClient::new(Vec::new()),
|
ScriptedClient::new(Vec::new()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -511,23 +519,23 @@ async fn standalone_metadata_fails_closed_on_incomplete_or_newer_records() -> Te
|
|||||||
.resolve()?;
|
.resolve()?;
|
||||||
let host =
|
let host =
|
||||||
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
StandaloneHost::start_with_model_client(launch, ScriptedClient::new(Vec::new())).await?;
|
||||||
let session_id = host.session_id();
|
let worker_id = host.worker_id();
|
||||||
host.shutdown().await?;
|
host.shutdown().await?;
|
||||||
let store = StandaloneSessionStore::open(&state_dir)?;
|
let store = StandaloneWorkerStore::open(&state_dir)?;
|
||||||
let session_dir = state_dir.join(session_id.to_string());
|
let worker_dir = state_dir.join(worker_id.to_string());
|
||||||
std::fs::write(session_dir.join("commit.pending"), b"interrupted\n")?;
|
std::fs::write(worker_dir.join("commit.pending"), b"interrupted\n")?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.load(session_id),
|
store.load(worker_id),
|
||||||
Err(StandaloneStoreError::IncompleteCommit(id)) if id == session_id
|
Err(StandaloneStoreError::IncompleteCommit(id)) if id == worker_id
|
||||||
));
|
));
|
||||||
std::fs::remove_file(session_dir.join("commit.pending"))?;
|
std::fs::remove_file(worker_dir.join("commit.pending"))?;
|
||||||
let record_path = session_dir.join("record.json");
|
let record_path = worker_dir.join("record.json");
|
||||||
let mut record: serde_json::Value = serde_json::from_slice(&std::fs::read(&record_path)?)?;
|
let mut record: serde_json::Value = serde_json::from_slice(&std::fs::read(&record_path)?)?;
|
||||||
record["schema_version"] = serde_json::json!(u32::MAX);
|
record["schema_version"] = serde_json::json!(u32::MAX);
|
||||||
std::fs::write(&record_path, serde_json::to_vec_pretty(&record)?)?;
|
std::fs::write(&record_path, serde_json::to_vec_pretty(&record)?)?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.load(session_id),
|
store.load(worker_id),
|
||||||
Err(StandaloneStoreError::NewerSchema { id, .. }) if id == session_id
|
Err(StandaloneStoreError::NewerSchema { id, .. }) if id == worker_id
|
||||||
));
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ use tokio::sync::mpsc;
|
|||||||
|
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||||
use client::transport::Socket;
|
use client::transport::Socket;
|
||||||
use client::{
|
use client::{BackendRuntimeTarget, Client, StandaloneWorkerResumeIntent, connect_backend_runtime};
|
||||||
BackendRuntimeTarget, Client, StandaloneSessionResumeIntent, connect_backend_runtime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
|
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
|
||||||
use crate::composer_keys::{ComposerEditAction, composer_edit_action};
|
use crate::composer_keys::{ComposerEditAction, composer_edit_action};
|
||||||
@@ -193,18 +191,18 @@ pub(crate) async fn run_standalone(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn run_standalone_restore(
|
pub(crate) async fn run_standalone_restore(
|
||||||
intent: StandaloneSessionResumeIntent,
|
intent: StandaloneWorkerResumeIntent,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let session_id = intent.session_id.parse().map_err(|error| {
|
let worker_id = intent.worker_id.parse().map_err(|error| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
format!("Invalid standalone session ID: {error}"),
|
format!("Invalid standalone Worker ID: {error}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let host = StandaloneHost::restore(intent.state_dir, session_id)
|
let host = StandaloneHost::restore(intent.state_dir, worker_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| io::Error::other(format!("Standalone restore failed: {error}")))?;
|
.map_err(|error| io::Error::other(format!("Standalone restore failed: {error}")))?;
|
||||||
let worker_label = format!("standalone-{}", session_id.short());
|
let worker_label = host.record().worker_name.clone();
|
||||||
let history_root = host.record().cwd.canonical_path.clone();
|
let history_root = host.record().cwd.canonical_path.clone();
|
||||||
run_standalone_host(host, worker_label, history_root).await
|
run_standalone_host(host, worker_label, history_root).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ pub enum LaunchMode {
|
|||||||
worker_name: Option<String>,
|
worker_name: Option<String>,
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
},
|
},
|
||||||
/// Restore one client-owned standalone session. The current cwd is the default scope;
|
/// Restore one client-owned standalone Worker. The current cwd is the default scope;
|
||||||
/// `include_all` opts into all standalone sessions under the same client data root.
|
/// `include_all` opts into all standalone Workers under the same client data root.
|
||||||
StandaloneResume { include_all: bool },
|
StandaloneResume { include_all: bool },
|
||||||
/// List Backend Workers and attach to the selected Worker.
|
/// List Backend Workers and attach to the selected Worker.
|
||||||
Workers {
|
Workers {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use client::{StandaloneSessionListIntent, StandaloneSessionResumeIntent, Target};
|
use client::{StandaloneWorkerListIntent, StandaloneWorkerResumeIntent, Target};
|
||||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
@@ -9,7 +9,7 @@ use ratatui::layout::{Constraint, Layout};
|
|||||||
use ratatui::prelude::{Color, Line, Modifier, Span, Style};
|
use ratatui::prelude::{Color, Line, Modifier, Span, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::{TerminalOptions, Viewport};
|
use ratatui::{TerminalOptions, Viewport};
|
||||||
use standalone::{StandaloneListScope, StandaloneSessionRecord, StandaloneSessionStore};
|
use standalone::{StandaloneListScope, StandaloneWorkerRecord, StandaloneWorkerStore};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
const LIMIT: usize = 100;
|
const LIMIT: usize = 100;
|
||||||
@@ -17,28 +17,28 @@ const LIMIT: usize = 100;
|
|||||||
pub(crate) fn pick(
|
pub(crate) fn pick(
|
||||||
target: &dyn Target,
|
target: &dyn Target,
|
||||||
include_all: bool,
|
include_all: bool,
|
||||||
) -> Result<Option<StandaloneSessionResumeIntent>, StandalonePickerError> {
|
) -> Result<Option<StandaloneWorkerResumeIntent>, StandalonePickerError> {
|
||||||
let intent = target
|
let intent = target
|
||||||
.standalone_session_list(include_all)
|
.standalone_worker_list(include_all)
|
||||||
.map_err(StandalonePickerError::Target)?;
|
.map_err(StandalonePickerError::Target)?;
|
||||||
let records = load_records(&intent)?;
|
let records = load_records(&intent)?;
|
||||||
if records.is_empty() {
|
if records.is_empty() {
|
||||||
return Err(StandalonePickerError::NoSessions { include_all });
|
return Err(StandalonePickerError::NoWorkers { include_all });
|
||||||
}
|
}
|
||||||
let selected = run_picker(records)?;
|
let selected = run_picker(records)?;
|
||||||
selected
|
selected
|
||||||
.map(|record| {
|
.map(|record| {
|
||||||
target
|
target
|
||||||
.standalone_session_resume(record.session_id.to_string())
|
.standalone_worker_resume(record.worker_id.to_string())
|
||||||
.map_err(StandalonePickerError::Target)
|
.map_err(StandalonePickerError::Target)
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_records(
|
fn load_records(
|
||||||
intent: &StandaloneSessionListIntent,
|
intent: &StandaloneWorkerListIntent,
|
||||||
) -> Result<Vec<StandaloneSessionRecord>, StandalonePickerError> {
|
) -> Result<Vec<StandaloneWorkerRecord>, StandalonePickerError> {
|
||||||
let store = StandaloneSessionStore::open(&intent.state_dir)
|
let store = StandaloneWorkerStore::open(&intent.state_dir)
|
||||||
.map_err(StandalonePickerError::StateStore)?;
|
.map_err(StandalonePickerError::StateStore)?;
|
||||||
store
|
store
|
||||||
.list(
|
.list(
|
||||||
@@ -54,8 +54,8 @@ fn load_records(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_picker(
|
fn run_picker(
|
||||||
records: Vec<StandaloneSessionRecord>,
|
records: Vec<StandaloneWorkerRecord>,
|
||||||
) -> Result<Option<StandaloneSessionRecord>, StandalonePickerError> {
|
) -> Result<Option<StandaloneWorkerRecord>, StandalonePickerError> {
|
||||||
let height = u16::try_from(records.len().saturating_add(3).min(20)).unwrap_or(20);
|
let height = u16::try_from(records.len().saturating_add(3).min(20)).unwrap_or(20);
|
||||||
let mut terminal = Terminal::with_options(
|
let mut terminal = Terminal::with_options(
|
||||||
CrosstermBackend::new(io::stdout()),
|
CrosstermBackend::new(io::stdout()),
|
||||||
@@ -94,14 +94,14 @@ fn run_picker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(frame: &mut ratatui::Frame<'_>, records: &[StandaloneSessionRecord], selected: usize) {
|
fn draw(frame: &mut ratatui::Frame<'_>, records: &[StandaloneWorkerRecord], selected: usize) {
|
||||||
let mut constraints = vec![Constraint::Length(1)];
|
let mut constraints = vec![Constraint::Length(1)];
|
||||||
constraints.extend(records.iter().map(|_| Constraint::Length(1)));
|
constraints.extend(records.iter().map(|_| Constraint::Length(1)));
|
||||||
constraints.push(Constraint::Length(1));
|
constraints.push(Constraint::Length(1));
|
||||||
let rows = Layout::vertical(constraints).split(frame.area());
|
let rows = Layout::vertical(constraints).split(frame.area());
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::from(Span::styled(
|
Paragraph::new(Line::from(Span::styled(
|
||||||
"resume standalone session",
|
"resume standalone Worker",
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
))),
|
))),
|
||||||
rows[0],
|
rows[0],
|
||||||
@@ -120,7 +120,10 @@ fn draw(frame: &mut ratatui::Frame<'_>, records: &[StandaloneSessionRecord], sel
|
|||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(Line::from(vec![
|
Paragraph::new(Line::from(vec![
|
||||||
Span::raw(marker),
|
Span::raw(marker),
|
||||||
Span::styled(record.session_id.short(), style),
|
Span::styled(
|
||||||
|
format!("{} ({})", record.worker_name, record.worker_id.short()),
|
||||||
|
style,
|
||||||
|
),
|
||||||
Span::raw(format!(
|
Span::raw(format!(
|
||||||
" [{:?}] updated:{} {}",
|
" [{:?}] updated:{} {}",
|
||||||
record.status, record.updated_at_unix_ms, cwd
|
record.status, record.updated_at_unix_ms, cwd
|
||||||
@@ -139,13 +142,13 @@ fn draw(frame: &mut ratatui::Frame<'_>, records: &[StandaloneSessionRecord], sel
|
|||||||
pub(crate) enum StandalonePickerError {
|
pub(crate) enum StandalonePickerError {
|
||||||
#[error("standalone target error: {0}")]
|
#[error("standalone target error: {0}")]
|
||||||
Target(#[source] client::TargetError),
|
Target(#[source] client::TargetError),
|
||||||
#[error("standalone session state is unavailable: {0}")]
|
#[error("standalone Worker state is unavailable: {0}")]
|
||||||
StateStore(#[source] standalone::StandaloneStoreError),
|
StateStore(#[source] standalone::StandaloneStoreError),
|
||||||
#[error(
|
#[error(
|
||||||
"no standalone sessions found for this cwd; use `yoi --local --resume --all` to include all cwd identities"
|
"no standalone Workers found for this cwd; use `yoi --local --resume --all` to include all cwd identities"
|
||||||
)]
|
)]
|
||||||
NoSessions { include_all: bool },
|
NoWorkers { include_all: bool },
|
||||||
#[error("standalone session picker I/O failed: {0}")]
|
#[error("standalone Worker picker I/O failed: {0}")]
|
||||||
Io(#[source] io::Error),
|
Io(#[source] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,105 +1,9 @@
|
|||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{fmt, str::FromStr};
|
|
||||||
use uuid::{Uuid, Version};
|
|
||||||
|
|
||||||
|
pub use protocol::{WorkerId, WorkerIdParseError};
|
||||||
pub use workdir::workspace::RuntimeWorkerRef;
|
pub use workdir::workspace::RuntimeWorkerRef;
|
||||||
|
|
||||||
/// Stable Workspace-owned Worker identity.
|
|
||||||
///
|
|
||||||
/// Runtime placement is deliberately not part of this value. New identities are
|
|
||||||
/// allocated by Workspace authority before a Runtime create request is sent.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct WorkerId(Uuid);
|
|
||||||
|
|
||||||
impl WorkerId {
|
|
||||||
pub fn now_v7() -> Self {
|
|
||||||
Self(Uuid::now_v7())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a legacy Runtime-local numeric id into a syntactically valid
|
|
||||||
/// migration-only UUIDv7 value. New Worker allocation must use `now_v7`.
|
|
||||||
pub fn from_legacy_u64(value: u64) -> Self {
|
|
||||||
let mut bytes = [0_u8; 16];
|
|
||||||
bytes[8..].copy_from_slice(&value.to_be_bytes());
|
|
||||||
bytes[6] = 0x70;
|
|
||||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
||||||
Self(Uuid::from_bytes(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_legacy_binding(workspace_id: &str, runtime_id: &str, value: u64) -> Self {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(b"yoi.workspace-worker-id.v1\0");
|
|
||||||
hasher.update(workspace_id.as_bytes());
|
|
||||||
hasher.update([0]);
|
|
||||||
hasher.update(runtime_id.as_bytes());
|
|
||||||
hasher.update([0]);
|
|
||||||
hasher.update(value.to_be_bytes());
|
|
||||||
let digest = hasher.finalize();
|
|
||||||
let mut bytes = [0_u8; 16];
|
|
||||||
bytes.copy_from_slice(&digest[..16]);
|
|
||||||
// Migrated ids sort before normally allocated UUIDv7 values while retaining
|
|
||||||
// deterministic collision-resistant payload bits.
|
|
||||||
bytes[..6].fill(0);
|
|
||||||
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
||||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
||||||
Self(Uuid::from_bytes(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(value: &str) -> Option<Self> {
|
|
||||||
let value = Uuid::parse_str(value).ok()?;
|
|
||||||
(value.get_version() == Some(Version::SortRand)).then_some(Self(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn as_uuid(self) -> Uuid {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for WorkerId {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(formatter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for WorkerId {
|
|
||||||
type Err = WorkerIdParseError;
|
|
||||||
|
|
||||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
||||||
Self::parse(value).ok_or(WorkerIdParseError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for WorkerId {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for WorkerId {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let value = String::deserialize(deserializer)?;
|
|
||||||
Self::parse(&value).ok_or_else(|| de::Error::custom("Worker id must be a UUIDv7"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct WorkerIdParseError;
|
|
||||||
|
|
||||||
impl fmt::Display for WorkerIdParseError {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
formatter.write_str("Worker id must be a UUIDv7")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for WorkerIdParseError {}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct LegacyWorkerIdentityMapping {
|
pub struct LegacyWorkerIdentityMapping {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
@@ -140,7 +44,7 @@ pub fn legacy_worker_identity_mapping_digest(mappings: &[LegacyWorkerIdentityMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Runtime-local authority reference for Worker operations. The contained id is
|
/// Runtime-local authority reference for Worker operations. The contained id is
|
||||||
/// nevertheless the Workspace-owned stable identity; the Runtime does not mint it.
|
/// nevertheless the stable Worker identity; the Runtime does not mint it.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
pub struct WorkerRef {
|
pub struct WorkerRef {
|
||||||
pub worker_id: WorkerId,
|
pub worker_id: WorkerId,
|
||||||
@@ -164,14 +68,6 @@ impl TryFrom<&RuntimeWorkerRef> for WorkerRef {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn worker_id_accepts_only_uuid_v7() {
|
|
||||||
let worker_id = WorkerId::now_v7();
|
|
||||||
assert_eq!(WorkerId::parse(&worker_id.to_string()), Some(worker_id));
|
|
||||||
assert!(WorkerId::parse("30").is_none());
|
|
||||||
assert!(WorkerId::parse(&Uuid::nil().to_string()).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_worker_ref_preserves_stable_worker_identity() {
|
fn runtime_worker_ref_preserves_stable_worker_identity() {
|
||||||
let worker_id = WorkerId::now_v7();
|
let worker_id = WorkerId::now_v7();
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ fn standalone_target() -> Result<Box<dyn Target>, ParseError> {
|
|||||||
})?
|
})?
|
||||||
.join("client")
|
.join("client")
|
||||||
.join("standalone")
|
.join("standalone")
|
||||||
.join("sessions");
|
.join("workers");
|
||||||
Ok(Box::new(StandaloneTarget::new(state_dir)))
|
Ok(Box::new(StandaloneTarget::new(state_dir)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -798,7 +798,7 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
|||||||
}
|
}
|
||||||
if session.is_some() {
|
if session.is_some() {
|
||||||
return Err(ParseError(
|
return Err(ParseError(
|
||||||
"--local does not accept legacy --session; use --local --resume for Standalone session restore"
|
"--local does not accept legacy --session; use --local --resume for Standalone Worker restore"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -834,7 +834,7 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
|||||||
|
|
||||||
if target.kind() == TargetKind::Standalone && (session.is_some() || socket_override.is_some()) {
|
if target.kind() == TargetKind::Standalone && (session.is_some() || socket_override.is_some()) {
|
||||||
return Err(ParseError(
|
return Err(ParseError(
|
||||||
"Standalone does not accept legacy Worker session or socket selectors; use --resume for the standalone session store"
|
"Standalone does not accept legacy Worker session or socket selectors; use --resume for the standalone Worker store"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -951,7 +951,7 @@ fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
|||||||
)?;
|
)?;
|
||||||
if target.kind() != TargetKind::Backend {
|
if target.kind() != TargetKind::Backend {
|
||||||
return Err(ParseError(
|
return Err(ParseError(
|
||||||
"yoi workers requires a Backend connection target; use yoi --local --resume for Standalone sessions"
|
"yoi workers requires a Backend connection target; use yoi --local --resume for Standalone Workers"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1750,8 +1750,8 @@ Usage:
|
|||||||
|
|
||||||
Target selection:
|
Target selection:
|
||||||
--local Use the client-owned one-process Standalone host
|
--local Use the client-owned one-process Standalone host
|
||||||
--resume With --local, restore from the Standalone session store
|
--resume With --local, restore from the Standalone Worker store
|
||||||
--all With Standalone restore, include sessions from every cwd identity
|
--all With Standalone restore, include Workers from every cwd identity
|
||||||
--backend <URL> Use a Workspace Backend explicitly
|
--backend <URL> Use a Workspace Backend explicitly
|
||||||
--workspace-id <ID> Scope Backend routes to a Workspace id
|
--workspace-id <ID> Scope Backend routes to a Workspace id
|
||||||
|
|
||||||
@@ -1761,7 +1761,7 @@ Target selection:
|
|||||||
|
|
||||||
Connection-aware commands:
|
Connection-aware commands:
|
||||||
yoi Standalone: new Console. Backend: Worker picker.
|
yoi Standalone: new Console. Backend: Worker picker.
|
||||||
yoi resume Standalone session picker or stopped Backend Worker picker.
|
yoi resume Standalone Worker picker or stopped Backend Worker picker.
|
||||||
yoi workers Backend Workspace Worker picker.
|
yoi workers Backend Workspace Worker picker.
|
||||||
yoi panel Backend Workspace dashboard.
|
yoi panel Backend Workspace dashboard.
|
||||||
|
|
||||||
@@ -1800,7 +1800,7 @@ Usage:
|
|||||||
yoi --backend <URL> [--workspace-id <ID>] workers [-r|--stopped] [--workspace <PATH>] [--runtime-id <ID>]
|
yoi --backend <URL> [--workspace-id <ID>] workers [-r|--stopped] [--workspace <PATH>] [--runtime-id <ID>]
|
||||||
|
|
||||||
Authority:
|
Authority:
|
||||||
Lists Workers from the selected Backend Workspace. Standalone sessions are restored with
|
Lists Workers from the selected Backend Workspace. Standalone Workers are restored with
|
||||||
`yoi --local --resume` and are not part of the Workspace Worker catalog.
|
`yoi --local --resume` and are not part of the Workspace Worker catalog.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@@ -1822,13 +1822,13 @@ Usage:
|
|||||||
yoi [TARGET] resume [--workspace <PATH>|--all] [--runtime-id <ID>]
|
yoi [TARGET] resume [--workspace <PATH>|--all] [--runtime-id <ID>]
|
||||||
|
|
||||||
Target options:
|
Target options:
|
||||||
--local Restore from the client-owned Standalone session store
|
--local Restore from the client-owned Standalone Worker store
|
||||||
--backend <URL> Restore a stopped Backend Workspace Worker
|
--backend <URL> Restore a stopped Backend Workspace Worker
|
||||||
--workspace-id <ID> Scope Backend routes to a Workspace id
|
--workspace-id <ID> Scope Backend routes to a Workspace id
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--workspace <PATH> Scope Standalone sessions to this cwd identity (defaults to cwd)
|
--workspace <PATH> Scope Standalone Workers to this cwd identity (defaults to cwd)
|
||||||
--all Include Standalone sessions from every cwd identity
|
--all Include Standalone Workers from every cwd identity
|
||||||
--runtime-id <ID> Restrict the Backend stopped-Worker picker to a Runtime id
|
--runtime-id <ID> Restrict the Backend stopped-Worker picker to a Runtime id
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
"#;
|
"#;
|
||||||
@@ -2222,8 +2222,8 @@ backend = "shared"
|
|||||||
mode,
|
mode,
|
||||||
LaunchMode::StandaloneResume { include_all: false }
|
LaunchMode::StandaloneResume { include_all: false }
|
||||||
));
|
));
|
||||||
let intent = target.standalone_session_list(false).unwrap();
|
let intent = target.standalone_worker_list(false).unwrap();
|
||||||
assert!(intent.state_dir.ends_with("client/standalone/sessions"));
|
assert!(intent.state_dir.ends_with("client/standalone/workers"));
|
||||||
assert!(!intent.include_all);
|
assert!(!intent.include_all);
|
||||||
|
|
||||||
let mode = parse_args_from(["--local", "--resume", "--all"]).unwrap();
|
let mode = parse_args_from(["--local", "--resume", "--all"]).unwrap();
|
||||||
@@ -2264,7 +2264,7 @@ backend = "shared"
|
|||||||
let err = parse_args_slice_with_connection_resolver(&session_args, &resolver).unwrap_err();
|
let err = parse_args_slice_with_connection_resolver(&session_args, &resolver).unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.0,
|
err.0,
|
||||||
"--local does not accept legacy --session; use --local --resume for Standalone session restore"
|
"--local does not accept legacy --session; use --local --resume for Standalone Worker restore"
|
||||||
);
|
);
|
||||||
|
|
||||||
let socket_args = [
|
let socket_args = [
|
||||||
@@ -2903,12 +2903,12 @@ backend = "shared"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_resume_help_uses_standalone_session_store_terminology() {
|
fn parse_resume_help_uses_standalone_worker_store_terminology() {
|
||||||
match parse_args_from(["resume", "--help"]).unwrap() {
|
match parse_args_from(["resume", "--help"]).unwrap() {
|
||||||
Mode::ResumeHelp => {}
|
Mode::ResumeHelp => {}
|
||||||
_ => panic!("expected ResumeHelp mode"),
|
_ => panic!("expected ResumeHelp mode"),
|
||||||
}
|
}
|
||||||
assert!(RESUME_HELP.contains("Standalone session store"));
|
assert!(RESUME_HELP.contains("Standalone Worker store"));
|
||||||
assert!(RESUME_HELP.contains("Backend stopped-Worker picker"));
|
assert!(RESUME_HELP.contains("Backend stopped-Worker picker"));
|
||||||
assert!(!RESUME_HELP.contains("local Worker records"));
|
assert!(!RESUME_HELP.contains("local Worker records"));
|
||||||
assert!(!RESUME_HELP.contains("local workspace"));
|
assert!(!RESUME_HELP.contains("local workspace"));
|
||||||
|
|||||||
Reference in New Issue
Block a user