implement refactor
This commit is contained in:
parent
a7ac85917c
commit
375a15d9e9
7 changed files with 1177 additions and 301 deletions
|
|
@ -49,6 +49,10 @@ crate.spec(
|
|||
package = "uuid",
|
||||
version = "1.0",
|
||||
)
|
||||
crate.spec(
|
||||
package = "sha2",
|
||||
version = "0.10",
|
||||
)
|
||||
crate.spec(
|
||||
features = ["bundled"],
|
||||
package = "rusqlite",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -39,6 +39,7 @@ rust_library(
|
|||
"@crates//:tower-http",
|
||||
"@crates//:tracing",
|
||||
"@crates//:uuid",
|
||||
"@crates//:sha2",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -227,6 +227,9 @@ message PartitionDetail {
|
|||
// Wants that reference this partition
|
||||
repeated string want_ids = 5;
|
||||
repeated string taint_ids = 6;
|
||||
// The unique identifier for this partition instance (UUID as string)
|
||||
// Each time a partition is built, it gets a new UUID derived from the job_run_id
|
||||
string uuid = 7;
|
||||
}
|
||||
message PartitionStatus {
|
||||
PartitionStatusCode code = 1;
|
||||
|
|
@ -248,7 +251,18 @@ enum PartitionStatusCode {
|
|||
}
|
||||
|
||||
message TaintDetail {
|
||||
// TODO
|
||||
// The unique identifier for this taint
|
||||
string taint_id = 1;
|
||||
// The root taint ID (for taint hierarchies)
|
||||
string root_taint_id = 2;
|
||||
// The parent taint ID (for taint hierarchies)
|
||||
string parent_taint_id = 3;
|
||||
// The partitions affected by this taint
|
||||
repeated PartitionRef partitions = 4;
|
||||
// Source of the taint event
|
||||
EventSource source = 5;
|
||||
// Optional comment describing the taint
|
||||
optional string comment = 6;
|
||||
}
|
||||
|
||||
message JobRunStatus {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,35 @@
|
|||
use crate::{PartitionDetail, PartitionRef, PartitionStatus, PartitionStatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// State: Partition has been referenced but not yet built
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MissingState {}
|
||||
/// Derive a deterministic UUID from job_run_id and partition_ref.
|
||||
/// This ensures replay produces the same UUIDs.
|
||||
pub fn derive_partition_uuid(job_run_id: &str, partition_ref: &str) -> Uuid {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(job_run_id.as_bytes());
|
||||
hasher.update(partition_ref.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
Uuid::from_slice(&hash[0..16]).expect("SHA256 produces at least 16 bytes")
|
||||
}
|
||||
|
||||
/// State: Partition is currently being built by one or more jobs
|
||||
/// State: Partition is currently being built by a job
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildingState {
|
||||
pub building_by: Vec<String>, // job_run_ids
|
||||
pub job_run_id: String,
|
||||
}
|
||||
|
||||
/// State: Partition is waiting for upstream dependencies to be built
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpstreamBuildingState {
|
||||
pub job_run_id: String,
|
||||
pub missing_deps: Vec<PartitionRef>, // partition refs that are missing
|
||||
}
|
||||
|
||||
/// State: Upstream dependencies are satisfied, partition is ready to retry building
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpForRetryState {
|
||||
pub original_job_run_id: String, // job that had the dep miss
|
||||
}
|
||||
|
||||
/// State: Partition has been successfully built
|
||||
|
|
@ -18,13 +39,20 @@ pub struct LiveState {
|
|||
pub built_by: String, // job_run_id
|
||||
}
|
||||
|
||||
/// State: Partition build failed
|
||||
/// State: Partition build failed (hard failure, not retryable)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FailedState {
|
||||
pub failed_at: u64,
|
||||
pub failed_by: String, // job_run_id
|
||||
}
|
||||
|
||||
/// State: Partition failed because upstream dependencies failed (terminal)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpstreamFailedState {
|
||||
pub failed_at: u64,
|
||||
pub failed_upstream_refs: Vec<PartitionRef>, // which upstream partitions failed
|
||||
}
|
||||
|
||||
/// State: Partition has been marked as invalid/tainted
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaintedState {
|
||||
|
|
@ -32,21 +60,25 @@ pub struct TaintedState {
|
|||
pub taint_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Generic partition struct parameterized by state
|
||||
/// Generic partition struct parameterized by state.
|
||||
/// Each partition has a unique UUID derived from the job_run_id that created it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartitionWithState<S> {
|
||||
pub uuid: Uuid,
|
||||
pub partition_ref: PartitionRef,
|
||||
pub want_ids: Vec<String>,
|
||||
pub state: S,
|
||||
}
|
||||
|
||||
/// Wrapper enum for storing partitions in collections
|
||||
/// Wrapper enum for storing partitions in collections.
|
||||
/// Note: Missing state has been removed - partitions are only created when jobs start building them.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Partition {
|
||||
Missing(PartitionWithState<MissingState>),
|
||||
Building(PartitionWithState<BuildingState>),
|
||||
UpstreamBuilding(PartitionWithState<UpstreamBuildingState>),
|
||||
UpForRetry(PartitionWithState<UpForRetryState>),
|
||||
Live(PartitionWithState<LiveState>),
|
||||
Failed(PartitionWithState<FailedState>),
|
||||
UpstreamFailed(PartitionWithState<UpstreamFailedState>),
|
||||
Tainted(PartitionWithState<TaintedState>),
|
||||
}
|
||||
|
||||
|
|
@ -54,14 +86,6 @@ pub enum Partition {
|
|||
/// is critical that these be treated with respect, not just summoned because it's convenient.
|
||||
/// These should be created ephemerally from typestate objects via .get_ref() and used
|
||||
/// immediately — never stored long-term, as partition state can change.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MissingPartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<MissingState> {
|
||||
pub fn get_ref(&self) -> MissingPartitionRef {
|
||||
MissingPartitionRef(self.partition_ref.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BuildingPartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<BuildingState> {
|
||||
|
|
@ -70,6 +94,22 @@ impl PartitionWithState<BuildingState> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct UpstreamBuildingPartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<UpstreamBuildingState> {
|
||||
pub fn get_ref(&self) -> UpstreamBuildingPartitionRef {
|
||||
UpstreamBuildingPartitionRef(self.partition_ref.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct UpForRetryPartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<UpForRetryState> {
|
||||
pub fn get_ref(&self) -> UpForRetryPartitionRef {
|
||||
UpForRetryPartitionRef(self.partition_ref.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LivePartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<LiveState> {
|
||||
|
|
@ -86,6 +126,14 @@ impl PartitionWithState<FailedState> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct UpstreamFailedPartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<UpstreamFailedState> {
|
||||
pub fn get_ref(&self) -> UpstreamFailedPartitionRef {
|
||||
UpstreamFailedPartitionRef(self.partition_ref.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TaintedPartitionRef(pub PartitionRef);
|
||||
impl PartitionWithState<TaintedState> {
|
||||
|
|
@ -94,61 +142,101 @@ impl PartitionWithState<TaintedState> {
|
|||
}
|
||||
}
|
||||
|
||||
// Type-safe transition methods for MissingState
|
||||
impl PartitionWithState<MissingState> {
|
||||
/// Transition from Missing to Building when a job starts building this partition
|
||||
pub fn start_building(self, job_run_id: String) -> PartitionWithState<BuildingState> {
|
||||
// Type-safe transition methods for BuildingState
|
||||
impl PartitionWithState<BuildingState> {
|
||||
/// Create a new partition directly in Building state.
|
||||
/// UUID is derived from job_run_id + partition_ref for deterministic replay.
|
||||
pub fn new(job_run_id: String, partition_ref: PartitionRef) -> Self {
|
||||
let uuid = derive_partition_uuid(&job_run_id, &partition_ref.r#ref);
|
||||
PartitionWithState {
|
||||
uuid,
|
||||
partition_ref,
|
||||
state: BuildingState { job_run_id },
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from Building to Live when a job successfully completes
|
||||
pub fn complete(self, timestamp: u64) -> PartitionWithState<LiveState> {
|
||||
PartitionWithState {
|
||||
uuid: self.uuid,
|
||||
partition_ref: self.partition_ref,
|
||||
want_ids: self.want_ids,
|
||||
state: BuildingState {
|
||||
building_by: vec![job_run_id],
|
||||
state: LiveState {
|
||||
built_at: timestamp,
|
||||
built_by: self.state.job_run_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from Building to Failed when a job fails (hard failure)
|
||||
pub fn fail(self, timestamp: u64) -> PartitionWithState<FailedState> {
|
||||
PartitionWithState {
|
||||
uuid: self.uuid,
|
||||
partition_ref: self.partition_ref,
|
||||
state: FailedState {
|
||||
failed_at: timestamp,
|
||||
failed_by: self.state.job_run_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from Building to UpstreamBuilding when job reports missing dependencies
|
||||
pub fn dep_miss(
|
||||
self,
|
||||
missing_deps: Vec<PartitionRef>,
|
||||
) -> PartitionWithState<UpstreamBuildingState> {
|
||||
PartitionWithState {
|
||||
uuid: self.uuid,
|
||||
partition_ref: self.partition_ref,
|
||||
state: UpstreamBuildingState {
|
||||
job_run_id: self.state.job_run_id,
|
||||
missing_deps,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe transition methods for BuildingState
|
||||
impl PartitionWithState<BuildingState> {
|
||||
/// Transition from Building to Live when a job successfully completes
|
||||
pub fn complete(self, job_run_id: String, timestamp: u64) -> PartitionWithState<LiveState> {
|
||||
// Type-safe transition methods for UpstreamBuildingState
|
||||
impl PartitionWithState<UpstreamBuildingState> {
|
||||
/// Transition from UpstreamBuilding to UpForRetry when all upstream deps are satisfied
|
||||
pub fn upstreams_satisfied(self) -> PartitionWithState<UpForRetryState> {
|
||||
PartitionWithState {
|
||||
uuid: self.uuid,
|
||||
partition_ref: self.partition_ref,
|
||||
want_ids: self.want_ids,
|
||||
state: LiveState {
|
||||
built_at: timestamp,
|
||||
built_by: job_run_id,
|
||||
state: UpForRetryState {
|
||||
original_job_run_id: self.state.job_run_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from Building to Failed when a job fails
|
||||
pub fn fail(self, job_run_id: String, timestamp: u64) -> PartitionWithState<FailedState> {
|
||||
/// Transition from UpstreamBuilding to UpstreamFailed when an upstream dep fails
|
||||
pub fn upstream_failed(
|
||||
self,
|
||||
failed_upstream_refs: Vec<PartitionRef>,
|
||||
timestamp: u64,
|
||||
) -> PartitionWithState<UpstreamFailedState> {
|
||||
PartitionWithState {
|
||||
uuid: self.uuid,
|
||||
partition_ref: self.partition_ref,
|
||||
want_ids: self.want_ids,
|
||||
state: FailedState {
|
||||
state: UpstreamFailedState {
|
||||
failed_at: timestamp,
|
||||
failed_by: job_run_id,
|
||||
failed_upstream_refs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Add another job to the list of jobs building this partition
|
||||
pub fn add_building_job(mut self, job_run_id: String) -> Self {
|
||||
if !self.state.building_by.contains(&job_run_id) {
|
||||
self.state.building_by.push(job_run_id);
|
||||
}
|
||||
self
|
||||
/// Check if a specific upstream ref is in our missing deps
|
||||
pub fn is_waiting_for(&self, upstream_ref: &str) -> bool {
|
||||
self.state
|
||||
.missing_deps
|
||||
.iter()
|
||||
.any(|d| d.r#ref == upstream_ref)
|
||||
}
|
||||
|
||||
/// Transition from Building back to Missing when a job discovers missing dependencies
|
||||
pub fn reset_to_missing(self) -> PartitionWithState<MissingState> {
|
||||
PartitionWithState {
|
||||
partition_ref: self.partition_ref,
|
||||
want_ids: self.want_ids,
|
||||
state: MissingState {},
|
||||
}
|
||||
/// Remove a satisfied upstream from missing deps. Returns remaining count.
|
||||
pub fn satisfy_upstream(mut self, upstream_ref: &str) -> (Self, usize) {
|
||||
self.state.missing_deps.retain(|r| r.r#ref != upstream_ref);
|
||||
let remaining = self.state.missing_deps.len();
|
||||
(self, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,8 +245,8 @@ impl PartitionWithState<LiveState> {
|
|||
/// Transition from Live to Tainted when a taint is applied
|
||||
pub fn taint(self, taint_id: String, timestamp: u64) -> PartitionWithState<TaintedState> {
|
||||
PartitionWithState {
|
||||
uuid: self.uuid,
|
||||
partition_ref: self.partition_ref,
|
||||
want_ids: self.want_ids,
|
||||
state: TaintedState {
|
||||
tainted_at: timestamp,
|
||||
taint_ids: vec![taint_id],
|
||||
|
|
@ -180,100 +268,115 @@ impl PartitionWithState<TaintedState> {
|
|||
|
||||
// Helper methods on the Partition enum
|
||||
impl Partition {
|
||||
/// Create a new partition in the Missing state
|
||||
pub fn new_missing(partition_ref: PartitionRef) -> Self {
|
||||
Partition::Missing(PartitionWithState {
|
||||
partition_ref,
|
||||
want_ids: vec![],
|
||||
state: MissingState {},
|
||||
})
|
||||
/// Get the UUID from any state
|
||||
pub fn uuid(&self) -> Uuid {
|
||||
match self {
|
||||
Partition::Building(p) => p.uuid,
|
||||
Partition::UpstreamBuilding(p) => p.uuid,
|
||||
Partition::UpForRetry(p) => p.uuid,
|
||||
Partition::Live(p) => p.uuid,
|
||||
Partition::Failed(p) => p.uuid,
|
||||
Partition::UpstreamFailed(p) => p.uuid,
|
||||
Partition::Tainted(p) => p.uuid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the partition reference from any state
|
||||
pub fn partition_ref(&self) -> &PartitionRef {
|
||||
match self {
|
||||
Partition::Missing(p) => &p.partition_ref,
|
||||
Partition::Building(p) => &p.partition_ref,
|
||||
Partition::UpstreamBuilding(p) => &p.partition_ref,
|
||||
Partition::UpForRetry(p) => &p.partition_ref,
|
||||
Partition::Live(p) => &p.partition_ref,
|
||||
Partition::Failed(p) => &p.partition_ref,
|
||||
Partition::UpstreamFailed(p) => &p.partition_ref,
|
||||
Partition::Tainted(p) => &p.partition_ref,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get want_ids from any state
|
||||
pub fn want_ids(&self) -> &Vec<String> {
|
||||
match self {
|
||||
Partition::Missing(p) => &p.want_ids,
|
||||
Partition::Building(p) => &p.want_ids,
|
||||
Partition::Live(p) => &p.want_ids,
|
||||
Partition::Failed(p) => &p.want_ids,
|
||||
Partition::Tainted(p) => &p.want_ids,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get mutable want_ids from any state
|
||||
pub fn want_ids_mut(&mut self) -> &mut Vec<String> {
|
||||
match self {
|
||||
Partition::Missing(p) => &mut p.want_ids,
|
||||
Partition::Building(p) => &mut p.want_ids,
|
||||
Partition::Live(p) => &mut p.want_ids,
|
||||
Partition::Failed(p) => &mut p.want_ids,
|
||||
Partition::Tainted(p) => &mut p.want_ids,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if partition is in Live state
|
||||
pub fn is_live(&self) -> bool {
|
||||
matches!(self, Partition::Live(_))
|
||||
}
|
||||
|
||||
/// Check if partition is satisfied (Live or Tainted both count as "available")
|
||||
pub fn is_satisfied(&self) -> bool {
|
||||
matches!(self, Partition::Live(_) | Partition::Tainted(_))
|
||||
}
|
||||
|
||||
/// Check if partition is in a terminal state (Live, Failed, or Tainted)
|
||||
/// Check if partition is in a terminal state (Live, Failed, UpstreamFailed, or Tainted)
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Partition::Live(_) | Partition::Failed(_) | Partition::Tainted(_)
|
||||
Partition::Live(_)
|
||||
| Partition::Failed(_)
|
||||
| Partition::UpstreamFailed(_)
|
||||
| Partition::Tainted(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if partition is currently being built
|
||||
/// Check if partition is currently being built (includes UpstreamBuilding as it holds a "lease")
|
||||
pub fn is_building(&self) -> bool {
|
||||
matches!(self, Partition::Building(_))
|
||||
matches!(
|
||||
self,
|
||||
Partition::Building(_) | Partition::UpstreamBuilding(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if partition is missing (referenced but not built)
|
||||
pub fn is_missing(&self) -> bool {
|
||||
matches!(self, Partition::Missing(_))
|
||||
/// Check if partition is in UpForRetry state (ready to be rebuilt)
|
||||
pub fn is_up_for_retry(&self) -> bool {
|
||||
matches!(self, Partition::UpForRetry(_))
|
||||
}
|
||||
|
||||
/// Convert to PartitionDetail for API responses and queries
|
||||
/// Check if partition is failed (hard failure)
|
||||
pub fn is_failed(&self) -> bool {
|
||||
matches!(self, Partition::Failed(_))
|
||||
}
|
||||
|
||||
/// Check if partition is upstream failed
|
||||
pub fn is_upstream_failed(&self) -> bool {
|
||||
matches!(self, Partition::UpstreamFailed(_))
|
||||
}
|
||||
|
||||
/// Check if partition is tainted
|
||||
pub fn is_tainted(&self) -> bool {
|
||||
matches!(self, Partition::Tainted(_))
|
||||
}
|
||||
|
||||
/// Convert to PartitionDetail for API responses and queries.
|
||||
/// Note: want_ids is now empty - this will be populated by BuildState from the inverted index.
|
||||
pub fn to_detail(&self) -> PartitionDetail {
|
||||
match self {
|
||||
Partition::Missing(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
status: Some(PartitionStatus {
|
||||
code: PartitionStatusCode::PartitionWanted as i32,
|
||||
name: "PartitionWanted".to_string(),
|
||||
}),
|
||||
want_ids: p.want_ids.clone(),
|
||||
job_run_ids: vec![],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: None,
|
||||
},
|
||||
Partition::Building(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
status: Some(PartitionStatus {
|
||||
code: PartitionStatusCode::PartitionBuilding as i32,
|
||||
name: "PartitionBuilding".to_string(),
|
||||
}),
|
||||
want_ids: p.want_ids.clone(),
|
||||
job_run_ids: p.state.building_by.clone(),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![p.state.job_run_id.clone()],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: None,
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
Partition::UpstreamBuilding(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
status: Some(PartitionStatus {
|
||||
code: PartitionStatusCode::PartitionBuilding as i32, // Use Building status for UpstreamBuilding
|
||||
name: "PartitionUpstreamBuilding".to_string(),
|
||||
}),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![p.state.job_run_id.clone()],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: None,
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
Partition::UpForRetry(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
status: Some(PartitionStatus {
|
||||
code: PartitionStatusCode::PartitionBuilding as i32, // Still "building" conceptually
|
||||
name: "PartitionUpForRetry".to_string(),
|
||||
}),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![p.state.original_job_run_id.clone()],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: None,
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
Partition::Live(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
|
|
@ -281,10 +384,11 @@ impl Partition {
|
|||
code: PartitionStatusCode::PartitionLive as i32,
|
||||
name: "PartitionLive".to_string(),
|
||||
}),
|
||||
want_ids: p.want_ids.clone(),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![p.state.built_by.clone()],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: Some(p.state.built_at),
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
Partition::Failed(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
|
|
@ -292,10 +396,23 @@ impl Partition {
|
|||
code: PartitionStatusCode::PartitionFailed as i32,
|
||||
name: "PartitionFailed".to_string(),
|
||||
}),
|
||||
want_ids: p.want_ids.clone(),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![p.state.failed_by.clone()],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: Some(p.state.failed_at),
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
Partition::UpstreamFailed(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
status: Some(PartitionStatus {
|
||||
code: PartitionStatusCode::PartitionFailed as i32, // Use Failed status
|
||||
name: "PartitionUpstreamFailed".to_string(),
|
||||
}),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![],
|
||||
taint_ids: vec![],
|
||||
last_updated_timestamp: Some(p.state.failed_at),
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
Partition::Tainted(p) => PartitionDetail {
|
||||
r#ref: Some(p.partition_ref.clone()),
|
||||
|
|
@ -303,11 +420,92 @@ impl Partition {
|
|||
code: PartitionStatusCode::PartitionTainted as i32,
|
||||
name: "PartitionTainted".to_string(),
|
||||
}),
|
||||
want_ids: p.want_ids.clone(),
|
||||
want_ids: vec![], // Populated by BuildState
|
||||
job_run_ids: vec![],
|
||||
taint_ids: p.state.taint_ids.clone(),
|
||||
last_updated_timestamp: Some(p.state.tainted_at),
|
||||
uuid: p.uuid.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_derive_partition_uuid_deterministic() {
|
||||
let uuid1 = derive_partition_uuid("job-123", "data/beta");
|
||||
let uuid2 = derive_partition_uuid("job-123", "data/beta");
|
||||
assert_eq!(uuid1, uuid2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_partition_uuid_different_inputs() {
|
||||
let uuid1 = derive_partition_uuid("job-123", "data/beta");
|
||||
let uuid2 = derive_partition_uuid("job-456", "data/beta");
|
||||
let uuid3 = derive_partition_uuid("job-123", "data/alpha");
|
||||
assert_ne!(uuid1, uuid2);
|
||||
assert_ne!(uuid1, uuid3);
|
||||
assert_ne!(uuid2, uuid3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partition_building_transitions() {
|
||||
let partition = PartitionWithState::<BuildingState>::new(
|
||||
"job-123".to_string(),
|
||||
PartitionRef {
|
||||
r#ref: "data/beta".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
// Can transition to Live
|
||||
let live = partition.clone().complete(1000);
|
||||
assert_eq!(live.state.built_at, 1000);
|
||||
assert_eq!(live.state.built_by, "job-123");
|
||||
|
||||
// Can transition to Failed
|
||||
let failed = partition.clone().fail(2000);
|
||||
assert_eq!(failed.state.failed_at, 2000);
|
||||
assert_eq!(failed.state.failed_by, "job-123");
|
||||
|
||||
// Can transition to UpstreamBuilding (dep miss)
|
||||
let upstream_building = partition.dep_miss(vec![PartitionRef {
|
||||
r#ref: "data/alpha".to_string(),
|
||||
}]);
|
||||
assert_eq!(upstream_building.state.missing_deps.len(), 1);
|
||||
assert_eq!(upstream_building.state.missing_deps[0].r#ref, "data/alpha");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upstream_building_transitions() {
|
||||
let building = PartitionWithState::<BuildingState>::new(
|
||||
"job-123".to_string(),
|
||||
PartitionRef {
|
||||
r#ref: "data/beta".to_string(),
|
||||
},
|
||||
);
|
||||
let upstream_building = building.dep_miss(vec![PartitionRef {
|
||||
r#ref: "data/alpha".to_string(),
|
||||
}]);
|
||||
|
||||
// Can transition to UpForRetry
|
||||
let up_for_retry = upstream_building.clone().upstreams_satisfied();
|
||||
assert_eq!(up_for_retry.state.original_job_run_id, "job-123");
|
||||
|
||||
// Can transition to UpstreamFailed
|
||||
let upstream_failed = upstream_building.upstream_failed(
|
||||
vec![PartitionRef {
|
||||
r#ref: "data/alpha".to_string(),
|
||||
}],
|
||||
3000,
|
||||
);
|
||||
assert_eq!(upstream_failed.state.failed_at, 3000);
|
||||
assert_eq!(upstream_failed.state.failed_upstream_refs.len(), 1);
|
||||
assert_eq!(
|
||||
upstream_failed.state.failed_upstream_refs[0].r#ref,
|
||||
"data/alpha"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ use crate::{EventSource, PartitionRef, WantCreateEventV1, WantDetail, WantStatus
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// State: Want has just been created, state not yet determined by sensing partition states
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewState {}
|
||||
|
||||
/// State: Want has been created and is ready to be scheduled
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IdleState {}
|
||||
|
|
@ -95,6 +99,7 @@ pub struct WantWithState<S> {
|
|||
/// Wrapper enum for storing wants in collections
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Want {
|
||||
New(WantWithState<NewState>),
|
||||
Idle(WantWithState<IdleState>),
|
||||
Building(WantWithState<BuildingState>),
|
||||
UpstreamBuilding(WantWithState<UpstreamBuildingState>),
|
||||
|
|
@ -108,6 +113,14 @@ pub enum Want {
|
|||
/// is critical that these be treated with respect, not just summoned because it's convenient.
|
||||
/// These should be created ephemerally from typestate objects via .get_ref() and used
|
||||
/// immediately — never stored long-term, as partition state can change.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct NewWantId(pub String);
|
||||
impl WantWithState<NewState> {
|
||||
pub fn get_id(&self) -> NewWantId {
|
||||
NewWantId(self.want.want_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct IdleWantId(pub String);
|
||||
impl WantWithState<IdleState> {
|
||||
|
|
@ -164,8 +177,8 @@ impl WantWithState<CanceledState> {
|
|||
}
|
||||
}
|
||||
|
||||
// From impl for creating want from event
|
||||
impl From<WantCreateEventV1> for WantWithState<IdleState> {
|
||||
// From impl for creating want from event - creates in New state for sensing
|
||||
impl From<WantCreateEventV1> for WantWithState<NewState> {
|
||||
fn from(event: WantCreateEventV1) -> Self {
|
||||
WantWithState {
|
||||
want: WantInfo {
|
||||
|
|
@ -178,9 +191,94 @@ impl From<WantCreateEventV1> for WantWithState<IdleState> {
|
|||
comment: event.comment,
|
||||
last_updated_at: current_timestamp(),
|
||||
},
|
||||
state: NewState {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe transition methods for NewState
|
||||
impl WantWithState<NewState> {
|
||||
/// Transition from New to Idle when partitions don't exist or are ready to schedule
|
||||
pub fn to_idle(self) -> WantWithState<IdleState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: IdleState {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from New to Building when partitions are currently being built
|
||||
pub fn to_building(self, started_at: u64) -> WantWithState<BuildingState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: BuildingState { started_at },
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from New to Successful when all partitions are already Live
|
||||
pub fn to_successful(self, completed_at: u64) -> WantWithState<SuccessfulState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: SuccessfulState { completed_at },
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from New to Failed when any partition has failed
|
||||
pub fn to_failed(
|
||||
self,
|
||||
failed_partition_refs: Vec<PartitionRef>,
|
||||
reason: String,
|
||||
) -> WantWithState<FailedState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: FailedState {
|
||||
failed_at: current_timestamp(),
|
||||
failed_partition_refs,
|
||||
failure_reason: reason,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from New to UpstreamBuilding when partitions are waiting for upstream deps
|
||||
pub fn to_upstream_building(
|
||||
self,
|
||||
upstream_want_ids: Vec<String>,
|
||||
) -> WantWithState<UpstreamBuildingState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: UpstreamBuildingState { upstream_want_ids },
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from New to UpstreamFailed when upstream dependencies have failed
|
||||
pub fn to_upstream_failed(
|
||||
self,
|
||||
failed_wants: Vec<String>,
|
||||
) -> WantWithState<UpstreamFailedState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: UpstreamFailedState {
|
||||
failed_at: current_timestamp(),
|
||||
failed_wants,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition from New to Canceled when want is explicitly canceled
|
||||
/// (Rarely used - wants are typically transitioned before cancel can arrive)
|
||||
pub fn cancel(
|
||||
self,
|
||||
canceled_by: Option<EventSource>,
|
||||
comment: Option<String>,
|
||||
) -> WantWithState<CanceledState> {
|
||||
WantWithState {
|
||||
want: self.want.updated_timestamp(),
|
||||
state: CanceledState {
|
||||
canceled_at: current_timestamp(),
|
||||
canceled_by,
|
||||
comment,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe transition methods for IdleState
|
||||
|
|
@ -413,6 +511,7 @@ impl Want {
|
|||
|
||||
pub fn want(&self) -> &WantInfo {
|
||||
match self {
|
||||
Want::New(w) => &w.want,
|
||||
Want::Idle(w) => &w.want,
|
||||
Want::Building(w) => &w.want,
|
||||
Want::UpstreamBuilding(w) => &w.want,
|
||||
|
|
@ -436,6 +535,7 @@ impl Want {
|
|||
comment: self.want().comment.clone(),
|
||||
last_updated_timestamp: self.want().last_updated_at,
|
||||
status: match self {
|
||||
Want::New(_) => Some(WantStatusCode::WantNew.into()),
|
||||
Want::Idle(_) => Some(WantStatusCode::WantIdle.into()),
|
||||
Want::Building(_) => Some(WantStatusCode::WantBuilding.into()),
|
||||
Want::UpstreamBuilding(_) => Some(WantStatusCode::WantUpstreamBuilding.into()),
|
||||
|
|
|
|||
Loading…
Reference in a new issue