Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: nextest cleanup race condition #3334

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

bonega
Copy link

@bonega bonega commented Jul 7, 2024

fixes #2123 #[sqlx::test] should play nicely with nextest

Nextest provides a NEXTEST_RUN_ID that we can use to clear up tests belonging to the same cargo invocation.

See for a more general fix for cargo test runners.

Currently I haven't implemented support for mysql and sqlite, I'm looking for feedback before going ahead and doing that.

@bonega bonega changed the title Work around nextest cleanup race condition Fix: nextest cleanup race condition Jul 8, 2024
Copy link
Collaborator

@abonander abonander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks decent so far. Requesting changes so when you hit "request review" I know to check back.

@abonander
Copy link
Collaborator

abonander commented Jul 19, 2024

@bonega another thought is, instead of generating an arbitrary database name, we could generate one based on the path of the test function that's running. If the database name is always the same, then there's no real extra cleanup step necessary besides dropping and recreating the database before running the test and dropping it afterward. And it would make it a lot easier to find and inspect the database in question.

I would honestly make it always clean up the test database afterward unless an environment variable is set; the existing behavior seems to just confuse and annoy people more than anything. Bikeshedding: SQLX_TEST_KEEP_DB?

Do you know of any test runners that execute the same function multiple times concurrently? I think we could even handle that by holding a lock on the row in the test-databases table as a mutex. Arguably though, that table wouldn't even need to exist.

I think the main reason I didn't just do this originally was because I didn't want to have to think of a way to convert a path to a database name. However, as it turns out, MySQL and Postgres both allow (quoted) database names to contain any character besides NUL (\0) so we don't really have to come up with any special mapping scheme; we could just use the fully qualified path of the test function itself.

The biggest potential issue is that both databases have a length limit of 64 and 63 characters, respectively, so we'd have to either abbreviate or just truncate. I guess truncating makes the most sense. 63 characters is quite a lot for a path, it's unlikely to collide in most situations. If there is a collision, the mutual exclusion would just make the tests execute serially. And we could always make the attribute take an argument to override the database name.

@bonega
Copy link
Author

bonega commented Jul 22, 2024

Hey, just giving a heads-up that I am super busy with moving apartment, will take a closer look on this in a couple of days.

@abonander
Copy link
Collaborator

@bonega do you have time to look at my last comment?

@bonega
Copy link
Author

bonega commented Jul 30, 2024

@bonega do you have time to look at my last comment?

It is starting to wind down now, think I will have time today or tomorrow

@bonega
Copy link
Author

bonega commented Aug 1, 2024

@abonander
I think it seems to be a generally good idea.
Though I worry a bit about the truncating/locking. I have seen some very long test names before.

I will do some prototyping and see if it seems workable.

@abonander
Copy link
Collaborator

To get the module path you'll probably need to make #[sqlx::test] emit a recursive invocation of itself with module_path!() in its arguments. It's very fortunate that arguments to attributes are expanded eagerly now.

@bonega
Copy link
Author

bonega commented Aug 11, 2024

Hey, Sorry for being unresponsive.

I have checked around a little.
The test path can easily get long since it also includes the modules.
Also I don't want to impose naming requirements on the end-users.

By doing a truncate and lock I fear that we might regress the performance for some of the current users.

@abonander
Copy link
Collaborator

@bonega we could instead hash the path. A SHA-256 hash is incredibly unlikely to collide and can be encoded in 64 hex characters, so in Base64 (with dash and underscore) would be more like... 40?

I think deterministic naming is the right answer here, because then each test function can be wholly responsible for its own database and doesn't need any sort of memoization.

@bonega
Copy link
Author

bonega commented Sep 1, 2024

@abonander Hey, I am getting back into shape now.

Is this in the direction of what you wanted?
Will do some more refactoring and fixing up if it looks alright.

Copy link
Collaborator

@abonander abonander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nits, otherwise it looks all right.

Cargo.toml Outdated Show resolved Hide resolved
sqlx-core/src/testing/mod.rs Outdated Show resolved Hide resolved
Ok(Some(num_deleted))
})
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to check this trait implementation for proper quoting of the database name. It doesn't look like it does it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bonega don't forget this. Even with URL_SAFE the database name still needs to be quoted because it may contain dashes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked and it seems to work.

