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