feat(agent): track execution context activity

This commit is contained in:
t8y2 committed 2026-06-22 23:17:11 +08:00
1 parent ee29694776
commit ac3d3a7948
6 files changed
+336 -27

No files matched your search

+6 -3
View File
@@ -1211,6 +1211,7 @@ export const useQueryStore = defineStore("query", () => {
} }
const countSql = options.countSql; const countSql = options.countSql;
const clientSessionId = tabClientSessionId({ id: options.tabId }, "count"); const clientSessionId = tabClientSessionId({ id: options.tabId }, "count");
const countExecutionId = `${options.executionId}:count`;
if (typeof options.pageLimit === "number" && resultRowCount < options.pageLimit) { if (typeof options.pageLimit === "number" && resultRowCount < options.pageLimit) {
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, (options.pageOffset ?? 0) + resultRowCount); setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, (options.pageOffset ?? 0) + resultRowCount);
@@ -1220,7 +1221,7 @@ export const useQueryStore = defineStore("query", () => {
void (async () => { void (async () => {
try { try {
console.info("[DBX][executeTabSql:count:start]", { traceId: options.traceId, elapsed: options.elapsed() }); console.info("[DBX][executeTabSql:count:start]", { traceId: options.traceId, elapsed: options.elapsed() });
const countResult = await api.executeQuery(options.connectionId, options.database, countSql, options.schema, undefined, { const countResult = await api.executeQuery(options.connectionId, options.database, countSql, options.schema, countExecutionId, {
clientSessionId, clientSessionId,
timeoutSecs: options.timeoutSecs, timeoutSecs: options.timeoutSecs,
}); });
@@ -2040,6 +2041,7 @@ export const useQueryStore = defineStore("query", () => {
let executionTimeMs = 0; let executionTimeMs = 0;
let offset = 0; let offset = 0;
const clientSessionId = tabClientSessionId(tab, "export"); const clientSessionId = tabClientSessionId(tab, "export");
const exportExecutionId = uuid();
try { try {
while (true) { while (true) {
@@ -2055,7 +2057,7 @@ export const useQueryStore = defineStore("query", () => {
limit: pageLimit, limit: pageLimit,
offset, offset,
}); });
const results = await api.executeMulti(tab.connectionId, tab.database, sql, undefined, undefined, { const results = await api.executeMulti(tab.connectionId, tab.database, sql, undefined, exportExecutionId, {
maxRows: pageLimit, maxRows: pageLimit,
fetchSize: pageLimit, fetchSize: pageLimit,
clientSessionId, clientSessionId,
@@ -2106,6 +2108,7 @@ export const useQueryStore = defineStore("query", () => {
let offset = 0; let offset = 0;
let sessionId: string | undefined; let sessionId: string | undefined;
const clientSessionId = tabClientSessionId(tab, "export"); const clientSessionId = tabClientSessionId(tab, "export");
const exportExecutionId = uuid();
try { try {
while (true) { while (true) {
@@ -2126,7 +2129,7 @@ export const useQueryStore = defineStore("query", () => {
timeoutSecs: queryTimeoutSecs, timeoutSecs: queryTimeoutSecs,
} }
: { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, clientSessionId, timeoutSecs: queryTimeoutSecs }; : { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, clientSessionId, timeoutSecs: queryTimeoutSecs };
const results = await api.executeMulti(tab.connectionId, tab.database, plan.sqlToExecute, tab.schema, undefined, executionOptions); const results = await api.executeMulti(tab.connectionId, tab.database, plan.sqlToExecute, tab.schema, exportExecutionId, executionOptions);
const result = results[0]; const result = results[0];
if (!result) break; if (!result) break;
if (columns.length === 0) columns = result.columns; if (columns.length === 0) columns = result.columns;
+184 -11
View File
@@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::{Duration, Instant};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@@ -84,6 +84,7 @@ pub enum PoolKind {
pub struct AppState { pub struct AppState {
pub connections: Arc<RwLock<HashMap<String, PoolKind>>>, pub connections: Arc<RwLock<HashMap<String, PoolKind>>>,
keepalive_tasks: Arc<RwLock<HashMap<String, JoinHandle<()>>>>, keepalive_tasks: Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
pool_activity: Arc<RwLock<HashMap<String, PoolActivity>>>,
pub configs: RwLock<HashMap<String, ConnectionConfig>>, pub configs: RwLock<HashMap<String, ConnectionConfig>>,
pub running_queries: RunningQueries, pub running_queries: RunningQueries,
pub tunnels: TunnelManager, pub tunnels: TunnelManager,
@@ -95,6 +96,40 @@ pub struct AppState {
pub mq_registry: crate::mq::MqAdminRegistry, pub mq_registry: crate::mq::MqAdminRegistry,
} }
#[derive(Clone, Copy)]
struct PoolActivity {
last_used_at: Instant,
}
impl PoolActivity {
fn now() -> Self {
Self { last_used_at: Instant::now() }
}
}
pub struct PoolActivityTouch {
pool_key: String,
connections: Arc<RwLock<HashMap<String, PoolKind>>>,
pool_activity: Arc<RwLock<HashMap<String, PoolActivity>>>,
}
impl Drop for PoolActivityTouch {
fn drop(&mut self) {
let pool_key = self.pool_key.clone();
let connections = self.connections.clone();
let pool_activity = self.pool_activity.clone();
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return;
};
handle.spawn(async move {
if !connections.read().await.contains_key(&pool_key) {
return;
}
pool_activity.write().await.insert(pool_key, PoolActivity::now());
});
}
}
pub fn metadata_connection_config(config: &ConnectionConfig) -> ConnectionConfig { pub fn metadata_connection_config(config: &ConnectionConfig) -> ConnectionConfig {
let mut db_config = config.canonicalized(); let mut db_config = config.canonicalized();
if database_capabilities::is_metadata_connection_scoped(&db_config.db_type) { if database_capabilities::is_metadata_connection_scoped(&db_config.db_type) {
@@ -263,6 +298,7 @@ impl AppState {
Self { Self {
connections: Arc::new(RwLock::new(HashMap::new())), connections: Arc::new(RwLock::new(HashMap::new())),
keepalive_tasks: Arc::new(RwLock::new(HashMap::new())), keepalive_tasks: Arc::new(RwLock::new(HashMap::new())),
pool_activity: Arc::new(RwLock::new(HashMap::new())),
configs: RwLock::new(HashMap::new()), configs: RwLock::new(HashMap::new()),
running_queries: RunningQueries::default(), running_queries: RunningQueries::default(),
tunnels: TunnelManager::new(), tunnels: TunnelManager::new(),
@@ -326,6 +362,7 @@ impl AppState {
pub async fn insert_connection_pool(&self, pool_key: String, pool: PoolKind, config: &ConnectionConfig) { pub async fn insert_connection_pool(&self, pool_key: String, pool: PoolKind, config: &ConnectionConfig) {
self.stop_keepalive_task(&pool_key).await; self.stop_keepalive_task(&pool_key).await;
self.pool_activity.write().await.insert(pool_key.clone(), PoolActivity::now());
self.start_keepalive_task(&pool_key, &pool, config).await; self.start_keepalive_task(&pool_key, &pool, config).await;
let previous = self.connections.write().await.insert(pool_key, pool); let previous = self.connections.write().await.insert(pool_key, pool);
if let Some(pool) = previous { if let Some(pool) = previous {
@@ -335,37 +372,77 @@ impl AppState {
async fn start_keepalive_task(&self, pool_key: &str, pool: &PoolKind, config: &ConnectionConfig) { async fn start_keepalive_task(&self, pool_key: &str, pool: &PoolKind, config: &ConnectionConfig) {
let interval_secs = config.keepalive_interval_secs; let interval_secs = config.keepalive_interval_secs;
if interval_secs == 0 { let idle_timeout_secs = config.idle_timeout_secs;
let idle_cleanup_enabled = is_session_scoped_pool_key(pool_key) && idle_timeout_secs > 0;
let mut target = keepalive_target_from_pool(pool, config);
if interval_secs == 0 && !idle_cleanup_enabled {
return; return;
} }
let Some(mut target) = keepalive_target_from_pool(pool, config) else { if interval_secs > 0 && target.is_none() {
log::debug!( log::debug!(
"Connection keepalive requested for '{pool_key}', but this database driver does not keep a pingable client handle." "Connection keepalive requested for '{pool_key}', but this database driver does not keep a pingable client handle."
); );
return; if !idle_cleanup_enabled {
return;
}
}; };
let key = pool_key.to_string(); let key = pool_key.to_string();
let interval = Duration::from_secs(interval_secs.max(1)); let interval = if interval_secs > 0 {
Duration::from_secs(interval_secs.max(1))
} else {
Duration::from_secs(idle_timeout_secs.min(60).max(1))
};
let timeout = Duration::from_secs(config.effective_connect_timeout_secs().max(1)); let timeout = Duration::from_secs(config.effective_connect_timeout_secs().max(1));
let connections = self.connections.clone(); let connections = self.connections.clone();
let keepalive_tasks = self.keepalive_tasks.clone(); let keepalive_tasks = self.keepalive_tasks.clone();
let pool_activity = self.pool_activity.clone();
let running_queries = self.running_queries.clone();
let idle_timeout = Duration::from_secs(idle_timeout_secs.max(1));
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
loop { loop {
tokio::time::sleep(interval).await; tokio::time::sleep(interval).await;
let result = tokio::time::timeout(timeout, ping_keepalive_target(&mut target, timeout)).await;
match result { if running_queries.is_pool_active(&key) {
Ok(Ok(())) => {} continue;
Ok(Err(err)) => { }
log::warn!("Connection keepalive failed for '{key}': {err}; invalidating pool");
if idle_cleanup_enabled {
let idle_for = {
let activity = pool_activity.read().await;
activity.get(&key).map(|activity| activity.last_used_at.elapsed())
};
if idle_for.is_some_and(|elapsed| elapsed >= idle_timeout) {
log::info!(
"Closing idle session-scoped connection pool '{key}' after {}s",
idle_timeout.as_secs()
);
keepalive_tasks.write().await.remove(&key); keepalive_tasks.write().await.remove(&key);
pool_activity.write().await.remove(&key);
let removed = connections.write().await.remove(&key); let removed = connections.write().await.remove(&key);
if let Some(pool) = removed { if let Some(pool) = removed {
close_pool_kind_with_timeout(key, pool).await; close_pool_kind_with_timeout(key, pool).await;
} }
break; break;
} }
Err(_) => log::warn!("Connection keepalive timed out for '{key}' after {}s", timeout.as_secs()), }
if let Some(target) = target.as_mut() {
let result = tokio::time::timeout(timeout, ping_keepalive_target(target, timeout)).await;
match result {
Ok(Ok(())) => {}
Ok(Err(err)) => {
log::warn!("Connection keepalive failed for '{key}': {err}; invalidating pool");
keepalive_tasks.write().await.remove(&key);
pool_activity.write().await.remove(&key);
let removed = connections.write().await.remove(&key);
if let Some(pool) = removed {
close_pool_kind_with_timeout(key, pool).await;
}
break;
}
Err(_) => log::warn!("Connection keepalive timed out for '{key}' after {}s", timeout.as_secs()),
}
} }
} }
}); });
@@ -391,6 +468,18 @@ impl AppState {
} }
} }
pub async fn touch_pool_activity(&self, pool_key: &str) {
self.pool_activity.write().await.insert(pool_key.to_string(), PoolActivity::now());
}
pub fn pool_activity_touch(&self, pool_key: &str) -> PoolActivityTouch {
PoolActivityTouch {
pool_key: pool_key.to_string(),
connections: self.connections.clone(),
pool_activity: self.pool_activity.clone(),
}
}
pub async fn get_or_create_pool(&self, connection_id: &str, database: Option<&str>) -> Result<String, String> { pub async fn get_or_create_pool(&self, connection_id: &str, database: Option<&str>) -> Result<String, String> {
self.get_or_create_pool_for_session(connection_id, database, None).await self.get_or_create_pool_for_session(connection_id, database, None).await
} }
@@ -413,6 +502,7 @@ impl AppState {
if conns.contains_key(&pool_key) { if conns.contains_key(&pool_key) {
drop(conns); drop(conns);
if !self.remove_stale_connection_pool(&pool_key).await { if !self.remove_stale_connection_pool(&pool_key).await {
self.touch_pool_activity(&pool_key).await;
return Ok(pool_key); return Ok(pool_key);
} }
} else { } else {
@@ -904,6 +994,10 @@ impl AppState {
} }
async fn remove_stale_connection_pool(&self, pool_key: &str) -> bool { async fn remove_stale_connection_pool(&self, pool_key: &str) -> bool {
if self.running_queries.is_pool_active(pool_key) {
return false;
}
let stale = { let stale = {
let connections = self.connections.read().await; let connections = self.connections.read().await;
let Some(pool) = connections.get(pool_key) else { let Some(pool) = connections.get(pool_key) else {
@@ -1085,6 +1179,7 @@ impl AppState {
} }
self.stop_keepalive_task(pool_key).await; self.stop_keepalive_task(pool_key).await;
self.pool_activity.write().await.remove(pool_key);
let removed = self.connections.write().await.remove(pool_key); let removed = self.connections.write().await.remove(pool_key);
if let Some(pool) = removed { if let Some(pool) = removed {
close_pool_kind(pool).await; close_pool_kind(pool).await;
@@ -1115,6 +1210,7 @@ impl AppState {
self.reset_connection_transport(connection_id).await; self.reset_connection_transport(connection_id).await;
} else { } else {
self.stop_keepalive_task(&pool_key).await; self.stop_keepalive_task(&pool_key).await;
self.pool_activity.write().await.remove(&pool_key);
let removed = self.connections.write().await.remove(&pool_key); let removed = self.connections.write().await.remove(&pool_key);
if let Some(pool) = removed { if let Some(pool) = removed {
close_pool_kind(pool).await; close_pool_kind(pool).await;
@@ -1143,6 +1239,7 @@ impl AppState {
return Ok(false); return Ok(false);
} }
self.stop_keepalive_task(&pool_key).await; self.stop_keepalive_task(&pool_key).await;
self.pool_activity.write().await.remove(&pool_key);
let removed = self.connections.write().await.remove(&pool_key); let removed = self.connections.write().await.remove(&pool_key);
if let Some(pool) = removed { if let Some(pool) = removed {
close_pool_kind(pool).await; close_pool_kind(pool).await;
@@ -1154,6 +1251,7 @@ impl AppState {
pub async fn remove_pool_by_key(&self, pool_key: &str) -> bool { pub async fn remove_pool_by_key(&self, pool_key: &str) -> bool {
self.stop_keepalive_task(pool_key).await; self.stop_keepalive_task(pool_key).await;
self.pool_activity.write().await.remove(pool_key);
let removed = self.connections.write().await.remove(pool_key); let removed = self.connections.write().await.remove(pool_key);
if let Some(pool) = removed { if let Some(pool) = removed {
close_pool_kind(pool).await; close_pool_kind(pool).await;
@@ -1182,6 +1280,12 @@ impl AppState {
.cloned() .cloned()
.collect(); .collect();
self.stop_keepalive_tasks(&keys_to_remove).await; self.stop_keepalive_tasks(&keys_to_remove).await;
{
let mut activity = self.pool_activity.write().await;
for key in &keys_to_remove {
activity.remove(key);
}
}
let mut conns = self.connections.write().await; let mut conns = self.connections.write().await;
let mut removed = Vec::with_capacity(keys_to_remove.len()); let mut removed = Vec::with_capacity(keys_to_remove.len());
for key in keys_to_remove { for key in keys_to_remove {
@@ -1472,6 +1576,12 @@ impl AppState {
// Remove dead pools // Remove dead pools
if !dead_keys.is_empty() { if !dead_keys.is_empty() {
self.stop_keepalive_tasks(&dead_keys).await; self.stop_keepalive_tasks(&dead_keys).await;
{
let mut activity = self.pool_activity.write().await;
for key in &dead_keys {
activity.remove(key);
}
}
let mut conns = self.connections.write().await; let mut conns = self.connections.write().await;
for key in &dead_keys { for key in &dead_keys {
if let Some(pool) = conns.remove(key) { if let Some(pool) = conns.remove(key) {
@@ -1517,6 +1627,12 @@ impl AppState {
.cloned() .cloned()
.collect(); .collect();
self.stop_keepalive_tasks(&keys_to_remove).await; self.stop_keepalive_tasks(&keys_to_remove).await;
{
let mut activity = self.pool_activity.write().await;
for key in &keys_to_remove {
activity.remove(key);
}
}
let mut conns = self.connections.write().await; let mut conns = self.connections.write().await;
let mut removed = Vec::with_capacity(keys_to_remove.len()); let mut removed = Vec::with_capacity(keys_to_remove.len());
for key in keys_to_remove { for key in keys_to_remove {
@@ -1545,6 +1661,12 @@ impl AppState {
}) })
.collect(); .collect();
self.stop_keepalive_tasks(&keys_to_remove).await; self.stop_keepalive_tasks(&keys_to_remove).await;
{
let mut activity = self.pool_activity.write().await;
for key in &keys_to_remove {
activity.remove(key);
}
}
let mut conns = self.connections.write().await; let mut conns = self.connections.write().await;
let mut removed = Vec::with_capacity(keys_to_remove.len()); let mut removed = Vec::with_capacity(keys_to_remove.len());
for key in keys_to_remove { for key in keys_to_remove {
@@ -1697,6 +1819,10 @@ fn session_scoped_pool_key(base_pool_key: String, client_session_id: Option<&str
.unwrap_or(base_pool_key) .unwrap_or(base_pool_key)
} }
fn is_session_scoped_pool_key(pool_key: &str) -> bool {
pool_key.contains(":session:")
}
pub(crate) fn config_for_pool_key<'a>( pub(crate) fn config_for_pool_key<'a>(
pool_key: &str, pool_key: &str,
configs: &'a HashMap<String, ConnectionConfig>, configs: &'a HashMap<String, ConnectionConfig>,
@@ -2763,6 +2889,23 @@ mod tests {
); );
} }
#[test]
fn session_scoped_pool_keys_are_sanitized_and_detected() {
let key = super::session_scoped_pool_key_for(
Some(DatabaseType::Mysql),
"mysql-conn:analytics".to_string(),
Some("tab-1:count"),
);
assert_eq!(key, "mysql-conn:analytics:session:tab-1_count");
assert!(super::is_session_scoped_pool_key(&key));
assert!(!super::is_session_scoped_pool_key("mysql-conn:analytics"));
assert_eq!(
super::session_scoped_pool_key_for(Some(DatabaseType::DuckDb), "duckdb-conn".to_string(), Some("tab-1")),
"duckdb-conn"
);
}
#[test] #[test]
fn mysql_direct_connections_skip_tcp_probe() { fn mysql_direct_connections_skip_tcp_probe() {
let mut config = mysql_config(Some("app")); let mut config = mysql_config(Some("app"));
@@ -2884,6 +3027,36 @@ mod tests {
let _ = std::fs::remove_dir_all(dir); let _ = std::fs::remove_dir_all(dir);
} }
#[tokio::test]
async fn pool_activity_touch_updates_existing_pool_only() {
let (state, dir) = test_app_state().await;
let pool = crate::db::sqlite::connect_path(":memory:").await.unwrap();
let pool_key = "conn:session:tab-1";
state.connections.write().await.insert(pool_key.to_string(), PoolKind::Sqlite(pool));
state.pool_activity.write().await.insert(
pool_key.to_string(),
super::PoolActivity { last_used_at: std::time::Instant::now() - std::time::Duration::from_secs(10) },
);
{
let _touch = state.pool_activity_touch(pool_key);
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let elapsed = state.pool_activity.read().await.get(pool_key).unwrap().last_used_at.elapsed();
assert!(elapsed < std::time::Duration::from_secs(10));
{
let _touch = state.pool_activity_touch(pool_key);
state.connections.write().await.remove(pool_key);
state.pool_activity.write().await.remove(pool_key);
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
assert!(!state.pool_activity.read().await.contains_key(pool_key));
let _ = std::fs::remove_dir_all(dir);
}
#[cfg(feature = "duckdb-bundled")] #[cfg(feature = "duckdb-bundled")]
#[tokio::test] #[tokio::test]
async fn duckdb_client_session_reuses_base_pool_to_avoid_file_locks() { async fn duckdb_client_session_reuses_base_pool_to_avoid_file_locks() {
+16
View File
@@ -805,6 +805,12 @@ pub async fn do_execute(
cancel_token: Option<CancellationToken>, cancel_token: Option<CancellationToken>,
options: QueryExecutionOptions, options: QueryExecutionOptions,
) -> Result<db::QueryResult, String> { ) -> Result<db::QueryResult, String> {
if let Some(execution_id) = options.execution_id.as_deref() {
state.running_queries.set_pool_key(execution_id, pool_key.to_string());
}
state.touch_pool_activity(pool_key).await;
let _activity_touch = state.pool_activity_touch(pool_key);
let query_timeout = resolve_query_timeout(options.timeout_secs); let query_timeout = resolve_query_timeout(options.timeout_secs);
let (_duckdb_attached_names, conn_name_if_readonly) = { let (_duckdb_attached_names, conn_name_if_readonly) = {
let configs = state.configs.read().await; let configs = state.configs.read().await;
@@ -1257,6 +1263,11 @@ async fn execute_postgres_drop_database(
let pool_key = state let pool_key = state
.get_or_create_pool_for_session(connection_id, Some(admin_database), options.client_session_id.as_deref()) .get_or_create_pool_for_session(connection_id, Some(admin_database), options.client_session_id.as_deref())
.await?; .await?;
if let Some(execution_id) = options.execution_id.as_deref() {
state.running_queries.set_pool_key(execution_id, pool_key.clone());
}
state.touch_pool_activity(&pool_key).await;
let _activity_touch = state.pool_activity_touch(pool_key.as_str());
if is_canceled(&cancel_token) { if is_canceled(&cancel_token) {
return Err(canceled_error()); return Err(canceled_error());
@@ -1390,6 +1401,11 @@ pub async fn execute_multi_core_with_options(
.get_or_create_pool_for_session(connection_id, Some(database), options.client_session_id.as_deref()) .get_or_create_pool_for_session(connection_id, Some(database), options.client_session_id.as_deref())
.await? .await?
}; };
if let Some(execution_id) = options.execution_id.as_deref() {
state.running_queries.set_pool_key(execution_id, pool_key.clone());
}
state.touch_pool_activity(&pool_key).await;
let _activity_touch = state.pool_activity_touch(pool_key.as_str());
let is_sqlserver = { let is_sqlserver = {
let connections = state.connections.read().await; let connections = state.connections.read().await;
+103 -4
View File
@@ -4,16 +4,64 @@ use tokio_util::sync::CancellationToken;
type InterruptFn = Box<dyn Fn() + Send + 'static>; type InterruptFn = Box<dyn Fn() + Send + 'static>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RunningTaskKind {
Query,
Count,
Explain,
Export,
Unknown,
}
impl Default for RunningTaskKind {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Clone, Debug, Default)]
pub struct RunningTaskMetadata {
pub kind: RunningTaskKind,
pub connection_id: Option<String>,
pub database: Option<String>,
pub client_session_id: Option<String>,
}
impl RunningTaskMetadata {
pub fn query(
connection_id: impl Into<String>,
database: impl Into<String>,
client_session_id: Option<String>,
) -> Self {
let kind = task_kind_from_client_session_id(client_session_id.as_deref());
Self { kind, connection_id: Some(connection_id.into()), database: Some(database.into()), client_session_id }
}
}
#[derive(Clone)]
struct RunningTask {
token: CancellationToken,
metadata: RunningTaskMetadata,
pool_key: Option<String>,
}
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct RunningQueries { pub struct RunningQueries {
inner: Arc<Mutex<HashMap<String, CancellationToken>>>, inner: Arc<Mutex<HashMap<String, RunningTask>>>,
interrupts: Arc<Mutex<HashMap<String, InterruptFn>>>, interrupts: Arc<Mutex<HashMap<String, InterruptFn>>>,
} }
impl RunningQueries { impl RunningQueries {
pub fn register(&self, execution_id: String) -> RegisteredQuery { pub fn register(&self, execution_id: String) -> RegisteredQuery {
self.register_task(execution_id, RunningTaskMetadata::default())
}
pub fn register_task(&self, execution_id: String, metadata: RunningTaskMetadata) -> RegisteredQuery {
let token = CancellationToken::new(); let token = CancellationToken::new();
self.inner.lock().unwrap_or_else(|e| e.into_inner()).insert(execution_id.clone(), token.clone()); self.inner
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(execution_id.clone(), RunningTask { token: token.clone(), metadata, pool_key: None });
RegisteredQuery { execution_id, token, running_queries: self.clone() } RegisteredQuery { execution_id, token, running_queries: self.clone() }
} }
@@ -23,7 +71,8 @@ impl RunningQueries {
} }
pub fn cancel(&self, execution_id: &str) -> bool { pub fn cancel(&self, execution_id: &str) -> bool {
let token = self.inner.lock().unwrap_or_else(|e| e.into_inner()).get(execution_id).cloned(); let token =
self.inner.lock().unwrap_or_else(|e| e.into_inner()).get(execution_id).map(|task| task.token.clone());
let interrupt = self.interrupts.lock().unwrap_or_else(|e| e.into_inner()).remove(execution_id); let interrupt = self.interrupts.lock().unwrap_or_else(|e| e.into_inner()).remove(execution_id);
if let Some(interrupt) = interrupt { if let Some(interrupt) = interrupt {
@@ -37,17 +86,51 @@ impl RunningQueries {
} }
} }
pub fn set_pool_key(&self, execution_id: &str, pool_key: impl Into<String>) {
if let Some(task) = self.inner.lock().unwrap_or_else(|e| e.into_inner()).get_mut(execution_id) {
task.pool_key = Some(pool_key.into());
}
}
pub fn is_pool_active(&self, pool_key: &str) -> bool {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).values().any(|task| {
let _kind = task.metadata.kind;
task.pool_key.as_deref() == Some(pool_key)
})
}
#[cfg(test)] #[cfg(test)]
pub fn has(&self, execution_id: &str) -> bool { pub fn has(&self, execution_id: &str) -> bool {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).contains_key(execution_id) self.inner.lock().unwrap_or_else(|e| e.into_inner()).contains_key(execution_id)
} }
#[cfg(test)]
pub fn task_kind(&self, execution_id: &str) -> Option<RunningTaskKind> {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).get(execution_id).map(|task| task.metadata.kind)
}
fn remove(&self, execution_id: &str) { fn remove(&self, execution_id: &str) {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).remove(execution_id); self.inner.lock().unwrap_or_else(|e| e.into_inner()).remove(execution_id);
self.interrupts.lock().unwrap_or_else(|e| e.into_inner()).remove(execution_id); self.interrupts.lock().unwrap_or_else(|e| e.into_inner()).remove(execution_id);
} }
} }
fn task_kind_from_client_session_id(client_session_id: Option<&str>) -> RunningTaskKind {
let Some(session_id) = client_session_id else {
return RunningTaskKind::Query;
};
let session_id = session_id.trim().to_ascii_lowercase();
if session_id.ends_with(":count") {
RunningTaskKind::Count
} else if session_id.ends_with(":explain") {
RunningTaskKind::Explain
} else if session_id.ends_with(":export") {
RunningTaskKind::Export
} else {
RunningTaskKind::Query
}
}
pub struct RegisteredQuery { pub struct RegisteredQuery {
execution_id: String, execution_id: String,
token: CancellationToken, token: CancellationToken,
@@ -68,7 +151,7 @@ impl Drop for RegisteredQuery {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::RunningQueries; use super::{RunningQueries, RunningTaskKind, RunningTaskMetadata};
#[test] #[test]
fn cancel_marks_registered_query_as_cancelled() { fn cancel_marks_registered_query_as_cancelled() {
@@ -103,4 +186,20 @@ mod tests {
assert!(!running.has("exec-1")); assert!(!running.has("exec-1"));
} }
#[test]
fn register_task_tracks_kind_and_pool_activity() {
let running = RunningQueries::default();
let registered = running.register_task(
"exec-1".to_string(),
RunningTaskMetadata::query("conn-1", "main", Some("tab-1:export".to_string())),
);
running.set_pool_key("exec-1", "conn-1:main:session:tab-1_export");
assert_eq!(running.task_kind("exec-1"), Some(RunningTaskKind::Export));
assert!(running.is_pool_active("conn-1:main:session:tab-1_export"));
drop(registered);
assert!(!running.is_pool_active("conn-1:main:session:tab-1_export"));
}
} }
+9 -2
View File
@@ -6,6 +6,7 @@ use serde::Deserialize;
use crate::error::AppError; use crate::error::AppError;
use crate::state::WebState; use crate::state::WebState;
use dbx_core::query_cancel::RunningTaskMetadata;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -269,7 +270,10 @@ pub async fn execute_query(
) -> Result<Json<serde_json::Value>, AppError> { ) -> Result<Json<serde_json::Value>, AppError> {
let execution_id = req.execution_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); let execution_id = req.execution_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let registered = state.app.running_queries.register(execution_id.clone()); let registered = state.app.running_queries.register_task(
execution_id.clone(),
RunningTaskMetadata::query(req.connection_id.clone(), req.database.clone(), req.client_session_id.clone()),
);
let cancel_token = registered.token(); let cancel_token = registered.token();
let result = dbx_core::query::execute_sql_statement_with_options( let result = dbx_core::query::execute_sql_statement_with_options(
@@ -302,7 +306,10 @@ pub async fn execute_multi(
) -> Result<Json<serde_json::Value>, AppError> { ) -> Result<Json<serde_json::Value>, AppError> {
let execution_id = req.execution_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); let execution_id = req.execution_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let registered = state.app.running_queries.register(execution_id.clone()); let registered = state.app.running_queries.register_task(
execution_id.clone(),
RunningTaskMetadata::query(req.connection_id.clone(), req.database.clone(), req.client_session_id.clone()),
);
let cancel_token = registered.token(); let cancel_token = registered.token();
let result = dbx_core::query::execute_multi_core_with_options( let result = dbx_core::query::execute_multi_core_with_options(
+18 -7
View File
@@ -5,6 +5,7 @@ use tauri::State;
use crate::commands::connection::AppState; use crate::commands::connection::AppState;
use dbx_core::db; use dbx_core::db;
use dbx_core::models::connection::DatabaseType; use dbx_core::models::connection::DatabaseType;
use dbx_core::query_cancel::RunningTaskMetadata;
use dbx_core::sql::split_sql_statements; use dbx_core::sql::split_sql_statements;
#[tauri::command] #[tauri::command]
@@ -23,8 +24,13 @@ pub async fn execute_query(
client_session_id: Option<String>, client_session_id: Option<String>,
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
) -> Result<db::QueryResult, String> { ) -> Result<db::QueryResult, String> {
let registered_query = let execution_id = execution_id.filter(|id| !id.trim().is_empty());
execution_id.as_ref().filter(|id| !id.trim().is_empty()).map(|id| state.running_queries.register(id.clone())); let registered_query = execution_id.as_ref().map(|id| {
state.running_queries.register_task(
id.clone(),
RunningTaskMetadata::query(connection_id.clone(), database.clone(), client_session_id.clone()),
)
});
let cancel_token = registered_query.as_ref().map(|query| query.token()); let cancel_token = registered_query.as_ref().map(|query| query.token());
dbx_core::query::execute_sql_statement_with_options( dbx_core::query::execute_sql_statement_with_options(
@@ -41,7 +47,7 @@ pub async fn execute_query(
result_session_id, result_session_id,
client_session_id, client_session_id,
timeout_secs, timeout_secs,
execution_id: execution_id.filter(|id| !id.trim().is_empty()), execution_id,
}, },
) )
.await .await
@@ -63,10 +69,15 @@ pub async fn execute_multi(
client_session_id: Option<String>, client_session_id: Option<String>,
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
) -> Result<Vec<db::QueryResult>, String> { ) -> Result<Vec<db::QueryResult>, String> {
let registered_query = let execution_id = execution_id.filter(|id| !id.trim().is_empty());
execution_id.as_ref().filter(|id| !id.trim().is_empty()).map(|id| state.running_queries.register(id.clone())); let registered_query = execution_id.as_ref().map(|id| {
state.running_queries.register_task(
id.clone(),
RunningTaskMetadata::query(connection_id.clone(), database.clone(), client_session_id.clone()),
)
});
let cancel_token = registered_query.as_ref().map(|query| query.token()); let cancel_token = registered_query.as_ref().map(|query| query.token());
let trace_id = execution_id.clone().as_deref().unwrap_or("no-execution-id").to_string(); let trace_id = execution_id.as_deref().unwrap_or("no-execution-id").to_string();
let started_at = Instant::now(); let started_at = Instant::now();
log::info!( log::info!(
"[query][execute_multi:start] trace_id={} connection_id={} database={} schema={:?} sql={}", "[query][execute_multi:start] trace_id={} connection_id={} database={} schema={:?} sql={}",
@@ -91,7 +102,7 @@ pub async fn execute_multi(
result_session_id, result_session_id,
client_session_id, client_session_id,
timeout_secs, timeout_secs,
execution_id: execution_id.filter(|id| !id.trim().is_empty()), execution_id,
}, },
) )
.await; .await;