#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DbNormalReturnWorkProof {
identity: [u8; 32],
sqlx_major: u16,
sqlx_minor: u16,
sqlx_patch: u16,
max_ping_commands: u8,
max_protocol_writes: u8,
max_protocol_reads: u8,
requires_open_pool: bool,
requires_no_after_release_hook: bool,
requires_no_max_lifetime: bool,
requires_zero_min_connections: bool,
requires_deployment_service_attestation: bool,
supports_budget_to_poison_discard: bool,
}
impl DbNormalReturnWorkProof {
const SQLX_0_8_6_MYSQL: Self = Self {
identity: [
0x53, 0x44, 0x4c, 0x2d, 0x44, 0x42, 0x2d, 0x52, 0x45, 0x54, 0x55, 0x52, 0x4e, 0x2d,
0x30, 0x31, 0x2d, 0x53, 0x51, 0x4c, 0x58, 0x2d, 0x30, 0x38, 0x30, 0x36, 0x2d, 0x4d,
0x59, 0x53, 0x51, 0x4c,
],
sqlx_major: 0,
sqlx_minor: 8,
sqlx_patch: 6,
max_ping_commands: 1,
max_protocol_writes: 1,
max_protocol_reads: 1,
requires_open_pool: true,
requires_no_after_release_hook: true,
requires_no_max_lifetime: true,
requires_zero_min_connections: true,
requires_deployment_service_attestation: true,
supports_budget_to_poison_discard: true,
};
pub const fn identity(self) -> [u8; 32] {
self.identity
}
pub const fn sqlx_version(self) -> (u16, u16, u16) {
(self.sqlx_major, self.sqlx_minor, self.sqlx_patch)
}
pub const fn max_ping_commands(self) -> u8 {
self.max_ping_commands
}
pub const fn max_protocol_writes(self) -> u8 {
self.max_protocol_writes
}
pub const fn max_protocol_reads(self) -> u8 {
self.max_protocol_reads
}
pub const fn requires_open_pool(self) -> bool {
self.requires_open_pool
}
pub const fn requires_no_after_release_hook(self) -> bool {
self.requires_no_after_release_hook
}
pub const fn requires_no_max_lifetime(self) -> bool {
self.requires_no_max_lifetime
}
pub const fn requires_zero_min_connections(self) -> bool {
self.requires_zero_min_connections
}
pub const fn requires_deployment_service_attestation(self) -> bool {
self.requires_deployment_service_attestation
}
pub const fn supports_budget_to_poison_discard(self) -> bool {
self.supports_budget_to_poison_discard
}
}
#[doc(hidden)]
pub const fn db_normal_return_work_proof() -> DbNormalReturnWorkProof {
DbNormalReturnWorkProof::SQLX_0_8_6_MYSQL
}
#[cfg(test)]
mod tests {
use std::{
env,
future::Future,
pin::{Pin, pin},
process::Command,
task::{Context, Poll},
};
use saddle_core::ComponentLifecycle;
use saddle_observability::Observer;
use sqlx::Executor;
use super::*;
use crate::{Database, DatabaseConfig};
struct FixedBudget {
pending_polls: Option<u8>,
}
impl FixedBudget {
const fn pending() -> Self {
Self {
pending_polls: None,
}
}
const fn after_one_poll() -> Self {
Self {
pending_polls: Some(1),
}
}
}
impl Future for FixedBudget {
type Output = ();
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
match self.pending_polls {
None => Poll::Pending,
Some(0) => Poll::Ready(()),
Some(remaining) => {
self.pending_polls = Some(remaining - 1);
context.waker().wake_by_ref();
Poll::Pending
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ReturnOutcome {
Returned,
PoisonDiscarded,
}
async fn return_or_discard<R, B>(return_to_pool: R, budget: B) -> ReturnOutcome
where
R: Future<Output = ()>,
B: Future<Output = ()>,
{
let mut return_to_pool = pin!(return_to_pool);
let mut budget = pin!(budget);
std::future::poll_fn(|context| {
if return_to_pool.as_mut().poll(context).is_ready() {
return Poll::Ready(ReturnOutcome::Returned);
}
if budget.as_mut().poll(context).is_ready() {
return Poll::Ready(ReturnOutcome::PoisonDiscarded);
}
Poll::Pending
})
.await
}
async fn database(url: &str) -> Database {
Database::connect(
DatabaseConfig::new(url).max_connections(1),
Observer::with_writer(Default::default(), std::io::sink()).unwrap(),
)
.await
.unwrap()
}
#[test]
fn proof_has_identity_and_work_units_but_no_duration() {
let proof = db_normal_return_work_proof();
assert_ne!(proof.identity(), [0; 32]);
assert_eq!(proof.sqlx_version(), (0, 8, 6));
assert_eq!(proof.max_ping_commands(), 1);
assert_eq!(proof.max_protocol_writes(), 1);
assert_eq!(proof.max_protocol_reads(), 1);
assert!(proof.requires_open_pool());
assert!(proof.requires_no_after_release_hook());
assert!(proof.requires_no_max_lifetime());
assert!(proof.requires_zero_min_connections());
assert!(proof.requires_deployment_service_attestation());
assert!(proof.supports_budget_to_poison_discard());
assert_eq!(std::mem::size_of::<DbNormalReturnWorkProof>(), 48);
}
#[tokio::test]
async fn real_mariadb_normal_return_or_unresponsive_budget_discards_once() {
let Ok(url) = env::var("SADDLE_TEST_DATABASE_URL") else {
eprintln!("skipping normal-return proof: SADDLE_TEST_DATABASE_URL is not set");
return;
};
let server_pid: u32 = env::var("SADDLE_TEST_DATABASE_SERVER_PID")
.expect("real unresponsive proof requires the isolated MariaDB pid")
.parse()
.unwrap();
let normal = database(&url).await;
let mut connection = normal.pool.try_acquire().unwrap();
connection.execute("SELECT 1").await.unwrap();
let outcome = return_or_discard(connection.return_to_pool(), FixedBudget::pending()).await;
assert_eq!(outcome, ReturnOutcome::Returned);
assert_eq!((normal.pool.size(), normal.pool.num_idle()), (1, 1));
normal.shutdown().await.unwrap();
let unresponsive = database(&url).await;
let mut connection = unresponsive.pool.try_acquire().unwrap();
connection.execute("SELECT 1").await.unwrap();
let return_to_pool = connection.return_to_pool();
assert!(
Command::new("kill")
.args(["-STOP", &server_pid.to_string()])
.status()
.unwrap()
.success()
);
let outcome = return_or_discard(return_to_pool, FixedBudget::after_one_poll()).await;
assert_eq!(outcome, ReturnOutcome::PoisonDiscarded);
assert_eq!(
(unresponsive.pool.size(), unresponsive.pool.num_idle()),
(0, 0)
);
assert!(
Command::new("kill")
.args(["-CONT", &server_pid.to_string()])
.status()
.unwrap()
.success()
);
unresponsive.shutdown().await.unwrap();
}
}