1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Calendar-aware SLA obligations (issue #1826).
//!
//! An obligation is a deadline in business time: "respond within 2 business
//! days". The clock of an obligation runs only in the working time of a
//! [`BusinessCalendar`]. It stops on weekends, on holidays and outside working
//! hours.
//!
//! The engine reads time only from the injected clock. It does not read the
//! system clock. Thus a test can move a quarter of business time forward in
//! less than one second.
//!
//! # Parts
//!
//! - [`BusinessCalendar`]: working hours, weekends and holidays.
//! - [`BusinessDuration`]: a budget such as `"2 business days"`.
//! - [`Obligation`]: a deadline on one subject (for example, one ticket).
//! - [`ObligationStatus`]: the remaining budget and the breach state.
//! - [`SlaPlugin`]: installs the calendars, the store and the breach handlers.
//! - [`Sla`]: the handler extractor. Use it to track, meet and read obligations.
//! - [`SlaBreach`]: the typed escalation payload.
//!
//! # Example
//!
//! ```rust,no_run
//! use autumn_web::prelude::*;
//! use autumn_web::sla::{BusinessCalendar, Obligation, Sla, SlaPlugin, WorkingHours};
//!
//! #[post("/tickets/{id}")]
//! async fn open_ticket(sla: Sla, Path(id): Path<i64>) -> AutumnResult<String> {
//! let obligation = Obligation::new("first_response", format!("ticket:{id}"))
//! .within("2 business days".parse()?)
//! .calendar("support");
//! let status = sla.track(&obligation).await?;
//! Ok(format!("due at {:?}", status.due_at))
//! }
//!
//! # fn plugin() -> Result<SlaPlugin, autumn_web::sla::SlaError> {
//! let plugin = SlaPlugin::new()
//! .calendar("support", BusinessCalendar::weekdays("09:00-17:00".parse()?))
//! .on_breach("first_response", |_state, breach| async move {
//! tracing::warn!(key = %breach.key, "first response is late");
//! Ok(())
//! });
//! # Ok(plugin) }
//! ```
//!
//! # Exactly once
//!
//! [`Sla::track`] puts a check job on the job queue at the deadline. At the
//! deadline the check job reads the store. If the obligation was not met in
//! time, it claims the escalation in the store. Then it puts one
//! [`ESCALATE_JOB`] on the queue. The claim is the lock: a second check does
//! not claim again. The unique key of [`ESCALATE_JOB`] stops a duplicate
//! while one is on the queue.
//!
//! The default [`MemoryObligationStore`] is local to one process. For more than
//! one replica, all replicas must use one shared [`ObligationStore`].
//!
//! See `docs/guide/sla.md` for the guide.
// autumn-determinism-gate: production code in this module must read time and
// mint identifiers through the framework's injected seams (ClockSource /
// Entropy), never `Instant::now()` / `Utc::now()` / `SystemTime::now()` /
// `Uuid::new_v4()` directly. See CONTRIBUTING.md "Determinism seam gate"
// (issue #1797). Justify exceptions with
// #[allow(clippy::disallowed_methods, reason = "…")] at the narrowest scope.
// autumn-panic-gate: request-path module — production code path must be panic-free.
// See CONTRIBUTING.md "Request-path panic gate". Justify exceptions with
// #[allow(clippy::<lint>, reason = "…")] at the narrowest scope.
pub use ;
pub use BusinessDuration;
pub use ;
pub use ;
pub use ;
/// An error from the SLA engine.