ruststream_sqlx/inbox/broker/connected.rs
1//! The connected inbox broker, the state its handles share, and the closed witness `shutdown`
2//! returns.
3
4use std::fmt;
5use std::future::Future;
6use std::num::NonZeroU32;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex, PoisonError};
9use std::time::Duration;
10
11use futures::future::join_all;
12use ruststream::ConnectedBroker;
13use ruststream_sqlx_dialect::Dialect;
14use sqlx::{Database, Pool};
15use tokio::runtime::Handle;
16use tokio_util::sync::CancellationToken;
17
18use crate::inbox::FormDialect;
19use crate::inbox::database::notify::{self, Listening};
20use crate::inbox::database::{BuiltIn, QueueDatabase};
21use crate::inbox::error::SqlxBrokerError;
22use crate::inbox::form::advisory::LockBook;
23use crate::inbox::form::advisory::session::Closing;
24use crate::inbox::publish::{Routes, TableWake, Wakes};
25use crate::inbox::threads::InboxThreads;
26
27use super::{PoolOwner, SqlxBroker};
28
29/// What every handle of one connection shares.
30pub(crate) struct Shared<DB: Database> {
31 pub(crate) pool: Pool<DB>,
32 /// Who closes the pool: the broker closes one it built, at the end of `shutdown`.
33 pool_owner: PoolOwner,
34 /// The routes, each with the form of its table on the connection's dialect.
35 pub(crate) routes: Routes<DB, FormDialect, &'static TableWake>,
36 /// The wake-ups of the subscriptions of each table the connection's publishers write.
37 pub(crate) wakes: Wakes,
38 pub(crate) poll_interval: Duration,
39 /// The lease a subscription in the lease form takes, unless it names its own.
40 pub(crate) lease: Duration,
41 /// The runtime `connect` ran on: the lease keepers run there, and so do the releases of lease
42 /// deliveries dropped unsettled.
43 pub(crate) runtime: Handle,
44 /// The most connections the service opens on the database, where it set a limit.
45 connection_limit: Option<NonZeroU32>,
46 /// The connections the dedicated threads of the subscriptions opened so far open at most.
47 thread_connections: Mutex<u64>,
48 /// The `Closed` flag: set by `shutdown`, read with one atomic load per publish and per claim.
49 closed: AtomicBool,
50 /// Wakes the claim loops waiting for their next claim when `shutdown` sets the flag, and stops
51 /// the lease keepers, whose tokens are its children.
52 pub(crate) stopping: CancellationToken,
53 /// The queues this connection reads, by table and group; a table without groups is one queue.
54 pub(crate) queues: Mutex<Vec<(&'static str, Option<String>)>>,
55 /// The books of the connection's advisory subscriptions, each with the deliveries in work and
56 /// the sessions that hold their locks.
57 pub(crate) locks: Mutex<Vec<&'static LockBook<DB>>>,
58 /// The sessions of the connection being closed after they ended holding a lock or a
59 /// transaction: `shutdown` waits until none is.
60 pub(crate) closing: &'static Closing,
61 /// The listening connection, with `listen_notify`.
62 pub(crate) listening: Option<Listening>,
63 /// The test harness's books of this connection.
64 #[cfg(feature = "testing")]
65 pub(crate) harness: crate::inbox::testing::Harness,
66}
67
68impl<DB: Database> Shared<DB> {
69 /// Whether `shutdown` ran: the flag every handle of the connection reads before it works.
70 pub(crate) fn is_closed(&self) -> bool {
71 self.closed.load(Ordering::Acquire)
72 }
73
74 /// Counts `threads`, the dedicated threads of the subscription `name` opens, against the
75 /// connection limit: the pool's size and every subscription's threads so far must fit it.
76 ///
77 /// # Errors
78 ///
79 /// [`SqlxBrokerError::ConnectionLimit`] when the sum passes the limit; the threads are not
80 /// counted then.
81 pub(crate) fn count_threads(
82 &self,
83 name: &str,
84 threads: &InboxThreads,
85 ) -> Result<(), SqlxBrokerError> {
86 let pool = u64::from(self.pool.options().get_max_connections());
87 let mut counted = self
88 .thread_connections
89 .lock()
90 .unwrap_or_else(PoisonError::into_inner);
91 let wanted = counted.saturating_add(threads.total_connections());
92 if let Some(limit) = self.connection_limit
93 && pool.saturating_add(wanted) > u64::from(limit.get())
94 {
95 drop(counted);
96 return Err(SqlxBrokerError::ConnectionLimit {
97 subscription: name.to_owned(),
98 limit: limit.get(),
99 pool,
100 threads: wanted,
101 });
102 }
103 *counted = wanted;
104 drop(counted);
105 Ok(())
106 }
107}
108
109/// The connected inbox broker: the typed witness that the database answered.
110///
111/// It hands out publishers and opens subscriptions; [`shutdown`](ConnectedBroker::shutdown) stops
112/// its claim loops and the extension of its leases, and refuses every handle it handed out. It
113/// returns once the server has ended every claim the runtime cut short as it stopped the
114/// subscriptions. A delivery in work keeps its lease after `shutdown` and settles as before; its
115/// lease is no longer extended. In the advisory lock form `shutdown` releases the lock of every delivery in
116/// work and returns once no lock of the broker is left; such a delivery's settlement then fails
117/// with [`SqlxBrokerError::Closed`].
118///
119/// # Examples
120///
121/// ```
122/// # #[cfg(feature = "postgres")]
123/// # mod demo {
124/// use ruststream::OutgoingMessage;
125/// use ruststream_sqlx::prelude::*;
126/// use serde::Serialize;
127/// use sqlx::{PgConnection, PgPool, Postgres};
128///
129/// #[derive(Inbox, sqlx::FromRow)]
130/// #[inbox(table = "cleanup_jobs")]
131/// pub struct Cleanup {
132/// #[field(id, generated)]
133/// id: i64,
134/// #[field(payload)]
135/// payload: Vec<u8>,
136/// }
137///
138/// impl Publish<Postgres> for Cleanup {
139/// async fn publish(
140/// conn: &mut PgConnection,
141/// message: &OutgoingMessage<'_>,
142/// ) -> Result<(), sqlx::Error> {
143/// sqlx::query("INSERT INTO cleanup_jobs (payload) VALUES ($1)")
144/// .bind(message.payload())
145/// .execute(conn)
146/// .await?;
147/// Ok(())
148/// }
149/// }
150///
151/// #[derive(Serialize, Outgoing)]
152/// #[outgoing(name = "cleanup")]
153/// struct Sweep {
154/// older_than_days: u32,
155/// }
156///
157/// pub fn app(pool: PgPool) -> RustStream {
158/// RustStream::new(AppInfo::new("maintenance", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
159/// // Once the broker connected, the policy pairs against the `ConnectedSqlxBroker` and the
160/// // hook schedules the first sweep.
161/// b.after_startup(Repository::<Cleanup>::default(), async move |cleanups| {
162/// cleanups.message(&Sweep { older_than_days: 30 }).publish().await
163/// });
164/// })
165/// }
166/// # }
167/// # fn main() {}
168/// ```
169pub struct ConnectedSqlxBroker<DB: Database, D = BuiltIn<DB>> {
170 pub(crate) shared: Arc<Shared<DB>>,
171 /// The dialect the connection's subscriptions build their statements with.
172 pub(crate) dialect: Arc<D>,
173}
174
175impl<DB: Database, D: Dialect> fmt::Debug for ConnectedSqlxBroker<DB, D> {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 f.debug_struct("ConnectedSqlxBroker")
178 .field("dialect", &self.dialect.name())
179 .field("closed", &self.shared.is_closed())
180 .finish_non_exhaustive()
181 }
182}
183
184impl<DB: Database, D> ConnectedSqlxBroker<DB, D> {
185 /// The connected form of `broker` on `pool`, which `owner` closes, whose statements `dialect`
186 /// builds, whose internal tasks run on `runtime`, and which listens on `listening` with
187 /// `listen_notify`.
188 pub(crate) fn new(
189 broker: SqlxBroker<DB, D>,
190 (pool, pool_owner): (Pool<DB>, PoolOwner),
191 dialect: Arc<D>,
192 runtime: Handle,
193 listening: Option<Listening>,
194 ) -> Self {
195 let wakes = Wakes::default();
196 let routes = broker.routes.resolve(
197 |form_of| form_of(&dialect),
198 |route| wakes.table(&route.description().spec),
199 );
200 Self {
201 shared: Arc::new(Shared {
202 pool,
203 pool_owner,
204 routes,
205 wakes,
206 poll_interval: broker.poll_interval,
207 lease: broker.lease,
208 connection_limit: broker.connection_limit,
209 thread_connections: Mutex::new(0),
210 closing: Closing::leak(runtime.clone()),
211 runtime,
212 closed: AtomicBool::new(false),
213 stopping: CancellationToken::new(),
214 queues: Mutex::new(Vec::new()),
215 locks: Mutex::new(Vec::new()),
216 listening,
217 #[cfg(feature = "testing")]
218 harness: crate::inbox::testing::Harness::default(),
219 }),
220 dialect,
221 }
222 }
223}
224
225impl<DB: QueueDatabase, D: Dialect + 'static> ConnectedBroker for ConnectedSqlxBroker<DB, D> {
226 type Error = SqlxBrokerError;
227 type Closed = ClosedSqlxBroker;
228
229 /// Stops the claim loops and the extension of every lease, and refuses every handle handed
230 /// out before. A delivery in work in the lease or row lock form settles as before; its lease
231 /// runs out unless it settles first.
232 ///
233 /// A claim the runtime cut short when it stopped a subscription may still run on the server:
234 /// its connection waits for the server to end the claim's statement and roll its transaction
235 /// back, then closes, and `shutdown` waits for it, so the stopped broker holds no lock on the
236 /// table.
237 ///
238 /// In the advisory lock form it then releases the lock of every delivery in work: each
239 /// session unlocks its key and goes back to the pool, or closes where the database does not
240 /// confirm the release. It waits for each session lent to a settlement in flight, and for the
241 /// sessions still closing after a delivery or a claim was dropped, and returns once no lock of
242 /// the broker is left. In a service the runtime has stopped its subscriptions by then, and its
243 /// shutdown timeout bounds the handlers that hold a session.
244 ///
245 /// A pool the broker built ([`from_url`](SqlxBroker::from_url)) is closed last: it lends no
246 /// new connection, and a delivery in work keeps the one it holds, which closes when it comes
247 /// back. A settlement after shutdown that needs a new connection, as in the lease form, then
248 /// fails, and its row comes back when its lease runs out. The service's own pool
249 /// ([`new`](SqlxBroker::new)) stays open.
250 fn shutdown(self) -> impl Future<Output = Result<Self::Closed, Self::Error>> + Send {
251 // Why a flag rather than a type: a publisher handed out before shutdown may write through
252 // the service's pool, which stays open, so it would otherwise keep writing.
253 self.shared.closed.store(true, Ordering::Release);
254 self.shared.stopping.cancel();
255 self.shared.closing.begin_shutdown();
256 let shared = self.shared;
257 async move {
258 let books = shared
259 .locks
260 .lock()
261 .unwrap_or_else(PoisonError::into_inner)
262 .clone();
263 let released = join_all(books.into_iter().map(LockBook::release_all)).await;
264 // A session closing after its delivery or its claim was dropped may still hold its
265 // lock, and a claim the runtime cut short may still run on the server.
266 shared.closing.settled().await;
267 notify::close(&shared).await;
268 if shared.pool_owner == PoolOwner::Broker {
269 // The call marks the pool closed at once; its future closes the idle connections,
270 // then waits for those lent to deliveries still in work, which close as they come
271 // back. Shutdown does not wait for them, so the wait runs on its own.
272 drop(shared.pool.close());
273 let pool = shared.pool.clone();
274 shared.runtime.spawn(async move { pool.close().await });
275 }
276 Ok(ClosedSqlxBroker {
277 locks_released: released.into_iter().sum(),
278 connections_closed: shared.closing.forced(),
279 })
280 }
281 }
282}
283
284/// The broker after `shutdown`: what it did to the advisory locks its deliveries held.
285///
286/// A broker without advisory subscriptions reports none of either.
287///
288/// # Examples
289///
290/// ```no_run
291/// # #[cfg(feature = "postgres")]
292/// # async fn run(pool: sqlx::PgPool) -> Result<(), ruststream_sqlx::SqlxBrokerError> {
293/// use ruststream::{Broker, ConnectedBroker};
294/// use ruststream_sqlx::{ClosedSqlxBroker, SqlxBroker};
295///
296/// let closed: ClosedSqlxBroker = SqlxBroker::new(pool).connect().await?.shutdown().await?;
297/// tracing::info!(
298/// locks_released = closed.locks_released(),
299/// connections_closed = closed.connections_closed(),
300/// "inbox closed",
301/// );
302/// # Ok(())
303/// # }
304/// ```
305#[derive(Debug)]
306pub struct ClosedSqlxBroker {
307 locks_released: usize,
308 connections_closed: usize,
309}
310
311impl ClosedSqlxBroker {
312 /// Locks `shutdown` released with `Unlock` (or freed from the process registry), whose sessions
313 /// went back to the pool.
314 ///
315 /// # Examples
316 ///
317 /// ```no_run
318 /// # #[cfg(feature = "postgres")]
319 /// # async fn run(pool: sqlx::PgPool) -> Result<(), ruststream_sqlx::SqlxBrokerError> {
320 /// use ruststream::{Broker, ConnectedBroker};
321 /// use ruststream_sqlx::SqlxBroker;
322 ///
323 /// let closed = SqlxBroker::new(pool).connect().await?.shutdown().await?;
324 /// if closed.locks_released() > 0 {
325 /// tracing::info!(
326 /// released = closed.locks_released(),
327 /// "deliveries in work went back to their queues",
328 /// );
329 /// }
330 /// # Ok(())
331 /// # }
332 /// ```
333 #[must_use]
334 pub const fn locks_released(&self) -> usize {
335 self.locks_released
336 }
337
338 /// Sessions that held a lock and were closed instead: where `shutdown`'s release failed, and
339 /// where a delivery dropped unsettled, or a claim dropped midway, left its session closing when
340 /// `shutdown` began.
341 ///
342 /// # Examples
343 ///
344 /// ```no_run
345 /// # #[cfg(feature = "postgres")]
346 /// # async fn run(pool: sqlx::PgPool) -> Result<(), ruststream_sqlx::SqlxBrokerError> {
347 /// use ruststream::{Broker, ConnectedBroker};
348 /// use ruststream_sqlx::SqlxBroker;
349 ///
350 /// let closed = SqlxBroker::new(pool).connect().await?.shutdown().await?;
351 /// if closed.connections_closed() > 0 {
352 /// tracing::warn!(
353 /// closed = closed.connections_closed(),
354 /// "sessions holding a lock were closed rather than released",
355 /// );
356 /// }
357 /// # Ok(())
358 /// # }
359 /// ```
360 #[must_use]
361 pub const fn connections_closed(&self) -> usize {
362 self.connections_closed
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::ClosedSqlxBroker;
369
370 #[test]
371 fn the_closed_broker_reports_the_counts_its_debug_shows() {
372 let closed = ClosedSqlxBroker {
373 locks_released: 2,
374 connections_closed: 1,
375 };
376 assert_eq!(
377 (closed.locks_released(), closed.connections_closed()),
378 (2, 1)
379 );
380 assert_eq!(
381 format!("{closed:?}"),
382 "ClosedSqlxBroker { locks_released: 2, connections_closed: 1 }"
383 );
384 }
385}