Skip to main content

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}