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::{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}