aion_core/describe.rs
1//! Describe-workflow response projection.
2//!
3//! The ops console's `POST /workflows/describe` read consumes exactly this shape:
4//! a workflow [`WorkflowSummary`] projection plus the run's event [`Event`]
5//! history as plain JSON. Defining it here lets the same type be exported to
6//! TypeScript (so the generated bindings match the wire by construction) and be
7//! produced directly by the HTTP handler at the transport boundary.
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::{ActivityId, Event, LeaseRecording, ReadProvenance, RunId, WorkflowSummary};
13
14/// Response to a describe-workflow request.
15///
16/// `history` is the run's events as plain serialized [`Event`] values (never a
17/// protobuf-derived envelope), so the ops console decodes each entry directly.
18/// When `include_history` is false the server returns an empty `history`.
19///
20/// `unserved` is computed independently of `include_history`: it is the answer
21/// to "can anything still advance this run", and an operator who asked for the
22/// summary alone still needs it.
23#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq)]
24pub struct DescribeWorkflowResponse {
25 /// Workflow summary projected from authoritative history, when the workflow
26 /// exists.
27 pub summary: Option<WorkflowSummary>,
28 /// The run's event history as plain serialized events.
29 pub history: Vec<Event>,
30 /// Every activity this run's history records as dispatched and unterminated
31 /// that the fleet cannot currently serve.
32 ///
33 /// EMPTY is the healthy answer, and it is the only healthy answer: an
34 /// activity a live compatible worker could take is never listed here. A
35 /// non-empty list is the difference between "a worker is working on it" and
36 /// "it is parked with nobody to take it", which the projected
37 /// [`crate::WorkflowStatus`] alone cannot express — `Running` covers both.
38 pub unserved: Vec<UnservedActivity>,
39 /// Every generation boundary in this workflow's WHOLE history, ascending —
40 /// one entry per `WorkflowStarted`.
41 ///
42 /// This is the answer to "which run does the event at sequence `n` belong
43 /// to": the last boundary at or before `n`. It is stated by the server
44 /// because only the server holds the whole history, and a reader that
45 /// derives it from a window can only be right when the window happens to
46 /// contain the boundary.
47 ///
48 /// 🔴 THIS EXISTS BECAUSE DERIVING IT CLIENT-SIDE FAILS SILENTLY AND
49 /// TOTALLY. The ops console scanned its loaded window (500 events) for
50 /// `WorkflowStarted`; on any workflow longer than one window the boundary is
51 /// never in it, so every attempt row rendered "run unknown" — an accurate
52 /// admission of ignorance on every row, which trains an operator to ignore
53 /// the column. Computed from the same history the summary is projected
54 /// from, so it costs no additional read and cannot disagree with it.
55 pub generations: Vec<RunGeneration>,
56 /// What the serving install says about itself (ADR-016): the count a
57 /// reader holds an UNATTRIBUTED attempt against. Stamped by the server
58 /// at read time, never projected from history.
59 ///
60 /// `None` is "provenance not reported" — a response from a server that
61 /// predates the field. It is kept distinct from `Some(0)` on purpose: zero
62 /// is a measurement the install made, absence is a measurement nobody
63 /// made, and a reader that rendered the second as the first would show a
64 /// count that was never counted.
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub provenance: Option<ReadProvenance>,
67 /// What the WHOLE history says about lease recording (WA-010 R4):
68 /// leases recorded and attempts dispatched, counted by the server over
69 /// the full history — never inferable from a loaded window. A reader
70 /// holds an unattributed attempt against these and against
71 /// `provenance`; it never states "recorded before lease events existed",
72 /// which no fold can know. `None` = not reported (pre-field server).
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub lease_recording: Option<LeaseRecording>,
75}
76
77/// One generation boundary: the sequence at which a `WorkflowStarted` opened
78/// `run_id`.
79///
80/// Only `WorkflowStarted` opens a generation. `WorkflowReopened` also carries a
81/// `run_id`, but it names the run it REOPENS rather than beginning a new one, so
82/// treating it as a boundary would state the same fact twice and would misread a
83/// reopen that arrives out of order.
84#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
85pub struct RunGeneration {
86 /// Workflow sequence of the `WorkflowStarted` that opened this run.
87 pub seq: u64,
88 /// The run that sequence opened.
89 pub run_id: RunId,
90}
91
92/// One in-flight activity whose recorded dispatch address is not being served.
93///
94/// The activity's identity, address, and dispatch instant come from the run's
95/// own recorded history; the verdict and the counts come from the live
96/// connected-worker fleet at read time. Both halves are named so an operator can
97/// see which is which.
98#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
99pub struct UnservedActivity {
100 /// Activity ordinal recorded in history.
101 pub activity_id: ActivityId,
102 /// Activity type the dispatch needs served.
103 pub activity_type: String,
104 /// Task queue the dispatch was recorded against.
105 pub task_queue: String,
106 /// Node affinity recorded on the dispatch, if any.
107 pub node: Option<String>,
108 /// One-based delivery attempt recorded on the `ActivityStarted`.
109 pub attempt: u32,
110 /// When the engine recorded the dispatch — NOT when a worker took it, which
111 /// is precisely the thing that never happened.
112 pub dispatched_at: DateTime<Utc>,
113 /// Canonical queue-service reason (`NO_QUEUE_DECLARATION`,
114 /// `NO_LIVE_POLLERS`, `POLLERS_INCOMPATIBLE`), in the same vocabulary the
115 /// dispatch refusals and the server logs use.
116 pub reason: String,
117 /// One sentence naming what an operator has to fix.
118 pub detail: String,
119 /// Workers connected for the `(namespace, task_queue)` pool, whatever they
120 /// serve.
121 pub workers_in_pool: u64,
122 /// Of those, workers advertising this activity type.
123 pub workers_serving_activity: u64,
124 /// Of those, workers that also satisfy the dispatch's node pin.
125 pub compatible_workers: u64,
126 /// Whether THIS server process is currently holding the dispatch in its
127 /// selection wait.
128 ///
129 /// False with a non-empty verdict is its own signal: history says the
130 /// activity is in flight, the fleet cannot serve it, and no selection in
131 /// this process is even waiting for it — the shape a run left behind by a
132 /// restart takes.
133 pub dispatch_parked: bool,
134}