1use crate::*;
2use ledgence_worker_api::{CloudEvent, ExecutionFailure, ExecutionReport, ProgramDescriptor};
3
4pub type Timestamp = u64;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct Scope {
10 pub tenant_id: String,
11 pub namespace: String,
12}
13impl Scope {
14 pub fn validate(&self) -> Result<()> {
15 validate_text(&self.tenant_id, 128)?;
16 validate_text(&self.namespace, 128)
17 }
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct SubmitCommand {
23 pub idempotency_key: String,
24 pub input: SubmitTask,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub origin_trace: Option<TraceContext>,
27}
28impl SubmitCommand {
29 pub fn decode(bytes: &[u8]) -> Result<Self> {
32 let command: Self = decode_unique_json(bytes, SUBMISSION_MAX_BYTES)?;
33 command.input.validate()?;
34 validate_text(&command.idempotency_key, 255)?;
35 Ok(command)
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum TaskState {
42 Queued,
43 Active,
44 Succeeded,
45 Failed,
46 Cancelled,
47}
48impl TaskState {
49 pub fn is_terminal(self) -> bool {
50 matches!(self, Self::Succeeded | Self::Failed | Self::Cancelled)
51 }
52}
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum AttemptState {
56 Active,
57 Succeeded,
58 Failed,
59 Cancelled,
60 Lost,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct TaskSnapshot {
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub parent_workflow_id: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub root_workflow_id: Option<String>,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub workflow_id: Option<String>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub workflow_activation_id: Option<String>,
78 pub task_id: String,
79 pub run_id: String,
80 pub idempotency_key: String,
81 pub input: SubmitTask,
82 pub descriptor: ProgramDescriptor,
83 pub origin_trace: Option<TraceContext>,
84 pub state: TaskState,
85 pub submitted_at: Timestamp,
86 pub available_at: Timestamp,
87 pub terminal_at: Option<Timestamp>,
88 pub current_attempt_id: Option<String>,
89 pub attempt_count: u32,
90 pub cancel_requested_at: Option<Timestamp>,
91}
92impl TaskSnapshot {
93 pub fn scope(&self) -> Scope {
94 Scope {
95 tenant_id: self.input.tenant_id.clone(),
96 namespace: self.input.namespace.clone(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct WorkerSession {
105 pub id: String,
106 pub scope: Scope,
107 pub queue: String,
108 pub concurrency: u32,
109 pub expires_at: Timestamp,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct AcquireCommand {
115 pub scope: Scope,
116 pub queue: String,
117 pub worker_session_id: String,
118 pub consumer_id: u32,
119 pub sequence: u64,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct ConsumerCursor {
125 pub command: AcquireCommand,
126 pub assignment: Option<AttemptRef>,
127}
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct AttemptRef {
130 pub task_id: String,
131 pub attempt_id: String,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct LeaseOwner {
137 pub scope: Scope,
138 pub task_id: String,
139 pub attempt_id: String,
140 pub lease_id: String,
141 pub generation: u32,
142 pub worker_session_id: String,
143 pub consumer_id: u32,
144}
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct Lease {
147 pub owner: LeaseOwner,
148 pub expires_at: Timestamp,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153pub struct Authority {
154 pub owner: LeaseOwner,
155 pub expires_at: Timestamp,
156 pub remaining_ms: u64,
157 pub execution_remaining_ms: u64,
159 pub renew_sequence: u64,
160 pub cancel_requested: bool,
161 pub dispatch_allowed: bool,
162}
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct Assignment {
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub workflow_activation_id: Option<String>,
167 pub descriptor: ProgramDescriptor,
168 pub event: CloudEvent,
169 pub lease: Lease,
170 pub authority: Authority,
171 pub attempt_deadline: Timestamp,
172}
173impl Assignment {
174 pub fn validate_workflow_identity(&self) -> Result<()> {
176 let activation = self
177 .event
178 .value()
179 .get("ldgactivationid")
180 .and_then(serde_json::Value::as_str);
181 if self.workflow_activation_id.as_deref() != activation
182 || activation.is_some_and(|id| id != self.lease.owner.task_id)
183 {
184 return Err(ContractError::InvalidInput(
185 "workflow assignment identity mismatch".into(),
186 ));
187 }
188 Ok(())
189 }
190}
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(tag = "disposition", rename_all = "snake_case")]
193pub enum AcquireReply {
194 Empty {
195 sequence: u64,
196 },
197 Assigned {
198 sequence: u64,
199 assignment: Box<Assignment>,
200 },
201 OwnershipLost {
202 sequence: u64,
203 assignment: AttemptRef,
204 },
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum RenewIntent {
210 KeepAlive,
211 Dispatch,
212}
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(deny_unknown_fields)]
215pub struct RenewCommand {
216 pub owner: LeaseOwner,
217 pub sequence: u64,
218 pub intent: RenewIntent,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum Quiescence {
225 Confirmed,
226 Unconfirmed,
227}
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(tag = "kind", content = "report", rename_all = "snake_case")]
230pub enum AttemptReport {
231 Completed(ExecutionReport),
232 Failed(ExecutionFailure),
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(deny_unknown_fields)]
237pub struct SettleCommand {
238 pub owner: LeaseOwner,
239 pub operation_id: String,
240 pub report: AttemptReport,
241 pub quiescence: Quiescence,
242 pub processing_trace: Option<TraceContext>,
243}
244impl SettleCommand {
245 pub fn decode(bytes: &[u8]) -> Result<Self> {
248 let command: Self = decode_unique_json(bytes, SETTLEMENT_MAX_BYTES)?;
249 validate_text(&command.operation_id, 128)?;
250 if let AttemptReport::Completed(report) = &command.report
251 && let ledgence_worker_api::ProgramOutcome::Success { output } = &report.outcome
252 {
253 validate_task_output(output, report.context.identity.activation_id.is_some())?;
254 }
255 Ok(command)
256 }
257}
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct SettlementReceipt {
260 pub operation_id: String,
261 pub task_id: String,
262 pub attempt_id: String,
263 pub accepted_at: Timestamp,
264}
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct AcceptedSettlement {
267 pub command: SettleCommand,
268 pub receipt: SettlementReceipt,
269}
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct SettleReply {
272 pub receipt: SettlementReceipt,
273 pub already_accepted: bool,
274 pub task_state: TaskState,
276}
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct AttemptSnapshot {
279 pub event: CloudEvent,
280 pub descriptor: ProgramDescriptor,
281 pub lease: Lease,
282 pub deadline: Timestamp,
283 pub authority_deadline: Timestamp,
284 pub state: AttemptState,
285 pub execution_may_have_started: bool,
286 pub last_renewal: Option<RenewCommand>,
287 pub quiescence: Quiescence,
288 pub settlement: Option<AcceptedSettlement>,
289 pub finished_at: Option<Timestamp>,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum TransitionReason {
295 Submitted,
296 Claimed,
297 DispatchAuthorized,
298 CancelRequested,
299 Cancelled,
300 ReportAccepted,
301 CleanupConfirmed,
302 Succeeded,
303 Failed,
304 RetryScheduled,
305 LeaseExpired,
306}
307#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct HistoryEvent {
311 pub task_id: String,
312 pub attempt_id: Option<String>,
313 pub at: Timestamp,
314 pub reason: TransitionReason,
315}
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct RecordedHistoryEvent {
318 pub sequence: u64,
319 pub event: HistoryEvent,
320}
321
322pub trait TaskService: Send + Sync {
325 fn claim_dispatch<'a>(&'a self, _command: &'a ClaimCommand) -> ContractFuture<'a, ClaimReply> {
329 Box::pin(async {
330 Err(ContractError::InvalidInput(
331 "targeted dispatch claims are unsupported".into(),
332 ))
333 })
334 }
335
336 fn open_session<'a>(
337 &'a self,
338 scope: &'a Scope,
339 queue: &'a str,
340 concurrency: u32,
341 ) -> ContractFuture<'a, WorkerSession>;
342 fn extend_session<'a>(
343 &'a self,
344 worker_session_id: &'a str,
345 ) -> ContractFuture<'a, WorkerSession>;
346 fn submit<'a>(&'a self, command: &'a SubmitCommand) -> ContractFuture<'a, TaskSnapshot>;
347 fn list_tasks<'a>(
350 &'a self,
351 scope: &'a Scope,
352 query: &'a TaskListQuery,
353 ) -> ContractFuture<'a, TaskPage>;
354 fn status<'a>(&'a self, scope: &'a Scope, task_id: &'a str) -> ContractFuture<'a, TaskStatus>;
356 fn result<'a>(&'a self, scope: &'a Scope, task_id: &'a str) -> ContractFuture<'a, TaskResult>;
358 fn inspect<'a>(
359 &'a self,
360 scope: &'a Scope,
361 task_id: &'a str,
362 ) -> ContractFuture<'a, TaskSnapshot>;
363 fn inspect_attempt<'a>(
364 &'a self,
365 scope: &'a Scope,
366 task_id: &'a str,
367 attempt_id: &'a str,
368 ) -> ContractFuture<'a, AttemptSnapshot>;
369 fn history<'a>(
372 &'a self,
373 scope: &'a Scope,
374 task_id: &'a str,
375 after_sequence: u64,
376 ) -> ContractFuture<'a, Vec<RecordedHistoryEvent>>;
377 fn acquire<'a>(
378 &'a self,
379 command: &'a AcquireCommand,
380 options: AcquireOptions,
381 ) -> ContractFuture<'a, AcquireReply>;
382 fn renew<'a>(&'a self, command: &'a RenewCommand) -> ContractFuture<'a, Authority>;
383 fn settle<'a>(&'a self, command: &'a SettleCommand) -> ContractFuture<'a, SettleReply>;
384 fn confirm_quiescence<'a>(&'a self, owner: &'a LeaseOwner) -> ContractFuture<'a, TaskState>;
385 fn cancel<'a>(&'a self, scope: &'a Scope, task_id: &'a str) -> ContractFuture<'a, TaskState>;
386}