1use std::cell::RefCell;
9use std::collections::{BTreeMap, BTreeSet};
10use std::rc::Rc;
11
12use crate::ctx::Ctx;
13use crate::graph::{Graph, GraphNodeOpts};
14use crate::identity::{canonical_tuple_key, compound_tuple_key};
15use crate::json::JsonValue;
16use crate::messaging::DataIssue;
17use crate::node::{Node, NodeOpts};
18use crate::scheduled_readiness::{
19 readiness_issue, ScheduledReadinessAuditRecord, ScheduledReadinessOverdue,
20 ScheduledReadinessReady, ScheduledReadinessRequested, SourceRef,
21};
22use crate::work_queue::{WorkQueueCommand, WorkQueueDerivedState, WorkQueueRecord};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum WorkQueueReadinessScheduleKind {
27 AdmissionDelay,
29 WorkScheduled,
31 RetryScheduled,
33 LeaseExpiration,
35}
36
37impl WorkQueueReadinessScheduleKind {
38 fn as_str(self) -> &'static str {
39 match self {
40 Self::AdmissionDelay => "admission-delay",
41 Self::WorkScheduled => "work-scheduled",
42 Self::RetryScheduled => "retry-scheduled",
43 Self::LeaseExpiration => "lease-expiration",
44 }
45 }
46
47 fn from_str(value: &str) -> Option<Self> {
48 match value {
49 "admission-delay" => Some(Self::AdmissionDelay),
50 "work-scheduled" => Some(Self::WorkScheduled),
51 "retry-scheduled" => Some(Self::RetryScheduled),
52 "lease-expiration" => Some(Self::LeaseExpiration),
53 _ => None,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum WorkQueueReadinessStatusState {
61 Translated,
63 Candidate,
65 Ignored,
67 Overdue,
69 Issue,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum WorkQueueReadinessCandidateKind {
76 ClaimEligible,
78 LeaseExpirationEligible,
80}
81
82impl WorkQueueReadinessCandidateKind {
83 fn as_str(self) -> &'static str {
84 match self {
85 Self::ClaimEligible => "claim-eligible",
86 Self::LeaseExpirationEligible => "lease-expiration-eligible",
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct WorkQueueReadinessStatus {
94 pub status_id: String,
96 pub queue_id: String,
98 pub work_id: Option<String>,
100 pub schedule_id: Option<String>,
102 pub state: WorkQueueReadinessStatusState,
104 pub schedule_kind: Option<WorkQueueReadinessScheduleKind>,
106 pub ready_at_ms: Option<u64>,
108 pub now_ms: Option<u64>,
110 pub issue_code: Option<String>,
112 pub details: Option<String>,
114}
115
116#[derive(Debug, Clone, PartialEq)]
117pub struct WorkQueueReadinessCandidate {
119 pub candidate_id: String,
121 pub queue_id: String,
123 pub work_id: String,
125 pub schedule_id: String,
127 pub candidate_kind: WorkQueueReadinessCandidateKind,
129 pub schedule_kind: WorkQueueReadinessScheduleKind,
131 pub ready_at_ms: u64,
133 pub now_ms: u64,
135 pub lease_id: Option<String>,
137 pub attempt: Option<u32>,
139 pub worker_id: Option<String>,
141 pub source_refs: Vec<SourceRef>,
143}
144
145#[derive(Debug, Clone, PartialEq, Default)]
146pub struct WorkQueueReadinessViews {
148 pub schedules_by_id: BTreeMap<String, ScheduledReadinessRequested>,
150 pub candidates_by_id: BTreeMap<String, WorkQueueReadinessCandidate>,
152 pub status_by_id: BTreeMap<String, WorkQueueReadinessStatus>,
154}
155
156#[derive(Clone)]
157pub struct WorkQueueScheduledReadinessOptions<T> {
159 pub name: Option<String>,
161 pub records: Vec<Node<WorkQueueRecord<T>>>,
163}
164
165impl<T> WorkQueueScheduledReadinessOptions<T> {
166 pub fn new(records: Vec<Node<WorkQueueRecord<T>>>) -> Self {
168 Self {
169 name: None,
170 records,
171 }
172 }
173
174 pub fn named(mut self, name: impl Into<String>) -> Self {
176 self.name = Some(name.into());
177 self
178 }
179}
180
181#[derive(Clone)]
182pub struct WorkQueueReadinessHandoffOptions<T> {
184 pub name: Option<String>,
186 pub records: Vec<Node<WorkQueueRecord<T>>>,
188 pub ready: Vec<Node<ScheduledReadinessReady>>,
190 pub overdue: Vec<Node<ScheduledReadinessOverdue>>,
192}
193
194impl<T> WorkQueueReadinessHandoffOptions<T> {
195 pub fn new(
197 records: Vec<Node<WorkQueueRecord<T>>>,
198 ready: Vec<Node<ScheduledReadinessReady>>,
199 ) -> Self {
200 Self {
201 name: None,
202 records,
203 ready,
204 overdue: Vec::new(),
205 }
206 }
207
208 pub fn named(mut self, name: impl Into<String>) -> Self {
210 self.name = Some(name.into());
211 self
212 }
213
214 pub fn with_overdue(mut self, overdue: Vec<Node<ScheduledReadinessOverdue>>) -> Self {
216 self.overdue = overdue;
217 self
218 }
219}
220
221#[derive(Clone)]
222pub struct WorkQueueScheduledReadinessBundle {
224 pub readiness_schedules: Node<ScheduledReadinessRequested>,
226 pub status: Node<WorkQueueReadinessStatus>,
228 pub issues: Node<DataIssue>,
230 pub audit: Node<ScheduledReadinessAuditRecord>,
232 pub views: Node<WorkQueueReadinessViews>,
234}
235
236#[derive(Clone)]
237pub struct WorkQueueReadinessHandoffBundle {
239 pub candidates: Node<WorkQueueReadinessCandidate>,
241 pub status: Node<WorkQueueReadinessStatus>,
243 pub issues: Node<DataIssue>,
245 pub audit: Node<ScheduledReadinessAuditRecord>,
247 pub views: Node<WorkQueueReadinessViews>,
249}
250
251#[derive(Clone)]
252pub struct WorkQueueLeaseExpirationCommandProjectorOptions {
254 pub name: Option<String>,
256 pub candidates: Vec<Node<WorkQueueReadinessCandidate>>,
258 pub command_prefix: Option<String>,
260}
261
262impl WorkQueueLeaseExpirationCommandProjectorOptions {
263 pub fn new(candidates: Vec<Node<WorkQueueReadinessCandidate>>) -> Self {
265 Self {
266 name: None,
267 candidates,
268 command_prefix: None,
269 }
270 }
271
272 pub fn named(mut self, name: impl Into<String>) -> Self {
274 self.name = Some(name.into());
275 self
276 }
277
278 pub fn command_prefix(mut self, command_prefix: impl Into<String>) -> Self {
280 self.command_prefix = Some(command_prefix.into());
281 self
282 }
283}
284
285#[derive(Clone)]
286enum WorkQueueReadinessFact {
287 Schedule(ScheduledReadinessRequested),
288 Candidate(WorkQueueReadinessCandidate),
289 Status(WorkQueueReadinessStatus),
290 Issue(DataIssue),
291 Audit(ScheduledReadinessAuditRecord),
292 Views(WorkQueueReadinessViews),
293}
294
295#[derive(Default)]
296struct TranslatorState {
297 schedules_by_id: BTreeMap<String, ScheduledReadinessRequested>,
298 status_by_id: BTreeMap<String, WorkQueueReadinessStatus>,
299 emitted: BTreeSet<String>,
300 audit_seq: u64,
301}
302
303#[derive(Debug, Clone)]
304struct QueueWorkState {
305 queue_id: String,
306 state: WorkQueueDerivedState,
307 effective_ready_at_ms: Option<u64>,
308 effective_schedule_kind: Option<WorkQueueReadinessScheduleKind>,
309 lease_id: Option<String>,
310 attempt: Option<u32>,
311 worker_id: Option<String>,
312 lease_expires_at_ms: Option<u64>,
313}
314
315#[derive(Default)]
316struct HandoffState {
317 works_by_id: BTreeMap<(String, String), QueueWorkState>,
318 ready_by_id: BTreeMap<String, ScheduledReadinessReady>,
319 candidates_by_id: BTreeMap<String, WorkQueueReadinessCandidate>,
320 status_by_id: BTreeMap<String, WorkQueueReadinessStatus>,
321 emitted: BTreeSet<String>,
322 audit_seq: u64,
323}
324
325pub fn work_queue_scheduled_readiness_projector<T: Clone + 'static>(
327 graph: &Graph,
328 opts: WorkQueueScheduledReadinessOptions<T>,
329) -> WorkQueueScheduledReadinessBundle {
330 let name = opts
331 .name
332 .clone()
333 .unwrap_or_else(|| "workQueueScheduledReadiness".to_owned());
334 let deps = opts.records.iter().map(Node::erased).collect::<Vec<_>>();
335 let dep_count = opts.records.len();
336 let state = Rc::new(RefCell::new(TranslatorState::default()));
337 let runtime = graph.node_opts::<WorkQueueReadinessFact, _>(
338 deps,
339 {
340 let state = state.clone();
341 move |ctx| {
342 let mut state = state.borrow_mut();
343 for index in 0..dep_count {
344 for record in ctx.batch::<WorkQueueRecord<T>>(index) {
345 if let Some(schedule) = schedule_from_record(record.as_ref()) {
346 emit_translated_schedule(ctx, &mut state, schedule);
347 }
348 }
349 }
350 emit_fact(
351 ctx,
352 WorkQueueReadinessFact::Views(WorkQueueReadinessViews {
353 schedules_by_id: state.schedules_by_id.clone(),
354 candidates_by_id: BTreeMap::new(),
355 status_by_id: state.status_by_id.clone(),
356 }),
357 );
358 }
359 },
360 {
361 let mut node_opts = node_opts(
362 format!("{name}/runtime"),
363 "workQueueScheduledReadinessProjector",
364 );
365 node_opts.node.partial = true;
366 node_opts
367 },
368 );
369 WorkQueueScheduledReadinessBundle {
370 readiness_schedules: project_fact(
371 graph,
372 &runtime,
373 format!("{name}/schedules"),
374 "workQueueReadinessSchedules",
375 |fact| match fact {
376 WorkQueueReadinessFact::Schedule(value) => Some(value.clone()),
377 _ => None,
378 },
379 ),
380 status: project_fact(
381 graph,
382 &runtime,
383 format!("{name}/status"),
384 "workQueueReadinessStatus",
385 |fact| match fact {
386 WorkQueueReadinessFact::Status(value) => Some(value.clone()),
387 _ => None,
388 },
389 ),
390 issues: project_fact(
391 graph,
392 &runtime,
393 format!("{name}/issues"),
394 "workQueueReadinessIssues",
395 |fact| match fact {
396 WorkQueueReadinessFact::Issue(value) => Some(value.clone()),
397 _ => None,
398 },
399 ),
400 audit: project_fact(
401 graph,
402 &runtime,
403 format!("{name}/audit"),
404 "workQueueReadinessAudit",
405 |fact| match fact {
406 WorkQueueReadinessFact::Audit(value) => Some(value.clone()),
407 _ => None,
408 },
409 ),
410 views: project_fact(
411 graph,
412 &runtime,
413 format!("{name}/views"),
414 "workQueueReadinessViews",
415 |fact| match fact {
416 WorkQueueReadinessFact::Views(value) => Some(value.clone()),
417 _ => None,
418 },
419 ),
420 }
421}
422
423pub fn work_queue_readiness_handoff_projector<T: Clone + 'static>(
425 graph: &Graph,
426 opts: WorkQueueReadinessHandoffOptions<T>,
427) -> WorkQueueReadinessHandoffBundle {
428 let name = opts
429 .name
430 .clone()
431 .unwrap_or_else(|| "workQueueReadinessHandoff".to_owned());
432 let record_count = opts.records.len();
433 let ready_start = record_count;
434 let overdue_start = ready_start + opts.ready.len();
435 let mut deps = Vec::with_capacity(opts.records.len() + opts.ready.len() + opts.overdue.len());
436 deps.extend(opts.records.iter().map(Node::erased));
437 deps.extend(opts.ready.iter().map(Node::erased));
438 deps.extend(opts.overdue.iter().map(Node::erased));
439 let state = Rc::new(RefCell::new(HandoffState::default()));
440 let runtime = graph.node_opts::<WorkQueueReadinessFact, _>(
441 deps,
442 {
443 let state = state.clone();
444 move |ctx| {
445 let mut state = state.borrow_mut();
446 for index in 0..record_count {
447 for record in ctx.batch::<WorkQueueRecord<T>>(index) {
448 apply_record(&mut state, record.as_ref());
449 }
450 }
451 for ready in state.ready_by_id.values().cloned().collect::<Vec<_>>() {
452 handoff_ready_inner(ctx, &mut state, &ready);
453 }
454 for index in ready_start..overdue_start {
455 for ready in ctx.batch::<ScheduledReadinessReady>(index) {
456 handoff_ready(ctx, &mut state, ready.as_ref());
457 }
458 }
459 for index in overdue_start..(overdue_start + opts.overdue.len()) {
460 for overdue in ctx.batch::<ScheduledReadinessOverdue>(index) {
461 handoff_overdue(ctx, &mut state, overdue.as_ref());
462 }
463 }
464 emit_fact(
465 ctx,
466 WorkQueueReadinessFact::Views(WorkQueueReadinessViews {
467 schedules_by_id: BTreeMap::new(),
468 candidates_by_id: state.candidates_by_id.clone(),
469 status_by_id: state.status_by_id.clone(),
470 }),
471 );
472 }
473 },
474 {
475 let mut node_opts = node_opts(
476 format!("{name}/runtime"),
477 "workQueueReadinessHandoffProjector",
478 );
479 node_opts.node.partial = true;
480 node_opts
481 },
482 );
483 WorkQueueReadinessHandoffBundle {
484 candidates: project_fact(
485 graph,
486 &runtime,
487 format!("{name}/candidates"),
488 "workQueueReadinessCandidates",
489 |fact| match fact {
490 WorkQueueReadinessFact::Candidate(value) => Some(value.clone()),
491 _ => None,
492 },
493 ),
494 status: project_fact(
495 graph,
496 &runtime,
497 format!("{name}/status"),
498 "workQueueReadinessStatus",
499 |fact| match fact {
500 WorkQueueReadinessFact::Status(value) => Some(value.clone()),
501 _ => None,
502 },
503 ),
504 issues: project_fact(
505 graph,
506 &runtime,
507 format!("{name}/issues"),
508 "workQueueReadinessIssues",
509 |fact| match fact {
510 WorkQueueReadinessFact::Issue(value) => Some(value.clone()),
511 _ => None,
512 },
513 ),
514 audit: project_fact(
515 graph,
516 &runtime,
517 format!("{name}/audit"),
518 "workQueueReadinessAudit",
519 |fact| match fact {
520 WorkQueueReadinessFact::Audit(value) => Some(value.clone()),
521 _ => None,
522 },
523 ),
524 views: project_fact(
525 graph,
526 &runtime,
527 format!("{name}/views"),
528 "workQueueReadinessViews",
529 |fact| match fact {
530 WorkQueueReadinessFact::Views(value) => Some(value.clone()),
531 _ => None,
532 },
533 ),
534 }
535}
536
537pub fn work_queue_lease_expiration_command_projector<T: Clone + 'static>(
539 graph: &Graph,
540 opts: WorkQueueLeaseExpirationCommandProjectorOptions,
541) -> Node<WorkQueueCommand<T>> {
542 let name = opts
543 .name
544 .clone()
545 .unwrap_or_else(|| "workQueueLeaseExpirationCommands".to_owned());
546 let prefix = opts
547 .command_prefix
548 .clone()
549 .unwrap_or_else(|| "readiness-expire".to_owned());
550 let dep_count = opts.candidates.len();
551 graph.node_opts::<WorkQueueCommand<T>, _>(
552 opts.candidates.iter().map(Node::erased).collect(),
553 move |ctx| {
554 for index in 0..dep_count {
555 for candidate in ctx.batch::<WorkQueueReadinessCandidate>(index) {
556 if let Some(command) =
557 work_queue_lease_expiration_command::<T>(candidate.as_ref(), &prefix)
558 {
559 ctx.emit(command);
560 }
561 }
562 }
563 },
564 node_opts(name, "workQueueLeaseExpirationCommandProjector"),
565 )
566}
567
568pub fn work_queue_lease_expiration_command<T>(
570 candidate: &WorkQueueReadinessCandidate,
571 command_prefix: &str,
572) -> Option<WorkQueueCommand<T>> {
573 if candidate.candidate_kind != WorkQueueReadinessCandidateKind::LeaseExpirationEligible {
574 return None;
575 }
576 Some(WorkQueueCommand::ExpireLeases {
577 command_id: compound_tuple_key(command_prefix, &[&candidate.candidate_id]),
578 queue_id: Some(candidate.queue_id.clone()),
579 idempotency_key: Some(compound_tuple_key(
580 command_prefix,
581 &[&candidate.candidate_id],
582 )),
583 work_ids: vec![candidate.work_id.clone()],
584 limit: Some(1),
585 now_ms: Some(candidate.now_ms),
586 })
587}
588
589fn schedule_from_record<T>(record: &WorkQueueRecord<T>) -> Option<ScheduledReadinessRequested> {
590 match record {
591 WorkQueueRecord::WorkAdmitted {
592 record_seq,
593 queue_id,
594 work_id,
595 not_before_ms: Some(ready_at_ms),
596 deadline_ms,
597 ..
598 } => Some(readiness_schedule(ReadinessScheduleInput {
599 queue_id,
600 work_id,
601 kind: WorkQueueReadinessScheduleKind::AdmissionDelay,
602 schedule_id: compound_tuple_key(
603 "work-queue-admission-readiness",
604 &[queue_id, work_id, &record_seq.to_string()],
605 ),
606 ready_at_ms: *ready_at_ms,
607 deadline_ms: *deadline_ms,
608 record_seq: *record_seq,
609 lease: None,
610 })),
611 WorkQueueRecord::WorkScheduled {
612 record_seq,
613 queue_id,
614 work_id,
615 command_id,
616 schedule_id,
617 not_before_ms,
618 deadline_ms,
619 ..
620 } => Some(readiness_schedule(ReadinessScheduleInput {
621 queue_id,
622 work_id,
623 kind: WorkQueueReadinessScheduleKind::WorkScheduled,
624 schedule_id: compound_tuple_key(
625 "work-queue-scheduled-readiness",
626 &[
627 queue_id,
628 work_id,
629 schedule_id.as_deref().unwrap_or(command_id),
630 &record_seq.to_string(),
631 ],
632 ),
633 ready_at_ms: *not_before_ms,
634 deadline_ms: *deadline_ms,
635 record_seq: *record_seq,
636 lease: None,
637 })),
638 WorkQueueRecord::RetryScheduled {
639 record_seq,
640 queue_id,
641 work_id,
642 command_id,
643 retry_at_ms,
644 ..
645 } => Some(readiness_schedule(ReadinessScheduleInput {
646 queue_id,
647 work_id,
648 kind: WorkQueueReadinessScheduleKind::RetryScheduled,
649 schedule_id: compound_tuple_key(
650 "work-queue-retry-readiness",
651 &[queue_id, work_id, command_id, &record_seq.to_string()],
652 ),
653 ready_at_ms: *retry_at_ms,
654 deadline_ms: None,
655 record_seq: *record_seq,
656 lease: None,
657 })),
658 WorkQueueRecord::WorkClaimed {
659 record_seq,
660 queue_id,
661 work_id,
662 lease_id,
663 attempt,
664 worker_id,
665 lease_expires_at_ms,
666 ..
667 }
668 | WorkQueueRecord::LeaseRenewed {
669 record_seq,
670 queue_id,
671 work_id,
672 lease_id,
673 attempt,
674 worker_id,
675 lease_expires_at_ms,
676 ..
677 } => Some(readiness_schedule(ReadinessScheduleInput {
678 queue_id,
679 work_id,
680 kind: WorkQueueReadinessScheduleKind::LeaseExpiration,
681 schedule_id: compound_tuple_key(
682 "work-queue-lease-readiness",
683 &[
684 queue_id,
685 work_id,
686 lease_id,
687 &attempt.to_string(),
688 &record_seq.to_string(),
689 ],
690 ),
691 ready_at_ms: *lease_expires_at_ms,
692 deadline_ms: None,
693 record_seq: *record_seq,
694 lease: Some((lease_id.clone(), *attempt, worker_id.clone())),
695 })),
696 _ => None,
697 }
698}
699
700struct ReadinessScheduleInput<'a> {
701 queue_id: &'a str,
702 work_id: &'a str,
703 kind: WorkQueueReadinessScheduleKind,
704 schedule_id: String,
705 ready_at_ms: u64,
706 deadline_ms: Option<u64>,
707 record_seq: u64,
708 lease: Option<(String, u32, String)>,
709}
710
711fn readiness_schedule(input: ReadinessScheduleInput<'_>) -> ScheduledReadinessRequested {
712 let mut metadata = BTreeMap::from([
713 (
714 "queueId".to_owned(),
715 JsonValue::from(input.queue_id.to_owned()),
716 ),
717 (
718 "workId".to_owned(),
719 JsonValue::from(input.work_id.to_owned()),
720 ),
721 (
722 "scheduleKind".to_owned(),
723 JsonValue::from(input.kind.as_str().to_owned()),
724 ),
725 ("recordSeq".to_owned(), JsonValue::from(input.record_seq)),
726 ]);
727 if let Some((lease_id, attempt, worker_id)) = input.lease {
728 metadata.insert("leaseId".to_owned(), JsonValue::from(lease_id));
729 metadata.insert("attempt".to_owned(), JsonValue::from(attempt));
730 metadata.insert("workerId".to_owned(), JsonValue::from(worker_id));
731 }
732 ScheduledReadinessRequested {
733 schedule_id: input.schedule_id.clone(),
734 subject_refs: vec![
735 SourceRef::new("work-queue", input.queue_id.to_owned()),
736 SourceRef::new("work-queue-work", input.work_id.to_owned()),
737 ],
738 ready_at_ms: input.ready_at_ms,
739 deadline_ms: input.deadline_ms,
740 reason: Some(input.kind.as_str().to_owned()),
741 policy_refs: Vec::new(),
742 source_refs: vec![SourceRef::new(
743 format!("work-queue-{}", input.kind.as_str()),
744 input.schedule_id,
745 )],
746 metadata: Some(metadata),
747 }
748}
749
750fn emit_translated_schedule(
751 ctx: &Ctx,
752 state: &mut TranslatorState,
753 schedule: ScheduledReadinessRequested,
754) {
755 let schedule_id = schedule.schedule_id.clone();
756 if state
757 .schedules_by_id
758 .insert(schedule_id.clone(), schedule.clone())
759 .is_none()
760 {
761 emit_fact(ctx, WorkQueueReadinessFact::Schedule(schedule.clone()));
762 emit_status(
763 ctx,
764 state,
765 WorkQueueReadinessStatus {
766 status_id: compound_tuple_key("work-queue-readiness-translated", &[&schedule_id]),
767 queue_id: metadata_string(&schedule, "queueId").unwrap_or_default(),
768 work_id: metadata_string(&schedule, "workId"),
769 schedule_id: Some(schedule_id),
770 state: WorkQueueReadinessStatusState::Translated,
771 schedule_kind: metadata_kind(&schedule),
772 ready_at_ms: Some(schedule.ready_at_ms),
773 now_ms: None,
774 issue_code: None,
775 details: None,
776 },
777 );
778 emit_translator_audit(ctx, state, "work-queue-readiness-translated", &schedule);
779 }
780}
781
782fn apply_record<T>(state: &mut HandoffState, record: &WorkQueueRecord<T>) {
783 match record {
784 WorkQueueRecord::WorkAdmitted {
785 queue_id,
786 work_id,
787 not_before_ms,
788 ..
789 } => {
790 prune_candidates_for_work(state, queue_id, work_id);
791 state.works_by_id.insert(
792 work_key(queue_id, work_id),
793 QueueWorkState {
794 queue_id: queue_id.clone(),
795 state: if not_before_ms.is_some() {
796 WorkQueueDerivedState::Scheduled
797 } else {
798 WorkQueueDerivedState::Ready
799 },
800 effective_ready_at_ms: *not_before_ms,
801 effective_schedule_kind: not_before_ms
802 .map(|_| WorkQueueReadinessScheduleKind::AdmissionDelay),
803 lease_id: None,
804 attempt: None,
805 worker_id: None,
806 lease_expires_at_ms: None,
807 },
808 );
809 }
810 WorkQueueRecord::WorkScheduled {
811 queue_id,
812 work_id,
813 not_before_ms,
814 ..
815 } => set_work_delayed_state(
816 state,
817 queue_id,
818 work_id,
819 WorkQueueDerivedState::Scheduled,
820 Some(*not_before_ms),
821 Some(WorkQueueReadinessScheduleKind::WorkScheduled),
822 ),
823 WorkQueueRecord::RetryScheduled {
824 queue_id,
825 work_id,
826 retry_at_ms,
827 ..
828 } => set_work_delayed_state(
829 state,
830 queue_id,
831 work_id,
832 WorkQueueDerivedState::RetryWait,
833 Some(*retry_at_ms),
834 Some(WorkQueueReadinessScheduleKind::RetryScheduled),
835 ),
836 WorkQueueRecord::WorkClaimed {
837 queue_id,
838 work_id,
839 lease_id,
840 attempt,
841 worker_id,
842 lease_expires_at_ms,
843 ..
844 }
845 | WorkQueueRecord::LeaseRenewed {
846 queue_id,
847 work_id,
848 lease_id,
849 attempt,
850 worker_id,
851 lease_expires_at_ms,
852 ..
853 } => {
854 prune_candidates_for_work(state, queue_id, work_id);
855 state.works_by_id.insert(
856 work_key(queue_id, work_id),
857 QueueWorkState {
858 queue_id: queue_id.clone(),
859 state: WorkQueueDerivedState::Leased,
860 effective_ready_at_ms: Some(*lease_expires_at_ms),
861 effective_schedule_kind: Some(WorkQueueReadinessScheduleKind::LeaseExpiration),
862 lease_id: Some(lease_id.clone()),
863 attempt: Some(*attempt),
864 worker_id: Some(worker_id.clone()),
865 lease_expires_at_ms: Some(*lease_expires_at_ms),
866 },
867 );
868 }
869 WorkQueueRecord::WorkReleased {
870 queue_id, work_id, ..
871 }
872 | WorkQueueRecord::LeaseExpired {
873 queue_id, work_id, ..
874 } => set_work_state(state, queue_id, work_id, WorkQueueDerivedState::Ready),
875 WorkQueueRecord::WorkCompleted {
876 queue_id, work_id, ..
877 } => set_work_state(state, queue_id, work_id, WorkQueueDerivedState::Completed),
878 WorkQueueRecord::WorkCanceled {
879 queue_id, work_id, ..
880 } => set_work_state(state, queue_id, work_id, WorkQueueDerivedState::Canceled),
881 WorkQueueRecord::WorkDeadLettered {
882 queue_id, work_id, ..
883 } => set_work_state(
884 state,
885 queue_id,
886 work_id,
887 WorkQueueDerivedState::DeadLettered,
888 ),
889 WorkQueueRecord::AttemptFailed { .. }
890 | WorkQueueRecord::AttemptCompleted { .. }
891 | WorkQueueRecord::AdmissionDeduped { .. } => {}
892 }
893}
894
895fn set_work_state(
896 state: &mut HandoffState,
897 queue_id: &str,
898 work_id: &str,
899 next: WorkQueueDerivedState,
900) {
901 let terminal = matches!(
902 next,
903 WorkQueueDerivedState::Completed
904 | WorkQueueDerivedState::Canceled
905 | WorkQueueDerivedState::DeadLettered
906 );
907 if terminal {
908 prune_candidates_for_work(state, queue_id, work_id);
909 }
910 set_work_delayed_state(state, queue_id, work_id, next, None, None);
911}
912
913fn set_work_delayed_state(
914 state: &mut HandoffState,
915 queue_id: &str,
916 work_id: &str,
917 next: WorkQueueDerivedState,
918 effective_ready_at_ms: Option<u64>,
919 effective_schedule_kind: Option<WorkQueueReadinessScheduleKind>,
920) {
921 prune_candidates_for_work(state, queue_id, work_id);
922 state
923 .works_by_id
924 .entry(work_key(queue_id, work_id))
925 .and_modify(|work| {
926 work.state = next;
927 work.effective_ready_at_ms = effective_ready_at_ms;
928 work.effective_schedule_kind = effective_schedule_kind;
929 if next != WorkQueueDerivedState::Leased {
930 work.lease_id = None;
931 work.attempt = None;
932 work.worker_id = None;
933 work.lease_expires_at_ms = None;
934 }
935 })
936 .or_insert_with(|| QueueWorkState {
937 queue_id: queue_id.to_owned(),
938 state: next,
939 effective_ready_at_ms,
940 effective_schedule_kind,
941 lease_id: None,
942 attempt: None,
943 worker_id: None,
944 lease_expires_at_ms: None,
945 });
946}
947
948fn handoff_ready(ctx: &Ctx, state: &mut HandoffState, ready: &ScheduledReadinessReady) {
949 state
950 .ready_by_id
951 .insert(ready.schedule_id.clone(), ready.clone());
952 handoff_ready_inner(ctx, state, ready);
953}
954
955fn handoff_ready_inner(ctx: &Ctx, state: &mut HandoffState, ready: &ScheduledReadinessReady) {
956 let Some(queue_id) = ready_metadata_string(ready, "queueId") else {
957 emit_handoff_issue(
958 ctx,
959 state,
960 ready.schedule_id.clone(),
961 "missing queueId metadata",
962 );
963 return;
964 };
965 let Some(work_id) = ready_metadata_string(ready, "workId") else {
966 emit_handoff_issue(
967 ctx,
968 state,
969 ready.schedule_id.clone(),
970 "missing workId metadata",
971 );
972 return;
973 };
974 let Some(schedule_kind) = ready_metadata_string(ready, "scheduleKind")
975 .and_then(|value| WorkQueueReadinessScheduleKind::from_str(&value))
976 else {
977 emit_handoff_issue(
978 ctx,
979 state,
980 ready.schedule_id.clone(),
981 "missing scheduleKind metadata",
982 );
983 return;
984 };
985 let Some(work) = state
986 .works_by_id
987 .get(&work_key(&queue_id, &work_id))
988 .cloned()
989 else {
990 emit_handoff_status(
991 ctx,
992 state,
993 ignored_status(&queue_id, &work_id, ready, schedule_kind, "unknown-work"),
994 );
995 return;
996 };
997 if work.queue_id != queue_id {
998 emit_handoff_status(
999 ctx,
1000 state,
1001 ignored_status(&queue_id, &work_id, ready, schedule_kind, "queue-mismatch"),
1002 );
1003 return;
1004 }
1005 let candidate = match schedule_kind {
1006 WorkQueueReadinessScheduleKind::AdmissionDelay
1007 | WorkQueueReadinessScheduleKind::WorkScheduled
1008 | WorkQueueReadinessScheduleKind::RetryScheduled => {
1009 if work.effective_ready_at_ms != Some(ready.ready_at_ms) {
1010 emit_handoff_status(
1011 ctx,
1012 state,
1013 ignored_status(
1014 &queue_id,
1015 &work_id,
1016 ready,
1017 schedule_kind,
1018 "superseded-readiness",
1019 ),
1020 );
1021 return;
1022 }
1023 if work
1024 .effective_schedule_kind
1025 .is_some_and(|effective| effective != schedule_kind)
1026 && matches!(
1027 work.state,
1028 WorkQueueDerivedState::Scheduled | WorkQueueDerivedState::RetryWait
1029 )
1030 {
1031 emit_handoff_status(
1032 ctx,
1033 state,
1034 ignored_status(
1035 &queue_id,
1036 &work_id,
1037 ready,
1038 schedule_kind,
1039 "stale-readiness-kind",
1040 ),
1041 );
1042 return;
1043 }
1044 if matches!(
1045 work.state,
1046 WorkQueueDerivedState::Completed
1047 | WorkQueueDerivedState::Canceled
1048 | WorkQueueDerivedState::DeadLettered
1049 | WorkQueueDerivedState::Leased
1050 ) {
1051 prune_candidates_for_work(state, &queue_id, &work_id);
1052 emit_handoff_status(
1053 ctx,
1054 state,
1055 ignored_status(&queue_id, &work_id, ready, schedule_kind, "stale-readiness"),
1056 );
1057 return;
1058 }
1059 WorkQueueReadinessCandidate {
1060 candidate_id: compound_tuple_key(
1061 "work-queue-readiness-candidate",
1062 &[
1063 &ready.schedule_id,
1064 WorkQueueReadinessCandidateKind::ClaimEligible.as_str(),
1065 ],
1066 ),
1067 queue_id,
1068 work_id,
1069 schedule_id: ready.schedule_id.clone(),
1070 candidate_kind: WorkQueueReadinessCandidateKind::ClaimEligible,
1071 schedule_kind,
1072 ready_at_ms: ready.ready_at_ms,
1073 now_ms: ready.now_ms,
1074 lease_id: None,
1075 attempt: None,
1076 worker_id: None,
1077 source_refs: ready.source_refs.clone(),
1078 }
1079 }
1080 WorkQueueReadinessScheduleKind::LeaseExpiration => {
1081 let lease_id = ready_metadata_string(ready, "leaseId");
1082 let attempt =
1083 ready_metadata_u64(ready, "attempt").and_then(|value| u32::try_from(value).ok());
1084 if work.state != WorkQueueDerivedState::Leased
1085 || work.lease_id != lease_id
1086 || work.attempt != attempt
1087 || work.lease_expires_at_ms != Some(ready.ready_at_ms)
1088 {
1089 prune_candidates_for_work(state, &queue_id, &work_id);
1090 emit_handoff_status(
1091 ctx,
1092 state,
1093 ignored_status(
1094 &queue_id,
1095 &work_id,
1096 ready,
1097 schedule_kind,
1098 "stale-lease-readiness",
1099 ),
1100 );
1101 return;
1102 }
1103 WorkQueueReadinessCandidate {
1104 candidate_id: compound_tuple_key(
1105 "work-queue-readiness-candidate",
1106 &[
1107 &ready.schedule_id,
1108 WorkQueueReadinessCandidateKind::LeaseExpirationEligible.as_str(),
1109 ],
1110 ),
1111 queue_id,
1112 work_id,
1113 schedule_id: ready.schedule_id.clone(),
1114 candidate_kind: WorkQueueReadinessCandidateKind::LeaseExpirationEligible,
1115 schedule_kind,
1116 ready_at_ms: ready.ready_at_ms,
1117 now_ms: ready.now_ms,
1118 lease_id,
1119 attempt,
1120 worker_id: work.worker_id,
1121 source_refs: ready.source_refs.clone(),
1122 }
1123 }
1124 };
1125 emit_candidate(ctx, state, candidate);
1126}
1127
1128fn work_key(queue_id: &str, work_id: &str) -> (String, String) {
1129 (queue_id.to_owned(), work_id.to_owned())
1130}
1131
1132fn prune_candidates_for_work(state: &mut HandoffState, queue_id: &str, work_id: &str) {
1133 state
1134 .candidates_by_id
1135 .retain(|_, candidate| candidate.queue_id != queue_id || candidate.work_id != work_id);
1136}
1137
1138fn handoff_overdue(ctx: &Ctx, state: &mut HandoffState, overdue: &ScheduledReadinessOverdue) {
1139 let queue_id = overdue_metadata_string(overdue, "queueId").unwrap_or_default();
1140 let work_id = overdue_metadata_string(overdue, "workId");
1141 let schedule_kind = overdue_metadata_string(overdue, "scheduleKind")
1142 .and_then(|value| WorkQueueReadinessScheduleKind::from_str(&value));
1143 emit_handoff_status(
1144 ctx,
1145 state,
1146 WorkQueueReadinessStatus {
1147 status_id: compound_tuple_key("work-queue-readiness-overdue", &[&overdue.schedule_id]),
1148 queue_id,
1149 work_id,
1150 schedule_id: Some(overdue.schedule_id.clone()),
1151 state: WorkQueueReadinessStatusState::Overdue,
1152 schedule_kind,
1153 ready_at_ms: Some(overdue.ready_at_ms),
1154 now_ms: Some(overdue.now_ms),
1155 issue_code: None,
1156 details: Some("deadline visibility only; no queue lifecycle mutation".to_owned()),
1157 },
1158 );
1159}
1160
1161fn emit_candidate(ctx: &Ctx, state: &mut HandoffState, candidate: WorkQueueReadinessCandidate) {
1162 state
1163 .candidates_by_id
1164 .insert(candidate.candidate_id.clone(), candidate.clone());
1165 if state
1166 .emitted
1167 .insert(compound_tuple_key("candidate", &[&candidate.candidate_id]))
1168 {
1169 emit_fact(ctx, WorkQueueReadinessFact::Candidate(candidate.clone()));
1170 emit_handoff_status(
1171 ctx,
1172 state,
1173 WorkQueueReadinessStatus {
1174 status_id: compound_tuple_key(
1175 "work-queue-readiness-candidate-status",
1176 &[&candidate.candidate_id],
1177 ),
1178 queue_id: candidate.queue_id.clone(),
1179 work_id: Some(candidate.work_id.clone()),
1180 schedule_id: Some(candidate.schedule_id.clone()),
1181 state: WorkQueueReadinessStatusState::Candidate,
1182 schedule_kind: Some(candidate.schedule_kind),
1183 ready_at_ms: Some(candidate.ready_at_ms),
1184 now_ms: Some(candidate.now_ms),
1185 issue_code: None,
1186 details: Some(format!("{:?}", candidate.candidate_kind)),
1187 },
1188 );
1189 emit_handoff_audit(ctx, state, "work-queue-readiness-candidate", &candidate);
1190 }
1191}
1192
1193fn emit_status(ctx: &Ctx, state: &mut TranslatorState, status: WorkQueueReadinessStatus) {
1194 state
1195 .status_by_id
1196 .insert(status.status_id.clone(), status.clone());
1197 if state
1198 .emitted
1199 .insert(compound_tuple_key("status", &[&format!("{status:?}")]))
1200 {
1201 emit_fact(ctx, WorkQueueReadinessFact::Status(status));
1202 }
1203}
1204
1205fn emit_handoff_status(ctx: &Ctx, state: &mut HandoffState, status: WorkQueueReadinessStatus) {
1206 state
1207 .status_by_id
1208 .insert(status.status_id.clone(), status.clone());
1209 if state
1210 .emitted
1211 .insert(compound_tuple_key("status", &[&format!("{status:?}")]))
1212 {
1213 emit_fact(ctx, WorkQueueReadinessFact::Status(status));
1214 }
1215}
1216
1217fn emit_handoff_issue(ctx: &Ctx, state: &mut HandoffState, schedule_id: String, detail: &str) {
1218 let issue = readiness_issue(
1219 "work-queue-readiness-malformed-ready",
1220 "workQueue readiness handoff requires workQueue metadata on ready facts.",
1221 &schedule_id,
1222 Some(detail.to_owned()),
1223 "error",
1224 );
1225 if state
1226 .emitted
1227 .insert(canonical_tuple_key(&["issue", &schedule_id, detail]))
1228 {
1229 emit_fact(ctx, WorkQueueReadinessFact::Issue(issue));
1230 }
1231}
1232
1233fn emit_translator_audit(
1234 ctx: &Ctx,
1235 state: &mut TranslatorState,
1236 kind: &str,
1237 schedule: &ScheduledReadinessRequested,
1238) {
1239 state.audit_seq += 1;
1240 emit_fact(
1241 ctx,
1242 WorkQueueReadinessFact::Audit(ScheduledReadinessAuditRecord {
1243 id: format!("work-queue-readiness-audit-{}", state.audit_seq),
1244 kind: kind.to_owned(),
1245 subject_id: Some(schedule.schedule_id.clone()),
1246 source_refs: schedule.source_refs.clone(),
1247 metadata: schedule.metadata.clone(),
1248 }),
1249 );
1250}
1251
1252fn emit_handoff_audit(
1253 ctx: &Ctx,
1254 state: &mut HandoffState,
1255 kind: &str,
1256 candidate: &WorkQueueReadinessCandidate,
1257) {
1258 state.audit_seq += 1;
1259 emit_fact(
1260 ctx,
1261 WorkQueueReadinessFact::Audit(ScheduledReadinessAuditRecord {
1262 id: format!("work-queue-readiness-handoff-audit-{}", state.audit_seq),
1263 kind: kind.to_owned(),
1264 subject_id: Some(candidate.schedule_id.clone()),
1265 source_refs: candidate.source_refs.clone(),
1266 metadata: Some(BTreeMap::from([
1267 (
1268 "queueId".to_owned(),
1269 JsonValue::from(candidate.queue_id.clone()),
1270 ),
1271 (
1272 "workId".to_owned(),
1273 JsonValue::from(candidate.work_id.clone()),
1274 ),
1275 ])),
1276 }),
1277 );
1278}
1279
1280fn ignored_status(
1281 queue_id: &str,
1282 work_id: &str,
1283 ready: &ScheduledReadinessReady,
1284 schedule_kind: WorkQueueReadinessScheduleKind,
1285 detail: &str,
1286) -> WorkQueueReadinessStatus {
1287 WorkQueueReadinessStatus {
1288 status_id: compound_tuple_key(
1289 "work-queue-readiness-ignored",
1290 &[&ready.schedule_id, detail],
1291 ),
1292 queue_id: queue_id.to_owned(),
1293 work_id: Some(work_id.to_owned()),
1294 schedule_id: Some(ready.schedule_id.clone()),
1295 state: WorkQueueReadinessStatusState::Ignored,
1296 schedule_kind: Some(schedule_kind),
1297 ready_at_ms: Some(ready.ready_at_ms),
1298 now_ms: Some(ready.now_ms),
1299 issue_code: None,
1300 details: Some(detail.to_owned()),
1301 }
1302}
1303
1304fn metadata_string(schedule: &ScheduledReadinessRequested, key: &str) -> Option<String> {
1305 schedule
1306 .metadata
1307 .as_ref()?
1308 .get(key)?
1309 .as_str()
1310 .map(str::to_owned)
1311}
1312
1313fn metadata_kind(schedule: &ScheduledReadinessRequested) -> Option<WorkQueueReadinessScheduleKind> {
1314 metadata_string(schedule, "scheduleKind")
1315 .as_deref()
1316 .and_then(WorkQueueReadinessScheduleKind::from_str)
1317}
1318
1319fn ready_metadata_string(ready: &ScheduledReadinessReady, key: &str) -> Option<String> {
1320 ready
1321 .metadata
1322 .as_ref()?
1323 .get(key)?
1324 .as_str()
1325 .map(str::to_owned)
1326}
1327
1328fn ready_metadata_u64(ready: &ScheduledReadinessReady, key: &str) -> Option<u64> {
1329 ready.metadata.as_ref()?.get(key)?.as_u64()
1330}
1331
1332fn overdue_metadata_string(overdue: &ScheduledReadinessOverdue, key: &str) -> Option<String> {
1333 overdue
1334 .metadata
1335 .as_ref()?
1336 .get(key)?
1337 .as_str()
1338 .map(str::to_owned)
1339}
1340
1341fn project_fact<T: Clone + 'static>(
1342 graph: &Graph,
1343 runtime: &Node<WorkQueueReadinessFact>,
1344 name: String,
1345 factory: &'static str,
1346 select: impl Fn(&WorkQueueReadinessFact) -> Option<T> + 'static,
1347) -> Node<T> {
1348 graph.node_opts::<T, _>(
1349 vec![runtime.erased()],
1350 move |ctx| {
1351 for fact in ctx.batch::<WorkQueueReadinessFact>(0) {
1352 if let Some(value) = select(&fact) {
1353 ctx.emit(value);
1354 }
1355 }
1356 },
1357 node_opts(name, factory),
1358 )
1359}
1360
1361fn emit_fact(ctx: &Ctx, fact: WorkQueueReadinessFact) {
1362 ctx.emit(fact);
1363}
1364
1365fn node_opts(name: impl Into<String>, factory: impl Into<String>) -> GraphNodeOpts {
1366 let mut opts = GraphNodeOpts::named(name);
1367 opts.node = NodeOpts {
1368 factory: Some(factory.into()),
1369 complete_when_deps_complete: false,
1370 error_when_deps_error: false,
1371 ..opts.node
1372 };
1373 opts
1374}