1#![cfg_attr(
12 not(test),
13 deny(
14 clippy::unwrap_used,
15 clippy::unreachable,
16 clippy::expect_used,
17 clippy::panic,
18 clippy::unimplemented,
19 clippy::todo
20 )
21)]
22
23use std::future::Future;
24
25#[cfg(feature = "db-writer")]
26mod db_writer;
27
28use aster_forge_runtime::{
29 RuntimeComponentBundleRegistration, RuntimeComponentKind, RuntimeComponentRegistry,
30 StartupPhaseFailurePolicy, runtime_component,
31};
32
33#[cfg(feature = "db-writer")]
34pub use db_writer::{
35 AUDIT_WRITER_START_PHASE, AuditLogBufferConfig, AuditWriter, DEFAULT_AUDIT_LOG_BATCH_SIZE,
36 DEFAULT_AUDIT_LOG_DELAYED_FLUSH_AFTER, DEFAULT_AUDIT_LOG_QUEUE_CAPACITY,
37 audit_component_with_writer, audit_component_with_writer_after, audit_writer_component,
38};
39
40pub const AUDIT_LOGS_COMPONENT: &str = "audit_logs";
42pub const AUDIT_MANAGER_COMPONENT: &str = "audit_manager";
44pub const SERVER_START_AUDIT_PHASE: &str = "server_start_audit";
46pub const SERVER_SHUTDOWN_AUDIT_PHASE: &str = "server_shutdown_audit";
48pub const AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE: &str = "audit_manager_flush";
50
51#[cfg(feature = "mail-outbox-dependency")]
52const DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES: &[&str] =
53 &[aster_forge_mail::MAIL_OUTBOX_COMPONENT];
54#[cfg(not(feature = "mail-outbox-dependency"))]
55const DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES: &[&str] = &[];
56
57pub fn audit_component<T, StartFn, StartFut, ShutdownFn, ShutdownFut, FlushFn, FlushFut>(
66 resources: T,
67 record_server_start: StartFn,
68 record_server_shutdown: ShutdownFn,
69 flush_audit_manager: FlushFn,
70) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
71where
72 T: Clone + Send + 'static,
73 StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
74 StartFut: Future<Output = Result<(), String>> + Send + 'static,
75 ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
76 ShutdownFut: Future<Output = Result<(), String>> + Send + 'static,
77 FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
78 FlushFut: Future<Output = Result<(), String>> + Send + 'static,
79{
80 audit_component_after(
81 resources,
82 DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
83 record_server_start,
84 record_server_shutdown,
85 flush_audit_manager,
86 )
87}
88
89pub fn audit_component_infallible<
96 T,
97 StartFn,
98 StartFut,
99 ShutdownFn,
100 ShutdownFut,
101 FlushFn,
102 FlushFut,
103>(
104 resources: T,
105 record_server_start: StartFn,
106 record_server_shutdown: ShutdownFn,
107 flush_audit_manager: FlushFn,
108) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
109where
110 T: Clone + Send + 'static,
111 StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
112 StartFut: Future<Output = ()> + Send + 'static,
113 ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
114 ShutdownFut: Future<Output = ()> + Send + 'static,
115 FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
116 FlushFut: Future<Output = ()> + Send + 'static,
117{
118 audit_component_after_infallible(
119 resources,
120 DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
121 record_server_start,
122 record_server_shutdown,
123 flush_audit_manager,
124 )
125}
126
127pub fn audit_component_after<T, StartFn, StartFut, ShutdownFn, ShutdownFut, FlushFn, FlushFut>(
134 resources: T,
135 shutdown_dependencies: &'static [&'static str],
136 record_server_start: StartFn,
137 record_server_shutdown: ShutdownFn,
138 flush_audit_manager: FlushFn,
139) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
140where
141 T: Clone + Send + 'static,
142 StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
143 StartFut: Future<Output = Result<(), String>> + Send + 'static,
144 ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
145 ShutdownFut: Future<Output = Result<(), String>> + Send + 'static,
146 FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
147 FlushFut: Future<Output = Result<(), String>> + Send + 'static,
148{
149 let startup_resources = resources.clone();
150 let server_start = server_start_audit_component(startup_resources, record_server_start);
151 let server_shutdown = server_shutdown_audit_component_after(
152 resources,
153 shutdown_dependencies,
154 record_server_shutdown,
155 );
156 let audit_manager = audit_manager_component(flush_audit_manager);
157
158 runtime_component(move |registry: &mut RuntimeComponentRegistry| {
159 registry
160 .register_bundle(server_start)
161 .register_bundle(server_shutdown)
162 .register_bundle(audit_manager);
163 })
164}
165
166pub fn audit_component_after_infallible<
172 T,
173 StartFn,
174 StartFut,
175 ShutdownFn,
176 ShutdownFut,
177 FlushFn,
178 FlushFut,
179>(
180 resources: T,
181 shutdown_dependencies: &'static [&'static str],
182 record_server_start: StartFn,
183 record_server_shutdown: ShutdownFn,
184 flush_audit_manager: FlushFn,
185) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
186where
187 T: Clone + Send + 'static,
188 StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
189 StartFut: Future<Output = ()> + Send + 'static,
190 ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
191 ShutdownFut: Future<Output = ()> + Send + 'static,
192 FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
193 FlushFut: Future<Output = ()> + Send + 'static,
194{
195 audit_component_after(
196 resources,
197 shutdown_dependencies,
198 move |resources| async move {
199 record_server_start(resources).await;
200 Ok(())
201 },
202 move |resources| async move {
203 record_server_shutdown(resources).await;
204 Ok(())
205 },
206 move |()| async move {
207 flush_audit_manager(()).await;
208 Ok(())
209 },
210 )
211}
212
213pub fn shutdown_audit_component<T, RecordFn, RecordFut, FlushFn, FlushFut>(
221 resources: T,
222 record_server_shutdown: RecordFn,
223 flush_audit_manager: FlushFn,
224) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
225where
226 T: Send + 'static,
227 RecordFn: FnOnce(T) -> RecordFut + Send + Sync + 'static,
228 RecordFut: Future<Output = Result<(), String>> + Send + 'static,
229 FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
230 FlushFut: Future<Output = Result<(), String>> + Send + 'static,
231{
232 shutdown_audit_component_after(
233 resources,
234 DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
235 record_server_shutdown,
236 flush_audit_manager,
237 )
238}
239
240pub fn shutdown_audit_component_after<T, RecordFn, RecordFut, FlushFn, FlushFut>(
243 resources: T,
244 shutdown_dependencies: &'static [&'static str],
245 record_server_shutdown: RecordFn,
246 flush_audit_manager: FlushFn,
247) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
248where
249 T: Send + 'static,
250 RecordFn: FnOnce(T) -> RecordFut + Send + Sync + 'static,
251 RecordFut: Future<Output = Result<(), String>> + Send + 'static,
252 FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
253 FlushFut: Future<Output = Result<(), String>> + Send + 'static,
254{
255 let server_shutdown = server_shutdown_audit_component_after(
256 resources,
257 shutdown_dependencies,
258 record_server_shutdown,
259 );
260 let audit_manager = audit_manager_component(flush_audit_manager);
261
262 runtime_component(move |registry: &mut RuntimeComponentRegistry| {
263 registry
264 .register_bundle(server_shutdown)
265 .register_bundle(audit_manager);
266 })
267}
268
269pub fn server_start_audit_component<T, F, Fut>(
271 resources: T,
272 record_server_start: F,
273) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
274where
275 T: Send + 'static,
276 F: FnOnce(T) -> Fut + Send + Sync + 'static,
277 Fut: Future<Output = Result<(), String>> + Send + 'static,
278{
279 runtime_component(move |registry: &mut RuntimeComponentRegistry| {
280 let mut resources = Some(resources);
281 let mut record_server_start = Some(record_server_start);
282 registry.component_startup(
283 AUDIT_LOGS_COMPONENT,
284 RuntimeComponentKind::Product,
285 SERVER_START_AUDIT_PHASE,
286 StartupPhaseFailurePolicy::Required,
287 move || {
288 let resources = resources.take();
289 let record_server_start = record_server_start.take();
290 async move {
291 let Some(resources) = resources else {
292 return Err(
293 "server start audit startup phase resources already consumed"
294 .to_string(),
295 );
296 };
297 let Some(record_server_start) = record_server_start else {
298 return Err("server start audit startup phase callback already consumed"
299 .to_string());
300 };
301 record_server_start(resources).await
302 }
303 },
304 );
305 })
306}
307
308pub fn server_shutdown_audit_component<T, F, Fut>(
314 resources: T,
315 record_server_shutdown: F,
316) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<T>>
317where
318 T: Send + 'static,
319 F: FnOnce(T) -> Fut + Send + Sync + 'static,
320 Fut: Future<Output = Result<(), String>> + Send + 'static,
321{
322 server_shutdown_audit_component_after(
323 resources,
324 DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
325 record_server_shutdown,
326 )
327}
328
329pub fn server_shutdown_audit_component_after<T, F, Fut>(
331 resources: T,
332 shutdown_dependencies: &'static [&'static str],
333 record_server_shutdown: F,
334) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<T>>
335where
336 T: Send + 'static,
337 F: FnOnce(T) -> Fut + Send + Sync + 'static,
338 Fut: Future<Output = Result<(), String>> + Send + 'static,
339{
340 aster_forge_runtime::shutdown_resource_component_after(
341 AUDIT_LOGS_COMPONENT,
342 RuntimeComponentKind::Product,
343 SERVER_SHUTDOWN_AUDIT_PHASE,
344 shutdown_dependencies,
345 resources,
346 record_server_shutdown,
347 )
348}
349
350pub fn audit_manager_component<F, Fut>(
352 flush_audit_manager: F,
353) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<()>>
354where
355 F: FnOnce(()) -> Fut + Send + Sync + 'static,
356 Fut: Future<Output = Result<(), String>> + Send + 'static,
357{
358 aster_forge_runtime::shutdown_resource_component_after(
359 AUDIT_MANAGER_COMPONENT,
360 RuntimeComponentKind::Product,
361 AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE,
362 &[AUDIT_LOGS_COMPONENT],
363 (),
364 flush_audit_manager,
365 )
366}
367
368#[cfg(test)]
369mod tests {
370 use std::sync::{
371 Arc,
372 atomic::{AtomicUsize, Ordering},
373 };
374
375 use aster_forge_runtime::RuntimeComponentBundle;
376
377 use super::{
378 AUDIT_LOGS_COMPONENT, AUDIT_MANAGER_COMPONENT, AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE,
379 SERVER_SHUTDOWN_AUDIT_PHASE, SERVER_START_AUDIT_PHASE, audit_component,
380 audit_component_after, audit_component_after_infallible, audit_component_infallible,
381 audit_manager_component, server_shutdown_audit_component,
382 server_shutdown_audit_component_after, server_start_audit_component,
383 shutdown_audit_component,
384 };
385
386 #[cfg(not(feature = "mail-outbox-dependency"))]
387 #[test]
388 fn server_shutdown_audit_component_registers_without_dependencies() {
389 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
390 server_shutdown_audit_component((), |()| async { Ok(()) }).register(registry);
391 });
392
393 let descriptor = registry
394 .descriptor(AUDIT_LOGS_COMPONENT)
395 .expect("audit logs component should be registered");
396 assert_eq!(
397 descriptor.kind,
398 aster_forge_runtime::RuntimeComponentKind::Product
399 );
400 assert!(descriptor.dependencies.is_empty());
401 assert_eq!(
402 descriptor
403 .shutdown
404 .first()
405 .expect("audit logs shutdown should be registered")
406 .phase_name,
407 SERVER_SHUTDOWN_AUDIT_PHASE
408 );
409 }
410
411 #[cfg(feature = "mail-outbox-dependency")]
412 #[test]
413 fn server_shutdown_audit_component_registers_mail_outbox_dependency() {
414 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
415 server_shutdown_audit_component((), |()| async { Ok(()) }).register(registry);
416 });
417
418 let descriptor = registry
419 .descriptor(AUDIT_LOGS_COMPONENT)
420 .expect("audit logs component should be registered");
421 assert_eq!(
422 descriptor.kind,
423 aster_forge_runtime::RuntimeComponentKind::Product
424 );
425 assert_eq!(
426 descriptor.dependencies,
427 vec![aster_forge_mail::MAIL_OUTBOX_COMPONENT]
428 );
429 assert_eq!(
430 descriptor
431 .shutdown
432 .first()
433 .expect("audit logs shutdown should be registered")
434 .phase_name,
435 SERVER_SHUTDOWN_AUDIT_PHASE
436 );
437 }
438
439 #[test]
440 fn server_shutdown_audit_component_after_registers_caller_dependencies() {
441 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
442 server_shutdown_audit_component_after(
443 (),
444 &["background_tasks", "mail_outbox"],
445 |()| async { Ok(()) },
446 )
447 .register(registry);
448 });
449
450 let descriptor = registry
451 .descriptor(AUDIT_LOGS_COMPONENT)
452 .expect("audit logs component should be registered");
453 assert_eq!(
454 descriptor.dependencies,
455 vec!["background_tasks", "mail_outbox"]
456 );
457 }
458
459 #[tokio::test]
460 async fn server_start_audit_component_registers_and_runs_startup_phase() {
461 let calls = Arc::new(AtomicUsize::new(0));
462 let calls_for_start = calls.clone();
463 let mut registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
464 server_start_audit_component((), move |()| {
465 let calls = calls_for_start.clone();
466 async move {
467 calls.fetch_add(1, Ordering::SeqCst);
468 Ok(())
469 }
470 })
471 .register(registry);
472 });
473
474 let descriptor = registry
475 .descriptor(AUDIT_LOGS_COMPONENT)
476 .expect("audit logs component should be registered");
477 assert_eq!(descriptor.startup.len(), 1);
478 assert_eq!(descriptor.startup[0].phase_name, SERVER_START_AUDIT_PHASE);
479
480 let report = registry.startup().await;
481
482 assert!(!report.aborted());
483 assert_eq!(calls.load(Ordering::SeqCst), 1);
484 }
485
486 #[test]
487 fn audit_manager_component_registers_after_audit_logs() {
488 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
489 audit_manager_component(|()| async { Ok(()) }).register(registry);
490 });
491
492 let descriptor = registry
493 .descriptor(AUDIT_MANAGER_COMPONENT)
494 .expect("audit manager component should be registered");
495 assert_eq!(descriptor.dependencies, vec![AUDIT_LOGS_COMPONENT]);
496 assert_eq!(
497 descriptor
498 .shutdown
499 .first()
500 .expect("audit manager shutdown should be registered")
501 .phase_name,
502 AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE
503 );
504 }
505
506 #[tokio::test]
507 async fn audit_component_registers_startup_shutdown_and_manager_flush() {
508 let order = Arc::new(std::sync::Mutex::new(Vec::new()));
509 let order_for_start = order.clone();
510 let order_for_record = order.clone();
511 let order_for_flush = order.clone();
512
513 let mut registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
514 audit_component(
515 (),
516 move |()| {
517 let order = order_for_start.clone();
518 async move {
519 order
520 .lock()
521 .expect("audit component test order should lock")
522 .push("start");
523 Ok(())
524 }
525 },
526 move |()| {
527 let order = order_for_record.clone();
528 async move {
529 order
530 .lock()
531 .expect("audit component test order should lock")
532 .push("record");
533 Ok(())
534 }
535 },
536 move |()| {
537 let order = order_for_flush.clone();
538 async move {
539 order
540 .lock()
541 .expect("audit component test order should lock")
542 .push("flush");
543 Ok(())
544 }
545 },
546 )
547 .register(registry);
548 });
549
550 let startup_report = registry.startup().await;
551 assert!(!startup_report.aborted());
552 let shutdown_report = registry.shutdown().await;
553
554 assert!(!shutdown_report.has_failures());
555 assert_eq!(
556 order
557 .lock()
558 .expect("audit component test order should lock")
559 .as_slice(),
560 ["start", "record", "flush"]
561 );
562 }
563
564 #[tokio::test]
565 async fn infallible_audit_component_runs_unit_returning_hooks_in_order() {
566 let order = Arc::new(std::sync::Mutex::new(Vec::new()));
567 let order_for_start = order.clone();
568 let order_for_record = order.clone();
569 let order_for_flush = order.clone();
570
571 let mut registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
572 audit_component_infallible(
573 (),
574 move |()| {
575 let order = order_for_start.clone();
576 async move {
577 order
578 .lock()
579 .expect("audit component test order should lock")
580 .push("start");
581 }
582 },
583 move |()| {
584 let order = order_for_record.clone();
585 async move {
586 order
587 .lock()
588 .expect("audit component test order should lock")
589 .push("record");
590 }
591 },
592 move |()| {
593 let order = order_for_flush.clone();
594 async move {
595 order
596 .lock()
597 .expect("audit component test order should lock")
598 .push("flush");
599 }
600 },
601 )
602 .register(registry);
603 });
604
605 let startup_report = registry.startup().await;
606 assert!(!startup_report.aborted());
607 let shutdown_report = registry.shutdown().await;
608
609 assert!(!shutdown_report.has_failures());
610 assert_eq!(
611 order
612 .lock()
613 .expect("audit component test order should lock")
614 .as_slice(),
615 ["start", "record", "flush"]
616 );
617 }
618
619 #[test]
620 fn audit_component_after_registers_shutdown_dependencies() {
621 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
622 audit_component_after(
623 (),
624 &["mail_outbox"],
625 |()| async { Ok(()) },
626 |()| async { Ok(()) },
627 |()| async { Ok(()) },
628 )
629 .register(registry);
630 });
631
632 let descriptor = registry
633 .descriptor(AUDIT_LOGS_COMPONENT)
634 .expect("audit logs component should be registered");
635 assert_eq!(descriptor.dependencies, vec!["mail_outbox"]);
636 }
637
638 #[test]
639 fn infallible_audit_component_after_registers_shutdown_dependencies() {
640 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
641 audit_component_after_infallible(
642 (),
643 &["background_tasks"],
644 |()| async {},
645 |()| async {},
646 |()| async {},
647 )
648 .register(registry);
649 });
650
651 let descriptor = registry
652 .descriptor(AUDIT_LOGS_COMPONENT)
653 .expect("audit logs component should be registered");
654 assert_eq!(descriptor.dependencies, vec!["background_tasks"]);
655 }
656
657 #[cfg(feature = "mail-outbox-dependency")]
658 #[test]
659 fn audit_component_registers_mail_outbox_dependency_when_feature_enabled() {
660 let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
661 audit_component(
662 (),
663 |()| async { Ok(()) },
664 |()| async { Ok(()) },
665 |()| async { Ok(()) },
666 )
667 .register(registry);
668 });
669
670 let descriptor = registry
671 .descriptor(AUDIT_LOGS_COMPONENT)
672 .expect("audit logs component should be registered");
673 assert_eq!(
674 descriptor.dependencies,
675 vec![aster_forge_mail::MAIL_OUTBOX_COMPONENT]
676 );
677 }
678
679 #[tokio::test]
680 async fn shutdown_audit_component_runs_shutdown_record_before_manager_flush() {
681 let order = Arc::new(std::sync::Mutex::new(Vec::new()));
682 let order_for_record = order.clone();
683 let order_for_flush = order.clone();
684
685 let mut registry = aster_forge_runtime::RuntimeComponentRegistry::new();
686 registry.register_bundle(shutdown_audit_component(
687 (),
688 move |()| {
689 let order = order_for_record.clone();
690 async move {
691 order
692 .lock()
693 .expect("audit component test order should lock")
694 .push("record");
695 Ok(())
696 }
697 },
698 move |()| {
699 let order = order_for_flush.clone();
700 async move {
701 order
702 .lock()
703 .expect("audit component test order should lock")
704 .push("flush");
705 Ok(())
706 }
707 },
708 ));
709 let report = registry.shutdown().await;
710
711 assert!(!report.has_failures());
712 assert_eq!(
713 order
714 .lock()
715 .expect("audit component test order should lock")
716 .as_slice(),
717 ["record", "flush"]
718 );
719 }
720}