mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
* fix: patch sqlx so a cancelled BEGIN cannot poison a pooled connection Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzqmh52NU8fB9RBQNNkJGt * test: drop the migration run and fixed sleep from the sqlx patch guard Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzqmh52NU8fB9RBQNNkJGt * test: ignore the sqlx patch guard by default and point at it from where sqlx is changed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzqmh52NU8fB9RBQNNkJGt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
73 lines
2.9 KiB
Rust
73 lines
2.9 KiB
Rust
//! Guards the `sqlx` entries in `[patch.crates-io]` — `backend/Cargo.toml` carries the why.
|
|
//! Dropping the patch still compiles, so a test is what notices.
|
|
//!
|
|
//! Ignored by default: it only has something to say when the sqlx dependency moves, and it
|
|
//! spends a couple of seconds waiting on a deliberately slow round trip. Run it whenever you
|
|
//! touch sqlx — a version bump, a change to the patch entries, a fork rebase:
|
|
//!
|
|
//! ```text
|
|
//! cargo test -p windmill-common --test sqlx_begin_cancel_safe -- --ignored
|
|
//! ```
|
|
|
|
use sqlx::{Connection, PgConnection, Pool, Postgres};
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[sqlx::test]
|
|
#[ignore = "run with --ignored after any sqlx bump or change to [patch.crates-io]"]
|
|
async fn begin_cancelled_mid_round_trip_leaves_no_open_transaction(db: Pool<Postgres>) {
|
|
// One connection, so the session inspected below is the one the cancelled begin used.
|
|
let pool = sqlx::postgres::PgPoolOptions::new()
|
|
.max_connections(1)
|
|
.min_connections(0)
|
|
.connect_with((*db.connect_options()).clone())
|
|
.await
|
|
.expect("failed to build pool");
|
|
let pid: i32 = sqlx::query_scalar("SELECT pg_backend_pid()")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
// A plain `BEGIN` answers in well under a millisecond, which is too narrow to cancel
|
|
// reliably; appending a sleep widens the round trip and runs through the same
|
|
// `PgTransactionManager::begin` the patch fixes.
|
|
let cancelled = tokio::time::timeout(
|
|
Duration::from_millis(300),
|
|
pool.begin_with("BEGIN; SELECT pg_sleep(2);"),
|
|
)
|
|
.await;
|
|
assert!(cancelled.is_err(), "the begin must not have completed");
|
|
|
|
let mut admin = PgConnection::connect_with(&(*db.connect_options()).clone())
|
|
.await
|
|
.expect("failed to open an observing connection");
|
|
|
|
// sqlx only flushes the queued ROLLBACK once the abandoned statement has answered, so
|
|
// wait for the session to stop running rather than sleeping a fixed time a loaded runner
|
|
// could overshoot.
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
let state = loop {
|
|
let state: String = sqlx::query_scalar("SELECT state FROM pg_stat_activity WHERE pid = $1")
|
|
.bind(pid)
|
|
.fetch_optional(&mut admin)
|
|
.await
|
|
.unwrap()
|
|
.flatten()
|
|
.unwrap_or_default();
|
|
if state != "active" || Instant::now() >= deadline {
|
|
break state;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
};
|
|
|
|
assert!(
|
|
!state.starts_with("idle in transaction"),
|
|
"connection returned to the pool still inside a transaction (state {state:?}) — is \
|
|
the sqlx patch in backend/Cargo.toml still applied?"
|
|
);
|
|
|
|
sqlx::query_scalar::<_, i32>("SELECT 1")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.expect("pool must still serve queries");
|
|
}
|