A test in examples/postgres/axum-social-with-tests has the test_path: comment::test_create_comment
Which becomes the database name of _sqlx_test_hsAQWz-87IRR7sjnVDeMcHtoDLU3QyT6yWizSbWKFvNwoBt6Q60I

It is deleted correctly even though it contains dashes.

I see now that you're referencing the mysql implementation which I haven't started yet.
I will try and do it and the sqlite today.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to update that I am fighting with running the tests on (both my branch and main)

@bonega
Copy link
Author

bonega commented Sep 15, 2024

@abonander I still can't run the tests locally but I see that this is passing in CI.
Mysql is finished and from what I can see sqlite already does a database per test path.

Copy link
Collaborator

@abonander abonander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocking change ({db_name:?}), one non-blocking (cleanup_test_dbs()).

Also, what failure are you getting locally? I can't help without any details.

sqlx-mysql/src/testing/mod.rs Outdated Show resolved Hide resolved
sqlx-core/src/testing/mod.rs Outdated Show resolved Hide resolved

query("insert into _sqlx_test_databases(test_path) values (?)")
query("insert into _sqlx_test_databases(test_path) values ($1, $2)")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't valid syntax in MySQL.

Copy link
Author

@bonega bonega Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're totally right that it isn't the right syntax, also I didn't name the column 😞

For the general tests I can't run them because I get this error:
> ./x.py

 # unit test core
 $ cargo test --no-default-features --manifest-path sqlx-core/Cargo.toml --features json,offline,migrate,_rt-async-std,_tls-rustls 
   Compiling sqlx-core v0.8.1 (/home/andreasliljeqvist/git/sqlx/sqlx-core)
error[E0425]: cannot find value `provider` in this scope
  --> sqlx-core/src/net/tls/tls_rustls.rs:97:54
   |
97 |     let config = ClientConfig::builder_with_provider(provider.clone())
   |                                                      ^^^^^^^^ not found in this scope

I pushed another commit that I hope will fix your comment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really need to just get rid of x.py. I don't use it and it's not tested in CI so there's no reason for it to get fixed or kept up to date unless someone else bothers to.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abonander , sorry to tag, but wanted to ask: will you have time to see the corrected changes?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently on vacation and don't have access to a computer to cut a release. I will set aside time for SQLx next week.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the quick reply. have a great vacation!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to integrate the changes as we have a lot of tests (kamu-data/kamu-cli#919) and many of them use sqlx + nextest combination.

In the case of PostgreSQL, everything works as expected, but in the case of MySQL, the changes are not finalized:

failed to connect to setup test database: Database(MySqlDatabaseError { code: Some("42000"), number: 1064, message: "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '\"_sqlx_test_b4OPm0XBsXli4d_oBPTB7Qy_7snq3Wiwey-n0eGShY0FeWK3ecaW\"' at line 1" })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Based on the PostgreSQL implementation, I finished MySQL one: bonega#1 -- @bonega , please take a look and if you wish, you can use my changes (paste into your branch).


We have a large number of tests (sqlx + nextest), so in general I think it's fair to call it successful change testing

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addendum: during testing, a regression on not deleting SQLite databases was identified -- it is also fixed in bonega#1

@bonega
Copy link
Author

bonega commented Oct 27, 2024

@s373r Thank you.
I imported what I think was the gist of your changes.

Copy link

@s373r s373r left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments

let mut hasher = Sha512::new();
hasher.update(args.test_path.as_bytes());
let hash = hasher.finalize();
let hash = URL_SAFE.encode(&hash[..39]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to use - to _ here, since - is not a valid character for SQL identifiers.

let db_name = format!("_sqlx_test_{}", hash).replace("-", "_");

I've had errors related to this

@@ -15,6 +15,7 @@ impl TestSupport for Sqlite {
}

fn cleanup_test(db_name: &str) -> BoxFuture<'_, Result<(), Error>> {
let db_name = convert_path(db_name);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there will be no file deletion:

  • in run_test() (sqlx-core/src/testing/mod.rs) calls the database name transformation
if let Err(e) = DB::cleanup_test(&DB::db_name(&args)).await {
  • and here, we're trying to delete it with a new name that won't be found

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

#[sqlx::test] should play nicely with nextest
3 participants