1use std::collections::HashMap;
10use std::future::Future;
11use std::io;
12use std::mem;
13use std::panic::resume_unwind;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
16use std::time::{Duration, SystemTime};
17
18use ruststream::testing::{Backlog, Coordinator, InProcess, TestableBroker};
19use ruststream::{HeaderMap, OutgoingMessage, RawMessage};
20use ruststream_sqlx_dialect::{Dialect, Role};
21use tokio::runtime::Handle;
22use tokio::sync::oneshot;
23use tokio::task::spawn_blocking;
24use tokio::time::Instant;
25
26use super::broker::{ConnectedSqlxBroker, Shared, SqlxBroker};
27use super::database::QueueDatabase;
28use super::engine::{Claiming, Event, Events, Now, Values, arguments, intern};
29use super::error::SqlxBrokerError;
30use super::publish::{TableWake, insert_routed};
31use super::queue::Queue;
32
33#[derive(Debug, Clone, Copy)]
38pub(crate) struct TestClock {
39 system: SystemTime,
40 instant: Instant,
41}
42
43impl TestClock {
44 fn start() -> Self {
45 Self {
46 system: SystemTime::now(),
47 instant: Instant::now(),
48 }
49 }
50
51 pub(crate) fn now(self) -> SystemTime {
52 self.system + self.instant.elapsed()
53 }
54}
55
56pub(crate) async fn off_clock<T, Work>(work: Work) -> Option<T>
61where
62 T: Send + 'static,
63 Work: Future<Output = T> + Send + 'static,
64{
65 let runtime = Handle::current();
66 match spawn_blocking(move || runtime.block_on(work)).await {
67 Ok(output) => Some(output),
68 Err(join) if join.is_panic() => resume_unwind(join.into_panic()),
69 Err(_) => None,
70 }
71}
72
73pub(crate) fn cancelled() -> sqlx::Error {
75 sqlx::Error::Io(io::Error::other(
76 "the runtime shut down during a database call",
77 ))
78}
79
80#[derive(Debug, Clone, Copy)]
86pub(crate) struct QueueAt<'a> {
87 table: &'a str,
89 group: &'a str,
91}
92
93impl<'a> QueueAt<'a> {
94 pub(crate) fn written(wake: &'a TableWake, name: &'a str) -> Self {
96 Self {
97 table: wake.table(),
98 group: wake.group(name),
99 }
100 }
101
102 pub(crate) fn read(queue: &'a Queue) -> Self {
104 Self {
105 table: queue.table,
106 group: queue.spec.column(Role::Group).map_or("", |_| queue.name),
107 }
108 }
109
110 pub(crate) fn dead_letter(queue: &'a Queue, destination: &'a str) -> Self {
113 match queue.spec.column(Role::Group) {
114 Some(_) => Self {
115 table: queue.table,
116 group: destination,
117 },
118 None => Self {
119 table: destination,
120 group: "",
121 },
122 }
123 }
124
125 pub(crate) fn registered(table: &'a str, group: Option<&'a str>) -> Self {
127 Self {
128 table,
129 group: group.unwrap_or_default(),
130 }
131 }
132}
133
134#[derive(Debug, Default)]
136struct Tally {
137 ahead: usize,
139 returning: Vec<Return>,
143}
144
145impl Tally {
146 fn take_return(&mut self, ticket: Ticket) -> bool {
148 let Some(at) = self.returning.iter().position(|back| back.ticket == ticket) else {
149 return false;
150 };
151 self.returning.swap_remove(at);
152 true
153 }
154
155 fn take_earliest_return(&mut self) {
157 if let Some(at) = (0..self.returning.len()).min_by_key(|&at| self.returning[at].due) {
158 self.returning.swap_remove(at);
159 }
160 }
161}
162
163#[derive(Debug, Clone, Copy)]
165struct Return {
166 due: Instant,
167 ticket: Ticket,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub(crate) struct Ticket(u64);
173
174type Credits = HashMap<Box<str>, HashMap<Box<str>, Tally>>;
176
177#[derive(Debug, Default)]
180pub(crate) struct Harness {
181 coordinator: OnceLock<Coordinator>,
182 log: Mutex<Vec<RawMessage>>,
183 clock: OnceLock<TestClock>,
185 credits: Mutex<Credits>,
188 tickets: AtomicU64,
190 writes: Mutex<Option<oneshot::Receiver<()>>>,
192}
193
194impl Harness {
195 pub(crate) fn in_process(&self) -> bool {
197 self.clock.get().is_some()
198 }
199
200 pub(crate) fn now(&self) -> Now {
202 Now::test(self.clock.get().copied())
203 }
204
205 fn books(&self) -> Option<&Coordinator> {
207 self.clock.get().and_then(|_| self.coordinator.get())
208 }
209
210 fn credits(&self) -> MutexGuard<'_, Credits> {
211 self.credits.lock().unwrap_or_else(PoisonError::into_inner)
212 }
213
214 fn credit<'books>(
216 credits: &'books mut Credits,
217 queue: QueueAt<'_>,
218 ) -> Option<&'books mut Tally> {
219 credits.get_mut(queue.table)?.get_mut(queue.group)
220 }
221
222 pub(crate) fn opened(&self, queue: QueueAt<'_>, waiting: usize) {
225 self.credits()
226 .entry(queue.table.into())
227 .or_default()
228 .entry(queue.group.into())
229 .or_default()
230 .ahead += waiting;
231 if let Some(coordinator) = self.books() {
232 for _ in 0..waiting {
233 coordinator.enqueued();
234 }
235 }
236 }
237
238 pub(crate) fn closed(&self, queue: QueueAt<'_>) {
240 let mut credits = self.credits();
241 if let Some(groups) = credits.get_mut(queue.table) {
242 groups.remove(queue.group);
243 if groups.is_empty() {
244 credits.remove(queue.table);
245 }
246 }
247 }
248
249 pub(crate) fn expect(&self, queue: QueueAt<'_>) {
252 let Some(coordinator) = self.books() else {
253 return;
254 };
255 if let Some(tally) = Self::credit(&mut self.credits(), queue) {
256 tally.ahead += 1;
257 coordinator.enqueued();
258 }
259 }
260
261 pub(crate) fn refused(&self, queue: QueueAt<'_>) {
263 let Some(coordinator) = self.books() else {
264 return;
265 };
266 if let Some(tally) = Self::credit(&mut self.credits(), queue)
267 && tally.ahead > 0
268 {
269 tally.ahead -= 1;
270 coordinator.consumed();
271 }
272 }
273
274 pub(crate) fn holds_back(&self, queue: QueueAt<'_>, delay: Duration) -> Option<Ticket> {
278 self.books()?;
279 let ticket = Ticket(self.tickets.fetch_add(1, Ordering::Relaxed));
280 let due = Instant::now() + delay;
281 Self::credit(&mut self.credits(), queue)?
282 .returning
283 .push(Return { due, ticket });
284 Some(ticket)
285 }
286
287 pub(crate) fn withdraw(&self, queue: QueueAt<'_>, ticket: Ticket) {
289 if let Some(tally) = Self::credit(&mut self.credits(), queue) {
290 tally.take_return(ticket);
291 }
292 }
293
294 fn comes_due(&self, queue: QueueAt<'_>, ticket: Ticket) {
297 let Some(coordinator) = self.books() else {
298 return;
299 };
300 if let Some(tally) = Self::credit(&mut self.credits(), queue)
301 && tally.take_return(ticket)
302 {
303 tally.ahead += 1;
304 coordinator.enqueued();
305 }
306 }
307
308 pub(crate) fn claimed(&self, queue: QueueAt<'_>, count: usize) {
312 let Some(coordinator) = self.books() else {
313 return;
314 };
315 let covered = Self::credit(&mut self.credits(), queue).map_or(0, |tally| {
316 let covered = tally.ahead.min(count);
317 tally.ahead -= covered;
318 for _ in covered..count {
319 tally.take_earliest_return();
320 }
321 covered
322 });
323 for _ in covered..count {
324 coordinator.enqueued();
325 }
326 }
327
328 pub(crate) fn released(&self) {
330 if let Some(coordinator) = self.books() {
331 coordinator.consumed();
332 }
333 }
334
335 pub(crate) fn returned(&self, queue: QueueAt<'_>) {
337 self.expect(queue);
338 self.released();
339 }
340
341 pub(crate) fn published(&self, message: &OutgoingMessage<'_>) {
344 self.record(message);
345 }
346
347 fn record(&self, message: &OutgoingMessage<'_>) {
348 self.log
349 .lock()
350 .unwrap_or_else(PoisonError::into_inner)
351 .push(
352 RawMessage::new(message.name().to_owned(), message.payload().to_vec())
353 .with_headers(message.headers().clone()),
354 );
355 }
356}
357
358pub(crate) struct BatchMoves<DB: QueueDatabase> {
362 connection: OnceLock<Arc<Shared<DB>>>,
364 counted: Mutex<Vec<QueueAt<'static>>>,
365}
366
367impl<DB: QueueDatabase> Default for BatchMoves<DB> {
368 fn default() -> Self {
369 Self {
370 connection: OnceLock::new(),
371 counted: Mutex::default(),
372 }
373 }
374}
375
376impl<DB: QueueDatabase> BatchMoves<DB> {
377 pub(crate) fn watch(&self, connection: &Arc<Shared<DB>>) {
379 if connection.harness.books().is_some() {
380 let _ = self.connection.set(Arc::clone(connection));
381 }
382 }
383
384 fn counted(&self) -> MutexGuard<'_, Vec<QueueAt<'static>>> {
385 self.counted.lock().unwrap_or_else(PoisonError::into_inner)
386 }
387
388 pub(crate) fn counted_at(&self, at: QueueAt<'static>) {
391 if self.connection.get().is_some() {
392 self.counted().push(at);
393 }
394 }
395
396 pub(crate) fn ended(&self, committed: bool) {
398 if committed {
399 self.counted().clear();
400 } else {
401 self.undone();
402 }
403 }
404
405 pub(crate) fn undone(&self) {
407 let Some(connection) = self.connection.get() else {
408 return;
409 };
410 let undone = mem::take(&mut *self.counted());
411 for at in undone {
412 connection.harness.refused(at);
413 }
414 }
415}
416
417pub(crate) fn returns_after<DB: QueueDatabase>(
421 connection: &Arc<Shared<DB>>,
422 queue: &'static Queue,
423 delay: Duration,
424 ticket: Ticket,
425) {
426 let Some(coordinator) = connection.harness.books() else {
427 return;
428 };
429 let connection = Arc::clone(connection);
430 coordinator.schedule_redelivery(delay, move || {
431 connection.harness.comes_due(QueueAt::read(queue), ticket);
432 });
433}
434
435pub(crate) async fn open_books<DB, Row>(
445 connection: &Arc<Shared<DB>>,
446 dialect: &dyn Dialect,
447 queue: &'static Queue,
448 group: Option<&str>,
449) -> Result<(), SqlxBrokerError>
450where
451 DB: QueueDatabase,
452 Row: Events<DB>,
453{
454 let waiting = claimable::<DB, Row>(connection, dialect, queue).await?;
455 connection
456 .harness
457 .opened(QueueAt::registered(queue.table, group), waiting);
458 Ok(())
459}
460
461async fn claimable<DB, Row>(
471 connection: &Arc<Shared<DB>>,
472 dialect: &dyn Dialect,
473 queue: &'static Queue,
474) -> Result<usize, SqlxBrokerError>
475where
476 DB: QueueDatabase,
477 Row: Events<DB>,
478{
479 if connection.harness.books().is_none() || Row::SHAPE.custom_claim {
481 return Ok(0);
482 }
483 let counting =
484 dialect
485 .count_claimable(&queue.spec)
486 .map_err(|source| SqlxBrokerError::Dialect {
487 subscription: queue.name.to_owned(),
488 table: queue.table.to_owned(),
489 row: queue.row,
490 source,
491 })?;
492 let Some(counting) = counting else {
493 return Ok(0);
494 };
495 let statement = intern(&counting);
496 let (pool, now) = (connection.pool.clone(), connection.harness.now());
497 let counted = off_clock(async move {
498 let lease = queue.lease.map(|_| Row::lease(queue, now)).transpose()?;
500 let cx = Claiming {
501 queue,
502 limit: 0,
503 now,
504 };
505 let values = Values::claiming(cx, Event::Claim, lease.as_ref());
506 let arguments = arguments::<DB, Row>(statement, &values)?;
507 let mut conn = pool.acquire().await?;
508 DB::fetch_count(&mut conn, statement.sql, arguments).await
509 })
510 .await
511 .unwrap_or_else(|| Err(cancelled()))
512 .map_err(|source| SqlxBrokerError::Sqlx {
513 subscription: queue.name.to_owned(),
514 table: queue.table.to_owned(),
515 row: queue.row,
516 statement: statement.sql,
517 source: Box::new(source),
518 })?;
519 Ok(usize::try_from(counted).unwrap_or_default())
520}
521
522impl<DB: QueueDatabase, D: Dialect + 'static> InProcess for SqlxBroker<DB, D> {
523 async fn connect_in_process(self) -> Result<ConnectedSqlxBroker<DB, D>, SqlxBrokerError> {
526 let (pool, owner) = self.pool.open()?;
527 let choice = self.dialect.clone();
528 let checked = pool.clone();
529 let dialect = off_clock(async move {
530 let conn = checked
531 .acquire()
532 .await
533 .map_err(|source| SqlxBrokerError::Connect { source })?;
534 choice.resolve(&conn)
535 })
536 .await
537 .unwrap_or_else(|| {
538 Err(SqlxBrokerError::Connect {
539 source: cancelled(),
540 })
541 })?;
542 let listening = match self.listen {
543 Some(start) => Some(
544 off_clock(start(pool.clone()))
545 .await
546 .unwrap_or_else(|| Err(cancelled()))
547 .map_err(|source| SqlxBrokerError::Connect { source })?,
548 ),
549 None => None,
550 };
551 let connected =
552 ConnectedSqlxBroker::new(self, (pool, owner), dialect, Handle::current(), listening);
553 let _ = connected.shared.harness.clock.set(TestClock::start());
554 connected.shared.closing.run_in_process();
555 Ok(connected)
556 }
557}
558
559impl<DB: QueueDatabase, D: Dialect + 'static> TestableBroker for ConnectedSqlxBroker<DB, D> {
560 fn install_coordinator(&self, coordinator: Coordinator) {
561 let _ = self.shared.harness.coordinator.set(coordinator);
562 }
563
564 fn inject(&self, message: OutgoingMessage<'_>) {
565 let harness = &self.shared.harness;
566 harness.record(&message);
567 let wake = self
569 .shared
570 .routes
571 .find_with_wake(message.name())
572 .map(|(_, wake)| wake);
573 if let Some(wake) = wake {
574 harness.expect(QueueAt::written(wake, message.name()));
575 }
576 let (name, payload, headers) = message.into_parts();
577 let (name, payload) = (name.to_owned(), payload.to_vec());
578 let (written, next) = oneshot::channel();
581 let previous = harness
582 .writes
583 .lock()
584 .unwrap_or_else(PoisonError::into_inner)
585 .replace(next);
586 let shared = Arc::clone(&self.shared);
587 tokio::spawn(async move {
588 if let Some(previous) = previous {
589 let _ = previous.await;
590 }
591 inject(&shared, &name, &payload, headers).await;
592 let _ = written.send(());
593 });
594 }
595
596 fn published(&self, name: &str) -> Vec<RawMessage> {
597 self.shared
598 .harness
599 .log
600 .lock()
601 .unwrap_or_else(PoisonError::into_inner)
602 .iter()
603 .filter(|message| message.name() == name)
604 .cloned()
605 .collect()
606 }
607
608 fn backlog(&self) -> Backlog {
609 Backlog::Delivered
611 }
612}
613
614async fn inject<DB: QueueDatabase>(
616 shared: &Shared<DB>,
617 name: &str,
618 payload: &[u8],
619 headers: HeaderMap,
620) {
621 let message = OutgoingMessage::new(name, payload).with_headers(headers);
622 let written = match shared.routes.find_with_wake(name) {
623 Some((route, wake)) => insert_routed(shared, route, wake, &message).await,
624 None => Err(SqlxBrokerError::NoRoute {
625 name: name.to_owned(),
626 }),
627 };
628 if let Err(error) = written {
629 if let Some((_, wake)) = shared.routes.find_with_wake(name) {
630 shared.harness.refused(QueueAt::written(wake, name));
631 }
632 tracing::warn!(target: "ruststream_sqlx", %error, name, "an injected message was refused");
633 }
634}
635
636#[cfg(feature = "postgres")]
637ruststream::register_testable_broker!(SqlxBroker<sqlx::Postgres>);
638#[cfg(feature = "mysql")]
639ruststream::register_testable_broker!(SqlxBroker<sqlx::MySql>);
640#[cfg(feature = "sqlite")]
641ruststream::register_testable_broker!(SqlxBroker<sqlx::Sqlite>);
642#[cfg(feature = "any")]
643ruststream::register_testable_broker!(SqlxBroker<sqlx::Any>);