fix(sqlserver): handle query timeout and multi-result-set pagination
* Fix SQL Server query timeout and result pagination * Address SQL Server timeout review feedback * Cover SQL Server lock wait timeout --------- Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
1 parent
3a9bdc1cf6
commit
f73633f622
5 files changed
+216
-25
No files matched your search
+118
-14
@@ -852,7 +852,12 @@ fn is_os_connection_error(lower: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn timeout_error() -> String {
|
||||
format!("Query timed out after {} seconds", QUERY_TIMEOUT.as_secs())
|
||||
timeout_error_for(QUERY_TIMEOUT)
|
||||
}
|
||||
|
||||
fn timeout_error_for(timeout_duration: Duration) -> String {
|
||||
let seconds = timeout_duration.as_secs().max(1);
|
||||
format!("Query timed out after {seconds} seconds")
|
||||
}
|
||||
|
||||
pub fn canceled_error() -> String {
|
||||
@@ -882,15 +887,26 @@ pub async fn wait_for_query_with_timeout<F>(
|
||||
) -> Result<db::QueryResult, String>
|
||||
where
|
||||
F: Future<Output = Result<db::QueryResult, String>>,
|
||||
{
|
||||
wait_for_result_with_timeout(cancel_token, timeout_duration, future).await
|
||||
}
|
||||
|
||||
async fn wait_for_result_with_timeout<T, F>(
|
||||
cancel_token: Option<CancellationToken>,
|
||||
timeout_duration: Duration,
|
||||
future: F,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
if let Some(token) = cancel_token {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => Err(canceled_error()),
|
||||
result = timeout(timeout_duration, future) => result.map_err(|_| timeout_error())?,
|
||||
result = timeout(timeout_duration, future) => result.map_err(|_| timeout_error_for(timeout_duration))?,
|
||||
}
|
||||
} else {
|
||||
timeout(timeout_duration, future).await.map_err(|_| timeout_error())?
|
||||
timeout(timeout_duration, future).await.map_err(|_| timeout_error_for(timeout_duration))?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,9 +919,20 @@ pub async fn wait_for_query_opt<F>(
|
||||
) -> Result<db::QueryResult, String>
|
||||
where
|
||||
F: Future<Output = Result<db::QueryResult, String>>,
|
||||
{
|
||||
wait_for_result_opt(cancel_token, timeout_duration, future).await
|
||||
}
|
||||
|
||||
async fn wait_for_result_opt<T, F>(
|
||||
cancel_token: Option<CancellationToken>,
|
||||
timeout_duration: Option<Duration>,
|
||||
future: F,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
match timeout_duration {
|
||||
Some(d) => wait_for_query_with_timeout(cancel_token, d, future).await,
|
||||
Some(d) => wait_for_result_with_timeout(cancel_token, d, future).await,
|
||||
None => match cancel_token {
|
||||
Some(token) => {
|
||||
tokio::select! {
|
||||
@@ -919,6 +946,48 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_value_opt<T, F>(
|
||||
cancel_token: Option<CancellationToken>,
|
||||
timeout_duration: Option<Duration>,
|
||||
future: F,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
match timeout_duration {
|
||||
Some(timeout_duration) => {
|
||||
if let Some(token) = cancel_token {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => Err(canceled_error()),
|
||||
result = timeout(timeout_duration, future) => result.map_err(|_| timeout_error_for(timeout_duration)),
|
||||
}
|
||||
} else {
|
||||
timeout(timeout_duration, future).await.map_err(|_| timeout_error_for(timeout_duration))
|
||||
}
|
||||
}
|
||||
None => match cancel_token {
|
||||
Some(token) => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => Err(canceled_error()),
|
||||
result = future => Ok(result),
|
||||
}
|
||||
}
|
||||
None => Ok(future.await),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn sqlserver_pool_is_current(
|
||||
state: &AppState,
|
||||
pool_key: &str,
|
||||
client: &Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>,
|
||||
) -> bool {
|
||||
let connections = state.connections.read().await;
|
||||
matches!(connections.get(pool_key), Some(PoolKind::SqlServer(current)) if Arc::ptr_eq(current, client))
|
||||
}
|
||||
|
||||
fn resolve_query_timeout(timeout_secs: Option<u64>) -> Option<Duration> {
|
||||
match timeout_secs {
|
||||
Some(0) => None,
|
||||
@@ -1867,6 +1936,7 @@ async fn execute_multi_sqlserver(
|
||||
check_read_only_for_connection_multi(state, pool_key, &batches).await?;
|
||||
let mut all_results = Vec::new();
|
||||
let max_rows = options.max_rows;
|
||||
let query_timeout = resolve_query_timeout(options.timeout_secs);
|
||||
|
||||
for batch in &batches {
|
||||
if is_canceled(&cancel_token) {
|
||||
@@ -1892,17 +1962,28 @@ async fn execute_multi_sqlserver(
|
||||
};
|
||||
drop(connections);
|
||||
|
||||
let mut client = match cancel_token.as_ref() {
|
||||
Some(token) => tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => return Err(canceled_error()),
|
||||
guard = client.lock() => guard,
|
||||
},
|
||||
None => client.lock().await,
|
||||
let mut client_guard = match wait_for_value_opt(cancel_token.clone(), query_timeout, client.lock()).await {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
all_results.push(error_query_result(err));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let result = db::sqlserver::execute_batch_with_max_rows(&mut client, batch, max_rows).await;
|
||||
drop(client);
|
||||
if !sqlserver_pool_is_current(state, pool_key, &client).await {
|
||||
all_results.push(error_query_result(
|
||||
"SQL Server connection was reset while waiting for the query lock; please retry.".to_string(),
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
let result = wait_for_result_opt(
|
||||
cancel_token.clone(),
|
||||
query_timeout,
|
||||
db::sqlserver::execute_batch_with_max_rows(&mut client_guard, batch, max_rows),
|
||||
)
|
||||
.await;
|
||||
drop(client_guard);
|
||||
|
||||
match result {
|
||||
Ok(results) => all_results.extend(results),
|
||||
@@ -2887,6 +2968,7 @@ pub async fn rollback_manual_transaction(state: &AppState, txn_session_id: &str)
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::connection::{default_redis_key_separator, ConnectionConfig, DatabaseType};
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
use crate::storage::Storage;
|
||||
|
||||
fn test_connection_config(db_type: DatabaseType) -> ConnectionConfig {
|
||||
@@ -3025,7 +3107,29 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(result.unwrap_err(), timeout_error());
|
||||
assert_eq!(result.unwrap_err(), timeout_error_for(Duration::from_millis(10)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_value_opt_times_out_while_waiting_for_lock() {
|
||||
let lock = tokio::sync::Mutex::new(());
|
||||
let _guard = lock.lock().await;
|
||||
|
||||
let result = wait_for_value_opt(None, Some(Duration::from_millis(10)), lock.lock()).await;
|
||||
|
||||
assert_eq!(result.unwrap_err(), timeout_error_for(Duration::from_millis(10)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wait_for_value_opt_can_cancel_while_waiting_for_lock() {
|
||||
let lock = tokio::sync::Mutex::new(());
|
||||
let _guard = lock.lock().await;
|
||||
let token = CancellationToken::new();
|
||||
token.cancel();
|
||||
|
||||
let result = wait_for_value_opt(Some(token), Some(Duration::from_secs(30)), lock.lock()).await;
|
||||
|
||||
assert_eq!(result.unwrap_err(), QUERY_CANCELED);
|
||||
}
|
||||
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
|
||||
Reference in new issue
Block a user