Skip to main content

graphrefly/
work_queue.rs

1//! MessageBus-backed generic work lifecycle infrastructure.
2//!
3//! D299-D324/D330 keep this layer above retained messaging and below recipes:
4//! workQueue owns admission, claim/lease/retry/dead-letter records, status,
5//! issues, and read projections. It intentionally stays independent of the
6//! higher-layer recipe and orchestration types.
7
8pub mod readiness;
9
10use std::cell::{Cell, RefCell};
11use std::collections::{BTreeMap, HashSet};
12use std::panic::{catch_unwind, AssertUnwindSafe};
13use std::rc::Rc;
14
15use crate::ctx::Ctx;
16use crate::graph::{Graph, GraphNodeOpts};
17use crate::identity::{canonical_tuple_key, compound_tuple_key};
18use crate::messaging::{
19    attach_message_bus_deferred_command_sink, DataIssue, MessageBus, MessageBusAvailablePage,
20    MessageBusAvailableParams, MessageBusCommand, MessageBusStatus, MessageBusStatusKind,
21    MessageBusSubscriptionFrom, MessageBusSubscriptionOptions, MessageEnvelope,
22};
23use crate::node::{Core, Node, NodeOpts};
24use crate::protocol::{LockId, Message, PullDemand};
25use crate::resilience::{BackoffPolicy, RetryPolicy};
26
27#[derive(Debug, Clone, PartialEq)]
28/// `WorkQueueSubmit` data container.
29pub struct WorkQueueSubmit<T> {
30    /// `payload` field for payload.
31    pub payload: T,
32    /// `work_id` field for work id.
33    pub work_id: Option<String>,
34    /// `priority` field for priority.
35    pub priority: Option<i64>,
36    /// `tags` field for tags.
37    pub tags: Vec<String>,
38    /// `requirements` field for requirements.
39    pub requirements: Vec<String>,
40    /// `not_before_ms` field for not before ms.
41    pub not_before_ms: Option<u64>,
42    /// `deadline_ms` field for deadline ms.
43    pub deadline_ms: Option<u64>,
44}
45
46impl<T> WorkQueueSubmit<T> {
47    /// Creates or computes `new`.
48    pub fn new(payload: T) -> Self {
49        Self {
50            payload,
51            work_id: None,
52            priority: None,
53            tags: Vec::new(),
54            requirements: Vec::new(),
55            not_before_ms: None,
56            deadline_ms: None,
57        }
58    }
59
60    /// Updates or reads `with_work_id`.
61    pub fn with_work_id(mut self, work_id: impl Into<String>) -> Self {
62        self.work_id = Some(work_id.into());
63        self
64    }
65
66    /// Updates or reads `with_priority`.
67    pub fn with_priority(mut self, priority: i64) -> Self {
68        self.priority = Some(priority);
69        self
70    }
71
72    /// Updates or reads `with_not_before_ms`.
73    pub fn with_not_before_ms(mut self, not_before_ms: u64) -> Self {
74        self.not_before_ms = Some(not_before_ms);
75        self
76    }
77}
78
79#[derive(Clone)]
80/// `WorkQueueOptions` data container.
81pub struct WorkQueueOptions<T> {
82    /// `queue_id` field for queue id.
83    pub queue_id: String,
84    /// `bus` field for bus.
85    pub bus: MessageBus<WorkQueueSubmit<T>>,
86    /// `topic` field for topic.
87    pub topic: String,
88    /// `subscription_id` field for subscription id.
89    pub subscription_id: String,
90    /// `from` field for from.
91    pub from: MessageBusSubscriptionFrom,
92    /// `name` field for name.
93    pub name: Option<String>,
94    /// `now` field for now.
95    pub now: Rc<dyn Fn() -> u64>,
96    /// `lease_duration_ms` field for lease duration ms.
97    pub lease_duration_ms: u64,
98    /// `retry` field for retry.
99    pub retry: RetryPolicy,
100}
101
102impl<T> WorkQueueOptions<T> {
103    /// Creates or computes `new`.
104    pub fn new(
105        queue_id: impl Into<String>,
106        bus: MessageBus<WorkQueueSubmit<T>>,
107        topic: impl Into<String>,
108        subscription_id: impl Into<String>,
109    ) -> Self {
110        Self {
111            queue_id: queue_id.into(),
112            bus,
113            topic: topic.into(),
114            subscription_id: subscription_id.into(),
115            from: MessageBusSubscriptionFrom::Earliest,
116            name: None,
117            now: Rc::new(|| 0),
118            lease_duration_ms: 30_000,
119            retry: RetryPolicy::new(3, BackoffPolicy::None),
120        }
121    }
122
123    /// Updates or reads `named`.
124    pub fn named(mut self, name: impl Into<String>) -> Self {
125        self.name = Some(name.into());
126        self
127    }
128
129    /// Updates or reads `with_now`.
130    pub fn with_now(mut self, now: impl Fn() -> u64 + 'static) -> Self {
131        self.now = Rc::new(now);
132        self
133    }
134
135    /// Updates or reads `with_lease_duration_ms`.
136    pub fn with_lease_duration_ms(mut self, lease_duration_ms: u64) -> Self {
137        self.lease_duration_ms = lease_duration_ms;
138        self
139    }
140
141    /// Updates or reads `with_retry`.
142    pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
143        self.retry = retry;
144        self
145    }
146}
147
148#[derive(Debug, Clone, PartialEq)]
149/// `WorkQueueCommand` variants.
150pub enum WorkQueueCommand<T = ()> {
151    /// `Submit` variant.
152    Submit {
153        /// `payload` field for payload.
154        payload: T,
155        /// `command_id` field for command id.
156        command_id: String,
157        /// `queue_id` field for queue id.
158        queue_id: Option<String>,
159        /// `idempotency_key` field for idempotency key.
160        idempotency_key: Option<String>,
161    },
162    /// `Claim` variant.
163    Claim {
164        /// `command_id` field for command id.
165        command_id: String,
166        /// `queue_id` field for queue id.
167        queue_id: Option<String>,
168        /// `idempotency_key` field for idempotency key.
169        idempotency_key: Option<String>,
170        /// `worker_id` field for worker id.
171        worker_id: String,
172        /// `requested_work_ids` field for requested work ids.
173        requested_work_ids: Vec<String>,
174        /// `limit` field for limit.
175        limit: Option<usize>,
176        /// `lease_duration_ms` field for lease duration ms.
177        lease_duration_ms: Option<u64>,
178        /// `now_ms` field for now ms.
179        now_ms: Option<u64>,
180    },
181    /// `RenewLease` variant.
182    RenewLease {
183        /// `command_id` field for command id.
184        command_id: String,
185        /// `queue_id` field for queue id.
186        queue_id: Option<String>,
187        /// `idempotency_key` field for idempotency key.
188        idempotency_key: Option<String>,
189        /// `work_id` field for work id.
190        work_id: String,
191        /// `lease_id` field for lease id.
192        lease_id: String,
193        /// `attempt` field for attempt.
194        attempt: u32,
195        /// `worker_id` field for worker id.
196        worker_id: String,
197        /// `lease_duration_ms` field for lease duration ms.
198        lease_duration_ms: Option<u64>,
199        /// `lease_expires_at_ms` field for lease expires at ms.
200        lease_expires_at_ms: Option<u64>,
201        /// `now_ms` field for now ms.
202        now_ms: Option<u64>,
203    },
204    /// `Release` variant.
205    Release {
206        /// `command_id` field for command id.
207        command_id: String,
208        /// `queue_id` field for queue id.
209        queue_id: Option<String>,
210        /// `idempotency_key` field for idempotency key.
211        idempotency_key: Option<String>,
212        /// `work_id` field for work id.
213        work_id: String,
214        /// `lease_id` field for lease id.
215        lease_id: String,
216        /// `attempt` field for attempt.
217        attempt: u32,
218        /// `worker_id` field for worker id.
219        worker_id: String,
220        /// `reason` field for reason.
221        reason: Option<String>,
222        /// `now_ms` field for now ms.
223        now_ms: Option<u64>,
224    },
225    /// `Complete` variant.
226    Complete {
227        /// `command_id` field for command id.
228        command_id: String,
229        /// `queue_id` field for queue id.
230        queue_id: Option<String>,
231        /// `idempotency_key` field for idempotency key.
232        idempotency_key: Option<String>,
233        /// `work_id` field for work id.
234        work_id: String,
235        /// `lease_id` field for lease id.
236        lease_id: String,
237        /// `attempt` field for attempt.
238        attempt: u32,
239        /// `worker_id` field for worker id.
240        worker_id: String,
241        /// `result` field for result.
242        result: Option<String>,
243        /// `now_ms` field for now ms.
244        now_ms: Option<u64>,
245    },
246    /// `Fail` variant.
247    Fail {
248        /// `command_id` field for command id.
249        command_id: String,
250        /// `queue_id` field for queue id.
251        queue_id: Option<String>,
252        /// `idempotency_key` field for idempotency key.
253        idempotency_key: Option<String>,
254        /// `work_id` field for work id.
255        work_id: String,
256        /// `lease_id` field for lease id.
257        lease_id: String,
258        /// `attempt` field for attempt.
259        attempt: u32,
260        /// `worker_id` field for worker id.
261        worker_id: String,
262        /// `error` field for error.
263        error: Option<String>,
264        /// `retryable` field for retryable.
265        retryable: Option<bool>,
266        /// `now_ms` field for now ms.
267        now_ms: Option<u64>,
268    },
269    /// `Cancel` variant.
270    Cancel {
271        /// `command_id` field for command id.
272        command_id: String,
273        /// `queue_id` field for queue id.
274        queue_id: Option<String>,
275        /// `idempotency_key` field for idempotency key.
276        idempotency_key: Option<String>,
277        /// `work_id` field for work id.
278        work_id: String,
279        /// `reason` field for reason.
280        reason: Option<String>,
281        /// `now_ms` field for now ms.
282        now_ms: Option<u64>,
283    },
284    /// `Schedule` variant.
285    Schedule {
286        /// `command_id` field for command id.
287        command_id: String,
288        /// `queue_id` field for queue id.
289        queue_id: Option<String>,
290        /// `idempotency_key` field for idempotency key.
291        idempotency_key: Option<String>,
292        /// `work_id` field for work id.
293        work_id: String,
294        /// `schedule_id` field for schedule id.
295        schedule_id: Option<String>,
296        /// `not_before_ms` field for not before ms.
297        not_before_ms: u64,
298        /// `deadline_ms` field for deadline ms.
299        deadline_ms: Option<u64>,
300        /// `reason` field for reason.
301        reason: Option<String>,
302        /// `now_ms` field for now ms.
303        now_ms: Option<u64>,
304    },
305    /// `ExpireLeases` variant.
306    ExpireLeases {
307        /// `command_id` field for command id.
308        command_id: String,
309        /// `queue_id` field for queue id.
310        queue_id: Option<String>,
311        /// `idempotency_key` field for idempotency key.
312        idempotency_key: Option<String>,
313        /// `work_ids` field for work ids.
314        work_ids: Vec<String>,
315        /// `limit` field for limit.
316        limit: Option<usize>,
317        /// `now_ms` field for now ms.
318        now_ms: Option<u64>,
319    },
320}
321
322impl<T> WorkQueueCommand<T> {
323    fn command_id(&self) -> &str {
324        match self {
325            Self::Submit { command_id, .. }
326            | Self::Claim { command_id, .. }
327            | Self::RenewLease { command_id, .. }
328            | Self::Release { command_id, .. }
329            | Self::Complete { command_id, .. }
330            | Self::Fail { command_id, .. }
331            | Self::Cancel { command_id, .. }
332            | Self::Schedule { command_id, .. }
333            | Self::ExpireLeases { command_id, .. } => command_id,
334        }
335    }
336
337    fn queue_id(&self) -> Option<&str> {
338        match self {
339            Self::Submit { queue_id, .. }
340            | Self::Claim { queue_id, .. }
341            | Self::RenewLease { queue_id, .. }
342            | Self::Release { queue_id, .. }
343            | Self::Complete { queue_id, .. }
344            | Self::Fail { queue_id, .. }
345            | Self::Cancel { queue_id, .. }
346            | Self::Schedule { queue_id, .. }
347            | Self::ExpireLeases { queue_id, .. } => queue_id.as_deref(),
348        }
349    }
350
351    fn idempotency_key(&self) -> Option<&str> {
352        match self {
353            Self::Submit {
354                idempotency_key, ..
355            }
356            | Self::Claim {
357                idempotency_key, ..
358            }
359            | Self::RenewLease {
360                idempotency_key, ..
361            }
362            | Self::Release {
363                idempotency_key, ..
364            }
365            | Self::Complete {
366                idempotency_key, ..
367            }
368            | Self::Fail {
369                idempotency_key, ..
370            }
371            | Self::Cancel {
372                idempotency_key, ..
373            }
374            | Self::Schedule {
375                idempotency_key, ..
376            }
377            | Self::ExpireLeases {
378                idempotency_key, ..
379            } => idempotency_key.as_deref(),
380        }
381    }
382
383    fn now_ms(&self) -> Option<u64> {
384        match self {
385            Self::Claim { now_ms, .. }
386            | Self::RenewLease { now_ms, .. }
387            | Self::Release { now_ms, .. }
388            | Self::Complete { now_ms, .. }
389            | Self::Fail { now_ms, .. }
390            | Self::Cancel { now_ms, .. }
391            | Self::Schedule { now_ms, .. }
392            | Self::ExpireLeases { now_ms, .. } => *now_ms,
393            Self::Submit { .. } => None,
394        }
395    }
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399/// `WorkQueueMessageBusRef` data container.
400pub struct WorkQueueMessageBusRef {
401    /// `topic` field for topic.
402    pub topic: String,
403    /// `seq` field for seq.
404    pub seq: u64,
405    /// `subscription_id` field for subscription id.
406    pub subscription_id: String,
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410/// `WorkQueueDerivedState` variants.
411pub enum WorkQueueDerivedState {
412    /// `Scheduled` variant.
413    Scheduled,
414    /// `Ready` variant.
415    Ready,
416    /// `Leased` variant.
417    Leased,
418    /// `RetryWait` variant.
419    RetryWait,
420    /// `Completed` variant.
421    Completed,
422    /// `Canceled` variant.
423    Canceled,
424    /// `DeadLettered` variant.
425    DeadLettered,
426}
427
428#[derive(Debug, Clone, PartialEq)]
429/// `WorkQueueRecord` variants.
430pub enum WorkQueueRecord<T> {
431    /// `WorkAdmitted` variant.
432    WorkAdmitted {
433        /// `record_seq` field for record seq.
434        record_seq: u64,
435        /// `queue_id` field for queue id.
436        queue_id: String,
437        /// `work_id` field for work id.
438        work_id: String,
439        /// `payload` field for payload.
440        payload: T,
441        /// `message_bus` field for message bus.
442        message_bus: WorkQueueMessageBusRef,
443        /// `priority` field for priority.
444        priority: Option<i64>,
445        /// `tags` field for tags.
446        tags: Vec<String>,
447        /// `requirements` field for requirements.
448        requirements: Vec<String>,
449        /// `not_before_ms` field for not before ms.
450        not_before_ms: Option<u64>,
451        /// `deadline_ms` field for deadline ms.
452        deadline_ms: Option<u64>,
453        /// `recorded_at_ms` field for recorded at ms.
454        recorded_at_ms: u64,
455    },
456    /// `AdmissionDeduped` variant.
457    AdmissionDeduped {
458        /// `record_seq` field for record seq.
459        record_seq: u64,
460        /// `queue_id` field for queue id.
461        queue_id: String,
462        /// `work_id` field for work id.
463        work_id: String,
464        /// `message_bus` field for message bus.
465        message_bus: WorkQueueMessageBusRef,
466        /// `reason` field for reason.
467        reason: String,
468        /// `existing_work_id` field for existing work id.
469        existing_work_id: String,
470        /// `recorded_at_ms` field for recorded at ms.
471        recorded_at_ms: u64,
472    },
473    /// `WorkScheduled` variant.
474    WorkScheduled {
475        /// `record_seq` field for record seq.
476        record_seq: u64,
477        /// `queue_id` field for queue id.
478        queue_id: String,
479        /// `work_id` field for work id.
480        work_id: String,
481        /// `command_id` field for command id.
482        command_id: String,
483        /// `schedule_id` field for schedule id.
484        schedule_id: Option<String>,
485        /// `not_before_ms` field for not before ms.
486        not_before_ms: u64,
487        /// `deadline_ms` field for deadline ms.
488        deadline_ms: Option<u64>,
489        /// `reason` field for reason.
490        reason: Option<String>,
491        /// `recorded_at_ms` field for recorded at ms.
492        recorded_at_ms: u64,
493    },
494    /// `WorkClaimed` variant.
495    WorkClaimed {
496        /// `record_seq` field for record seq.
497        record_seq: u64,
498        /// `queue_id` field for queue id.
499        queue_id: String,
500        /// `work_id` field for work id.
501        work_id: String,
502        /// `command_id` field for command id.
503        command_id: String,
504        /// `lease_id` field for lease id.
505        lease_id: String,
506        /// `attempt` field for attempt.
507        attempt: u32,
508        /// `worker_id` field for worker id.
509        worker_id: String,
510        /// `claimed_at_ms` field for claimed at ms.
511        claimed_at_ms: u64,
512        /// `lease_expires_at_ms` field for lease expires at ms.
513        lease_expires_at_ms: u64,
514    },
515    /// `LeaseRenewed` variant.
516    LeaseRenewed {
517        /// `record_seq` field for record seq.
518        record_seq: u64,
519        /// `queue_id` field for queue id.
520        queue_id: String,
521        /// `work_id` field for work id.
522        work_id: String,
523        /// `command_id` field for command id.
524        command_id: String,
525        /// `lease_id` field for lease id.
526        lease_id: String,
527        /// `attempt` field for attempt.
528        attempt: u32,
529        /// `worker_id` field for worker id.
530        worker_id: String,
531        /// `previous_lease_expires_at_ms` field for previous lease expires at ms.
532        previous_lease_expires_at_ms: u64,
533        /// `lease_expires_at_ms` field for lease expires at ms.
534        lease_expires_at_ms: u64,
535        /// `renewed_at_ms` field for renewed at ms.
536        renewed_at_ms: u64,
537    },
538    /// `WorkReleased` variant.
539    WorkReleased {
540        /// `record_seq` field for record seq.
541        record_seq: u64,
542        /// `queue_id` field for queue id.
543        queue_id: String,
544        /// `work_id` field for work id.
545        work_id: String,
546        /// `command_id` field for command id.
547        command_id: String,
548        /// `lease_id` field for lease id.
549        lease_id: String,
550        /// `attempt` field for attempt.
551        attempt: u32,
552        /// `worker_id` field for worker id.
553        worker_id: String,
554        /// `released_at_ms` field for released at ms.
555        released_at_ms: u64,
556        /// `reason` field for reason.
557        reason: Option<String>,
558    },
559    /// `LeaseExpired` variant.
560    LeaseExpired {
561        /// `record_seq` field for record seq.
562        record_seq: u64,
563        /// `queue_id` field for queue id.
564        queue_id: String,
565        /// `work_id` field for work id.
566        work_id: String,
567        /// `command_id` field for command id.
568        command_id: Option<String>,
569        /// `lease_id` field for lease id.
570        lease_id: String,
571        /// `attempt` field for attempt.
572        attempt: u32,
573        /// `worker_id` field for worker id.
574        worker_id: String,
575        /// `lease_expires_at_ms` field for lease expires at ms.
576        lease_expires_at_ms: u64,
577        /// `expired_at_ms` field for expired at ms.
578        expired_at_ms: u64,
579    },
580    /// `AttemptCompleted` variant.
581    AttemptCompleted {
582        /// `record_seq` field for record seq.
583        record_seq: u64,
584        /// `queue_id` field for queue id.
585        queue_id: String,
586        /// `work_id` field for work id.
587        work_id: String,
588        /// `command_id` field for command id.
589        command_id: String,
590        /// `lease_id` field for lease id.
591        lease_id: String,
592        /// `attempt` field for attempt.
593        attempt: u32,
594        /// `worker_id` field for worker id.
595        worker_id: String,
596        /// `result` field for result.
597        result: Option<String>,
598        /// `recorded_at_ms` field for recorded at ms.
599        recorded_at_ms: u64,
600    },
601    /// `WorkCompleted` variant.
602    WorkCompleted {
603        /// `record_seq` field for record seq.
604        record_seq: u64,
605        /// `queue_id` field for queue id.
606        queue_id: String,
607        /// `work_id` field for work id.
608        work_id: String,
609        /// `command_id` field for command id.
610        command_id: String,
611        /// `lease_id` field for lease id.
612        lease_id: String,
613        /// `attempt` field for attempt.
614        attempt: u32,
615        /// `worker_id` field for worker id.
616        worker_id: String,
617        /// `result` field for result.
618        result: Option<String>,
619        /// `recorded_at_ms` field for recorded at ms.
620        recorded_at_ms: u64,
621    },
622    /// `AttemptFailed` variant.
623    AttemptFailed {
624        /// `record_seq` field for record seq.
625        record_seq: u64,
626        /// `queue_id` field for queue id.
627        queue_id: String,
628        /// `work_id` field for work id.
629        work_id: String,
630        /// `command_id` field for command id.
631        command_id: String,
632        /// `lease_id` field for lease id.
633        lease_id: String,
634        /// `attempt` field for attempt.
635        attempt: u32,
636        /// `worker_id` field for worker id.
637        worker_id: String,
638        /// `error` field for error.
639        error: Option<String>,
640        /// `retryable` field for retryable.
641        retryable: Option<bool>,
642        /// `recorded_at_ms` field for recorded at ms.
643        recorded_at_ms: u64,
644    },
645    /// `RetryScheduled` variant.
646    RetryScheduled {
647        /// `record_seq` field for record seq.
648        record_seq: u64,
649        /// `queue_id` field for queue id.
650        queue_id: String,
651        /// `work_id` field for work id.
652        work_id: String,
653        /// `command_id` field for command id.
654        command_id: String,
655        /// `retry_at_ms` field for retry at ms.
656        retry_at_ms: u64,
657        /// `delay_ms` field for delay ms.
658        delay_ms: u64,
659        /// `reason` field for reason.
660        reason: Option<String>,
661        /// `recorded_at_ms` field for recorded at ms.
662        recorded_at_ms: u64,
663    },
664    /// `WorkDeadLettered` variant.
665    WorkDeadLettered {
666        /// `record_seq` field for record seq.
667        record_seq: u64,
668        /// `queue_id` field for queue id.
669        queue_id: String,
670        /// `work_id` field for work id.
671        work_id: String,
672        /// `command_id` field for command id.
673        command_id: String,
674        /// `reason` field for reason.
675        reason: String,
676        /// `exhausted_attempts` field for exhausted attempts.
677        exhausted_attempts: Option<u32>,
678        /// `recorded_at_ms` field for recorded at ms.
679        recorded_at_ms: u64,
680    },
681    /// `WorkCanceled` variant.
682    WorkCanceled {
683        /// `record_seq` field for record seq.
684        record_seq: u64,
685        /// `queue_id` field for queue id.
686        queue_id: String,
687        /// `work_id` field for work id.
688        work_id: String,
689        /// `command_id` field for command id.
690        command_id: String,
691        /// `reason` field for reason.
692        reason: Option<String>,
693        /// `canceled_at_ms` field for canceled at ms.
694        canceled_at_ms: u64,
695        /// `canceled_lease_id` field for canceled lease id.
696        canceled_lease_id: Option<String>,
697        /// `attempt` field for attempt.
698        attempt: Option<u32>,
699    },
700}
701
702impl<T> WorkQueueRecord<T> {
703    /// Updates or reads `record_seq`.
704    pub fn record_seq(&self) -> u64 {
705        match self {
706            Self::WorkAdmitted { record_seq, .. }
707            | Self::AdmissionDeduped { record_seq, .. }
708            | Self::WorkScheduled { record_seq, .. }
709            | Self::WorkClaimed { record_seq, .. }
710            | Self::LeaseRenewed { record_seq, .. }
711            | Self::WorkReleased { record_seq, .. }
712            | Self::LeaseExpired { record_seq, .. }
713            | Self::AttemptCompleted { record_seq, .. }
714            | Self::WorkCompleted { record_seq, .. }
715            | Self::AttemptFailed { record_seq, .. }
716            | Self::RetryScheduled { record_seq, .. }
717            | Self::WorkDeadLettered { record_seq, .. }
718            | Self::WorkCanceled { record_seq, .. } => *record_seq,
719        }
720    }
721
722    /// Updates or reads `work_id`.
723    pub fn work_id(&self) -> &str {
724        match self {
725            Self::WorkAdmitted { work_id, .. }
726            | Self::AdmissionDeduped { work_id, .. }
727            | Self::WorkScheduled { work_id, .. }
728            | Self::WorkClaimed { work_id, .. }
729            | Self::LeaseRenewed { work_id, .. }
730            | Self::WorkReleased { work_id, .. }
731            | Self::LeaseExpired { work_id, .. }
732            | Self::AttemptCompleted { work_id, .. }
733            | Self::WorkCompleted { work_id, .. }
734            | Self::AttemptFailed { work_id, .. }
735            | Self::RetryScheduled { work_id, .. }
736            | Self::WorkDeadLettered { work_id, .. }
737            | Self::WorkCanceled { work_id, .. } => work_id,
738        }
739    }
740
741    fn admission_message_bus(&self) -> Option<&WorkQueueMessageBusRef> {
742        match self {
743            Self::WorkAdmitted { message_bus, .. } | Self::AdmissionDeduped { message_bus, .. } => {
744                Some(message_bus)
745            }
746            _ => None,
747        }
748    }
749}
750
751#[derive(Debug, Clone, Copy, PartialEq, Eq)]
752/// `WorkQueueStatusKind` variants.
753pub enum WorkQueueStatusKind {
754    /// `CommandAccepted` variant.
755    CommandAccepted,
756    /// `CommandRejected` variant.
757    CommandRejected,
758    /// `AdmissionAccepted` variant.
759    AdmissionAccepted,
760    /// `AdmissionRejected` variant.
761    AdmissionRejected,
762    /// `ProjectionReady` variant.
763    ProjectionReady,
764    /// `ProjectionPartial` variant.
765    ProjectionPartial,
766    /// `MaintenanceApplied` variant.
767    MaintenanceApplied,
768    /// `MaintenanceNoop` variant.
769    MaintenanceNoop,
770    /// `PolicyWarning` variant.
771    PolicyWarning,
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
775/// `WorkQueueStatus` data container.
776pub struct WorkQueueStatus {
777    /// `kind` field for kind.
778    pub kind: WorkQueueStatusKind,
779    /// `queue_id` field for queue id.
780    pub queue_id: String,
781    /// `work_id` field for work id.
782    pub work_id: Option<String>,
783    /// `command_id` field for command id.
784    pub command_id: Option<String>,
785    /// `record_seq` field for record seq.
786    pub record_seq: Option<u64>,
787    /// `as_of_record_seq` field for as of record seq.
788    pub as_of_record_seq: Option<u64>,
789    /// `issue_code` field for issue code.
790    pub issue_code: Option<String>,
791    /// `timestamp_ms` field for timestamp ms.
792    pub timestamp_ms: u64,
793    /// `details` field for details.
794    pub details: Option<String>,
795}
796
797#[derive(Debug, Clone, PartialEq)]
798/// `WorkQueueAvailableItem` data container.
799pub struct WorkQueueAvailableItem<T> {
800    /// `work_id` field for work id.
801    pub work_id: String,
802    /// `state` field for state.
803    pub state: WorkQueueDerivedState,
804    /// `payload` field for payload.
805    pub payload: T,
806    /// `admission_seq` field for admission seq.
807    pub admission_seq: u64,
808    /// `priority` field for priority.
809    pub priority: Option<i64>,
810    /// `tags` field for tags.
811    pub tags: Vec<String>,
812    /// `requirements` field for requirements.
813    pub requirements: Vec<String>,
814    /// `not_before_ms` field for not before ms.
815    pub not_before_ms: Option<u64>,
816    /// `retry_at_ms` field for retry at ms.
817    pub retry_at_ms: Option<u64>,
818    /// `deadline_ms` field for deadline ms.
819    pub deadline_ms: Option<u64>,
820}
821
822#[derive(Debug, Clone, PartialEq)]
823/// `WorkQueueAvailablePage` data container.
824pub struct WorkQueueAvailablePage<T> {
825    /// `items` field for items.
826    pub items: Vec<WorkQueueAvailableItem<T>>,
827    /// `next_after_work_id` field for next after work id.
828    pub next_after_work_id: Option<String>,
829    /// `next_after_admission_seq` field for next after admission seq.
830    pub next_after_admission_seq: Option<u64>,
831    /// `has_more` field for has more.
832    pub has_more: bool,
833    /// `as_of_record_seq` field for as of record seq.
834    pub as_of_record_seq: u64,
835}
836
837#[derive(Debug, Clone, PartialEq, Eq)]
838/// `WorkQueueActiveLease` data container.
839pub struct WorkQueueActiveLease {
840    /// `lease_id` field for lease id.
841    pub lease_id: String,
842    /// `attempt` field for attempt.
843    pub attempt: u32,
844    /// `worker_id` field for worker id.
845    pub worker_id: String,
846    /// `lease_expires_at_ms` field for lease expires at ms.
847    pub lease_expires_at_ms: u64,
848}
849
850#[derive(Debug, Clone, PartialEq)]
851/// `WorkQueueWorkSnapshot` data container.
852pub struct WorkQueueWorkSnapshot<T> {
853    /// `work_id` field for work id.
854    pub work_id: String,
855    /// `state` field for state.
856    pub state: Option<WorkQueueDerivedState>,
857    /// `payload` field for payload.
858    pub payload: Option<T>,
859    /// `active_lease` field for active lease.
860    pub active_lease: Option<WorkQueueActiveLease>,
861    /// `records` field for records.
862    pub records: Vec<WorkQueueRecord<T>>,
863    /// `as_of_record_seq` field for as of record seq.
864    pub as_of_record_seq: u64,
865}
866
867#[derive(Debug, Clone, PartialEq)]
868/// `WorkQueueDeadLetterPage` data container.
869pub struct WorkQueueDeadLetterPage<T> {
870    /// `entries` field for entries.
871    pub entries: Vec<WorkQueueRecord<T>>,
872    /// `next_after_dead_letter_seq` field for next after dead letter seq.
873    pub next_after_dead_letter_seq: Option<u64>,
874    /// `has_more` field for has more.
875    pub has_more: bool,
876    /// `as_of_record_seq` field for as of record seq.
877    pub as_of_record_seq: u64,
878}
879
880#[derive(Debug, Clone, PartialEq, Eq, Default)]
881/// `WorkQueueAvailableParams` data container.
882pub struct WorkQueueAvailableParams {
883    /// `limit` field for limit.
884    pub limit: Option<usize>,
885    /// `after_work_id` field for after work id.
886    pub after_work_id: Option<String>,
887    /// `after_admission_seq` field for after admission seq.
888    pub after_admission_seq: Option<u64>,
889    /// `now_ms` field for now ms.
890    pub now_ms: Option<u64>,
891}
892
893#[derive(Debug, Clone, PartialEq, Eq, Default)]
894/// `WorkQueueDeadLetterParams` data container.
895pub struct WorkQueueDeadLetterParams {
896    /// `limit` field for limit.
897    pub limit: Option<usize>,
898    /// `after_dead_letter_seq` field for after dead letter seq.
899    pub after_dead_letter_seq: Option<u64>,
900    /// `after_work_id` field for after work id.
901    pub after_work_id: Option<String>,
902}
903
904#[derive(Clone)]
905/// `WorkQueueProjection` data container.
906pub struct WorkQueueProjection<TPage> {
907    /// `snapshot` field for snapshot.
908    pub snapshot: Node<TPage>,
909    /// `snapshot_pull_id` field for snapshot pull id.
910    pub snapshot_pull_id: LockId,
911    /// `status` field for status.
912    pub status: Node<WorkQueueStatus>,
913    /// `issues` field for issues.
914    pub issues: Node<DataIssue>,
915}
916
917#[derive(Clone)]
918/// `WorkQueueAvailableProjection` data container.
919pub struct WorkQueueAvailableProjection<T> {
920    /// `available` field for available.
921    pub available: Node<WorkQueueAvailablePage<T>>,
922    /// `available_pull_id` field for available pull id.
923    pub available_pull_id: LockId,
924    /// `status` field for status.
925    pub status: Node<WorkQueueStatus>,
926    /// `issues` field for issues.
927    pub issues: Node<DataIssue>,
928}
929
930#[derive(Clone)]
931/// `WorkQueue` data container.
932pub struct WorkQueue<T> {
933    name: Rc<String>,
934    queue_id: Rc<String>,
935    topic: Rc<String>,
936    bus: MessageBus<WorkQueueSubmit<T>>,
937    command_seq: Rc<Cell<u64>>,
938    /// `commands` field for commands.
939    pub commands: Node<WorkQueueCommand<T>>,
940    /// `records` field for records.
941    pub records: Node<WorkQueueRecord<T>>,
942    /// `status` field for status.
943    pub status: Node<WorkQueueStatus>,
944    /// `issues` field for issues.
945    pub issues: Node<DataIssue>,
946    state: Rc<RefCell<RuntimeState<T>>>,
947    graph: Graph,
948    _admission_ack_commands: Node<MessageBusCommand<WorkQueueSubmit<T>>>,
949    _retain: Rc<WorkQueueRetain>,
950}
951
952struct WorkQueueRetain {
953    releases: RefCell<Vec<Box<dyn FnOnce()>>>,
954}
955
956impl WorkQueueRetain {
957    fn new(releases: Vec<Box<dyn FnOnce()>>) -> Self {
958        Self {
959            releases: RefCell::new(releases),
960        }
961    }
962}
963
964impl Drop for WorkQueueRetain {
965    fn drop(&mut self) {
966        for release in self.releases.borrow_mut().drain(..) {
967            release();
968        }
969    }
970}
971
972#[derive(Clone)]
973enum QueueEvent<T> {
974    Record(WorkQueueRecord<T>),
975    Status(WorkQueueStatus),
976    Issue(DataIssue),
977}
978
979#[derive(Clone)]
980struct WorkState<T> {
981    work_id: String,
982    payload: T,
983    state: WorkQueueDerivedState,
984    admission_seq: u64,
985    priority: Option<i64>,
986    tags: Vec<String>,
987    requirements: Vec<String>,
988    not_before_ms: Option<u64>,
989    retry_at_ms: Option<u64>,
990    deadline_ms: Option<u64>,
991    attempt: u32,
992    lease_id: Option<String>,
993    worker_id: Option<String>,
994    lease_expires_at_ms: Option<u64>,
995}
996
997struct RuntimeState<T> {
998    record_seq: u64,
999    lease_seq: u64,
1000    works: BTreeMap<String, WorkState<T>>,
1001    source_seqs: HashSet<String>,
1002    command_ids: HashSet<String>,
1003    idempotency_keys: HashSet<String>,
1004    records: Vec<WorkQueueRecord<T>>,
1005    dead_letters: Vec<WorkQueueRecord<T>>,
1006}
1007
1008impl<T> Default for RuntimeState<T> {
1009    fn default() -> Self {
1010        Self {
1011            record_seq: 0,
1012            lease_seq: 0,
1013            works: BTreeMap::new(),
1014            source_seqs: HashSet::new(),
1015            command_ids: HashSet::new(),
1016            idempotency_keys: HashSet::new(),
1017            records: Vec::new(),
1018            dead_letters: Vec::new(),
1019        }
1020    }
1021}
1022
1023impl<T: Clone + 'static> WorkQueue<T> {
1024    /// Updates or reads `submit`.
1025    pub fn submit(
1026        &self,
1027        payload: T,
1028        opts: WorkQueueSubmitOptions,
1029    ) -> MessageBusCommand<WorkQueueSubmit<T>> {
1030        let command_id = opts
1031            .command_id
1032            .unwrap_or_else(|| self.next_command_id("submit"));
1033        self.bus.publish(
1034            (*self.topic).clone(),
1035            WorkQueueSubmit {
1036                payload,
1037                work_id: opts.work_id,
1038                priority: opts.priority,
1039                tags: opts.tags,
1040                requirements: opts.requirements,
1041                not_before_ms: opts.not_before_ms,
1042                deadline_ms: opts.deadline_ms,
1043            },
1044            None,
1045            Some(command_id),
1046            opts.idempotency_key,
1047        )
1048    }
1049
1050    /// Updates or reads `claim`.
1051    pub fn claim(&self, opts: WorkQueueClaimOptions) -> WorkQueueCommand<T> {
1052        self.publish_command(WorkQueueCommand::Claim {
1053            command_id: opts
1054                .command_id
1055                .unwrap_or_else(|| self.next_command_id("claim")),
1056            queue_id: opts.queue_id,
1057            idempotency_key: opts.idempotency_key,
1058            worker_id: opts.worker_id,
1059            requested_work_ids: opts.requested_work_ids,
1060            limit: opts.limit,
1061            lease_duration_ms: opts.lease_duration_ms,
1062            now_ms: opts.now_ms,
1063        })
1064    }
1065
1066    /// Updates or reads `renew_lease`.
1067    pub fn renew_lease(
1068        &self,
1069        work_id: impl Into<String>,
1070        lease_id: impl Into<String>,
1071        attempt: u32,
1072        worker_id: impl Into<String>,
1073        command_id: impl Into<String>,
1074    ) -> WorkQueueCommand<T> {
1075        self.publish_command(WorkQueueCommand::RenewLease {
1076            command_id: command_id.into(),
1077            queue_id: None,
1078            idempotency_key: None,
1079            work_id: work_id.into(),
1080            lease_id: lease_id.into(),
1081            attempt,
1082            worker_id: worker_id.into(),
1083            lease_duration_ms: None,
1084            lease_expires_at_ms: None,
1085            now_ms: None,
1086        })
1087    }
1088
1089    /// Updates or reads `release`.
1090    pub fn release(
1091        &self,
1092        work_id: impl Into<String>,
1093        lease_id: impl Into<String>,
1094        attempt: u32,
1095        worker_id: impl Into<String>,
1096        command_id: impl Into<String>,
1097    ) -> WorkQueueCommand<T> {
1098        self.publish_command(WorkQueueCommand::Release {
1099            command_id: command_id.into(),
1100            queue_id: None,
1101            idempotency_key: None,
1102            work_id: work_id.into(),
1103            lease_id: lease_id.into(),
1104            attempt,
1105            worker_id: worker_id.into(),
1106            reason: None,
1107            now_ms: None,
1108        })
1109    }
1110
1111    /// Updates or reads `complete`.
1112    pub fn complete(
1113        &self,
1114        work_id: impl Into<String>,
1115        lease_id: impl Into<String>,
1116        attempt: u32,
1117        worker_id: impl Into<String>,
1118        command_id: impl Into<String>,
1119        result: Option<String>,
1120    ) -> WorkQueueCommand<T> {
1121        self.publish_command(WorkQueueCommand::Complete {
1122            command_id: command_id.into(),
1123            queue_id: None,
1124            idempotency_key: None,
1125            work_id: work_id.into(),
1126            lease_id: lease_id.into(),
1127            attempt,
1128            worker_id: worker_id.into(),
1129            result,
1130            now_ms: None,
1131        })
1132    }
1133
1134    /// Updates or reads `fail`.
1135    pub fn fail(
1136        &self,
1137        work_id: impl Into<String>,
1138        lease_id: impl Into<String>,
1139        attempt: u32,
1140        worker_id: impl Into<String>,
1141        command_id: impl Into<String>,
1142        retryable: Option<bool>,
1143    ) -> WorkQueueCommand<T> {
1144        self.publish_command(WorkQueueCommand::Fail {
1145            command_id: command_id.into(),
1146            queue_id: None,
1147            idempotency_key: None,
1148            work_id: work_id.into(),
1149            lease_id: lease_id.into(),
1150            attempt,
1151            worker_id: worker_id.into(),
1152            error: None,
1153            retryable,
1154            now_ms: None,
1155        })
1156    }
1157
1158    /// Updates or reads `cancel`.
1159    pub fn cancel(
1160        &self,
1161        work_id: impl Into<String>,
1162        command_id: impl Into<String>,
1163        reason: Option<String>,
1164    ) -> WorkQueueCommand<T> {
1165        self.publish_command(WorkQueueCommand::Cancel {
1166            command_id: command_id.into(),
1167            queue_id: None,
1168            idempotency_key: None,
1169            work_id: work_id.into(),
1170            reason,
1171            now_ms: None,
1172        })
1173    }
1174
1175    /// Updates or reads `schedule`.
1176    pub fn schedule(
1177        &self,
1178        work_id: impl Into<String>,
1179        not_before_ms: u64,
1180        command_id: impl Into<String>,
1181    ) -> WorkQueueCommand<T> {
1182        self.publish_command(WorkQueueCommand::Schedule {
1183            command_id: command_id.into(),
1184            queue_id: None,
1185            idempotency_key: None,
1186            work_id: work_id.into(),
1187            schedule_id: None,
1188            not_before_ms,
1189            deadline_ms: None,
1190            reason: None,
1191            now_ms: None,
1192        })
1193    }
1194
1195    /// Updates or reads `expire_leases`.
1196    pub fn expire_leases(&self, command_id: impl Into<String>) -> WorkQueueCommand<T> {
1197        self.publish_command(WorkQueueCommand::ExpireLeases {
1198            command_id: command_id.into(),
1199            queue_id: None,
1200            idempotency_key: None,
1201            work_ids: Vec::new(),
1202            limit: None,
1203            now_ms: None,
1204        })
1205    }
1206
1207    /// Updates or reads `available`.
1208    pub fn available(&self) -> WorkQueueAvailableProjection<T> {
1209        self.available_named(None::<String>)
1210    }
1211
1212    /// Updates or reads `available_named`.
1213    pub fn available_named(
1214        &self,
1215        name: Option<impl Into<String>>,
1216    ) -> WorkQueueAvailableProjection<T> {
1217        let available_pull_id = LockId::new(format!("{}/available", self.name));
1218        let state = self.state.clone();
1219        let snapshot = self.graph.node_opts::<WorkQueueAvailablePage<T>, _>(
1220            vec![self.records.erased()],
1221            move |ctx| {
1222                let params = pull_params::<WorkQueueAvailableParams>(ctx);
1223                ctx.emit(available_page(&state.borrow(), &params));
1224            },
1225            pull_node_opts(
1226                name.map(Into::into)
1227                    .unwrap_or_else(|| format!("{}/available", self.name)),
1228                "workQueueAvailable",
1229                available_pull_id.clone(),
1230            ),
1231        );
1232        WorkQueueAvailableProjection {
1233            available: snapshot,
1234            available_pull_id,
1235            status: self.status.clone(),
1236            issues: self.issues.clone(),
1237        }
1238    }
1239
1240    /// Updates or reads `work`.
1241    pub fn work(
1242        &self,
1243        work_id: impl Into<String>,
1244    ) -> WorkQueueProjection<WorkQueueWorkSnapshot<T>> {
1245        self.work_named(work_id, None::<String>)
1246    }
1247
1248    /// Updates or reads `work_named`.
1249    pub fn work_named(
1250        &self,
1251        work_id: impl Into<String>,
1252        name: Option<impl Into<String>>,
1253    ) -> WorkQueueProjection<WorkQueueWorkSnapshot<T>> {
1254        let work_id = work_id.into();
1255        let snapshot_pull_id = LockId::new(format!("{}/{work_id}/snapshot", self.name));
1256        let state = self.state.clone();
1257        let work_id_for_fn = work_id.clone();
1258        let snapshot = self.graph.node_opts::<WorkQueueWorkSnapshot<T>, _>(
1259            vec![self.records.erased()],
1260            move |ctx| {
1261                let _ = ctx.pull();
1262                ctx.emit(work_snapshot(&state.borrow(), &work_id_for_fn));
1263            },
1264            pull_node_opts(
1265                name.map(Into::into)
1266                    .unwrap_or_else(|| format!("{}/{work_id}", self.name)),
1267                "workQueueWorkSnapshot",
1268                snapshot_pull_id.clone(),
1269            ),
1270        );
1271        WorkQueueProjection {
1272            snapshot,
1273            snapshot_pull_id,
1274            status: self.status.clone(),
1275            issues: self.issues.clone(),
1276        }
1277    }
1278
1279    /// Updates or reads `dead_letter`.
1280    pub fn dead_letter(&self) -> WorkQueueProjection<WorkQueueDeadLetterPage<T>> {
1281        self.dead_letter_named(None::<String>)
1282    }
1283
1284    /// Updates or reads `dead_letter_named`.
1285    pub fn dead_letter_named(
1286        &self,
1287        name: Option<impl Into<String>>,
1288    ) -> WorkQueueProjection<WorkQueueDeadLetterPage<T>> {
1289        let snapshot_pull_id = LockId::new(format!("{}/deadLetter", self.name));
1290        let state = self.state.clone();
1291        let snapshot = self.graph.node_opts::<WorkQueueDeadLetterPage<T>, _>(
1292            vec![self.records.erased()],
1293            move |ctx| {
1294                let params = pull_params::<WorkQueueDeadLetterParams>(ctx);
1295                ctx.emit(dead_letter_page(&state.borrow(), &params));
1296            },
1297            pull_node_opts(
1298                name.map(Into::into)
1299                    .unwrap_or_else(|| format!("{}/deadLetter", self.name)),
1300                "workQueueDeadLetter",
1301                snapshot_pull_id.clone(),
1302            ),
1303        );
1304        WorkQueueProjection {
1305            snapshot,
1306            snapshot_pull_id,
1307            status: self.status.clone(),
1308            issues: self.issues.clone(),
1309        }
1310    }
1311
1312    fn publish_command(&self, command: WorkQueueCommand<T>) -> WorkQueueCommand<T> {
1313        self.commands.set(command.clone());
1314        command
1315    }
1316
1317    fn next_command_id(&self, kind: &str) -> String {
1318        let next = self.command_seq.get() + 1;
1319        self.command_seq.set(next);
1320        compound_tuple_key(
1321            "work-queue-command",
1322            &[&self.queue_id, kind, &next.to_string()],
1323        )
1324    }
1325}
1326
1327#[derive(Debug, Clone, Default, PartialEq, Eq)]
1328/// `WorkQueueSubmitOptions` data container.
1329pub struct WorkQueueSubmitOptions {
1330    /// `command_id` field for command id.
1331    pub command_id: Option<String>,
1332    /// `idempotency_key` field for idempotency key.
1333    pub idempotency_key: Option<String>,
1334    /// `work_id` field for work id.
1335    pub work_id: Option<String>,
1336    /// `priority` field for priority.
1337    pub priority: Option<i64>,
1338    /// `tags` field for tags.
1339    pub tags: Vec<String>,
1340    /// `requirements` field for requirements.
1341    pub requirements: Vec<String>,
1342    /// `not_before_ms` field for not before ms.
1343    pub not_before_ms: Option<u64>,
1344    /// `deadline_ms` field for deadline ms.
1345    pub deadline_ms: Option<u64>,
1346}
1347
1348#[derive(Debug, Clone, PartialEq, Eq)]
1349/// `WorkQueueClaimOptions` data container.
1350pub struct WorkQueueClaimOptions {
1351    /// `command_id` field for command id.
1352    pub command_id: Option<String>,
1353    /// `queue_id` field for queue id.
1354    pub queue_id: Option<String>,
1355    /// `idempotency_key` field for idempotency key.
1356    pub idempotency_key: Option<String>,
1357    /// `worker_id` field for worker id.
1358    pub worker_id: String,
1359    /// `requested_work_ids` field for requested work ids.
1360    pub requested_work_ids: Vec<String>,
1361    /// `limit` field for limit.
1362    pub limit: Option<usize>,
1363    /// `lease_duration_ms` field for lease duration ms.
1364    pub lease_duration_ms: Option<u64>,
1365    /// `now_ms` field for now ms.
1366    pub now_ms: Option<u64>,
1367}
1368
1369impl WorkQueueClaimOptions {
1370    /// Creates or computes `new`.
1371    pub fn new(worker_id: impl Into<String>) -> Self {
1372        Self {
1373            command_id: None,
1374            queue_id: None,
1375            idempotency_key: None,
1376            worker_id: worker_id.into(),
1377            requested_work_ids: Vec::new(),
1378            limit: None,
1379            lease_duration_ms: None,
1380            now_ms: None,
1381        }
1382    }
1383
1384    /// Updates or reads `command_id`.
1385    pub fn command_id(mut self, command_id: impl Into<String>) -> Self {
1386        self.command_id = Some(command_id.into());
1387        self
1388    }
1389
1390    /// Updates or reads `requested_work_ids`.
1391    pub fn requested_work_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
1392        self.requested_work_ids = ids.into_iter().map(Into::into).collect();
1393        self
1394    }
1395
1396    /// Updates or reads `idempotency_key`.
1397    pub fn idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self {
1398        self.idempotency_key = Some(idempotency_key.into());
1399        self
1400    }
1401
1402    /// Updates or reads `now_ms`.
1403    pub fn now_ms(mut self, now_ms: u64) -> Self {
1404        self.now_ms = Some(now_ms);
1405        self
1406    }
1407}
1408
1409/// Creates or computes `work_queue`.
1410pub fn work_queue<T: Clone + 'static>(graph: &Graph, opts: WorkQueueOptions<T>) -> WorkQueue<T> {
1411    assert_non_empty(&opts.queue_id, "workQueue.queueId");
1412    assert_non_empty(&opts.topic, "workQueue.topic");
1413    assert_non_empty(&opts.subscription_id, "workQueue.subscriptionId");
1414    assert!(
1415        opts.lease_duration_ms > 0,
1416        "workQueue: leaseDurationMs must be positive"
1417    );
1418    assert!(
1419        opts.retry.max_attempts > 0,
1420        "workQueue: retry.maxAttempts must be positive"
1421    );
1422
1423    let queue_id = Rc::new(opts.queue_id.clone());
1424    let name = Rc::new(
1425        opts.name
1426            .clone()
1427            .unwrap_or_else(|| format!("workQueue/{}", opts.queue_id)),
1428    );
1429    let topic = Rc::new(opts.topic.clone());
1430    let admission = opts.bus.subscription(
1431        MessageBusSubscriptionOptions::new(opts.topic.clone(), opts.subscription_id.clone())
1432            .from(opts.from)
1433            .named(format!("{name}/admission")),
1434    );
1435    let commands = graph.node_opts::<WorkQueueCommand<T>, _>(
1436        Vec::new(),
1437        work_queue_command_body::<T>(0),
1438        node_opts(format!("{name}/commands"), "workQueueCommands"),
1439    );
1440    let state = Rc::new(RefCell::new(RuntimeState::default()));
1441    let admission_kick = graph.state_opts(
1442        "poll".to_owned(),
1443        node_opts(format!("{name}/admissionKick"), "workQueueAdmissionKick"),
1444    );
1445    let admission_pages = graph.node_opts::<MessageBusAvailablePage<WorkQueueSubmit<T>>, _>(
1446        vec![
1447            admission.available.erased(),
1448            admission_kick.erased(),
1449            opts.bus.messages.erased(),
1450            opts.bus.status.erased(),
1451        ],
1452        {
1453            let admission_pull_id = admission.available_pull_id.clone();
1454            let topic = opts.topic.clone();
1455            let subscription_id = opts.subscription_id.clone();
1456            move |ctx| {
1457                for page in ctx.batch::<MessageBusAvailablePage<WorkQueueSubmit<T>>>(0) {
1458                    ctx.emit((*page).clone());
1459                }
1460                let mut should_pull = !ctx.batch::<String>(1).is_empty();
1461                for message in ctx.batch::<MessageEnvelope<WorkQueueSubmit<T>>>(2) {
1462                    if message.topic == topic {
1463                        should_pull = true;
1464                    }
1465                }
1466                for status in ctx.batch::<MessageBusStatus>(3) {
1467                    if should_poll_admission(&status, &topic, &subscription_id) {
1468                        should_pull = true;
1469                    }
1470                }
1471                if should_pull {
1472                    ctx.up_next_toward(
1473                        0,
1474                        vec![Message::Pull(PullDemand::with_params(
1475                            admission_pull_id.clone(),
1476                            MessageBusAvailableParams {
1477                                limit: Some(100),
1478                                after_seq: None,
1479                            },
1480                        ))],
1481                    );
1482                }
1483            }
1484        },
1485        {
1486            let mut opts = node_opts(format!("{name}/admissionPages"), "workQueueAdmissionPages");
1487            opts.node.partial = true;
1488            opts
1489        },
1490    );
1491    let runtime_state = state.clone();
1492    let runtime_opts = RuntimeOptions {
1493        queue_id: opts.queue_id.clone(),
1494        subscription_id: opts.subscription_id.clone(),
1495        now: opts.now.clone(),
1496        lease_duration_ms: opts.lease_duration_ms,
1497        retry: opts.retry,
1498    };
1499    let runtime = graph.node_opts::<QueueEvent<T>, _>(
1500        vec![commands.erased(), admission_pages.erased()],
1501        move |ctx| {
1502            for page in ctx.batch::<MessageBusAvailablePage<WorkQueueSubmit<T>>>(1) {
1503                for message in &page.messages {
1504                    let now_ms = timestamp_or_zero(&runtime_opts.now);
1505                    let events = {
1506                        let mut state = runtime_state.borrow_mut();
1507                        admit_message(&runtime_opts, &mut state, message, now_ms)
1508                    };
1509                    for event in events {
1510                        ctx.emit(event);
1511                    }
1512                }
1513            }
1514            for command in ctx.batch::<WorkQueueCommand<T>>(0) {
1515                let now_ms = command
1516                    .now_ms()
1517                    .unwrap_or_else(|| timestamp_or_zero(&runtime_opts.now));
1518                let events = {
1519                    let mut state = runtime_state.borrow_mut();
1520                    reduce_queue_command(&runtime_opts, &mut state, (*command).clone(), now_ms)
1521                };
1522                for event in events {
1523                    ctx.emit(event);
1524                }
1525            }
1526        },
1527        {
1528            let mut opts = node_opts(format!("{name}/runtime"), "workQueueRuntime");
1529            opts.node.partial = true;
1530            opts
1531        },
1532    );
1533    let records = graph.node_opts::<WorkQueueRecord<T>, _>(
1534        vec![runtime.erased()],
1535        move |ctx| {
1536            for event in ctx.batch::<QueueEvent<T>>(0) {
1537                if let QueueEvent::Record(record) = event.as_ref() {
1538                    ctx.emit(record.clone());
1539                }
1540            }
1541        },
1542        node_opts(format!("{name}/records"), "workQueueRecords"),
1543    );
1544    let status = graph.node_opts::<WorkQueueStatus, _>(
1545        vec![runtime.erased()],
1546        move |ctx| {
1547            for event in ctx.batch::<QueueEvent<T>>(0) {
1548                if let QueueEvent::Status(status) = event.as_ref() {
1549                    ctx.emit(status.clone());
1550                }
1551            }
1552        },
1553        node_opts(format!("{name}/status"), "workQueueStatus"),
1554    );
1555    let issues = graph.node_opts::<DataIssue, _>(
1556        vec![runtime.erased()],
1557        move |ctx| {
1558            for event in ctx.batch::<QueueEvent<T>>(0) {
1559                if let QueueEvent::Issue(issue) = event.as_ref() {
1560                    ctx.emit(issue.clone());
1561                }
1562            }
1563        },
1564        node_opts(format!("{name}/issues"), "workQueueIssues"),
1565    );
1566    let admission_ack_commands = graph.node_opts::<MessageBusCommand<WorkQueueSubmit<T>>, _>(
1567        vec![records.erased()],
1568        {
1569            move |ctx| {
1570                for record in ctx.batch::<WorkQueueRecord<T>>(0) {
1571                    if let Some(command) = admission_ack_command(&record) {
1572                        ctx.emit(command);
1573                    }
1574                }
1575            }
1576        },
1577        node_opts(
1578            format!("{name}/admissionAckCommands"),
1579            "workQueueAdmissionAckCommands",
1580        ),
1581    );
1582    let ack_release =
1583        attach_message_bus_deferred_command_sink(graph, &opts.bus, &admission_ack_commands);
1584    let runtime_release = graph.retain(&runtime, &format!("{name}.workQueue.runtime"));
1585    WorkQueue {
1586        name,
1587        queue_id,
1588        topic,
1589        bus: opts.bus,
1590        command_seq: Rc::new(Cell::new(0)),
1591        commands,
1592        records,
1593        status,
1594        issues,
1595        state,
1596        graph: graph.clone(),
1597        _admission_ack_commands: admission_ack_commands,
1598        _retain: Rc::new(WorkQueueRetain::new(vec![runtime_release, ack_release])),
1599    }
1600}
1601
1602struct RuntimeOptions {
1603    queue_id: String,
1604    subscription_id: String,
1605    now: Rc<dyn Fn() -> u64>,
1606    lease_duration_ms: u64,
1607    retry: RetryPolicy,
1608}
1609
1610fn work_queue_command_body<T: Clone + 'static>(
1611    command_source_count: usize,
1612) -> impl Fn(&Ctx) + 'static {
1613    move |ctx| {
1614        for index in 0..command_source_count {
1615            for command in ctx.batch::<WorkQueueCommand<T>>(index) {
1616                ctx.emit((*command).clone());
1617            }
1618        }
1619    }
1620}
1621
1622fn should_poll_admission(status: &MessageBusStatus, topic: &str, subscription_id: &str) -> bool {
1623    if status.topic.as_deref() != Some(topic) {
1624        return false;
1625    }
1626    matches!(
1627        status.kind,
1628        MessageBusStatusKind::MessagePublished | MessageBusStatusKind::RetentionTrimmed
1629    ) || (status.subscription_id.as_deref() == Some(subscription_id)
1630        && matches!(
1631            status.kind,
1632            MessageBusStatusKind::SubscriptionAcked | MessageBusStatusKind::SubscriptionSought
1633        ))
1634}
1635
1636fn admission_ack_command<T>(
1637    record: &WorkQueueRecord<T>,
1638) -> Option<MessageBusCommand<WorkQueueSubmit<T>>> {
1639    let message_bus = record.admission_message_bus()?;
1640    Some(MessageBusCommand::Ack {
1641        topic: message_bus.topic.clone(),
1642        subscription_id: message_bus.subscription_id.clone(),
1643        seq: message_bus.seq,
1644        command_id: Some(compound_tuple_key(
1645            "work-queue-admission-ack",
1646            &[
1647                record_queue_id(record),
1648                &message_bus.topic,
1649                &message_bus.subscription_id,
1650                &message_bus.seq.to_string(),
1651            ],
1652        )),
1653    })
1654}
1655
1656fn admit_message<T: Clone + 'static>(
1657    opts: &RuntimeOptions,
1658    state: &mut RuntimeState<T>,
1659    message: &MessageEnvelope<WorkQueueSubmit<T>>,
1660    now_ms: u64,
1661) -> Vec<QueueEvent<T>> {
1662    let source = canonical_tuple_key(&[&message.topic, &message.seq.to_string()]);
1663    if !state.source_seqs.insert(source) {
1664        return Vec::new();
1665    }
1666    let submit = &message.payload;
1667    let work_id = submit.work_id.clone().unwrap_or_else(|| {
1668        compound_tuple_key(
1669            "work-queue-work",
1670            &[&opts.queue_id, &message.topic, &message.seq.to_string()],
1671        )
1672    });
1673    let message_bus = WorkQueueMessageBusRef {
1674        topic: message.topic.clone(),
1675        seq: message.seq,
1676        subscription_id: opts.subscription_id.clone(),
1677    };
1678    if state.works.contains_key(&work_id) {
1679        let record = append_record(
1680            state,
1681            WorkQueueRecord::AdmissionDeduped {
1682                record_seq: 0,
1683                queue_id: opts.queue_id.clone(),
1684                work_id: work_id.clone(),
1685                message_bus,
1686                reason: "duplicate-work".to_owned(),
1687                existing_work_id: work_id.clone(),
1688                recorded_at_ms: now_ms,
1689            },
1690        );
1691        return vec![
1692            QueueEvent::Record(record.clone()),
1693            status_event(
1694                opts,
1695                WorkQueueStatusKind::AdmissionRejected,
1696                now_ms,
1697                StatusFields {
1698                    work_id: Some(work_id),
1699                    record_seq: Some(record.record_seq()),
1700                    issue_code: Some("duplicate-work".to_owned()),
1701                    ..StatusFields::default()
1702                },
1703            ),
1704        ];
1705    }
1706    let initial_state = if submit.not_before_ms.is_some_and(|t| t > now_ms) {
1707        WorkQueueDerivedState::Scheduled
1708    } else {
1709        WorkQueueDerivedState::Ready
1710    };
1711    state.works.insert(
1712        work_id.clone(),
1713        WorkState {
1714            work_id: work_id.clone(),
1715            payload: submit.payload.clone(),
1716            state: initial_state,
1717            admission_seq: message.seq,
1718            priority: submit.priority,
1719            tags: submit.tags.clone(),
1720            requirements: submit.requirements.clone(),
1721            not_before_ms: submit.not_before_ms,
1722            retry_at_ms: None,
1723            deadline_ms: submit.deadline_ms,
1724            attempt: 0,
1725            lease_id: None,
1726            worker_id: None,
1727            lease_expires_at_ms: None,
1728        },
1729    );
1730    let record = append_record(
1731        state,
1732        WorkQueueRecord::WorkAdmitted {
1733            record_seq: 0,
1734            queue_id: opts.queue_id.clone(),
1735            work_id: work_id.clone(),
1736            payload: submit.payload.clone(),
1737            message_bus,
1738            priority: submit.priority,
1739            tags: submit.tags.clone(),
1740            requirements: submit.requirements.clone(),
1741            not_before_ms: submit.not_before_ms,
1742            deadline_ms: submit.deadline_ms,
1743            recorded_at_ms: now_ms,
1744        },
1745    );
1746    vec![
1747        QueueEvent::Record(record.clone()),
1748        status_event(
1749            opts,
1750            WorkQueueStatusKind::AdmissionAccepted,
1751            now_ms,
1752            StatusFields {
1753                work_id: Some(work_id),
1754                record_seq: Some(record.record_seq()),
1755                ..StatusFields::default()
1756            },
1757        ),
1758    ]
1759}
1760
1761fn reduce_queue_command<T: Clone + 'static>(
1762    opts: &RuntimeOptions,
1763    state: &mut RuntimeState<T>,
1764    command: WorkQueueCommand<T>,
1765    now_ms: u64,
1766) -> Vec<QueueEvent<T>> {
1767    if let Some(error) = validate_queue_command(&opts.queue_id, &command) {
1768        return reject_queue_command(opts, &command, &error.0, &error.1, now_ms);
1769    }
1770    if state.command_ids.contains(command.command_id()) {
1771        return reject_queue_command(
1772            opts,
1773            &command,
1774            "duplicate-command",
1775            "duplicate commandId",
1776            now_ms,
1777        );
1778    }
1779    if let Some(idempotency_key) = command.idempotency_key() {
1780        if state.idempotency_keys.contains(idempotency_key) {
1781            return reject_queue_command(
1782                opts,
1783                &command,
1784                "duplicate-command",
1785                "duplicate idempotencyKey",
1786                now_ms,
1787            );
1788        }
1789    }
1790    state.command_ids.insert(command.command_id().to_owned());
1791    if let Some(idempotency_key) = command.idempotency_key() {
1792        state.idempotency_keys.insert(idempotency_key.to_owned());
1793    }
1794    match command {
1795        WorkQueueCommand::Submit { command_id, .. } => vec![status_event(
1796            opts,
1797            WorkQueueStatusKind::CommandAccepted,
1798            now_ms,
1799            StatusFields {
1800                command_id: Some(command_id),
1801                details: Some("submit uses messageBus".to_owned()),
1802                ..StatusFields::default()
1803            },
1804        )],
1805        WorkQueueCommand::Claim { .. } => claim_work(opts, state, command, now_ms),
1806        WorkQueueCommand::RenewLease { .. } => renew_lease(opts, state, command, now_ms),
1807        WorkQueueCommand::Release { .. } => release_work(opts, state, command, now_ms),
1808        WorkQueueCommand::Complete { .. } => complete_work(opts, state, command, now_ms),
1809        WorkQueueCommand::Fail { .. } => fail_work(opts, state, command, now_ms),
1810        WorkQueueCommand::Cancel { .. } => cancel_work(opts, state, command, now_ms),
1811        WorkQueueCommand::Schedule { .. } => schedule_work(opts, state, command, now_ms),
1812        WorkQueueCommand::ExpireLeases { .. } => expire_leases(opts, state, command, now_ms),
1813    }
1814}
1815
1816fn claim_work<T: Clone + 'static>(
1817    opts: &RuntimeOptions,
1818    state: &mut RuntimeState<T>,
1819    command: WorkQueueCommand<T>,
1820    now_ms: u64,
1821) -> Vec<QueueEvent<T>> {
1822    let WorkQueueCommand::Claim {
1823        command_id,
1824        worker_id,
1825        requested_work_ids,
1826        limit,
1827        lease_duration_ms,
1828        ..
1829    } = command
1830    else {
1831        return Vec::new();
1832    };
1833    let limit = positive_limit(limit.unwrap_or_else(|| requested_work_ids.len().max(1)));
1834    let requested = requested_work_ids.iter().cloned().collect::<HashSet<_>>();
1835    let mut events = Vec::new();
1836    let expired = state
1837        .works
1838        .values()
1839        .filter(|work| {
1840            (requested.is_empty() || requested.contains(&work.work_id)) && is_expired(work, now_ms)
1841        })
1842        .map(|work| work.work_id.clone())
1843        .collect::<Vec<_>>();
1844    for work_id in expired {
1845        if let Some(work) = state.works.get(&work_id).cloned() {
1846            events.extend(materialize_lease_expired(
1847                opts,
1848                state,
1849                work,
1850                Some(command_id.clone()),
1851                now_ms,
1852                None,
1853            ));
1854        }
1855    }
1856    let mut candidates = state
1857        .works
1858        .values()
1859        .filter(|work| requested.is_empty() || requested.contains(&work.work_id))
1860        .filter(|work| is_ready(work, now_ms))
1861        .map(|work| (work.admission_seq, work.work_id.clone()))
1862        .collect::<Vec<_>>();
1863    candidates
1864        .sort_by(|(a_seq, a_id), (b_seq, b_id)| a_seq.cmp(b_seq).then_with(|| a_id.cmp(b_id)));
1865    let candidates = candidates
1866        .into_iter()
1867        .take(limit)
1868        .map(|(_, work_id)| work_id)
1869        .collect::<Vec<_>>();
1870    if candidates.is_empty() {
1871        if requested.is_empty() {
1872            events.extend(reject_queue_command_by_id(
1873                opts,
1874                Some(command_id),
1875                "not-ready",
1876                "no ready work",
1877                now_ms,
1878            ));
1879        } else {
1880            events.extend(claim_miss_events(
1881                opts,
1882                state,
1883                &command_id,
1884                &requested,
1885                &HashSet::new(),
1886                now_ms,
1887            ));
1888        }
1889        return events;
1890    }
1891    let mut claimed = HashSet::new();
1892    let lease_duration_ms = lease_duration_ms.unwrap_or(opts.lease_duration_ms);
1893    let lease_expires_at_ms = match checked_timestamp(
1894        opts,
1895        Some(command_id.clone()),
1896        now_ms,
1897        lease_duration_ms,
1898        "lease duration overflows timestamp",
1899    ) {
1900        Ok(value) => value,
1901        Err(events) => return events,
1902    };
1903    for work_id in candidates {
1904        let lease_seq = state.lease_seq + 1;
1905        state.lease_seq = lease_seq;
1906        let work = state
1907            .works
1908            .get_mut(&work_id)
1909            .expect("candidate work id came from state");
1910        claimed.insert(work_id.clone());
1911        work.state = WorkQueueDerivedState::Leased;
1912        work.attempt += 1;
1913        work.lease_id = Some(compound_tuple_key(
1914            "work-queue-lease",
1915            &[&work.work_id, &lease_seq.to_string()],
1916        ));
1917        work.worker_id = Some(worker_id.clone());
1918        work.lease_expires_at_ms = Some(lease_expires_at_ms);
1919        let lease_id = work.lease_id.clone().expect("lease just set");
1920        let record = append_record(
1921            state,
1922            WorkQueueRecord::WorkClaimed {
1923                record_seq: 0,
1924                queue_id: opts.queue_id.clone(),
1925                work_id: work_id.clone(),
1926                command_id: command_id.clone(),
1927                lease_id,
1928                attempt: state.works[&work_id].attempt,
1929                worker_id: worker_id.clone(),
1930                claimed_at_ms: now_ms,
1931                lease_expires_at_ms: state.works[&work_id]
1932                    .lease_expires_at_ms
1933                    .expect("lease expiry just set"),
1934            },
1935        );
1936        events.push(QueueEvent::Record(record.clone()));
1937        events.push(status_event(
1938            opts,
1939            WorkQueueStatusKind::CommandAccepted,
1940            now_ms,
1941            StatusFields {
1942                work_id: Some(work_id),
1943                command_id: Some(command_id.clone()),
1944                record_seq: Some(record.record_seq()),
1945                ..StatusFields::default()
1946            },
1947        ));
1948    }
1949    if !requested.is_empty() {
1950        events.extend(claim_miss_events(
1951            opts,
1952            state,
1953            &command_id,
1954            &requested,
1955            &claimed,
1956            now_ms,
1957        ));
1958    }
1959    events
1960}
1961
1962fn claim_miss_events<T: Clone + 'static>(
1963    opts: &RuntimeOptions,
1964    state: &RuntimeState<T>,
1965    command_id: &str,
1966    requested: &HashSet<String>,
1967    claimed: &HashSet<String>,
1968    now_ms: u64,
1969) -> Vec<QueueEvent<T>> {
1970    let mut events = Vec::new();
1971    for work_id in requested {
1972        if claimed.contains(work_id) {
1973            continue;
1974        }
1975        let Some(work) = state.works.get(work_id) else {
1976            events.extend(reject_queue_command_by_id(
1977                opts,
1978                Some(command_id.to_owned()),
1979                "unknown-work",
1980                &format!("unknown work '{work_id}'"),
1981                now_ms,
1982            ));
1983            continue;
1984        };
1985        if is_ready(work, now_ms) {
1986            continue;
1987        }
1988        let code = if is_terminal(work.state) {
1989            "terminal-work"
1990        } else if work.state == WorkQueueDerivedState::Leased {
1991            "already-leased"
1992        } else {
1993            "not-ready"
1994        };
1995        events.extend(reject_queue_command_by_id(
1996            opts,
1997            Some(command_id.to_owned()),
1998            code,
1999            &format!("requested work '{work_id}' is not claimable"),
2000            now_ms,
2001        ));
2002    }
2003    events
2004}
2005
2006fn renew_lease<T: Clone + 'static>(
2007    opts: &RuntimeOptions,
2008    state: &mut RuntimeState<T>,
2009    command: WorkQueueCommand<T>,
2010    now_ms: u64,
2011) -> Vec<QueueEvent<T>> {
2012    let WorkQueueCommand::RenewLease {
2013        command_id,
2014        work_id,
2015        lease_id,
2016        attempt,
2017        worker_id,
2018        lease_duration_ms,
2019        lease_expires_at_ms,
2020        ..
2021    } = command
2022    else {
2023        return Vec::new();
2024    };
2025    let checked = current_lease(
2026        opts,
2027        state,
2028        LeaseCheck {
2029            work_id: &work_id,
2030            lease_id: &lease_id,
2031            attempt,
2032            worker_id: &worker_id,
2033            command_id: Some(command_id.clone()),
2034            now_ms,
2035        },
2036    );
2037    if let Err(events) = checked {
2038        return events;
2039    }
2040    let work = state.works.get_mut(&work_id).expect("checked lease");
2041    let previous = work.lease_expires_at_ms.expect("checked lease expiry");
2042    let next = match lease_expires_at_ms {
2043        Some(value) => value,
2044        None => match checked_timestamp(
2045            opts,
2046            Some(command_id.clone()),
2047            now_ms,
2048            lease_duration_ms.unwrap_or(opts.lease_duration_ms),
2049            "lease renewal duration overflows timestamp",
2050        ) {
2051            Ok(value) => value,
2052            Err(events) => return events,
2053        },
2054    };
2055    work.lease_expires_at_ms = Some(next);
2056    let record = append_record(
2057        state,
2058        WorkQueueRecord::LeaseRenewed {
2059            record_seq: 0,
2060            queue_id: opts.queue_id.clone(),
2061            work_id: work_id.clone(),
2062            command_id: command_id.clone(),
2063            lease_id,
2064            attempt,
2065            worker_id,
2066            previous_lease_expires_at_ms: previous,
2067            lease_expires_at_ms: next,
2068            renewed_at_ms: now_ms,
2069        },
2070    );
2071    accepted(opts, record, Some(work_id), Some(command_id), now_ms)
2072}
2073
2074fn release_work<T: Clone + 'static>(
2075    opts: &RuntimeOptions,
2076    state: &mut RuntimeState<T>,
2077    command: WorkQueueCommand<T>,
2078    now_ms: u64,
2079) -> Vec<QueueEvent<T>> {
2080    let WorkQueueCommand::Release {
2081        command_id,
2082        work_id,
2083        lease_id,
2084        attempt,
2085        worker_id,
2086        reason,
2087        ..
2088    } = command
2089    else {
2090        return Vec::new();
2091    };
2092    if let Err(events) = current_lease(
2093        opts,
2094        state,
2095        LeaseCheck {
2096            work_id: &work_id,
2097            lease_id: &lease_id,
2098            attempt,
2099            worker_id: &worker_id,
2100            command_id: Some(command_id.clone()),
2101            now_ms,
2102        },
2103    ) {
2104        return events;
2105    }
2106    clear_lease(state.works.get_mut(&work_id).expect("checked lease"));
2107    state.works.get_mut(&work_id).expect("checked lease").state = WorkQueueDerivedState::Ready;
2108    let record = append_record(
2109        state,
2110        WorkQueueRecord::WorkReleased {
2111            record_seq: 0,
2112            queue_id: opts.queue_id.clone(),
2113            work_id: work_id.clone(),
2114            command_id: command_id.clone(),
2115            lease_id,
2116            attempt,
2117            worker_id,
2118            released_at_ms: now_ms,
2119            reason,
2120        },
2121    );
2122    accepted(opts, record, Some(work_id), Some(command_id), now_ms)
2123}
2124
2125fn complete_work<T: Clone + 'static>(
2126    opts: &RuntimeOptions,
2127    state: &mut RuntimeState<T>,
2128    command: WorkQueueCommand<T>,
2129    now_ms: u64,
2130) -> Vec<QueueEvent<T>> {
2131    let WorkQueueCommand::Complete {
2132        command_id,
2133        work_id,
2134        lease_id,
2135        attempt,
2136        worker_id,
2137        result,
2138        ..
2139    } = command
2140    else {
2141        return Vec::new();
2142    };
2143    if let Err(events) = current_lease(
2144        opts,
2145        state,
2146        LeaseCheck {
2147            work_id: &work_id,
2148            lease_id: &lease_id,
2149            attempt,
2150            worker_id: &worker_id,
2151            command_id: Some(command_id.clone()),
2152            now_ms,
2153        },
2154    ) {
2155        return events;
2156    }
2157    let work = state.works.get_mut(&work_id).expect("checked lease");
2158    work.state = WorkQueueDerivedState::Completed;
2159    clear_lease(work);
2160    let attempt_record = append_record(
2161        state,
2162        WorkQueueRecord::AttemptCompleted {
2163            record_seq: 0,
2164            queue_id: opts.queue_id.clone(),
2165            work_id: work_id.clone(),
2166            command_id: command_id.clone(),
2167            lease_id: lease_id.clone(),
2168            attempt,
2169            worker_id: worker_id.clone(),
2170            result: result.clone(),
2171            recorded_at_ms: now_ms,
2172        },
2173    );
2174    let done = append_record(
2175        state,
2176        WorkQueueRecord::WorkCompleted {
2177            record_seq: 0,
2178            queue_id: opts.queue_id.clone(),
2179            work_id: work_id.clone(),
2180            command_id: command_id.clone(),
2181            lease_id,
2182            attempt,
2183            worker_id,
2184            result,
2185            recorded_at_ms: now_ms,
2186        },
2187    );
2188    vec![
2189        QueueEvent::Record(attempt_record),
2190        QueueEvent::Record(done.clone()),
2191        status_event(
2192            opts,
2193            WorkQueueStatusKind::CommandAccepted,
2194            now_ms,
2195            StatusFields {
2196                work_id: Some(work_id),
2197                command_id: Some(command_id),
2198                record_seq: Some(done.record_seq()),
2199                ..StatusFields::default()
2200            },
2201        ),
2202    ]
2203}
2204
2205fn fail_work<T: Clone + 'static>(
2206    opts: &RuntimeOptions,
2207    state: &mut RuntimeState<T>,
2208    command: WorkQueueCommand<T>,
2209    now_ms: u64,
2210) -> Vec<QueueEvent<T>> {
2211    let WorkQueueCommand::Fail {
2212        command_id,
2213        work_id,
2214        lease_id,
2215        attempt,
2216        worker_id,
2217        error,
2218        retryable,
2219        ..
2220    } = command
2221    else {
2222        return Vec::new();
2223    };
2224    if let Err(events) = current_lease(
2225        opts,
2226        state,
2227        LeaseCheck {
2228            work_id: &work_id,
2229            lease_id: &lease_id,
2230            attempt,
2231            worker_id: &worker_id,
2232            command_id: Some(command_id.clone()),
2233            now_ms,
2234        },
2235    ) {
2236        return events;
2237    }
2238    let attempt_for_policy = state.works.get(&work_id).expect("checked lease").attempt;
2239    let should_retry = retryable.unwrap_or(true) && opts.retry.should_retry(attempt_for_policy);
2240    let retry_delay_ms = if should_retry {
2241        opts.retry
2242            .next_delay_ms(attempt_for_policy.saturating_add(1))
2243            .unwrap_or_default()
2244    } else {
2245        0
2246    };
2247    let retry_at_ms = if should_retry {
2248        match checked_timestamp(
2249            opts,
2250            Some(command_id.clone()),
2251            now_ms,
2252            retry_delay_ms,
2253            "retry delay overflows timestamp",
2254        ) {
2255            Ok(value) => value,
2256            Err(events) => return events,
2257        }
2258    } else {
2259        now_ms
2260    };
2261    let failed = append_record(
2262        state,
2263        WorkQueueRecord::AttemptFailed {
2264            record_seq: 0,
2265            queue_id: opts.queue_id.clone(),
2266            work_id: work_id.clone(),
2267            command_id: command_id.clone(),
2268            lease_id,
2269            attempt,
2270            worker_id,
2271            error,
2272            retryable,
2273            recorded_at_ms: now_ms,
2274        },
2275    );
2276    let work = state.works.get_mut(&work_id).expect("checked lease");
2277    if !should_retry {
2278        work.state = WorkQueueDerivedState::DeadLettered;
2279        clear_lease(work);
2280        let dead = append_record(
2281            state,
2282            WorkQueueRecord::WorkDeadLettered {
2283                record_seq: 0,
2284                queue_id: opts.queue_id.clone(),
2285                work_id: work_id.clone(),
2286                command_id: command_id.clone(),
2287                reason: "attempts-exhausted".to_owned(),
2288                exhausted_attempts: Some(attempt),
2289                recorded_at_ms: now_ms,
2290            },
2291        );
2292        state.dead_letters.push(dead.clone());
2293        return vec![
2294            QueueEvent::Record(failed),
2295            QueueEvent::Record(dead.clone()),
2296            status_event(
2297                opts,
2298                WorkQueueStatusKind::CommandAccepted,
2299                now_ms,
2300                StatusFields {
2301                    work_id: Some(work_id),
2302                    command_id: Some(command_id),
2303                    record_seq: Some(dead.record_seq()),
2304                    ..StatusFields::default()
2305                },
2306            ),
2307        ];
2308    }
2309    work.state = if retry_delay_ms > 0 {
2310        WorkQueueDerivedState::RetryWait
2311    } else {
2312        WorkQueueDerivedState::Ready
2313    };
2314    work.retry_at_ms = Some(retry_at_ms);
2315    clear_lease(work);
2316    let retry = append_record(
2317        state,
2318        WorkQueueRecord::RetryScheduled {
2319            record_seq: 0,
2320            queue_id: opts.queue_id.clone(),
2321            work_id: work_id.clone(),
2322            command_id: command_id.clone(),
2323            retry_at_ms,
2324            delay_ms: retry_delay_ms,
2325            reason: Some("retry-policy".to_owned()),
2326            recorded_at_ms: now_ms,
2327        },
2328    );
2329    vec![
2330        QueueEvent::Record(failed),
2331        QueueEvent::Record(retry.clone()),
2332        status_event(
2333            opts,
2334            WorkQueueStatusKind::CommandAccepted,
2335            now_ms,
2336            StatusFields {
2337                work_id: Some(work_id),
2338                command_id: Some(command_id),
2339                record_seq: Some(retry.record_seq()),
2340                ..StatusFields::default()
2341            },
2342        ),
2343    ]
2344}
2345
2346fn cancel_work<T: Clone + 'static>(
2347    opts: &RuntimeOptions,
2348    state: &mut RuntimeState<T>,
2349    command: WorkQueueCommand<T>,
2350    now_ms: u64,
2351) -> Vec<QueueEvent<T>> {
2352    let WorkQueueCommand::Cancel {
2353        command_id,
2354        work_id,
2355        reason,
2356        ..
2357    } = command
2358    else {
2359        return Vec::new();
2360    };
2361    let Some(work) = state.works.get_mut(&work_id) else {
2362        return reject_queue_command_by_id(
2363            opts,
2364            Some(command_id),
2365            "unknown-work",
2366            "unknown work",
2367            now_ms,
2368        );
2369    };
2370    if is_terminal(work.state) {
2371        return reject_queue_command_by_id(
2372            opts,
2373            Some(command_id),
2374            "terminal-work",
2375            "work is terminal",
2376            now_ms,
2377        );
2378    }
2379    let canceled_lease_id = work.lease_id.clone();
2380    let attempt = (work.attempt > 0).then_some(work.attempt);
2381    work.state = WorkQueueDerivedState::Canceled;
2382    clear_lease(work);
2383    let record = append_record(
2384        state,
2385        WorkQueueRecord::WorkCanceled {
2386            record_seq: 0,
2387            queue_id: opts.queue_id.clone(),
2388            work_id: work_id.clone(),
2389            command_id: command_id.clone(),
2390            reason,
2391            canceled_at_ms: now_ms,
2392            canceled_lease_id,
2393            attempt,
2394        },
2395    );
2396    accepted(opts, record, Some(work_id), Some(command_id), now_ms)
2397}
2398
2399fn schedule_work<T: Clone + 'static>(
2400    opts: &RuntimeOptions,
2401    state: &mut RuntimeState<T>,
2402    command: WorkQueueCommand<T>,
2403    now_ms: u64,
2404) -> Vec<QueueEvent<T>> {
2405    let WorkQueueCommand::Schedule {
2406        command_id,
2407        work_id,
2408        schedule_id,
2409        not_before_ms,
2410        deadline_ms,
2411        reason,
2412        ..
2413    } = command
2414    else {
2415        return Vec::new();
2416    };
2417    let Some(work) = state.works.get_mut(&work_id) else {
2418        return reject_queue_command_by_id(
2419            opts,
2420            Some(command_id),
2421            "unknown-work",
2422            "unknown work",
2423            now_ms,
2424        );
2425    };
2426    if is_terminal(work.state) {
2427        return reject_queue_command_by_id(
2428            opts,
2429            Some(command_id),
2430            "terminal-work",
2431            "work is terminal",
2432            now_ms,
2433        );
2434    }
2435    if work.state == WorkQueueDerivedState::Leased {
2436        return reject_queue_command_by_id(
2437            opts,
2438            Some(command_id),
2439            "schedule-conflict",
2440            "leased work cannot be scheduled without release or cancel",
2441            now_ms,
2442        );
2443    }
2444    work.state = WorkQueueDerivedState::Scheduled;
2445    work.not_before_ms = Some(not_before_ms);
2446    work.deadline_ms = deadline_ms;
2447    let record = append_record(
2448        state,
2449        WorkQueueRecord::WorkScheduled {
2450            record_seq: 0,
2451            queue_id: opts.queue_id.clone(),
2452            work_id: work_id.clone(),
2453            command_id: command_id.clone(),
2454            schedule_id,
2455            not_before_ms,
2456            deadline_ms,
2457            reason,
2458            recorded_at_ms: now_ms,
2459        },
2460    );
2461    accepted(opts, record, Some(work_id), Some(command_id), now_ms)
2462}
2463
2464fn expire_leases<T: Clone + 'static>(
2465    opts: &RuntimeOptions,
2466    state: &mut RuntimeState<T>,
2467    command: WorkQueueCommand<T>,
2468    now_ms: u64,
2469) -> Vec<QueueEvent<T>> {
2470    let WorkQueueCommand::ExpireLeases {
2471        command_id,
2472        work_ids,
2473        limit,
2474        ..
2475    } = command
2476    else {
2477        return Vec::new();
2478    };
2479    let requested = work_ids.into_iter().collect::<HashSet<_>>();
2480    let expired = state
2481        .works
2482        .values()
2483        .filter(|work| is_expired(work, now_ms))
2484        .filter(|work| requested.is_empty() || requested.contains(&work.work_id))
2485        .take(positive_limit(limit.unwrap_or(usize::MAX)))
2486        .map(|work| work.work_id.clone())
2487        .collect::<Vec<_>>();
2488    if expired.is_empty() {
2489        return vec![status_event(
2490            opts,
2491            WorkQueueStatusKind::MaintenanceNoop,
2492            now_ms,
2493            StatusFields {
2494                command_id: Some(command_id),
2495                ..StatusFields::default()
2496            },
2497        )];
2498    }
2499    let mut events = Vec::new();
2500    for work_id in expired.iter() {
2501        if let Some(work) = state.works.get(work_id).cloned() {
2502            events.extend(materialize_lease_expired(
2503                opts,
2504                state,
2505                work,
2506                Some(command_id.clone()),
2507                now_ms,
2508                None,
2509            ));
2510        }
2511    }
2512    events.push(status_event(
2513        opts,
2514        WorkQueueStatusKind::MaintenanceApplied,
2515        now_ms,
2516        StatusFields {
2517            command_id: Some(command_id),
2518            details: Some(format!("expired={}", expired.len())),
2519            ..StatusFields::default()
2520        },
2521    ));
2522    events
2523}
2524
2525struct LeaseCheck<'a> {
2526    work_id: &'a str,
2527    lease_id: &'a str,
2528    attempt: u32,
2529    worker_id: &'a str,
2530    command_id: Option<String>,
2531    now_ms: u64,
2532}
2533
2534fn current_lease<T: Clone + 'static>(
2535    opts: &RuntimeOptions,
2536    state: &mut RuntimeState<T>,
2537    check: LeaseCheck<'_>,
2538) -> Result<(), Vec<QueueEvent<T>>> {
2539    let Some(work) = state.works.get(check.work_id).cloned() else {
2540        return Err(reject_queue_command_by_id(
2541            opts,
2542            check.command_id,
2543            "unknown-work",
2544            "unknown work",
2545            check.now_ms,
2546        ));
2547    };
2548    if is_terminal(work.state) {
2549        return Err(reject_queue_command_by_id(
2550            opts,
2551            check.command_id,
2552            "terminal-work",
2553            "work is terminal",
2554            check.now_ms,
2555        ));
2556    }
2557    if work.state != WorkQueueDerivedState::Leased {
2558        return Err(reject_queue_command_by_id(
2559            opts,
2560            check.command_id,
2561            "lease-not-current",
2562            "work is not leased",
2563            check.now_ms,
2564        ));
2565    }
2566    if work.lease_id.as_deref() != Some(check.lease_id) {
2567        return Err(reject_queue_command_by_id(
2568            opts,
2569            check.command_id,
2570            "stale-lease",
2571            "lease is not current",
2572            check.now_ms,
2573        ));
2574    }
2575    if work.attempt != check.attempt {
2576        return Err(reject_queue_command_by_id(
2577            opts,
2578            check.command_id,
2579            "attempt-mismatch",
2580            "attempt mismatch",
2581            check.now_ms,
2582        ));
2583    }
2584    if work.worker_id.as_deref() != Some(check.worker_id) {
2585        return Err(reject_queue_command_by_id(
2586            opts,
2587            check.command_id,
2588            "worker-mismatch",
2589            "worker mismatch",
2590            check.now_ms,
2591        ));
2592    }
2593    if is_expired(&work, check.now_ms) {
2594        let command_id = check.command_id;
2595        let mut events =
2596            materialize_lease_expired(opts, state, work, command_id.clone(), check.now_ms, None);
2597        events.extend(reject_queue_command_by_id(
2598            opts,
2599            command_id,
2600            "lease-expired",
2601            "lease expired",
2602            check.now_ms,
2603        ));
2604        return Err(events);
2605    }
2606    Ok(())
2607}
2608
2609fn materialize_lease_expired<T: Clone + 'static>(
2610    opts: &RuntimeOptions,
2611    state: &mut RuntimeState<T>,
2612    work: WorkState<T>,
2613    command_id: Option<String>,
2614    now_ms: u64,
2615    rejection: Option<(&str, &str)>,
2616) -> Vec<QueueEvent<T>> {
2617    let Some(current) = state.works.get_mut(&work.work_id) else {
2618        return Vec::new();
2619    };
2620    current.state = WorkQueueDerivedState::Ready;
2621    clear_lease(current);
2622    let record = append_record(
2623        state,
2624        WorkQueueRecord::LeaseExpired {
2625            record_seq: 0,
2626            queue_id: opts.queue_id.clone(),
2627            work_id: work.work_id.clone(),
2628            command_id: command_id.clone(),
2629            lease_id: work.lease_id.unwrap_or_default(),
2630            attempt: work.attempt,
2631            worker_id: work.worker_id.unwrap_or_default(),
2632            lease_expires_at_ms: work.lease_expires_at_ms.unwrap_or(now_ms),
2633            expired_at_ms: now_ms,
2634        },
2635    );
2636    let mut events = vec![QueueEvent::Record(record)];
2637    if let Some((code, message)) = rejection {
2638        events.extend(reject_queue_command_by_id(
2639            opts, command_id, code, message, now_ms,
2640        ));
2641    }
2642    events
2643}
2644
2645fn accepted<T: Clone>(
2646    opts: &RuntimeOptions,
2647    record: WorkQueueRecord<T>,
2648    work_id: Option<String>,
2649    command_id: Option<String>,
2650    now_ms: u64,
2651) -> Vec<QueueEvent<T>> {
2652    vec![
2653        QueueEvent::Record(record.clone()),
2654        status_event(
2655            opts,
2656            WorkQueueStatusKind::CommandAccepted,
2657            now_ms,
2658            StatusFields {
2659                work_id,
2660                command_id,
2661                record_seq: Some(record.record_seq()),
2662                ..StatusFields::default()
2663            },
2664        ),
2665    ]
2666}
2667
2668fn validate_queue_command<T>(
2669    queue_id: &str,
2670    command: &WorkQueueCommand<T>,
2671) -> Option<(String, String)> {
2672    if command.command_id().is_empty() {
2673        return Some((
2674            "malformed-command".to_owned(),
2675            "commandId must be non-empty".to_owned(),
2676        ));
2677    }
2678    if command.queue_id().is_some_and(|id| id != queue_id) {
2679        return Some((
2680            "queue-mismatch".to_owned(),
2681            "command queueId does not match this queue".to_owned(),
2682        ));
2683    }
2684    match command {
2685        WorkQueueCommand::Claim {
2686            worker_id,
2687            requested_work_ids,
2688            limit,
2689            lease_duration_ms,
2690            ..
2691        } => {
2692            if worker_id.is_empty() {
2693                return Some((
2694                    "malformed-command".to_owned(),
2695                    "claim workerId is required".to_owned(),
2696                ));
2697            }
2698            if limit.is_some_and(|limit| limit == 0)
2699                || lease_duration_ms.is_some_and(|duration| duration == 0)
2700                || requested_work_ids.iter().any(String::is_empty)
2701            {
2702                return Some((
2703                    "malformed-command".to_owned(),
2704                    "claim options are malformed".to_owned(),
2705                ));
2706            }
2707        }
2708        WorkQueueCommand::RenewLease {
2709            work_id,
2710            lease_id,
2711            attempt,
2712            worker_id,
2713            lease_duration_ms,
2714            ..
2715        } => {
2716            if work_id.is_empty()
2717                || lease_id.is_empty()
2718                || worker_id.is_empty()
2719                || *attempt == 0
2720                || lease_duration_ms.is_some_and(|duration| duration == 0)
2721            {
2722                return Some((
2723                    "malformed-command".to_owned(),
2724                    "lease command is malformed".to_owned(),
2725                ));
2726            }
2727        }
2728        WorkQueueCommand::Release {
2729            work_id,
2730            lease_id,
2731            attempt,
2732            worker_id,
2733            ..
2734        }
2735        | WorkQueueCommand::Complete {
2736            work_id,
2737            lease_id,
2738            attempt,
2739            worker_id,
2740            ..
2741        }
2742        | WorkQueueCommand::Fail {
2743            work_id,
2744            lease_id,
2745            attempt,
2746            worker_id,
2747            ..
2748        } => {
2749            if work_id.is_empty() || lease_id.is_empty() || worker_id.is_empty() || *attempt == 0 {
2750                return Some((
2751                    "malformed-command".to_owned(),
2752                    "lease command is malformed".to_owned(),
2753                ));
2754            }
2755        }
2756        WorkQueueCommand::Cancel { work_id, .. } | WorkQueueCommand::Schedule { work_id, .. } => {
2757            if work_id.is_empty() {
2758                return Some((
2759                    "malformed-command".to_owned(),
2760                    "workId must be non-empty".to_owned(),
2761                ));
2762            }
2763        }
2764        WorkQueueCommand::ExpireLeases {
2765            work_ids, limit, ..
2766        } => {
2767            if limit.is_some_and(|limit| limit == 0) || work_ids.iter().any(String::is_empty) {
2768                return Some((
2769                    "malformed-command".to_owned(),
2770                    "expire-leases options are malformed".to_owned(),
2771                ));
2772            }
2773        }
2774        WorkQueueCommand::Submit { .. } => {}
2775    }
2776    None
2777}
2778
2779fn reject_queue_command<T: Clone + 'static>(
2780    opts: &RuntimeOptions,
2781    command: &WorkQueueCommand<T>,
2782    code: &str,
2783    message: &str,
2784    now_ms: u64,
2785) -> Vec<QueueEvent<T>> {
2786    reject_queue_command_by_id(
2787        opts,
2788        Some(command.command_id().to_owned()),
2789        code,
2790        message,
2791        now_ms,
2792    )
2793}
2794
2795fn reject_queue_command_by_id<T>(
2796    opts: &RuntimeOptions,
2797    command_id: Option<String>,
2798    code: &str,
2799    message: &str,
2800    now_ms: u64,
2801) -> Vec<QueueEvent<T>> {
2802    vec![
2803        QueueEvent::Issue(DataIssue {
2804            kind: "issue".to_owned(),
2805            code: code.to_owned(),
2806            message: message.to_owned(),
2807            severity: "error".to_owned(),
2808            source: "workQueue".to_owned(),
2809            topic: None,
2810            details: Some(format!("queueId={}", opts.queue_id)),
2811        }),
2812        status_event(
2813            opts,
2814            WorkQueueStatusKind::CommandRejected,
2815            now_ms,
2816            StatusFields {
2817                command_id,
2818                issue_code: Some(code.to_owned()),
2819                ..StatusFields::default()
2820            },
2821        ),
2822    ]
2823}
2824
2825fn checked_timestamp<T>(
2826    opts: &RuntimeOptions,
2827    command_id: Option<String>,
2828    now_ms: u64,
2829    delta_ms: u64,
2830    message: &str,
2831) -> Result<u64, Vec<QueueEvent<T>>> {
2832    now_ms.checked_add(delta_ms).ok_or_else(|| {
2833        reject_queue_command_by_id(opts, command_id, "clock-overflow", message, now_ms)
2834    })
2835}
2836
2837#[derive(Default)]
2838struct StatusFields {
2839    work_id: Option<String>,
2840    command_id: Option<String>,
2841    record_seq: Option<u64>,
2842    as_of_record_seq: Option<u64>,
2843    issue_code: Option<String>,
2844    details: Option<String>,
2845}
2846
2847fn status_event<T>(
2848    opts: &RuntimeOptions,
2849    kind: WorkQueueStatusKind,
2850    timestamp_ms: u64,
2851    fields: StatusFields,
2852) -> QueueEvent<T> {
2853    QueueEvent::Status(WorkQueueStatus {
2854        kind,
2855        queue_id: opts.queue_id.clone(),
2856        work_id: fields.work_id,
2857        command_id: fields.command_id,
2858        record_seq: fields.record_seq,
2859        as_of_record_seq: fields.as_of_record_seq,
2860        issue_code: fields.issue_code,
2861        timestamp_ms,
2862        details: fields.details,
2863    })
2864}
2865
2866fn append_record<T: Clone>(
2867    state: &mut RuntimeState<T>,
2868    record: WorkQueueRecord<T>,
2869) -> WorkQueueRecord<T> {
2870    state.record_seq += 1;
2871    let record = with_record_seq(record, state.record_seq);
2872    state.records.push(record.clone());
2873    record
2874}
2875
2876fn with_record_seq<T>(record: WorkQueueRecord<T>, next: u64) -> WorkQueueRecord<T> {
2877    match record {
2878        WorkQueueRecord::WorkAdmitted {
2879            queue_id,
2880            work_id,
2881            payload,
2882            message_bus,
2883            priority,
2884            tags,
2885            requirements,
2886            not_before_ms,
2887            deadline_ms,
2888            recorded_at_ms,
2889            ..
2890        } => WorkQueueRecord::WorkAdmitted {
2891            record_seq: next,
2892            queue_id,
2893            work_id,
2894            payload,
2895            message_bus,
2896            priority,
2897            tags,
2898            requirements,
2899            not_before_ms,
2900            deadline_ms,
2901            recorded_at_ms,
2902        },
2903        WorkQueueRecord::AdmissionDeduped {
2904            queue_id,
2905            work_id,
2906            message_bus,
2907            reason,
2908            existing_work_id,
2909            recorded_at_ms,
2910            ..
2911        } => WorkQueueRecord::AdmissionDeduped {
2912            record_seq: next,
2913            queue_id,
2914            work_id,
2915            message_bus,
2916            reason,
2917            existing_work_id,
2918            recorded_at_ms,
2919        },
2920        WorkQueueRecord::WorkScheduled {
2921            queue_id,
2922            work_id,
2923            command_id,
2924            schedule_id,
2925            not_before_ms,
2926            deadline_ms,
2927            reason,
2928            recorded_at_ms,
2929            ..
2930        } => WorkQueueRecord::WorkScheduled {
2931            record_seq: next,
2932            queue_id,
2933            work_id,
2934            command_id,
2935            schedule_id,
2936            not_before_ms,
2937            deadline_ms,
2938            reason,
2939            recorded_at_ms,
2940        },
2941        WorkQueueRecord::WorkClaimed {
2942            queue_id,
2943            work_id,
2944            command_id,
2945            lease_id,
2946            attempt,
2947            worker_id,
2948            claimed_at_ms,
2949            lease_expires_at_ms,
2950            ..
2951        } => WorkQueueRecord::WorkClaimed {
2952            record_seq: next,
2953            queue_id,
2954            work_id,
2955            command_id,
2956            lease_id,
2957            attempt,
2958            worker_id,
2959            claimed_at_ms,
2960            lease_expires_at_ms,
2961        },
2962        WorkQueueRecord::LeaseRenewed {
2963            queue_id,
2964            work_id,
2965            command_id,
2966            lease_id,
2967            attempt,
2968            worker_id,
2969            previous_lease_expires_at_ms,
2970            lease_expires_at_ms,
2971            renewed_at_ms,
2972            ..
2973        } => WorkQueueRecord::LeaseRenewed {
2974            record_seq: next,
2975            queue_id,
2976            work_id,
2977            command_id,
2978            lease_id,
2979            attempt,
2980            worker_id,
2981            previous_lease_expires_at_ms,
2982            lease_expires_at_ms,
2983            renewed_at_ms,
2984        },
2985        WorkQueueRecord::WorkReleased {
2986            queue_id,
2987            work_id,
2988            command_id,
2989            lease_id,
2990            attempt,
2991            worker_id,
2992            released_at_ms,
2993            reason,
2994            ..
2995        } => WorkQueueRecord::WorkReleased {
2996            record_seq: next,
2997            queue_id,
2998            work_id,
2999            command_id,
3000            lease_id,
3001            attempt,
3002            worker_id,
3003            released_at_ms,
3004            reason,
3005        },
3006        WorkQueueRecord::LeaseExpired {
3007            queue_id,
3008            work_id,
3009            command_id,
3010            lease_id,
3011            attempt,
3012            worker_id,
3013            lease_expires_at_ms,
3014            expired_at_ms,
3015            ..
3016        } => WorkQueueRecord::LeaseExpired {
3017            record_seq: next,
3018            queue_id,
3019            work_id,
3020            command_id,
3021            lease_id,
3022            attempt,
3023            worker_id,
3024            lease_expires_at_ms,
3025            expired_at_ms,
3026        },
3027        WorkQueueRecord::AttemptCompleted {
3028            queue_id,
3029            work_id,
3030            command_id,
3031            lease_id,
3032            attempt,
3033            worker_id,
3034            result,
3035            recorded_at_ms,
3036            ..
3037        } => WorkQueueRecord::AttemptCompleted {
3038            record_seq: next,
3039            queue_id,
3040            work_id,
3041            command_id,
3042            lease_id,
3043            attempt,
3044            worker_id,
3045            result,
3046            recorded_at_ms,
3047        },
3048        WorkQueueRecord::WorkCompleted {
3049            queue_id,
3050            work_id,
3051            command_id,
3052            lease_id,
3053            attempt,
3054            worker_id,
3055            result,
3056            recorded_at_ms,
3057            ..
3058        } => WorkQueueRecord::WorkCompleted {
3059            record_seq: next,
3060            queue_id,
3061            work_id,
3062            command_id,
3063            lease_id,
3064            attempt,
3065            worker_id,
3066            result,
3067            recorded_at_ms,
3068        },
3069        WorkQueueRecord::AttemptFailed {
3070            queue_id,
3071            work_id,
3072            command_id,
3073            lease_id,
3074            attempt,
3075            worker_id,
3076            error,
3077            retryable,
3078            recorded_at_ms,
3079            ..
3080        } => WorkQueueRecord::AttemptFailed {
3081            record_seq: next,
3082            queue_id,
3083            work_id,
3084            command_id,
3085            lease_id,
3086            attempt,
3087            worker_id,
3088            error,
3089            retryable,
3090            recorded_at_ms,
3091        },
3092        WorkQueueRecord::RetryScheduled {
3093            queue_id,
3094            work_id,
3095            command_id,
3096            retry_at_ms,
3097            delay_ms,
3098            reason,
3099            recorded_at_ms,
3100            ..
3101        } => WorkQueueRecord::RetryScheduled {
3102            record_seq: next,
3103            queue_id,
3104            work_id,
3105            command_id,
3106            retry_at_ms,
3107            delay_ms,
3108            reason,
3109            recorded_at_ms,
3110        },
3111        WorkQueueRecord::WorkDeadLettered {
3112            queue_id,
3113            work_id,
3114            command_id,
3115            reason,
3116            exhausted_attempts,
3117            recorded_at_ms,
3118            ..
3119        } => WorkQueueRecord::WorkDeadLettered {
3120            record_seq: next,
3121            queue_id,
3122            work_id,
3123            command_id,
3124            reason,
3125            exhausted_attempts,
3126            recorded_at_ms,
3127        },
3128        WorkQueueRecord::WorkCanceled {
3129            queue_id,
3130            work_id,
3131            command_id,
3132            reason,
3133            canceled_at_ms,
3134            canceled_lease_id,
3135            attempt,
3136            ..
3137        } => WorkQueueRecord::WorkCanceled {
3138            record_seq: next,
3139            queue_id,
3140            work_id,
3141            command_id,
3142            reason,
3143            canceled_at_ms,
3144            canceled_lease_id,
3145            attempt,
3146        },
3147    }
3148}
3149
3150fn available_page<T: Clone>(
3151    state: &RuntimeState<T>,
3152    params: &WorkQueueAvailableParams,
3153) -> WorkQueueAvailablePage<T> {
3154    let limit = positive_limit(params.limit.unwrap_or(100));
3155    let mut items = state
3156        .works
3157        .values()
3158        .filter(|work| is_ready_for_projection(work, params.now_ms))
3159        .filter(
3160            |work| match (params.after_admission_seq, params.after_work_id.as_ref()) {
3161                (Some(after_seq), Some(after_id)) => {
3162                    (work.admission_seq, &work.work_id) > (after_seq, after_id)
3163                }
3164                (Some(after_seq), None) => work.admission_seq > after_seq,
3165                (None, Some(after_id)) => &work.work_id > after_id,
3166                (None, None) => true,
3167            },
3168        )
3169        .map(|work| WorkQueueAvailableItem {
3170            work_id: work.work_id.clone(),
3171            state: work.state,
3172            payload: work.payload.clone(),
3173            admission_seq: work.admission_seq,
3174            priority: work.priority,
3175            tags: work.tags.clone(),
3176            requirements: work.requirements.clone(),
3177            not_before_ms: work.not_before_ms,
3178            retry_at_ms: work.retry_at_ms,
3179            deadline_ms: work.deadline_ms,
3180        })
3181        .collect::<Vec<_>>();
3182    items.sort_by(|a, b| {
3183        a.admission_seq
3184            .cmp(&b.admission_seq)
3185            .then_with(|| a.work_id.cmp(&b.work_id))
3186    });
3187    let has_more = items.len() > limit;
3188    let page = items.into_iter().take(limit).collect::<Vec<_>>();
3189    let next_after_work_id = has_more
3190        .then(|| page.last().map(|item| item.work_id.clone()))
3191        .flatten();
3192    let next_after_admission_seq = has_more
3193        .then(|| page.last().map(|item| item.admission_seq))
3194        .flatten();
3195    WorkQueueAvailablePage {
3196        items: page,
3197        next_after_work_id,
3198        next_after_admission_seq,
3199        has_more,
3200        as_of_record_seq: state.record_seq,
3201    }
3202}
3203
3204fn work_snapshot<T: Clone>(state: &RuntimeState<T>, work_id: &str) -> WorkQueueWorkSnapshot<T> {
3205    let work = state.works.get(work_id);
3206    WorkQueueWorkSnapshot {
3207        work_id: work_id.to_owned(),
3208        state: work.map(|work| work.state),
3209        payload: work.map(|work| work.payload.clone()),
3210        active_lease: work.and_then(|work| {
3211            if work.state != WorkQueueDerivedState::Leased {
3212                return None;
3213            }
3214            Some(WorkQueueActiveLease {
3215                lease_id: work.lease_id.clone()?,
3216                attempt: work.attempt,
3217                worker_id: work.worker_id.clone()?,
3218                lease_expires_at_ms: work.lease_expires_at_ms?,
3219            })
3220        }),
3221        records: state
3222            .records
3223            .iter()
3224            .filter(|record| record.work_id() == work_id)
3225            .cloned()
3226            .collect(),
3227        as_of_record_seq: state.record_seq,
3228    }
3229}
3230
3231fn dead_letter_page<T: Clone>(
3232    state: &RuntimeState<T>,
3233    params: &WorkQueueDeadLetterParams,
3234) -> WorkQueueDeadLetterPage<T> {
3235    let limit = positive_limit(params.limit.unwrap_or(100));
3236    let entries = state
3237        .dead_letters
3238        .iter()
3239        .filter(|record| {
3240            params
3241                .after_dead_letter_seq
3242                .is_none_or(|after| record.record_seq() > after)
3243                && params
3244                    .after_work_id
3245                    .as_ref()
3246                    .is_none_or(|after| record.work_id() > after.as_str())
3247        })
3248        .cloned()
3249        .collect::<Vec<_>>();
3250    let has_more = entries.len() > limit;
3251    let page = entries.into_iter().take(limit).collect::<Vec<_>>();
3252    let next_after_dead_letter_seq = has_more
3253        .then(|| page.last().map(WorkQueueRecord::record_seq))
3254        .flatten();
3255    WorkQueueDeadLetterPage {
3256        entries: page,
3257        next_after_dead_letter_seq,
3258        has_more,
3259        as_of_record_seq: state.record_seq,
3260    }
3261}
3262
3263fn clear_lease<T>(work: &mut WorkState<T>) {
3264    work.lease_id = None;
3265    work.worker_id = None;
3266    work.lease_expires_at_ms = None;
3267}
3268
3269fn is_ready<T>(work: &WorkState<T>, now_ms: u64) -> bool {
3270    match work.state {
3271        WorkQueueDerivedState::Scheduled => work.not_before_ms.is_some_and(|t| t <= now_ms),
3272        WorkQueueDerivedState::RetryWait => work.retry_at_ms.is_some_and(|t| t <= now_ms),
3273        WorkQueueDerivedState::Ready => true,
3274        _ => false,
3275    }
3276}
3277
3278fn is_ready_for_projection<T>(work: &WorkState<T>, now_ms: Option<u64>) -> bool {
3279    match work.state {
3280        WorkQueueDerivedState::Scheduled | WorkQueueDerivedState::RetryWait => {
3281            now_ms.is_some_and(|now| is_ready(work, now))
3282        }
3283        WorkQueueDerivedState::Ready => true,
3284        _ => false,
3285    }
3286}
3287
3288fn is_expired<T>(work: &WorkState<T>, now_ms: u64) -> bool {
3289    work.state == WorkQueueDerivedState::Leased
3290        && work
3291            .lease_expires_at_ms
3292            .is_some_and(|expires| expires <= now_ms)
3293}
3294
3295fn is_terminal(state: WorkQueueDerivedState) -> bool {
3296    matches!(
3297        state,
3298        WorkQueueDerivedState::Completed
3299            | WorkQueueDerivedState::Canceled
3300            | WorkQueueDerivedState::DeadLettered
3301    )
3302}
3303
3304fn record_queue_id<T>(record: &WorkQueueRecord<T>) -> &str {
3305    match record {
3306        WorkQueueRecord::WorkAdmitted { queue_id, .. }
3307        | WorkQueueRecord::AdmissionDeduped { queue_id, .. }
3308        | WorkQueueRecord::WorkScheduled { queue_id, .. }
3309        | WorkQueueRecord::WorkClaimed { queue_id, .. }
3310        | WorkQueueRecord::LeaseRenewed { queue_id, .. }
3311        | WorkQueueRecord::WorkReleased { queue_id, .. }
3312        | WorkQueueRecord::LeaseExpired { queue_id, .. }
3313        | WorkQueueRecord::AttemptCompleted { queue_id, .. }
3314        | WorkQueueRecord::WorkCompleted { queue_id, .. }
3315        | WorkQueueRecord::AttemptFailed { queue_id, .. }
3316        | WorkQueueRecord::RetryScheduled { queue_id, .. }
3317        | WorkQueueRecord::WorkDeadLettered { queue_id, .. }
3318        | WorkQueueRecord::WorkCanceled { queue_id, .. } => queue_id,
3319    }
3320}
3321
3322fn pull_params<T: Clone + Default + 'static>(ctx: &Ctx) -> T {
3323    ctx.pull()
3324        .and_then(|pull| pull.params::<T>())
3325        .map(|params| (*params).clone())
3326        .unwrap_or_default()
3327}
3328
3329fn timestamp_or_zero(now: &Rc<dyn Fn() -> u64>) -> u64 {
3330    catch_unwind(AssertUnwindSafe(|| now())).unwrap_or(0)
3331}
3332
3333fn positive_limit(limit: usize) -> usize {
3334    assert!(limit > 0, "workQueue: limit must be positive");
3335    limit
3336}
3337
3338fn assert_non_empty(value: &str, owner: &str) {
3339    assert!(!value.is_empty(), "{owner}: must be a non-empty string");
3340}
3341
3342fn node_opts(name: impl Into<String>, factory: impl Into<String>) -> GraphNodeOpts {
3343    let mut opts = GraphNodeOpts::named(name);
3344    opts.node = NodeOpts {
3345        factory: Some(factory.into()),
3346        complete_when_deps_complete: false,
3347        error_when_deps_error: false,
3348        ..opts.node
3349    };
3350    opts
3351}
3352
3353fn pull_node_opts(
3354    name: impl Into<String>,
3355    factory: impl Into<String>,
3356    pull_id: LockId,
3357) -> GraphNodeOpts {
3358    let mut opts = node_opts(name, factory);
3359    opts.node.pull_id = Some(pull_id);
3360    opts.node.partial = true;
3361    opts
3362}
3363
3364fn _assert_no_process_or_worker_deps(_: Option<Core>) {}