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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! The drain/shutdown hook seam (AP2.1-10).
//!
//! [`DrainHook`] is the trait the Realtime lane (AP2.1-8) and the jobs lane
//! implement to participate in the coordinated graceful drain. The engine
//! calls every registered hook **after** the HTTP listener stops accepting
//! and drains in-flight HTTP connections, and **before** the engine's
//! subsystem resources (database, cache, storage, mail) are closed. The
//! hook is the place to close long-lived connections (WebSockets, SSE
//! streams, active job tasks) so the engine's resource teardown sees no
//! outstanding work.
//!
//! # Ordering contract (precise — for the master wiring AP2.1-8)
//!
//! On a termination signal the [`super::Lifecycle`] orchestration runs:
//!
//! 1. `lifecycle.begin_drain()` — readiness goes false **first** (the
//! upstream load balancer observing `/up/ready` stops sending traffic).
//! 2. The HTTP listener stops accepting new connections (axum graceful
//! shutdown is triggered).
//! 3. **Concurrently**, every registered [`DrainHook::drain`] is invoked so
//! long-lived connections (WebSockets) close and the in-flight HTTP drain
//! completes. The hooks and the HTTP drain race; the orchestration awaits
//! both.
//! 4. The engine's [`super::super::shutdown`] tears down subsystem resources
//! in reverse startup order (mail, storage, cache, worker, db).
//! 5. `lifecycle.mark_stopped()` — the process is about to exit.
//!
//! A hook that needs to close WebSockets should signal them to close in
//! `drain()` and await their closure. The Realtime lane's
//! `realtime::shutdown()` (AP2.1-8, self-contained this wave) is the
//! expected implementor; the master wires it by registering a `DrainHook`
//! that calls `realtime::shutdown().await` inside `drain()`.
//!
//! # Failure semantics
//!
//! A hook returning `Err` is recorded; the orchestration continues draining
//! the remaining hooks and resources — a partial drain is better than
//! leaving connections open. The first hook error is surfaced to the
//! operator alongside the shutdown result. A hook that panics is caught by
//! the orchestration's `JoinHandle` and recorded as a [`DrainError::Hook`];
//! it does not abort the drain (AGENTS.md §17: no panic path aborts
//! production teardown).
//!
//! # Feature gating
//!
//! The [`DrainHook`] trait, [`ShutdownHooks`] registration, and
//! [`DrainError`] are pure `std` — they compile with no feature flags so an
//! expert user on a custom runtime can register hooks and drive their own
//! execution. The concurrent [`ShutdownHooks::run`] executor uses the
//! certified Tokio runtime (`tokio::spawn`) and is therefore gated behind
//! the `macros` feature. Without `macros`, a caller drives hooks via
//! [`ShutdownHooks::iter`] (sequential, runtime-agnostic) or their own
//! executor.
use Arc;
/// A typed drain/shutdown hook failure. Preserved (not collapsed to `String`)
/// so the operator sees which hook failed and why (AGENTS.md §18).
/// The trait a lane implements to participate in the coordinated drain.
///
/// Register an implementor with
/// [`Lifecycle::register_drain_hook`](super::Lifecycle::register_drain_hook)
/// during startup (typically from the `state_fn`). The engine invokes
/// [`DrainHook::drain`] on every registered hook during shutdown, after the
/// HTTP listener stops accepting and in-flight HTTP is draining.
///
/// `drain()` must be idempotent and safe to call once. It should signal
/// long-lived connections to close and await their closure; it must not
/// close resources the engine's subsystem shutdown owns (the database pool,
/// cache socket, etc. — those are closed by the engine after the hooks).
/// The registry of drain hooks for one [`Lifecycle`](super::Lifecycle).
///
/// Built during startup; invoked once during shutdown. Stored behind an
/// `Arc` so the lifecycle handle and the orchestration share the same list.
pub