Skip to content

Commit a83395a

Browse files
bonegaabonander
andauthored
Fix: nextest cleanup race condition (#3334)
* remove unused trait fn `cleanup_test_dbs` * *wip* solve test cleanup race condition * check for exactly 63 chars in database name * move base64 dependency * change * Use url_safe base64 encoding * Assert quoting for database name * refactor * add mysql support? * borrow * fix borrows * ensure quoting * re-add trait cleanup_test_dbs * fix mysql insert * cargo lock * use actual field * cleanup converted path in sqlite * replace dashes with underscore in db name * refactor: remove redundant path conversion in cleanup_test and add db_name method --------- Co-authored-by: Austin Bonander <austin.bonander@gmail.com>
1 parent aae8000 commit a83395a

File tree

6 files changed

+145
-194
lines changed

6 files changed

+145
-194
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sqlx-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mac_address = { workspace = true, optional = true }
5454
uuid = { workspace = true, optional = true }
5555

5656
async-io = { version = "1.9.0", optional = true }
57+
base64 = { version = "0.22.0", default-features = false, features = ["std"] }
5758
bytes = "1.1.0"
5859
chrono = { version = "0.4.34", default-features = false, features = ["clock"], optional = true }
5960
crc = { version = "3", optional = true }

sqlx-core/src/testing/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use std::time::Duration;
33

44
use futures_core::future::BoxFuture;
55

6+
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
67
pub use fixtures::FixtureSnapshot;
8+
use sha2::{Digest, Sha512};
79

810
use crate::connection::{ConnectOptions, Connection};
911
use crate::database::Database;
@@ -41,6 +43,17 @@ pub trait TestSupport: Database {
4143
/// This snapshot can then be used to generate test fixtures.
4244
fn snapshot(conn: &mut Self::Connection)
4345
-> BoxFuture<'_, Result<FixtureSnapshot<Self>, Error>>;
46+
47+
/// Generate a unique database name for the given test path.
48+
fn db_name(args: &TestArgs) -> String {
49+
let mut hasher = Sha512::new();
50+
hasher.update(args.test_path.as_bytes());
51+
let hash = hasher.finalize();
52+
let hash = URL_SAFE.encode(&hash[..39]);
53+
let db_name = format!("_sqlx_test_{}", hash).replace('-', "_");
54+
debug_assert!(db_name.len() == 63);
55+
db_name
56+
}
4457
}
4558

4659
pub struct TestFixture {
@@ -217,7 +230,7 @@ where
217230
let res = test_fn(test_context.pool_opts, test_context.connect_opts).await;
218231

219232
if res.is_success() {
220-
if let Err(e) = DB::cleanup_test(&test_context.db_name).await {
233+
if let Err(e) = DB::cleanup_test(&DB::db_name(&args)).await {
221234
eprintln!(
222235
"failed to delete database {:?}: {}",
223236
test_context.db_name, e

sqlx-mysql/src/testing/mod.rs

Lines changed: 70 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
1-
use std::fmt::Write;
21
use std::ops::Deref;
32
use std::str::FromStr;
4-
use std::sync::atomic::{AtomicBool, Ordering};
5-
use std::time::{Duration, SystemTime};
3+
use std::time::Duration;
64

75
use futures_core::future::BoxFuture;
86

97
use once_cell::sync::OnceCell;
10-
11-
use crate::connection::Connection;
8+
use sqlx_core::connection::Connection;
9+
use sqlx_core::query_builder::QueryBuilder;
10+
use sqlx_core::query_scalar::query_scalar;
11+
use std::fmt::Write;
1212

1313
use crate::error::Error;
1414
use crate::executor::Executor;
1515
use crate::pool::{Pool, PoolOptions};
1616
use crate::query::query;
17-
use crate::query_builder::QueryBuilder;
18-
use crate::query_scalar::query_scalar;
1917
use crate::{MySql, MySqlConnectOptions, MySqlConnection};
2018

2119
pub(crate) use sqlx_core::testing::*;
2220

2321
// Using a blocking `OnceCell` here because the critical sections are short.
2422
static MASTER_POOL: OnceCell<Pool<MySql>> = OnceCell::new();
25-
// Automatically delete any databases created before the start of the test binary.
26-
static DO_CLEANUP: AtomicBool = AtomicBool::new(true);
2723

2824
impl TestSupport for MySql {
2925
fn test_context(args: &TestArgs) -> BoxFuture<'_, Result<TestContext<Self>, Error>> {
@@ -38,17 +34,7 @@ impl TestSupport for MySql {
3834
.acquire()
3935
.await?;
4036

41-
let db_id = db_id(db_name);
42-
43-
conn.execute(&format!("drop database if exists {db_name};")[..])
44-
.await?;
45-
46-
query("delete from _sqlx_test_databases where db_id = ?")
47-
.bind(db_id)
48-
.execute(&mut *conn)
49-
.await?;
50-
51-
Ok(())
37+
do_cleanup(&mut conn, db_name).await
5238
})
5339
}
5440

@@ -58,13 +44,55 @@ impl TestSupport for MySql {
5844

5945
let mut conn = MySqlConnection::connect(&url).await?;
6046

61-
let now = SystemTime::now()
62-
.duration_since(SystemTime::UNIX_EPOCH)
63-
.unwrap();
47+
let delete_db_names: Vec<String> =
48+
query_scalar("select db_name from _sqlx_test_databases")
49+
.fetch_all(&mut conn)
50+
.await?;
51+
52+
if delete_db_names.is_empty() {
53+
return Ok(None);
54+
}
55+
56+
let mut deleted_db_names = Vec::with_capacity(delete_db_names.len());
57+
58+
let mut command = String::new();
59+
60+
for db_name in &delete_db_names {
61+
command.clear();
62+
63+
let db_name = format!("_sqlx_test_database_{db_name}");
64+
65+
writeln!(command, "drop database if exists {db_name:?};").ok();
66+
match conn.execute(&*command).await {
67+
Ok(_deleted) => {
68+
deleted_db_names.push(db_name);
69+
}
70+
// Assume a database error just means the DB is still in use.
71+
Err(Error::Database(dbe)) => {
72+
eprintln!("could not clean test database {db_name:?}: {dbe}")
73+
}
74+
// Bubble up other errors
75+
Err(e) => return Err(e),
76+
}
77+
}
78+
79+
if deleted_db_names.is_empty() {
80+
return Ok(None);
81+
}
82+
83+
let mut query =
84+
QueryBuilder::new("delete from _sqlx_test_databases where db_name in (");
85+
86+
let mut separated = query.separated(",");
87+
88+
for db_name in &deleted_db_names {
89+
separated.push_bind(db_name);
90+
}
91+
92+
query.push(")").build().execute(&mut conn).await?;
6493

65-
let num_deleted = do_cleanup(&mut conn, now).await?;
6694
let _ = conn.close().await;
67-
Ok(Some(num_deleted))
95+
Ok(Some(delete_db_names.len()))
6896
})
6997
}
7098

@@ -117,42 +145,27 @@ async fn test_context(args: &TestArgs) -> Result<TestContext<MySql>, Error> {
117145
conn.execute(
118146
r#"
119147
create table if not exists _sqlx_test_databases (
120-
db_id bigint unsigned primary key auto_increment,
148+
db_name text primary key,
121149
test_path text not null,
122150
created_at timestamp not null default current_timestamp
123151
);
124152
"#,
125153
)
126154
.await?;
127155

128-
// Record the current time _before_ we acquire the `DO_CLEANUP` permit. This
129-
// prevents the first test thread from accidentally deleting new test dbs
130-
// created by other test threads if we're a bit slow.
131-
let now = SystemTime::now()
132-
.duration_since(SystemTime::UNIX_EPOCH)
133-
.unwrap();
134-
135-
// Only run cleanup if the test binary just started.
136-
if DO_CLEANUP.swap(false, Ordering::SeqCst) {
137-
do_cleanup(&mut conn, now).await?;
138-
}
156+
let db_name = MySql::db_name(args);
157+
do_cleanup(&mut conn, &db_name).await?;
139158

140-
query("insert into _sqlx_test_databases(test_path) values (?)")
159+
query("insert into _sqlx_test_databases(db_name, test_path) values (?, ?)")
160+
.bind(&db_name)
141161
.bind(args.test_path)
142162
.execute(&mut *conn)
143163
.await?;
144164

145-
// MySQL doesn't have `INSERT ... RETURNING`
146-
let new_db_id: u64 = query_scalar("select last_insert_id()")
147-
.fetch_one(&mut *conn)
148-
.await?;
149-
150-
let new_db_name = db_name(new_db_id);
151-
152-
conn.execute(&format!("create database {new_db_name}")[..])
165+
conn.execute(&format!("create database {db_name:?}")[..])
153166
.await?;
154167

155-
eprintln!("created database {new_db_name}");
168+
eprintln!("created database {db_name}");
156169

157170
Ok(TestContext {
158171
pool_opts: PoolOptions::new()
@@ -167,74 +180,18 @@ async fn test_context(args: &TestArgs) -> Result<TestContext<MySql>, Error> {
167180
.connect_options()
168181
.deref()
169182
.clone()
170-
.database(&new_db_name),
171-
db_name: new_db_name,
183+
.database(&db_name),
184+
db_name,
172185
})
173186
}
174187

175-
async fn do_cleanup(conn: &mut MySqlConnection, created_before: Duration) -> Result<usize, Error> {
176-
// since SystemTime is not monotonic we added a little margin here to avoid race conditions with other threads
177-
let created_before_as_secs = created_before.as_secs() - 2;
178-
let delete_db_ids: Vec<u64> = query_scalar(
179-
"select db_id from _sqlx_test_databases \
180-
where created_at < from_unixtime(?)",
181-
)
182-
.bind(created_before_as_secs)
183-
.fetch_all(&mut *conn)
184-
.await?;
185-
186-
if delete_db_ids.is_empty() {
187-
return Ok(0);
188-
}
189-
190-
let mut deleted_db_ids = Vec::with_capacity(delete_db_ids.len());
191-
192-
let mut command = String::new();
193-
194-
for db_id in delete_db_ids {
195-
command.clear();
196-
197-
let db_name = db_name(db_id);
198-
199-
writeln!(command, "drop database if exists {db_name}").ok();
200-
match conn.execute(&*command).await {
201-
Ok(_deleted) => {
202-
deleted_db_ids.push(db_id);
203-
}
204-
// Assume a database error just means the DB is still in use.
205-
Err(Error::Database(dbe)) => {
206-
eprintln!("could not clean test database {db_id:?}: {dbe}")
207-
}
208-
// Bubble up other errors
209-
Err(e) => return Err(e),
210-
}
211-
}
212-
213-
let mut query = QueryBuilder::new("delete from _sqlx_test_databases where db_id in (");
214-
215-
let mut separated = query.separated(",");
216-
217-
for db_id in &deleted_db_ids {
218-
separated.push_bind(db_id);
219-
}
220-
221-
query.push(")").build().execute(&mut *conn).await?;
222-
223-
Ok(deleted_db_ids.len())
224-
}
225-
226-
fn db_name(id: u64) -> String {
227-
format!("_sqlx_test_database_{id}")
228-
}
229-
230-
fn db_id(name: &str) -> u64 {
231-
name.trim_start_matches("_sqlx_test_database_")
232-
.parse()
233-
.unwrap_or_else(|_1| panic!("failed to parse ID from database name {name:?}"))
234-
}
188+
async fn do_cleanup(conn: &mut MySqlConnection, db_name: &str) -> Result<(), Error> {
189+
let delete_db_command = format!("drop database if exists {db_name:?};");
190+
conn.execute(&*delete_db_command).await?;
191+
query("delete from _sqlx_test.databases where db_name = $1::text")
192+
.bind(db_name)
193+
.execute(&mut *conn)
194+
.await?;
235195

236-
#[test]
237-
fn test_db_name_id() {
238-
assert_eq!(db_name(12345), "_sqlx_test_database_12345");
239-
assert_eq!(db_id("_sqlx_test_database_12345"), 12345);
196+
Ok(())
240197
}

0 commit comments

Comments
 (0)