Skip to content

Commit 1f91724

Browse files
authored
feat(cli): add --connect-timeout (#1889)
1 parent d52f301 commit 1f91724

File tree

7 files changed

+157
-78
lines changed

7 files changed

+157
-78
lines changed

Cargo.lock

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sqlx-cli/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ openssl = { version = "0.10.38", optional = true }
4747
# workaround for https://github.com/rust-lang/rust/issues/29497
4848
remove_dir_all = "0.7.0"
4949

50+
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
51+
5052
[features]
5153
default = ["postgres", "sqlite", "mysql", "native-tls"]
5254
rustls = ["sqlx/runtime-tokio-rustls"]

sqlx-cli/src/database.rs

+31-16
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,58 @@
11
use crate::migrate;
2+
use crate::opt::ConnectOpts;
23
use console::style;
34
use promptly::{prompt, ReadlineError};
45
use sqlx::any::Any;
56
use sqlx::migrate::MigrateDatabase;
67

7-
pub async fn create(uri: &str) -> anyhow::Result<()> {
8-
if !Any::database_exists(uri).await? {
9-
Any::create_database(uri).await?;
8+
pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> {
9+
// NOTE: only retry the idempotent action.
10+
// We're assuming that if this succeeds, then any following operations should also succeed.
11+
let exists = crate::retry_connect_errors(connect_opts, Any::database_exists).await?;
12+
13+
if !exists {
14+
Any::create_database(&connect_opts.database_url).await?;
1015
}
1116

1217
Ok(())
1318
}
1419

15-
pub async fn drop(uri: &str, confirm: bool) -> anyhow::Result<()> {
16-
if confirm && !ask_to_continue(uri) {
20+
pub async fn drop(connect_opts: &ConnectOpts, confirm: bool) -> anyhow::Result<()> {
21+
if confirm && !ask_to_continue(connect_opts) {
1722
return Ok(());
1823
}
1924

20-
if Any::database_exists(uri).await? {
21-
Any::drop_database(uri).await?;
25+
// NOTE: only retry the idempotent action.
26+
// We're assuming that if this succeeds, then any following operations should also succeed.
27+
let exists = crate::retry_connect_errors(connect_opts, Any::database_exists).await?;
28+
29+
if exists {
30+
Any::drop_database(&connect_opts.database_url).await?;
2231
}
2332

2433
Ok(())
2534
}
2635

27-
pub async fn reset(migration_source: &str, uri: &str, confirm: bool) -> anyhow::Result<()> {
28-
drop(uri, confirm).await?;
29-
setup(migration_source, uri).await
36+
pub async fn reset(
37+
migration_source: &str,
38+
connect_opts: &ConnectOpts,
39+
confirm: bool,
40+
) -> anyhow::Result<()> {
41+
drop(connect_opts, confirm).await?;
42+
setup(migration_source, connect_opts).await
3043
}
3144

32-
pub async fn setup(migration_source: &str, uri: &str) -> anyhow::Result<()> {
33-
create(uri).await?;
34-
migrate::run(migration_source, uri, false, false).await
45+
pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
46+
create(connect_opts).await?;
47+
migrate::run(migration_source, connect_opts, false, false).await
3548
}
3649

37-
fn ask_to_continue(uri: &str) -> bool {
50+
fn ask_to_continue(connect_opts: &ConnectOpts) -> bool {
3851
loop {
39-
let r: Result<String, ReadlineError> =
40-
prompt(format!("Drop database at {}? (y/n)", style(uri).cyan()));
52+
let r: Result<String, ReadlineError> = prompt(format!(
53+
"Drop database at {}? (y/n)",
54+
style(&connect_opts.database_url).cyan()
55+
));
4156
match r {
4257
Ok(response) => {
4358
if response == "n" || response == "N" {

sqlx-cli/src/lib.rs

+64-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use anyhow::Result;
2+
use futures::{Future, TryFutureExt};
3+
use sqlx::{AnyConnection, Connection};
4+
use std::io;
5+
use std::time::Duration;
26

3-
use crate::opt::{Command, DatabaseCommand, MigrateCommand};
7+
use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand};
48

59
mod database;
610
// mod migration;
@@ -23,11 +27,11 @@ pub async fn run(opt: Opt) -> Result<()> {
2327
source,
2428
dry_run,
2529
ignore_missing,
26-
database_url,
30+
connect_opts,
2731
} => {
2832
migrate::run(
2933
source.resolve(&migrate.source),
30-
&database_url,
34+
&connect_opts,
3135
dry_run,
3236
*ignore_missing,
3337
)
@@ -37,56 +41,98 @@ pub async fn run(opt: Opt) -> Result<()> {
3741
source,
3842
dry_run,
3943
ignore_missing,
40-
database_url,
44+
connect_opts,
4145
} => {
4246
migrate::revert(
4347
source.resolve(&migrate.source),
44-
&database_url,
48+
&connect_opts,
4549
dry_run,
4650
*ignore_missing,
4751
)
4852
.await?
4953
}
5054
MigrateCommand::Info {
5155
source,
52-
database_url,
53-
} => migrate::info(source.resolve(&migrate.source), &database_url).await?,
56+
connect_opts,
57+
} => migrate::info(source.resolve(&migrate.source), &connect_opts).await?,
5458
MigrateCommand::BuildScript { source, force } => {
5559
migrate::build_script(source.resolve(&migrate.source), force)?
5660
}
5761
},
5862

5963
Command::Database(database) => match database.command {
60-
DatabaseCommand::Create { database_url } => database::create(&database_url).await?,
64+
DatabaseCommand::Create { connect_opts } => database::create(&connect_opts).await?,
6165
DatabaseCommand::Drop {
6266
confirmation,
63-
database_url,
64-
} => database::drop(&database_url, !confirmation).await?,
67+
connect_opts,
68+
} => database::drop(&connect_opts, !confirmation.yes).await?,
6569
DatabaseCommand::Reset {
6670
confirmation,
6771
source,
68-
database_url,
69-
} => database::reset(&source, &database_url, !confirmation).await?,
72+
connect_opts,
73+
} => database::reset(&source, &connect_opts, !confirmation.yes).await?,
7074
DatabaseCommand::Setup {
7175
source,
72-
database_url,
73-
} => database::setup(&source, &database_url).await?,
76+
connect_opts,
77+
} => database::setup(&source, &connect_opts).await?,
7478
},
7579

7680
Command::Prepare {
7781
check: false,
7882
merged,
7983
args,
80-
database_url,
81-
} => prepare::run(&database_url, merged, args)?,
84+
connect_opts,
85+
} => prepare::run(&connect_opts, merged, args).await?,
8286

8387
Command::Prepare {
8488
check: true,
8589
merged,
8690
args,
87-
database_url,
88-
} => prepare::check(&database_url, merged, args)?,
91+
connect_opts,
92+
} => prepare::check(&connect_opts, merged, args).await?,
8993
};
9094

9195
Ok(())
9296
}
97+
98+
/// Attempt to connect to the database server, retrying up to `ops.connect_timeout`.
99+
async fn connect(opts: &ConnectOpts) -> sqlx::Result<AnyConnection> {
100+
retry_connect_errors(opts, AnyConnection::connect).await
101+
}
102+
103+
/// Attempt an operation that may return errors like `ConnectionRefused`,
104+
/// retrying up until `ops.connect_timeout`.
105+
///
106+
/// The closure is passed `&ops.database_url` for easy composition.
107+
async fn retry_connect_errors<'a, F, Fut, T>(
108+
opts: &'a ConnectOpts,
109+
mut connect: F,
110+
) -> sqlx::Result<T>
111+
where
112+
F: FnMut(&'a str) -> Fut,
113+
Fut: Future<Output = sqlx::Result<T>> + 'a,
114+
{
115+
backoff::future::retry(
116+
backoff::ExponentialBackoffBuilder::new()
117+
.with_max_elapsed_time(Some(Duration::from_secs(opts.connect_timeout)))
118+
.build(),
119+
|| {
120+
connect(&opts.database_url).map_err(|e| -> backoff::Error<sqlx::Error> {
121+
match e {
122+
sqlx::Error::Io(ref ioe) => match ioe.kind() {
123+
io::ErrorKind::ConnectionRefused
124+
| io::ErrorKind::ConnectionReset
125+
| io::ErrorKind::ConnectionAborted => {
126+
return backoff::Error::transient(e);
127+
}
128+
_ => (),
129+
},
130+
_ => (),
131+
}
132+
133+
backoff::Error::permanent(e)
134+
})
135+
},
136+
)
137+
.await
138+
}

sqlx-cli/src/migrate.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
use crate::opt::ConnectOpts;
12
use anyhow::{bail, Context};
23
use chrono::Utc;
34
use console::style;
45
use sqlx::migrate::{AppliedMigration, Migrate, MigrateError, MigrationType, Migrator};
5-
use sqlx::{AnyConnection, Connection};
66
use std::borrow::Cow;
77
use std::collections::{HashMap, HashSet};
88
use std::fmt::Write;
@@ -116,9 +116,9 @@ fn short_checksum(checksum: &[u8]) -> String {
116116
s
117117
}
118118

119-
pub async fn info(migration_source: &str, uri: &str) -> anyhow::Result<()> {
119+
pub async fn info(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> {
120120
let migrator = Migrator::new(Path::new(migration_source)).await?;
121-
let mut conn = AnyConnection::connect(uri).await?;
121+
let mut conn = crate::connect(&connect_opts).await?;
122122

123123
conn.ensure_migrations_table().await?;
124124

@@ -190,12 +190,12 @@ fn validate_applied_migrations(
190190

191191
pub async fn run(
192192
migration_source: &str,
193-
uri: &str,
193+
connect_opts: &ConnectOpts,
194194
dry_run: bool,
195195
ignore_missing: bool,
196196
) -> anyhow::Result<()> {
197197
let migrator = Migrator::new(Path::new(migration_source)).await?;
198-
let mut conn = AnyConnection::connect(uri).await?;
198+
let mut conn = crate::connect(connect_opts).await?;
199199

200200
conn.ensure_migrations_table().await?;
201201

@@ -249,12 +249,12 @@ pub async fn run(
249249

250250
pub async fn revert(
251251
migration_source: &str,
252-
uri: &str,
252+
connect_opts: &ConnectOpts,
253253
dry_run: bool,
254254
ignore_missing: bool,
255255
) -> anyhow::Result<()> {
256256
let migrator = Migrator::new(Path::new(migration_source)).await?;
257-
let mut conn = AnyConnection::connect(uri).await?;
257+
let mut conn = crate::connect(&connect_opts).await?;
258258

259259
conn.ensure_migrations_table().await?;
260260

0 commit comments

Comments
 (0)