1use std::{
2 collections::HashMap,
3 future::Future,
4 panic::{AssertUnwindSafe, catch_unwind},
5 sync::{
6 OnceLock,
7 atomic::{AtomicU64, Ordering},
8 mpsc,
9 },
10 time::{Duration, Instant, SystemTime},
11};
12
13use datum::{
14 Materializer, NotUsed, RunnableGraph, Source, StreamError, StreamInstrumentationRegistry,
15 StreamResult, Topic, TopicOverflow, TopicTryPublishError,
16};
17use ractor::{Actor, ActorProcessingErr, ActorRef};
18use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime as TokioRuntime};
19
20use crate::{
21 AgentConfig, AgentError, AgentResult, DesiredJobState, JobContext, JobControl,
22 JobDrainBehavior, JobEvent, JobEventKind, JobExitReason, JobId, JobMat, JobSpec, JobState,
23 JobStatus,
24 job::{RestartCause, new_generation_kill_switch},
25};
26
27type Reply<T> = mpsc::Sender<AgentResult<T>>;
28
29pub struct JobRegistry;
31
32impl JobRegistry {
33 pub fn start(config: AgentConfig) -> AgentResult<JobRegistryHandle> {
34 let event_buffer = config.event_buffer.max(1);
35 let events = Topic::new(event_buffer, TopicOverflow::Sliding)?;
36 let args = RegistryArgs {
37 instrumentation: config.instrumentation.clone(),
38 config,
39 events: events.clone(),
40 materializer: Materializer::new(),
41 };
42 let (actor, _join) =
43 block_on_agent_runtime(RegistryActor::spawn(None, RegistryActor, args)).and_then(
44 |result| {
45 result.map_err(|error| {
46 AgentError::Actor(format!("job registry failed to spawn: {error}"))
47 })
48 },
49 )?;
50 Ok(JobRegistryHandle { actor, events })
51 }
52}
53
54#[derive(Clone)]
56pub struct JobRegistryHandle {
57 actor: ActorRef<RegistryMessage>,
58 events: Topic<JobEvent>,
59}
60
61impl JobRegistryHandle {
62 pub fn submit(&self, spec: JobSpec) -> AgentResult<JobStatus> {
63 self.request(|reply| RegistryMessage::Submit {
64 spec: Box::new(spec),
65 reply,
66 })
67 }
68
69 pub fn start(&self, name: impl Into<String>) -> AgentResult<JobStatus> {
70 let name = name.into();
71 self.request(|reply| RegistryMessage::Start { name, reply })
72 }
73
74 pub fn restart(&self, name: impl Into<String>) -> AgentResult<JobStatus> {
75 let name = name.into();
76 self.request(|reply| RegistryMessage::Restart { name, reply })
77 }
78
79 pub fn drain(&self, name: impl Into<String>) -> AgentResult<JobStatus> {
80 let name = name.into();
81 self.request(|reply| RegistryMessage::Drain { name, reply })
82 }
83
84 pub fn stop(&self, name: impl Into<String>) -> AgentResult<JobStatus> {
85 let name = name.into();
86 self.request(|reply| RegistryMessage::Stop { name, reply })
87 }
88
89 pub fn status(&self, name: impl Into<String>) -> AgentResult<JobStatus> {
90 let name = name.into();
91 self.request(|reply| RegistryMessage::Status { name, reply })
92 }
93
94 pub fn list(&self) -> AgentResult<Vec<JobStatus>> {
95 self.request(|reply| RegistryMessage::List { reply })
96 }
97
98 #[must_use]
99 pub fn events(&self) -> Source<JobEvent> {
100 self.events.subscribe()
101 }
102
103 pub fn shutdown(&self) -> AgentResult<()> {
104 self.request(RegistryMessage::Shutdown)
105 }
106
107 fn request<T, F>(&self, make: F) -> AgentResult<T>
108 where
109 T: Send + 'static,
110 F: FnOnce(Reply<T>) -> RegistryMessage,
111 {
112 let (reply, receiver) = mpsc::channel();
113 self.actor
114 .send_message(make(reply))
115 .map_err(|_| AgentError::RegistryClosed)?;
116 receiver.recv().map_err(|_| AgentError::RegistryClosed)?
117 }
118}
119
120struct RegistryArgs {
121 instrumentation: StreamInstrumentationRegistry,
122 config: AgentConfig,
123 events: Topic<JobEvent>,
124 materializer: Materializer,
125}
126
127struct RegistryActor;
128
129impl Actor for RegistryActor {
130 type Msg = RegistryMessage;
131 type State = RegistryState;
132 type Arguments = RegistryArgs;
133
134 async fn pre_start(
135 &self,
136 _myself: ActorRef<Self::Msg>,
137 args: Self::Arguments,
138 ) -> Result<Self::State, ActorProcessingErr> {
139 Ok(RegistryState {
140 config: args.config,
141 materializer: args.materializer,
142 events: args.events,
143 instrumentation: args.instrumentation,
144 next_job_id: 1,
145 next_event_sequence: 1,
146 jobs: HashMap::new(),
147 })
148 }
149
150 async fn post_start(
151 &self,
152 myself: ActorRef<Self::Msg>,
153 state: &mut Self::State,
154 ) -> Result<(), ActorProcessingErr> {
155 let interval = state.config.poll_interval.max(Duration::from_millis(1));
156 spawn_completion_poller(myself, interval);
157 Ok(())
158 }
159
160 async fn post_stop(
161 &self,
162 _myself: ActorRef<Self::Msg>,
163 state: &mut Self::State,
164 ) -> Result<(), ActorProcessingErr> {
165 abort_all_jobs(state);
166 let materializer = state.materializer.clone();
167 let events = state.events.clone();
168 materializer.shutdown();
169 let _ = events.close();
170 Ok(())
171 }
172
173 async fn handle(
174 &self,
175 myself: ActorRef<Self::Msg>,
176 message: Self::Msg,
177 state: &mut Self::State,
178 ) -> Result<(), ActorProcessingErr> {
179 match message {
180 RegistryMessage::Submit { spec, reply } => {
181 send_reply(reply, submit_job(state, *spec));
182 }
183 RegistryMessage::Start { name, reply } => {
184 send_reply(reply, start_job(state, &myself, &name, StartKind::Initial));
185 }
186 RegistryMessage::Restart { name, reply } => {
187 send_reply(reply, restart_job(state, &myself, &name));
188 }
189 RegistryMessage::Drain { name, reply } => {
190 send_reply(reply, drain_job(state, &name));
191 }
192 RegistryMessage::Stop { name, reply } => {
193 send_reply(reply, stop_job(state, &name, JobExitReason::Stopped));
194 }
195 RegistryMessage::Status { name, reply } => {
196 send_reply(reply, status_by_name(state, &name));
197 }
198 RegistryMessage::List { reply } => {
199 send_reply(reply, Ok(list_statuses(state)));
200 }
201 RegistryMessage::Tick => {
202 poll_jobs(state, &myself);
203 }
204 RegistryMessage::RestartReady {
205 name,
206 expected_generation,
207 } => {
208 restart_ready(state, &myself, &name, expected_generation);
209 }
210 RegistryMessage::Shutdown(reply) => {
211 abort_all_jobs(state);
212 let _ = reply.send(Ok(()));
213 myself.stop(Some("datum-agent shutdown".to_owned()));
214 }
215 }
216 Ok(())
217 }
218}
219
220enum RegistryMessage {
221 Submit {
222 spec: Box<JobSpec>,
223 reply: Reply<JobStatus>,
224 },
225 Start {
226 name: String,
227 reply: Reply<JobStatus>,
228 },
229 Restart {
230 name: String,
231 reply: Reply<JobStatus>,
232 },
233 Drain {
234 name: String,
235 reply: Reply<JobStatus>,
236 },
237 Stop {
238 name: String,
239 reply: Reply<JobStatus>,
240 },
241 Status {
242 name: String,
243 reply: Reply<JobStatus>,
244 },
245 List {
246 reply: Reply<Vec<JobStatus>>,
247 },
248 Tick,
249 RestartReady {
250 name: String,
251 expected_generation: u64,
252 },
253 Shutdown(Reply<()>),
254}
255
256struct RegistryState {
257 config: AgentConfig,
258 materializer: Materializer,
259 events: Topic<JobEvent>,
260 instrumentation: StreamInstrumentationRegistry,
261 next_job_id: u64,
262 next_event_sequence: u64,
263 jobs: HashMap<String, JobEntry>,
264}
265
266struct JobEntry {
267 id: JobId,
268 spec: JobSpec,
269 state: JobState,
270 desired_state: DesiredJobState,
271 generation: u64,
272 run: Option<JobRun>,
273 backoff: RestartBackoffState,
274 starts_total: u64,
275 restarts_total: u64,
276 last_start_at: Option<SystemTime>,
277 last_exit_at: Option<SystemTime>,
278 last_exit_reason: Option<JobExitReason>,
279 backoff_until: Option<Instant>,
280 drain_deadline: Option<Instant>,
281 drain_supported: bool,
282}
283
284impl JobEntry {
285 fn new(id: JobId, spec: JobSpec) -> Self {
286 Self {
287 id,
288 spec,
289 state: JobState::Submitted,
290 desired_state: DesiredJobState::Stopped,
291 generation: 0,
292 run: None,
293 backoff: RestartBackoffState::default(),
294 starts_total: 0,
295 restarts_total: 0,
296 last_start_at: None,
297 last_exit_at: None,
298 last_exit_reason: None,
299 backoff_until: None,
300 drain_deadline: None,
301 drain_supported: true,
302 }
303 }
304}
305
306struct JobRun {
307 generation: u64,
308 control: JobControl,
309 completion: datum::StreamCompletion<NotUsed>,
310}
311
312#[derive(Default)]
313struct RestartBackoffState {
314 window_start: Option<Instant>,
315 restarts_in_window: usize,
316}
317
318impl RestartBackoffState {
319 fn reset(&mut self) {
320 self.window_start = None;
321 self.restarts_in_window = 0;
322 }
323
324 fn next_delay(&mut self, settings: &datum::RestartSettings) -> Option<Duration> {
325 let now = Instant::now();
326 match self.window_start {
327 Some(window_start)
328 if now.duration_since(window_start) <= settings.max_restarts_within() => {}
329 _ => {
330 self.window_start = Some(now);
331 self.restarts_in_window = 0;
332 }
333 }
334
335 if self.restarts_in_window >= settings.max_restarts() {
336 return None;
337 }
338
339 let exponent = (self.restarts_in_window.min(31) as i32).max(0);
340 let base = settings
341 .min_backoff()
342 .mul_f64(2_f64.powi(exponent))
343 .min(settings.max_backoff());
344 self.restarts_in_window += 1;
345
346 if settings.random_factor() == 0.0 || base.is_zero() {
347 Some(base)
348 } else {
349 let jitter = base.mul_f64(next_random_fraction() * settings.random_factor());
350 Some(base.saturating_add(jitter))
351 }
352 }
353}
354
355#[derive(Clone, Copy)]
356enum StartKind {
357 Initial,
358 AutomaticRestart { previous_generation: u64 },
359 ManualRestart,
360}
361
362fn submit_job(state: &mut RegistryState, spec: JobSpec) -> AgentResult<JobStatus> {
363 if spec.name.trim().is_empty() {
364 return Err(AgentError::InvalidJobName);
365 }
366 if state.jobs.contains_key(&spec.name) {
367 return Err(AgentError::JobAlreadyExists(spec.name));
368 }
369
370 let name = spec.name.clone();
371 let id = JobId(state.next_job_id);
372 state.next_job_id += 1;
373 state.jobs.insert(name.clone(), JobEntry::new(id, spec));
374 emit_event(state, &name, 0, JobEventKind::Submitted);
375 status_by_name(state, &name)
376}
377
378fn start_job(
379 state: &mut RegistryState,
380 myself: &ActorRef<RegistryMessage>,
381 name: &str,
382 kind: StartKind,
383) -> AgentResult<JobStatus> {
384 let (spec, job_id, generation) = {
385 let entry = state
386 .jobs
387 .get_mut(name)
388 .ok_or_else(|| AgentError::JobNotFound(name.to_owned()))?;
389
390 if matches!(
391 entry.state,
392 JobState::Starting | JobState::Running | JobState::Draining | JobState::BackingOff
393 ) && !matches!(kind, StartKind::AutomaticRestart { .. })
394 {
395 return Err(AgentError::JobAlreadyRunning(name.to_owned()));
396 }
397
398 entry.state = JobState::Starting;
399 entry.desired_state = DesiredJobState::Running;
400 entry.generation += 1;
401 entry.backoff_until = None;
402 entry.drain_deadline = None;
403 entry.run = None;
404 (entry.spec.clone(), entry.id, entry.generation)
405 };
406
407 let kill_switch = new_generation_kill_switch(&spec.name, generation);
408 let context = JobContext::new(
409 spec.name.clone(),
410 job_id,
411 generation,
412 kill_switch,
413 state.instrumentation.clone(),
414 );
415 let graph = invoke_factory(&spec, context);
416 let materializer = state
417 .materializer
418 .with_name_prefix(format!("datum-agent-{}", spec.name));
419 let materialized = graph.and_then(|graph| materialize_graph(&graph, &materializer));
420
421 match materialized {
422 Ok(mat) => {
423 let (completion, control) = mat.into_parts();
424 let mut restart_event = None;
425 {
426 let entry = state
427 .jobs
428 .get_mut(name)
429 .ok_or_else(|| AgentError::JobNotFound(name.to_owned()))?;
430 entry.run = Some(JobRun {
431 generation,
432 control: control.clone(),
433 completion,
434 });
435 entry.state = JobState::Running;
436 entry.desired_state = DesiredJobState::Running;
437 entry.starts_total += 1;
438 entry.last_start_at = Some(SystemTime::now());
439 entry.drain_supported = control.drain_supported();
440 if let StartKind::AutomaticRestart {
441 previous_generation,
442 } = kind
443 {
444 entry.restarts_total += 1;
445 restart_event = Some(JobEventKind::Restarted {
446 previous_generation,
447 });
448 }
449 }
450 if let Some(kind) = restart_event {
451 emit_event(state, name, generation, kind);
452 }
453 emit_event(state, name, generation, JobEventKind::Started);
454 status_by_name(state, name)
455 }
456 Err(error) => {
457 handle_start_failure(state, myself, name, generation, error.clone());
458 Err(AgentError::Stream(error))
459 }
460 }
461}
462
463fn restart_job(
464 state: &mut RegistryState,
465 myself: &ActorRef<RegistryMessage>,
466 name: &str,
467) -> AgentResult<JobStatus> {
468 if !state.jobs.contains_key(name) {
469 return Err(AgentError::JobNotFound(name.to_owned()));
470 }
471 let _ = stop_job(state, name, JobExitReason::Stopped)?;
472 if let Some(entry) = state.jobs.get_mut(name) {
473 entry.backoff.reset();
474 }
475 start_job(state, myself, name, StartKind::ManualRestart)
476}
477
478fn drain_job(state: &mut RegistryState, name: &str) -> AgentResult<JobStatus> {
479 let generation = {
480 let entry = state
481 .jobs
482 .get_mut(name)
483 .ok_or_else(|| AgentError::JobNotFound(name.to_owned()))?;
484
485 let Some(run) = entry.run.as_ref() else {
486 entry.state = JobState::Drained;
487 entry.desired_state = DesiredJobState::Stopped;
488 entry.last_exit_at = Some(SystemTime::now());
489 entry.last_exit_reason = Some(JobExitReason::Drained);
490 return status_by_name(state, name);
491 };
492
493 let generation = run.generation;
494 let drain_supported = run.control.drain_supported();
495 let control = run.control.clone();
496 let timeout = match entry.spec.drain_behavior {
497 JobDrainBehavior::Graceful { timeout } if drain_supported => timeout,
498 JobDrainBehavior::Graceful { .. } | JobDrainBehavior::CancelOnly => {
499 return Err(AgentError::DrainUnsupported(name.to_owned()));
500 }
501 };
502
503 if !control.shutdown() {
504 return Err(AgentError::DrainUnsupported(name.to_owned()));
505 }
506
507 entry.state = JobState::Draining;
508 entry.desired_state = DesiredJobState::Draining;
509 let deadline = Instant::now() + timeout;
510 entry.drain_deadline = Some(deadline);
511 generation
512 };
513
514 emit_event(state, name, generation, JobEventKind::Draining);
515 status_by_name(state, name)
516}
517
518fn stop_job(
519 state: &mut RegistryState,
520 name: &str,
521 reason: JobExitReason,
522) -> AgentResult<JobStatus> {
523 let generation = {
524 let entry = state
525 .jobs
526 .get_mut(name)
527 .ok_or_else(|| AgentError::JobNotFound(name.to_owned()))?;
528
529 let generation = entry.generation;
530 if let Some(run) = entry.run.take() {
531 let _ = run.control.abort(StreamError::Cancelled);
532 drop(run.completion);
533 }
534 entry.state = JobState::Stopped;
535 entry.desired_state = DesiredJobState::Stopped;
536 entry.backoff_until = None;
537 entry.drain_deadline = None;
538 entry.last_exit_at = Some(SystemTime::now());
539 entry.last_exit_reason = Some(reason.clone());
540 generation
541 };
542
543 emit_event(state, name, generation, JobEventKind::Stopped { reason });
544 status_by_name(state, name)
545}
546
547fn status_by_name(state: &RegistryState, name: &str) -> AgentResult<JobStatus> {
548 state
549 .jobs
550 .get(name)
551 .map(|entry| snapshot(state, entry))
552 .ok_or_else(|| AgentError::JobNotFound(name.to_owned()))
553}
554
555fn list_statuses(state: &RegistryState) -> Vec<JobStatus> {
556 let mut statuses = state
557 .jobs
558 .values()
559 .map(|entry| snapshot(state, entry))
560 .collect::<Vec<_>>();
561 statuses.sort_by(|left, right| left.name.cmp(&right.name));
562 statuses
563}
564
565fn poll_jobs(state: &mut RegistryState, myself: &ActorRef<RegistryMessage>) {
566 let names = state.jobs.keys().cloned().collect::<Vec<_>>();
567 for name in names {
568 let completed = state.jobs.get_mut(&name).and_then(|entry| {
569 entry.run.as_mut().and_then(|run| {
570 run.completion
571 .try_wait()
572 .map(|result| (run.generation, result))
573 })
574 });
575
576 if let Some((generation, result)) = completed {
577 finish_generation(state, myself, &name, generation, result);
578 }
579 }
580
581 let now = Instant::now();
582 let timed_out = state
583 .jobs
584 .iter()
585 .filter_map(|(name, entry)| match (entry.state, entry.drain_deadline) {
586 (JobState::Draining, Some(deadline)) if deadline <= now => Some(name.clone()),
587 _ => None,
588 })
589 .collect::<Vec<_>>();
590
591 for name in timed_out {
592 let _ = stop_job(state, &name, JobExitReason::DrainTimedOut);
593 }
594}
595
596fn finish_generation(
597 state: &mut RegistryState,
598 myself: &ActorRef<RegistryMessage>,
599 name: &str,
600 generation: u64,
601 result: StreamResult<NotUsed>,
602) {
603 let mut restart = None;
604 let event;
605 {
606 let Some(entry) = state.jobs.get_mut(name) else {
607 return;
608 };
609 if entry.run.as_ref().map(|run| run.generation) != Some(generation) {
610 return;
611 }
612 let _run = entry.run.take();
613 entry.drain_deadline = None;
614 entry.last_exit_at = Some(SystemTime::now());
615
616 match (entry.desired_state, result) {
617 (DesiredJobState::Draining, Ok(_)) => {
618 entry.state = JobState::Drained;
619 entry.desired_state = DesiredJobState::Stopped;
620 entry.last_exit_reason = Some(JobExitReason::Drained);
621 event = JobEventKind::Drained;
622 }
623 (DesiredJobState::Draining | DesiredJobState::Stopped, Err(error)) => {
624 let reason = if matches!(error, StreamError::Cancelled) {
625 JobExitReason::Stopped
626 } else {
627 JobExitReason::Failed(error)
628 };
629 entry.state = JobState::Stopped;
630 entry.desired_state = DesiredJobState::Stopped;
631 entry.last_exit_reason = Some(reason.clone());
632 event = JobEventKind::Stopped { reason };
633 }
634 (DesiredJobState::Stopped, Ok(_)) => {
635 entry.state = JobState::Stopped;
636 entry.last_exit_reason = Some(JobExitReason::Stopped);
637 event = JobEventKind::Stopped {
638 reason: JobExitReason::Stopped,
639 };
640 }
641 (DesiredJobState::Running, Ok(_)) => {
642 entry.last_exit_reason = Some(JobExitReason::Completed);
643 entry.state = JobState::Completed;
644 event = JobEventKind::Completed;
645 if let Some(settings) = entry
646 .spec
647 .restart_policy
648 .settings_for(RestartCause::Completion)
649 {
650 restart = Some((settings, RestartCause::Completion));
651 }
652 }
653 (DesiredJobState::Running, Err(error)) => {
654 let reason = JobExitReason::Failed(error);
655 entry.last_exit_reason = Some(reason.clone());
656 entry.state = JobState::Failed;
657 event = JobEventKind::Failed { reason };
658 if let Some(settings) = entry
659 .spec
660 .restart_policy
661 .settings_for(RestartCause::Failure)
662 {
663 restart = Some((settings, RestartCause::Failure));
664 }
665 }
666 }
667 }
668
669 emit_event(state, name, generation, event);
670 if let Some((settings, cause)) = restart {
671 schedule_restart(state, myself, name, generation, settings, cause);
672 }
673}
674
675fn handle_start_failure(
676 state: &mut RegistryState,
677 myself: &ActorRef<RegistryMessage>,
678 name: &str,
679 generation: u64,
680 error: StreamError,
681) {
682 let mut restart = None;
683 {
684 let Some(entry) = state.jobs.get_mut(name) else {
685 return;
686 };
687 let reason = JobExitReason::Failed(error);
688 entry.state = JobState::Failed;
689 entry.run = None;
690 entry.last_exit_at = Some(SystemTime::now());
691 entry.last_exit_reason = Some(reason.clone());
692 if let Some(settings) = entry
693 .spec
694 .restart_policy
695 .settings_for(RestartCause::Failure)
696 {
697 restart = Some(settings);
698 }
699 }
700
701 let reason = state
702 .jobs
703 .get(name)
704 .and_then(|entry| entry.last_exit_reason.clone())
705 .unwrap_or(JobExitReason::Failed(StreamError::AbruptTermination));
706 emit_event(state, name, generation, JobEventKind::Failed { reason });
707 if let Some(settings) = restart {
708 schedule_restart(
709 state,
710 myself,
711 name,
712 generation,
713 settings,
714 RestartCause::Failure,
715 );
716 }
717}
718
719fn schedule_restart(
720 state: &mut RegistryState,
721 myself: &ActorRef<RegistryMessage>,
722 name: &str,
723 generation: u64,
724 settings: datum::RestartSettings,
725 _cause: RestartCause,
726) {
727 let delay = {
728 let Some(entry) = state.jobs.get_mut(name) else {
729 return;
730 };
731 if entry.desired_state != DesiredJobState::Running {
732 return;
733 }
734 let Some(delay) = entry.backoff.next_delay(&settings) else {
735 entry.backoff_until = None;
736 return;
737 };
738 entry.state = JobState::BackingOff;
739 entry.backoff_until = Some(Instant::now() + delay);
740 delay
741 };
742
743 emit_event(
744 state,
745 name,
746 generation,
747 JobEventKind::RestartScheduled { delay },
748 );
749
750 let actor = myself.clone();
751 let name = name.to_owned();
752 tokio::spawn(async move {
753 tokio::time::sleep(delay).await;
754 let _ = actor.send_message(RegistryMessage::RestartReady {
755 name,
756 expected_generation: generation,
757 });
758 });
759}
760
761fn restart_ready(
762 state: &mut RegistryState,
763 myself: &ActorRef<RegistryMessage>,
764 name: &str,
765 expected_generation: u64,
766) {
767 let ready = state.jobs.get(name).is_some_and(|entry| {
768 entry.generation == expected_generation
769 && entry.state == JobState::BackingOff
770 && entry.desired_state == DesiredJobState::Running
771 });
772 if ready {
773 let _ = start_job(
774 state,
775 myself,
776 name,
777 StartKind::AutomaticRestart {
778 previous_generation: expected_generation,
779 },
780 );
781 }
782}
783
784fn abort_all_jobs(state: &mut RegistryState) {
785 let names = state.jobs.keys().cloned().collect::<Vec<_>>();
786 for name in names {
787 let _ = stop_job(state, &name, JobExitReason::Stopped);
788 }
789}
790
791fn snapshot(_state: &RegistryState, entry: &JobEntry) -> JobStatus {
792 JobStatus {
793 name: entry.spec.name.clone(),
794 job_id: entry.id,
795 state: entry.state,
796 desired_state: entry.desired_state,
797 generation: entry.generation,
798 starts_total: entry.starts_total,
799 restarts_total: entry.restarts_total,
800 last_start_at: entry.last_start_at,
801 last_exit_at: entry.last_exit_at,
802 last_exit_reason: entry.last_exit_reason.clone(),
803 backoff_until: entry.backoff_until,
804 drain_deadline: entry.drain_deadline,
805 drain_supported: entry.drain_supported,
806 active_streams: None,
807 instrumentation: None,
808 cluster: entry.spec.cluster.clone(),
809 }
810}
811
812fn emit_event(state: &mut RegistryState, name: &str, generation: u64, kind: JobEventKind) {
813 let Some(entry) = state.jobs.get(name) else {
814 return;
815 };
816 let event = JobEvent {
817 sequence: state.next_event_sequence,
818 timestamp: SystemTime::now(),
819 name: entry.spec.name.clone(),
820 job_id: entry.id,
821 generation,
822 kind,
823 };
824 state.next_event_sequence += 1;
825 match state.events.try_publish(event) {
826 Ok(()) | Err(TopicTryPublishError::Full(_)) | Err(TopicTryPublishError::Busy(_)) => {}
827 Err(TopicTryPublishError::Closed(_)) => {}
828 }
829}
830
831fn invoke_factory(spec: &JobSpec, context: JobContext) -> StreamResult<RunnableGraph<JobMat>> {
832 catch_unwind(AssertUnwindSafe(|| (spec.factory)(context)))
833 .unwrap_or_else(|_| Err(StreamError::Failed("job factory panicked".to_owned())))
834}
835
836fn materialize_graph(
837 graph: &RunnableGraph<JobMat>,
838 materializer: &Materializer,
839) -> StreamResult<JobMat> {
840 catch_unwind(AssertUnwindSafe(|| {
841 graph.run_with_materializer(materializer)
842 }))
843 .unwrap_or_else(|_| {
844 Err(StreamError::Failed(
845 "job materialization panicked".to_owned(),
846 ))
847 })
848}
849
850fn send_reply<T>(reply: Reply<T>, result: AgentResult<T>) {
851 let _ = reply.send(result);
852}
853
854fn spawn_completion_poller(actor: ActorRef<RegistryMessage>, interval: Duration) {
855 tokio::spawn(async move {
856 loop {
857 tokio::time::sleep(interval).await;
858 if actor.send_message(RegistryMessage::Tick).is_err() {
859 break;
860 }
861 }
862 });
863}
864
865fn agent_runtime() -> AgentResult<&'static TokioRuntime> {
866 static RUNTIME: OnceLock<Result<TokioRuntime, String>> = OnceLock::new();
867 match RUNTIME.get_or_init(|| {
868 TokioRuntimeBuilder::new_multi_thread()
869 .thread_name("datum-agent-ractor")
870 .enable_all()
871 .build()
872 .map_err(|error| format!("agent runtime failed to start: {error}"))
873 }) {
874 Ok(runtime) => Ok(runtime),
875 Err(error) => Err(AgentError::Actor(error.clone())),
876 }
877}
878
879fn block_on_agent_runtime<T, F>(future: F) -> AgentResult<T>
880where
881 T: Send + 'static,
882 F: Future<Output = T> + Send + 'static,
883{
884 let runtime = agent_runtime()?;
885 if tokio::runtime::Handle::try_current().is_ok() {
886 std::thread::spawn(move || runtime.block_on(future))
887 .join()
888 .map_err(|_| AgentError::Actor("agent runtime task panicked".to_owned()))
889 } else {
890 Ok(runtime.block_on(future))
891 }
892}
893
894fn next_random_fraction() -> f64 {
895 static BACKOFF_RANDOM_SEED: AtomicU64 = AtomicU64::new(0x9e37_79b9_7f4a_7c15);
896 let mut current = BACKOFF_RANDOM_SEED.load(Ordering::Relaxed);
897 loop {
898 let mut next = current;
899 next ^= next << 13;
900 next ^= next >> 7;
901 next ^= next << 17;
902 match BACKOFF_RANDOM_SEED.compare_exchange_weak(
903 current,
904 next,
905 Ordering::Relaxed,
906 Ordering::Relaxed,
907 ) {
908 Ok(_) => return ((next >> 11) as f64) / ((1_u64 << 53) as f64),
909 Err(observed) => current = observed,
910 }
911 }
912}