Add projected startup cause and secondary failure contracts
This commit is contained in:
Generated
+3
@@ -4768,6 +4768,7 @@ dependencies = [
|
|||||||
"agen",
|
"agen",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"error-contract",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fs4",
|
"fs4",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -7272,6 +7273,7 @@ dependencies = [
|
|||||||
"client",
|
"client",
|
||||||
"config-source",
|
"config-source",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"error-contract",
|
||||||
"flow",
|
"flow",
|
||||||
"fs-operation",
|
"fs-operation",
|
||||||
"fs4",
|
"fs4",
|
||||||
@@ -7328,6 +7330,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"decodal",
|
"decodal",
|
||||||
|
"error-contract",
|
||||||
"flow",
|
"flow",
|
||||||
"fs4",
|
"fs4",
|
||||||
"futures",
|
"futures",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod invocation;
|
pub mod invocation;
|
||||||
|
pub mod startup_failure;
|
||||||
#[cfg(feature = "stream")]
|
#[cfg(feature = "stream")]
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
//! Startup diagnostics are observations, never lifecycle, ownership, or retry authority.
|
||||||
|
//!
|
||||||
|
//! Attachments contain a domain's explicitly projected error-contract envelope. Receivers
|
||||||
|
//! decode through the existing codec, and must project again for a different audience;
|
||||||
|
//! deserializing stored bytes never creates a disclosure capability. Unknown and corrupt
|
||||||
|
//! attachments remain readable alongside metadata instead of breaking its lifecycle fields.
|
||||||
|
use error_contract::{
|
||||||
|
Audience, Budget, ContractError, DecodeFailure, ProjectedFailure, WireDetails, decode_failure,
|
||||||
|
encode_projected_failure,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A bounded, projected envelope at the Worker/Runtime or metadata boundary.
|
||||||
|
/// No constructor accepts Display text, arbitrary JSON fields, or an unprojected error.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct StartupFailureAttachment(String);
|
||||||
|
|
||||||
|
impl std::fmt::Debug for StartupFailureAttachment {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("StartupFailureAttachment")
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StartupFailureAttachment {
|
||||||
|
pub fn from_projected<A: Audience, D: WireDetails>(
|
||||||
|
failure: &ProjectedFailure<A, D>,
|
||||||
|
) -> Result<Self, ContractError> {
|
||||||
|
let encoded = encode_projected_failure(failure, Self::budget())?;
|
||||||
|
String::from_utf8(encoded.as_bytes().to_vec())
|
||||||
|
.map(Self)
|
||||||
|
.map_err(|_| ContractError::Encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Missing attachments are represented by the enclosing Option, not a fabricated cause.
|
||||||
|
pub fn decode<D: WireDetails>(&self) -> DecodeFailure<D> {
|
||||||
|
decode_failure(self.0.as_bytes(), Self::budget())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn budget() -> Budget {
|
||||||
|
Budget::new(
|
||||||
|
error_contract::MAX_FAILURE_BYTES,
|
||||||
|
error_contract::MAX_FAILURE_BYTES,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owner-selected startup stage; independent of resource cleanup disposition.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupStage {
|
||||||
|
Preflight,
|
||||||
|
Metadata,
|
||||||
|
Reconstruction,
|
||||||
|
Recovery,
|
||||||
|
Binding,
|
||||||
|
Feature,
|
||||||
|
ActivationEffects,
|
||||||
|
Controller,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe local Runtime cause when no more specific domain attachment was supplied.
|
||||||
|
/// The primary cause is selected where the failure occurs, not parsed from its diagnostic.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct RuntimeStartupCause {
|
||||||
|
pub stage: StartupStage,
|
||||||
|
pub reason: RuntimeStartupReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coarse local causes do not claim a domain authorization verdict.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum RuntimeStartupReason {
|
||||||
|
StartupFailed,
|
||||||
|
SegmentEmpty,
|
||||||
|
SystemPromptMissing,
|
||||||
|
MetadataPending,
|
||||||
|
MetadataMissing,
|
||||||
|
MetadataInactive,
|
||||||
|
ManifestMissing,
|
||||||
|
AllocationFailed,
|
||||||
|
SessionReadFailed,
|
||||||
|
MetadataReadFailed,
|
||||||
|
ProviderFailed,
|
||||||
|
AttributionFailed,
|
||||||
|
FeatureFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error_contract::FailureCause for RuntimeStartupCause {
|
||||||
|
fn identity(&self) -> error_contract::FailureIdentity {
|
||||||
|
error_contract::FailureIdentity {
|
||||||
|
domain: "worker_startup",
|
||||||
|
reason: "failed",
|
||||||
|
category: error_contract::Category::Internal,
|
||||||
|
details_version: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn validate(&self) -> Result<(), ContractError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WireDetails for RuntimeStartupCause {
|
||||||
|
fn supports(domain: &str, reason: &str, version: u16) -> bool {
|
||||||
|
domain == "worker_startup" && reason == "failed" && version == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This cause contains only an enum. Arbitrary provider messages, IO paths and credentials
|
||||||
|
// cannot enter its projection. Domain-specific causes must supply their own projector.
|
||||||
|
macro_rules! project_runtime {
|
||||||
|
($($audience:ty),*) => {$(
|
||||||
|
impl error_contract::ProjectFor<$audience> for RuntimeStartupCause {
|
||||||
|
type Authorization = ();
|
||||||
|
type Details = Self;
|
||||||
|
fn disclose(&self, facts: &error_contract::FailureFacts<Self>, _: &())
|
||||||
|
-> Result<error_contract::Disclosure<Self>, ContractError>
|
||||||
|
{
|
||||||
|
let mut disclosure = error_contract::Disclosure::new(self.clone(), "Worker startup failed.");
|
||||||
|
// Only fixed owner-selected operation/stage labels are published here.
|
||||||
|
for context in &facts.context().operations {
|
||||||
|
if context.operation == "worker_startup" && context.stage == "runtime" {
|
||||||
|
disclosure.context.operations.push(context.clone());
|
||||||
|
if let Some(last) = disclosure.context.operations.last_mut() {
|
||||||
|
last.operation_ref = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(disclosure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*};
|
||||||
|
}
|
||||||
|
project_runtime!(
|
||||||
|
error_contract::InternalTransfer,
|
||||||
|
error_contract::Storage,
|
||||||
|
error_contract::Browser
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupFailurePhase {
|
||||||
|
Cleanup,
|
||||||
|
RuntimeRecordDelete,
|
||||||
|
RuntimeSnapshotSave,
|
||||||
|
RuntimeRemovalPublish,
|
||||||
|
MetadataSave,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupCleanupFailure {
|
||||||
|
Ownership,
|
||||||
|
Workdir,
|
||||||
|
Artifacts,
|
||||||
|
Allocation,
|
||||||
|
Feature,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A separate secondary fact, not a replacement primary cause or a causal-chain link.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupSecondaryFailure {
|
||||||
|
Cleanup {
|
||||||
|
failure: StartupCleanupFailure,
|
||||||
|
},
|
||||||
|
Persistence {
|
||||||
|
phase: StartupFailurePhase,
|
||||||
|
outcome: StartupPersistenceOutcome,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persisted is evidence only for this attachment/record write, not execution completion.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupPersistenceOutcome {
|
||||||
|
Persisted,
|
||||||
|
NotSaved,
|
||||||
|
Conflict,
|
||||||
|
OutcomeUnknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Independent resource observations from the existing execution owner. These diagnostics
|
||||||
|
/// never promote a result to RolledBack or authorize replay; the backend disposition does.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupResources {
|
||||||
|
None,
|
||||||
|
WorkerStartup,
|
||||||
|
Unproven,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub enum StartupCleanup {
|
||||||
|
NotNeeded,
|
||||||
|
Required,
|
||||||
|
Confirmed,
|
||||||
|
Failed(StartupCleanupFailure),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
pub struct StartupResourceFacts {
|
||||||
|
pub resources: StartupResources,
|
||||||
|
pub cleanup: StartupCleanup,
|
||||||
|
pub durable_effects_possible: bool,
|
||||||
|
}
|
||||||
@@ -10,6 +10,11 @@ use protocol::{CompletionEntry, CompletionKind, Segment, WorkerId, WorkerStateSn
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use workdir::workspace::{MaterializerKind, RuntimeWorkerRef, RuntimeWorkingDirectorySummary};
|
use workdir::workspace::{MaterializerKind, RuntimeWorkerRef, RuntimeWorkingDirectorySummary};
|
||||||
|
|
||||||
|
pub use protocol::startup_failure::{
|
||||||
|
RuntimeStartupCause, StartupCleanupFailure, StartupFailureAttachment, StartupFailurePhase,
|
||||||
|
StartupPersistenceOutcome, StartupSecondaryFailure, StartupStage,
|
||||||
|
};
|
||||||
|
|
||||||
pub const RUNTIME_API_VERSION: &str = "v1";
|
pub const RUNTIME_API_VERSION: &str = "v1";
|
||||||
pub const RUNTIME_API_BASE_PATH: &str = "/v1";
|
pub const RUNTIME_API_BASE_PATH: &str = "/v1";
|
||||||
/// Runtime transport classification for a materialized Repository source.
|
/// Runtime transport classification for a materialized Repository source.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
error-contract.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
agen = { workspace = true }
|
agen = { workspace = true }
|
||||||
fs4.workspace = true
|
fs4.workspace = true
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ http-server = ["dep:axum", "dep:tower", "dep:reqwest"]
|
|||||||
ws-server = ["http-server", "axum/ws", "tokio/sync"]
|
ws-server = ["http-server", "axum/ws", "tokio/sync"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
error-contract.workspace = true
|
||||||
agen.workspace = true
|
agen.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ pub struct WorkerExecutionResult {
|
|||||||
pub submission: Option<WorkerSubmissionAck>,
|
pub submission: Option<WorkerSubmissionAck>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub notification: Option<WorkerNotificationAck>,
|
pub notification: Option<WorkerNotificationAck>,
|
||||||
|
/// Projected primary facts. Never stop, cleanup, or retry evidence.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub startup_failure: Option<protocol::startup_failure::StartupFailureAttachment>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub secondary_failures: Vec<protocol::startup_failure::StartupSecondaryFailure>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub startup_resources: Option<protocol::startup_failure::StartupResourceFacts>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub startup_failure_persistence: Option<protocol::startup_failure::StartupPersistenceOutcome>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backend result class for a Worker execution operation.
|
/// Backend result class for a Worker execution operation.
|
||||||
@@ -116,6 +125,10 @@ impl WorkerExecutionResult {
|
|||||||
message: None,
|
message: None,
|
||||||
submission: None,
|
submission: None,
|
||||||
notification: None,
|
notification: None,
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +154,10 @@ impl WorkerExecutionResult {
|
|||||||
disposition,
|
disposition,
|
||||||
}),
|
}),
|
||||||
notification: None,
|
notification: None,
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +174,10 @@ impl WorkerExecutionResult {
|
|||||||
notification: Some(WorkerNotificationAck {
|
notification: Some(WorkerNotificationAck {
|
||||||
notification_request_id: notification_request_id.into(),
|
notification_request_id: notification_request_id.into(),
|
||||||
}),
|
}),
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +189,10 @@ impl WorkerExecutionResult {
|
|||||||
message: Some(message.into()),
|
message: Some(message.into()),
|
||||||
submission: None,
|
submission: None,
|
||||||
notification: None,
|
notification: None,
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +204,10 @@ impl WorkerExecutionResult {
|
|||||||
message: Some(message.into()),
|
message: Some(message.into()),
|
||||||
submission: None,
|
submission: None,
|
||||||
notification: None,
|
notification: None,
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +219,10 @@ impl WorkerExecutionResult {
|
|||||||
message: Some(message.into()),
|
message: Some(message.into()),
|
||||||
submission: None,
|
submission: None,
|
||||||
notification: None,
|
notification: None,
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,9 +234,36 @@ impl WorkerExecutionResult {
|
|||||||
message: Some(message.into()),
|
message: Some(message.into()),
|
||||||
submission: None,
|
submission: None,
|
||||||
notification: None,
|
notification: None,
|
||||||
|
startup_failure: None,
|
||||||
|
secondary_failures: Vec::new(),
|
||||||
|
startup_resources: None,
|
||||||
|
startup_failure_persistence: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_startup_failure(
|
||||||
|
mut self,
|
||||||
|
failure: protocol::startup_failure::StartupFailureAttachment,
|
||||||
|
) -> Self {
|
||||||
|
self.startup_failure = Some(failure);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn startup_failure<D: error_contract::WireDetails>(
|
||||||
|
&self,
|
||||||
|
) -> Option<error_contract::DecodeFailure<D>> {
|
||||||
|
self.startup_failure
|
||||||
|
.as_ref()
|
||||||
|
.map(|failure| failure.decode())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_secondary_failure(
|
||||||
|
&mut self,
|
||||||
|
failure: protocol::startup_failure::StartupSecondaryFailure,
|
||||||
|
) {
|
||||||
|
self.secondary_failures.push(failure);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_accepted(&self) -> bool {
|
pub fn is_accepted(&self) -> bool {
|
||||||
self.outcome == WorkerExecutionOutcome::Accepted
|
self.outcome == WorkerExecutionOutcome::Accepted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ autobins = false
|
|||||||
default = []
|
default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
error-contract.workspace = true
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
agen = { workspace = true, features = ["codex"] }
|
agen = { workspace = true, features = ["codex"] }
|
||||||
session-store = { workspace = true }
|
session-store = { workspace = true }
|
||||||
|
|||||||
Reference in New Issue
Block a user