use sqlx::{Decode, Error, Type};
use crate::inbox::database::QueueDatabase;
use crate::inbox::engine::{
Claimed, Claiming, Event, Events, Settling, Stmt, Values, arguments, unprepared,
};
#[doc(hidden)]
#[derive(Debug)]
pub struct Candidates<Id> {
entries: Vec<(Option<Id>, String)>,
len: usize,
}
impl<Id> Default for Candidates<Id> {
fn default() -> Self {
Self {
entries: Vec::new(),
len: 0,
}
}
}
impl<Id> Candidates<Id> {
pub fn clear(&mut self) {
for (id, _) in &mut self.entries[..self.len] {
*id = None;
}
self.len = 0;
}
pub fn push(&mut self, id: Id, key: &str) {
match self.entries.get_mut(self.len) {
Some((kept, buffer)) => {
*kept = Some(id);
buffer.clear();
buffer.push_str(key);
}
None => self.entries.push((Some(id), key.to_owned())),
}
self.len += 1;
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn get(&self, index: usize) -> Option<(&Id, &str)> {
if index >= self.len {
return None;
}
let (id, key) = self.entries.get(index)?;
Some((id.as_ref()?, key.as_str()))
}
pub fn iter(&self) -> impl Iterator<Item = (&Id, &str)> {
self.entries[..self.len]
.iter()
.filter_map(|(id, key)| Some((id.as_ref()?, key.as_str())))
}
}
pub(crate) async fn candidates<DB, Row>(
conn: &mut DB::Connection,
cx: &Claiming,
out: &mut Candidates<Row::Id>,
) -> Result<(), Error>
where
DB: QueueDatabase,
Row: Events<DB>,
Row::Id: for<'r> Decode<'r, DB> + Type<DB>,
{
let statement = cx
.queue
.prepared
.claim
.ok_or_else(|| unprepared(Event::Claim))?;
let arguments = arguments::<DB, Row>(statement, &Values::claiming(*cx, Event::Claim, None))?;
out.clear();
DB::fetch_candidates(conn, statement.sql, arguments, out).await
}
pub(crate) async fn lock<DB: QueueDatabase, Row: Events<DB>>(
conn: &mut DB::Connection,
cx: &Claiming,
key: &str,
) -> Result<bool, Error> {
let statement = cx
.queue
.prepared
.lock
.ok_or_else(|| unprepared(Event::Lock))?;
let values = Values {
key: Some(key),
..Values::claiming(*cx, Event::Lock, None)
};
flag::<DB, Row>(conn, statement, &values).await
}
pub(crate) async fn unlock<DB: QueueDatabase, Row: Events<DB>>(
conn: &mut DB::Connection,
cx: &Settling,
key: &str,
) -> Result<bool, Error> {
let statement = cx
.queue
.prepared
.unlock
.ok_or_else(|| unprepared(Event::Unlock))?;
flag::<DB, Row>(conn, statement, &Values::unlocking(*cx, key)).await
}
async fn flag<DB: QueueDatabase, Row: Events<DB>>(
conn: &mut DB::Connection,
statement: Stmt,
values: &Values<'_, DB, Row>,
) -> Result<bool, Error> {
let arguments = arguments::<DB, Row>(statement, values)?;
DB::fetch_flag(conn, statement.sql, arguments).await
}
pub(crate) async fn take<DB, Row>(
conn: &mut DB::Connection,
cx: &Claiming,
id: &Row::Id,
out: &mut Vec<Claimed<Row>>,
) -> Result<bool, Error>
where
DB: QueueDatabase,
Row: Events<DB>,
Row::Id: for<'r> Decode<'r, DB> + Type<DB>,
{
let take = cx
.queue
.prepared
.take
.ok_or_else(|| unprepared(Event::Take))?;
let values = Values {
id: Some(id),
..Values::claiming(*cx, Event::Take, None)
};
let read = match cx.queue.prepared.take_then {
Some(then) => {
let arguments = arguments::<DB, Row>(take, &values)?;
if DB::execute(conn, take.sql, arguments).await? == 0 {
return Ok(false);
}
then
}
None => take,
};
let taken = out.len();
let arguments = arguments::<DB, Row>(read, &values)?;
DB::fetch_rows(conn, read.sql, arguments, cx.queue, out).await?;
Ok(out.len() > taken)
}
pub(crate) async fn take_id<DB: QueueDatabase, Row: Events<DB>>(
conn: &mut DB::Connection,
cx: &Claiming,
id: &Row::Id,
) -> Result<bool, Error> {
let take = cx
.queue
.prepared
.take
.ok_or_else(|| unprepared(Event::Take))?;
let values = Values {
id: Some(id),
..Values::claiming(*cx, Event::Take, None)
};
let arguments = arguments::<DB, Row>(take, &values)?;
if cx.queue.prepared.take_then.is_some() {
return Ok(DB::execute(conn, take.sql, arguments).await? > 0);
}
DB::fetch_found(conn, take.sql, arguments).await
}
pub(crate) fn match_taken<DB, Row>(id: &Row::Id, rows: Vec<Row>, out: &mut Vec<Claimed<Row>>)
where
DB: QueueDatabase,
Row: Events<DB>,
Row::Id: PartialEq,
{
let row = rows.into_iter().find(|row| Row::id(row) == id);
out.push(row.map_or_else(|| Claimed::Missing(id.clone()), Claimed::Row));
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use sqlx::sqlite::SqliteArguments;
use sqlx::{Connection, Error, Sqlite, SqliteConnection};
use super::Candidates;
use crate::inbox::database::QueueDatabase;
fn listed(candidates: &Candidates<i64>) -> Vec<(i64, String)> {
candidates
.iter()
.map(|(id, key)| (*id, key.to_owned()))
.collect()
}
#[tokio::test]
async fn the_candidates_write_each_key_over_the_buffer_a_claim_before_left() -> Result<(), Error>
{
let mut conn = SqliteConnection::connect("sqlite::memory:").await?;
let mut candidates = Candidates::default();
<Sqlite as QueueDatabase>::fetch_candidates(
&mut conn,
"SELECT 1, 'jobs-1' UNION ALL SELECT 2, 'jobs-22'",
SqliteArguments::default(),
&mut candidates,
)
.await?;
assert_eq!(
listed(&candidates),
[(1, "jobs-1".to_owned()), (2, "jobs-22".to_owned())]
);
let buffers: Vec<*const u8> = candidates.iter().map(|(_, key)| key.as_ptr()).collect();
candidates.clear();
assert!(candidates.is_empty());
<Sqlite as QueueDatabase>::fetch_candidates(
&mut conn,
"SELECT 3, 'jobs-3'",
SqliteArguments::default(),
&mut candidates,
)
.await?;
assert_eq!(listed(&candidates), [(3, "jobs-3".to_owned())]);
assert_eq!(candidates.len(), 1);
assert_eq!(
candidates.iter().map(|(_, key)| key.as_ptr()).next(),
Some(buffers[0]),
"the key is written over the buffer the first claim left"
);
candidates.clear();
<Sqlite as QueueDatabase>::fetch_candidates(
&mut conn,
"SELECT 4, 'jobs-4' UNION ALL SELECT 5, 'jobs-5' UNION ALL SELECT 6, 'jobs-6'",
SqliteArguments::default(),
&mut candidates,
)
.await?;
assert_eq!(candidates.len(), 3);
let reused: Vec<*const u8> = candidates.iter().map(|(_, key)| key.as_ptr()).collect();
assert_eq!(reused[..2], buffers[..]);
conn.close().await
}
#[tokio::test]
async fn a_statement_is_found_when_it_returns_a_row() -> Result<(), Error> {
let mut conn = SqliteConnection::connect("sqlite::memory:").await?;
let returned = <Sqlite as QueueDatabase>::fetch_found(
&mut conn,
"SELECT 1",
SqliteArguments::default(),
)
.await?;
assert!(returned);
let empty = <Sqlite as QueueDatabase>::fetch_found(
&mut conn,
"SELECT 1 WHERE 0",
SqliteArguments::default(),
)
.await?;
assert!(!empty);
conn.close().await
}
}