feat(transaction): configurable manual transaction idle timeout

Add a per-connection manual_transaction_timeout_secs (default 300s,
30-86400s) honored by every idle-rollback path. When a session that may
hold uncommitted work expires, stop and show the rollback notice instead
of replaying the statement into a fresh transaction; proven read-only
sessions still restart silently. Error and notice texts no longer
hard-code five minutes.
This commit is contained in:
t8y2
2026-10-16 13:52:24 +08:00
parent 3de078190e
commit f441d781ec
63 changed files with 378 additions and 99 deletions
@@ -77,7 +77,17 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { isWindows } from "@/lib/backend/platform";
import { applyMeilisearchBasePathToExternalConfig, applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
import { hasXuguConnectionDatabase } from "@/lib/connection/xuguDatabase";
import { DEFAULT_QUERY_TIMEOUT_SECS, MAX_CONNECT_TIMEOUT_SECS, MAX_IDLE_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS, supportsIdleTimeout } from "@/lib/connection/timeoutLimits";
import {
DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS,
DEFAULT_QUERY_TIMEOUT_SECS,
MAX_CONNECT_TIMEOUT_SECS,
MAX_IDLE_TIMEOUT_SECS,
MAX_MANUAL_TRANSACTION_TIMEOUT_SECS,
MAX_QUERY_TIMEOUT_SECS,
MIN_MANUAL_TRANSACTION_TIMEOUT_SECS,
normalizeManualTransactionTimeoutSecs,
supportsIdleTimeout,
} from "@/lib/connection/timeoutLimits";
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection";
import { applyConnectionDeepLinkUpdate, resolveConnectionDeepLinkUpdate } from "@/lib/connection/connectionDeepLinkUpdate";
import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseConnectionDeepLinkUpdate, parseServiceConnectionUrl, type ConnectionDeepLinkDraft, type ConnectionDeepLinkUpdate } from "@/lib/connection/connectionDeepLink";
@@ -156,7 +166,7 @@ import {
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleObjectFiltersNeedReset } from "@/lib/connection/connectionVisibleDatabases";
import { resolveVisibleDatabaseSaveAction } from "@/components/sidebar/visibleDatabasesDialogState";
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
import { isSchemaAware, isSingleDatabase, supportsDataDictionary } from "@/lib/database/databaseFeatureSupport";
import { isSchemaAware, isSingleDatabase, supportsDataDictionary, supportsTransaction } from "@/lib/database/databaseFeatureSupport";
import { normalizeConnectionScope, normalizeConnectionTimeouts } from "@/lib/connection/connectionSubmitNormalization";
import { databaseConnectionFormKind, databaseManifestEntry } from "@/lib/database/databaseDriverManifest";
import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue";
@@ -423,6 +433,7 @@ const defaultForm = (): ConnectionForm => ({
query_timeout_secs: settingsStore.editorSettings.globalQueryTimeoutSecs,
query_timeout_inherit: true,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS,
keepalive_interval_secs: 30,
ssl: false,
ca_cert_path: "",
@@ -3120,6 +3131,7 @@ watch(
query_timeout_secs: config.query_timeout_inherit === true ? settingsStore.editorSettings.globalQueryTimeoutSecs : (config.query_timeout_secs ?? DEFAULT_QUERY_TIMEOUT_SECS),
query_timeout_inherit: config.query_timeout_inherit === true,
idle_timeout_secs: config.idle_timeout_secs ?? 60,
manual_transaction_timeout_secs: normalizeManualTransactionTimeoutSecs(config.manual_transaction_timeout_secs),
keepalive_interval_secs: config.keepalive_interval_secs ?? 30,
sysdba: config.sysdba || isOracleSysUser(config),
oracle_oci_nls_lang: config.oracle_oci_nls_lang || "",
@@ -3856,6 +3868,7 @@ const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]);
const supportsMysqlTlsOptions = computed(() => mysqlTlsOptionsSupported(form.value.db_type, selectedType.value));
const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value));
const supportsIdleTimeoutSetting = computed(() => supportsIdleTimeout(form.value.db_type));
const supportsManualTransactionTimeoutSetting = computed(() => supportsTransaction(form.value.db_type));
const supportsDoltSystemTables = computed(() => isDoltDriverProfile(form.value.driver_profile));
const showDoltSystemTables = computed({
get: () => doltSystemTablesVisible(form.value),
@@ -4846,6 +4859,7 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
config.query_timeout_secs = form.value.query_timeout_secs;
config.query_timeout_inherit = form.value.query_timeout_inherit;
config.idle_timeout_secs = form.value.idle_timeout_secs;
config.manual_transaction_timeout_secs = normalizeManualTransactionTimeoutSecs(form.value.manual_transaction_timeout_secs);
config.keepalive_interval_secs = form.value.keepalive_interval_secs;
config.read_only = form.value.read_only;
config.save_password = form.value.save_password;
@@ -10324,6 +10338,13 @@ function openExternalUrl(url: string) {
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.idleTimeoutHint") }}</p>
</div>
</div>
<div v-show="supportsManualTransactionTimeoutSetting" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.manualTransactionTimeout") }}</Label>
<div class="col-span-3 space-y-1">
<Input v-model.number="form.manual_transaction_timeout_secs" type="number" :min="MIN_MANUAL_TRANSACTION_TIMEOUT_SECS" :max="MAX_MANUAL_TRANSACTION_TIMEOUT_SECS" step="1" />
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.manualTransactionTimeoutHint", { min: MIN_MANUAL_TRANSACTION_TIMEOUT_SECS, max: MAX_MANUAL_TRANSACTION_TIMEOUT_SECS }) }}</p>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.keepaliveInterval") }}</Label>
<div class="col-span-3 flex items-center gap-2">
+4 -2
View File
@@ -263,7 +263,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Əl ilə tranzaksiya (Agent server hissəsindən asılıdır)",
commit: "Təsdiqlə",
rollback: "Geri qaytar",
txnAutoRolledBack: "5 dəqiqə fəaliyyətsizlikdən sonra tranzaksiya avtomatik geri qaytarıldı",
txnAutoRolledBack: "Tranzaksiya fəaliyyətsizlik səbəbindən avtomatik geri qaytarıldı; təsdiqlənməmiş dəyişikliklər ləğv edildi",
autoCommitTxnRolledBack: 'Bu nişanın açıq qoyduğu tranzaksiya avtomatik geri qaytarıldı; icralar arasında saxlamaq üçün ayarlarda "Avtomatik commit rejimində açıq tranzaksiyaları saxla" seçimini aktivləşdirin',
autoCommitSessionTxnRolledBack: "Bu bağlantı autocommit söndürülmüş (SET autocommit = 0) işləyir, ona görə örtük tranzaksiya icradan sonra geri qaytarıldı; DBX bunu hər bağlantı üçün bir dəfə bildirir",
previewSql: "SQL önbaxışı",
@@ -1315,6 +1315,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Əlaqə",
idleTimeout: "Boşdayanma vaxt limiti (saniyə)",
idleTimeoutHint: "Bağlantı hovuzundakı boşda qalan bağlantıların geri qaytarılmasına tətbiq edilir.",
manualTransactionTimeout: "Tranzaksiyanın boşdayanma vaxt limiti (saniyə)",
manualTransactionTimeoutHint: "Bu müddət ərzində heç bir ifadə icra edilməyən əl ilə idarə olunan tranzaksiya avtomatik geri qaytarılır. İcazə verilən aralıq: {min}–{max} saniyə.",
keepaliveInterval: "Əlaqənin canlı saxlanma intervalı (saniyə)",
readOnly: "Yalnız oxuma",
readOnlyHint: "Bütün yazma əməliyyatlarını blokla (INSERT, UPDATE, DELETE və s.)",
@@ -2751,7 +2753,7 @@ export default withEnglishFallback({
legacyFailure: "Verilənlər bazası agenti təsnif edilməmiş xəta qaytardı.",
},
transaction: {
sessionExpired: "Əl ilə idarə olunan tranzaksiya {timeoutSecs} saniyə fəaliyyətsizlikdən sonra vaxtı bitdiyi üçün geri qaytarıldı. Yeni tranzaksiya avtomatik başladılacaq.",
sessionExpired: "Əl ilə idarə olunan tranzaksiya boşdayanma müddətini aşdı və geri qaytarıldı; təsdiqlənməmiş dəyişikliklər ləğv edildi. İfadə icra edilmədi — yeni tranzaksiya başlatmaq üçün yenidən icra edin.",
},
legacy: "Server tərəfi xəta qaytardı.",
unknown: "Server tərəfində gözlənilməz xəta baş verdi.",
+5 -3
View File
@@ -781,7 +781,7 @@ export default {
manualTransactionAgent: "Manual Transaction (depends on Agent backend)",
commit: "Commit",
rollback: "Rollback",
txnAutoRolledBack: "Transaction auto-rolled back after 5 minutes of inactivity",
txnAutoRolledBack: "Transaction auto-rolled back after being idle. Uncommitted changes were discarded.",
salesforceIdentityAdmin: "Profile has “Modify All Data” — bypasses field-level security and record sharing",
salesforceIdentityNonAdmin: "Profile has no “Modify All Data” — field permissions and record sharing apply",
salesforceIdentityUnknownRights: "Could not determine whether the profile has “Modify All Data”",
@@ -1951,6 +1951,8 @@ export default {
useConnectionQueryTimeout: "Connection",
idleTimeout: "Idle Timeout (seconds)",
idleTimeoutHint: "Applies to idle connection reclamation within the connection pool.",
manualTransactionTimeout: "Transaction Idle Timeout (seconds)",
manualTransactionTimeoutHint: "A manual transaction with no statement executed for this long is rolled back automatically. Allowed range: {min}–{max} seconds.",
keepaliveInterval: "Keepalive Interval (seconds)",
readOnly: "Read Only",
readOnlyHint: "Block all write operations (INSERT, UPDATE, DELETE, etc.)",
@@ -3538,10 +3540,10 @@ export default {
rollbackFailed: "The transaction ended, but rollback could not be confirmed. Verify the database before running the SQL again.",
commitUnknown: "The COMMIT response was lost. The transaction may already be committed. Verify the database before running the SQL again.",
controlConflict: "Manual mode manages transaction control and the database context. Remove BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS or USE from the script.",
idleTimeout: "The transaction ended after five minutes of inactivity. Start a new transaction to continue.",
idleTimeout: "The transaction was rolled back after going idle. Start a new transaction to continue.",
executionFailed: "SQL execution failed and this manual transaction ended.",
sessionExpired: "The manual transaction expired after {timeoutSecs} seconds of inactivity and was rolled back. A new transaction will be started automatically.",
sessionExpired: "The manual transaction timed out while idle and was rolled back; uncommitted changes were discarded. The statement was not executed — run it again to start a new transaction.",
},
legacy: "Backend returned an error.",
unknown: "An unexpected backend error occurred.",
+5 -3
View File
@@ -630,7 +630,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Transacción Manual (depende del backend del Agente)",
commit: "Confirmar",
rollback: "Revertir",
txnAutoRolledBack: "Transacción revertida automáticamente tras 5 minutos de inactividad",
txnAutoRolledBack: "Transacción revertida automáticamente por inactividad; se descartaron los cambios sin confirmar",
salesforceIdentityAdmin: "El perfil tiene “Modify All Data”: omite la seguridad a nivel de campo y el uso compartido de registros",
salesforceIdentityNonAdmin: "El perfil no tiene “Modify All Data”: los permisos de campos y el uso compartido siguen aplicándose",
salesforceIdentityUnknownRights: "No se pudo determinar si el perfil tiene “Modify All Data”",
@@ -1542,6 +1542,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Conexión",
idleTimeout: "Tiempo de espera inactivo (segundos)",
idleTimeoutHint: "Se aplica a la recuperación de conexiones inactivas dentro del grupo de conexiones.",
manualTransactionTimeout: "Tiempo de inactividad de la transacción (segundos)",
manualTransactionTimeoutHint: "Una transacción manual sin ninguna sentencia ejecutada durante este tiempo se revierte automáticamente. Rango permitido: {min}–{max} segundos.",
keepaliveInterval: "Intervalo de keepalive (segundos)",
readOnly: "Solo lectura",
readOnlyHint: "Bloquear todas las operaciones de escritura (INSERT, UPDATE, DELETE, etc.)",
@@ -3303,9 +3305,9 @@ export default withEnglishFallback({
rollbackFailed: "La transacción terminó, pero no se pudo confirmar la reversión. Verifique la base de datos antes de volver a ejecutar el SQL.",
commitUnknown: "Se perdió la respuesta de COMMIT. La transacción puede haberse confirmado. Verifique la base de datos antes de volver a ejecutar el SQL.",
controlConflict: "El modo manual administra las transacciones y el contexto de la base de datos. Quite BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS o USE del script.",
idleTimeout: "La transacción terminó tras cinco minutos de inactividad. Inicie una nueva transacción para continuar.",
idleTimeout: "La transacción se revirtió por inactividad. Inicie una nueva transacción para continuar.",
executionFailed: "La ejecución del SQL falló y esta transacción manual terminó.",
sessionExpired: "La transacción manual expiró después de {timeoutSecs} segundos de inactividad y se revirtió. Se iniciará una nueva transacción automáticamente.",
sessionExpired: "La transacción manual agotó su tiempo de inactividad y se revirtió; se descartaron los cambios sin confirmar. La sentencia no se ejecutó: vuelva a ejecutarla para iniciar una nueva transacción.",
},
legacy: "El backend devolvió un error.",
unknown: "Ocurrió un error inesperado del backend.",
+4 -2
View File
@@ -409,7 +409,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Transaksi Manual (bergantung pada backend Agent)",
commit: "Commit",
rollback: "Rollback",
txnAutoRolledBack: "Transaksi di-rollback otomatis setelah 5 menit tidak aktif",
txnAutoRolledBack: "Transaksi di-rollback otomatis karena tidak aktif; perubahan yang belum di-commit dibuang",
salesforceIdentityAdmin: "Profil memiliki “Modify All Data” — melewati keamanan tingkat field dan berbagi record",
salesforceIdentityNonAdmin: "Profil tidak memiliki “Modify All Data” — izin field dan berbagi record berlaku",
salesforceIdentityUnknownRights: "Tidak dapat menentukan apakah profil memiliki “Modify All Data”",
@@ -1578,6 +1578,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Koneksi",
idleTimeout: "Timeout Idle (detik)",
idleTimeoutHint: "Berlaku untuk pemulihan koneksi idle dalam connection pool.",
manualTransactionTimeout: "Timeout Idle Transaksi (detik)",
manualTransactionTimeoutHint: "Transaksi manual tanpa pernyataan yang dijalankan selama durasi ini akan di-rollback otomatis. Rentang yang diizinkan: {min}–{max} detik.",
keepaliveInterval: "Interval Keepalive (detik)",
readOnly: "Read Only",
readOnlyHint: "Blokir semua operasi tulis (INSERT, UPDATE, DELETE, dll.)",
@@ -3074,7 +3076,7 @@ export default withEnglishFallback({
legacyFailure: "Agent database mengembalikan galat yang tidak terklasifikasi.",
},
transaction: {
sessionExpired: "Transaksi manual kedaluwarsa setelah {timeoutSecs} detik tidak ada aktivitas dan telah di-rollback. Transaksi baru akan dimulai secara otomatis.",
sessionExpired: "Transaksi manual melewati batas waktu tidak aktif dan di-rollback; perubahan yang belum di-commit dibuang. Pernyataan tidak dijalankan — jalankan lagi untuk memulai transaksi baru.",
},
legacy: "Backend mengembalikan galat.",
unknown: "Terjadi galat backend yang tidak terduga.",
+5 -3
View File
@@ -624,7 +624,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Transazione Manuale (dipende dal backend dell'Agente)",
commit: "Commit",
rollback: "Rollback",
txnAutoRolledBack: "Transazione annullata automaticamente dopo 5 minuti di inattività",
txnAutoRolledBack: "Transazione annullata automaticamente per inattività; le modifiche non confermate sono state scartate",
autoCommitTxnRolledBack: 'La transazione esplicita lasciata aperta da questa scheda è stata annullata automaticamente; attiva "Mantieni le transazioni esplicite in autocommit" per conservarla tra le esecuzioni',
autoCommitSessionTxnRolledBack: "Questa connessione lavora con autocommit disattivato (SET autocommit = 0), quindi la sua transazione implicita è stata annullata dopo l'esecuzione; DBX lo segnala una volta per connessione",
previewSql: "Anteprima SQL",
@@ -1464,6 +1464,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Connessione",
idleTimeout: "Timeout Inattività (secondi)",
idleTimeoutHint: "Si applica al recupero delle connessioni inattive all'interno del pool di connessioni.",
manualTransactionTimeout: "Timeout inattività transazione (secondi)",
manualTransactionTimeoutHint: "Una transazione manuale senza istruzioni eseguite per questo periodo viene annullata automaticamente. Intervallo consentito: {min}–{max} secondi.",
keepaliveInterval: "Intervallo keepalive (secondi)",
readOnly: "Sola lettura",
readOnlyHint: "Blocca tutte le operazioni di scrittura (INSERT, UPDATE, DELETE, ecc.)",
@@ -3159,9 +3161,9 @@ export default withEnglishFallback({
rollbackFailed: "La transazione è terminata, ma il rollback non è stato confermato. Verificare il database prima di rieseguire il SQL.",
commitUnknown: "La risposta a COMMIT è stata persa. La transazione potrebbe essere già stata confermata. Verificare il database prima di rieseguire il SQL.",
controlConflict: "La modalità manuale gestisce le transazioni e il contesto del database. Rimuovere BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS o USE dallo script.",
idleTimeout: "La transazione è terminata dopo cinque minuti di inattività. Avviare una nuova transazione per continuare.",
idleTimeout: "La transazione è stata annullata per inattività. Avviare una nuova transazione per continuare.",
executionFailed: "L'esecuzione del SQL è fallita e questa transazione manuale è terminata.",
sessionExpired: "La transazione manuale è scaduta dopo {timeoutSecs} secondi di inattività ed è stata annullata. Verrà avviata automaticamente una nuova transazione.",
sessionExpired: "La transazione manuale è scaduta per inattività ed è stata annullata; le modifiche non confermate sono state scartate. L'istruzione non è stata eseguita: eseguirla di nuovo per avviare una nuova transazione.",
},
legacy: "Il backend ha restituito un errore.",
unknown: "Si è verificato un errore backend imprevisto.",
+5 -3
View File
@@ -625,7 +625,7 @@ export default withEnglishFallback({
manualTransactionAgent: "手動トランザクション(Agentバックエンドに依存)",
commit: "コミット",
rollback: "ロールバック",
txnAutoRolledBack: "5分間操作がなかったため、トランザクションは自動的にロールバックされました",
txnAutoRolledBack: "アイドルタイムアウトによりトランザクションが自動的にロールバックされ、未コミットの変更は破棄されました",
autoCommitTxnRolledBack: "このタブに残っていた明示的トランザクションは自動的にロールバックされました。実行をまたいで保持するには設定の「自動コミットでも明示的なトランザクションを保持」を有効にしてください",
autoCommitSessionTxnRolledBack: "この接続は autocommit=0 で動作しているため、実行後に暗黙のトランザクションがロールバックされました(同じ接続では一度だけ通知します)",
previewSql: "SQLをプレビュー",
@@ -1434,6 +1434,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "接続",
idleTimeout: "アイドルタイムアウト(秒)",
idleTimeoutHint: "コネクションプール内のアイドル接続の回収・解放に適用されます。",
manualTransactionTimeout: "トランザクションのアイドルタイムアウト(秒)",
manualTransactionTimeoutHint: "手動トランザクションでこの時間ステートメントが実行されない場合、自動的にロールバックされます。指定可能な範囲: {min}~{max} 秒。",
keepaliveInterval: "Keepalive 間隔(秒)",
readOnly: "読み取り専用",
readOnlyHint: "すべての書き込み操作(INSERT, UPDATE, DELETEなど)をブロックします",
@@ -3183,9 +3185,9 @@ export default withEnglishFallback({
rollbackFailed: "トランザクションは終了しましたが、ロールバックは確認できませんでした。SQL を再実行する前にデータベースを確認してください。",
commitUnknown: "COMMIT の応答が失われました。トランザクションは既にコミットされている可能性があります。SQL を再実行する前にデータベースを確認してください。",
controlConflict: "手動モードがトランザクション制御とデータベースのコンテキストを管理します。スクリプトから BEGIN/COMMIT/ROLLBACK/SAVE、SET IMPLICIT_TRANSACTIONS、USE を削除してください。",
idleTimeout: "5 分間操作がなかったためトランザクションが終了しました。続行するには新しいトランザクションを開始してください。",
idleTimeout: "アイドルタイムアウトによりトランザクションがロールバックされました。続行するには新しいトランザクションを開始してください。",
executionFailed: "SQL の実行に失敗し、この手動トランザクションは終了しました。",
sessionExpired: "手動トランザクションは {timeoutSecs} 秒間操作がなかったため期限切れとなり、ロールバックされました。新しいトランザクションを自動的に開始します。",
sessionExpired: "手動トランザクションはアイドルタイムアウトによりロールバックされ、未コミットの変更は破棄されました。このステートメントは実行されていません。再実行して新しいトランザクションを開始してください。",
},
legacy: "バックエンドがエラーを返しました。",
unknown: "予期しないバックエンドエラーが発生しました。",
+5 -3
View File
@@ -632,7 +632,7 @@ export default withEnglishFallback({
manualTransactionAgent: "수동 트랜잭션 (Agent 백엔드에 따라)",
commit: "커밋",
rollback: "롤백",
txnAutoRolledBack: "5분간 활동이 없어 트랜잭션이 자동으로 롤백되었습니다",
txnAutoRolledBack: "유휴 제한 시간 초과로 트랜잭션이 자동으로 롤백되어 커밋되지 않은 변경 사항이 취소되었습니다",
autoCommitTxnRolledBack: '이 탭에 남아 있던 명시적 트랜잭션이 자동으로 롤백되었습니다. 실행 간에 유지하려면 설정에서 "자동 커밋 모드에서 명시적 트랜잭션 유지"를 켜세요',
autoCommitSessionTxnRolledBack: "이 연결은 autocommit=0 상태이므로 실행 후 암시적 트랜잭션이 롤백되었습니다(같은 연결에서는 한 번만 알립니다)",
previewSql: "SQL 미리보기",
@@ -1618,6 +1618,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "연결",
idleTimeout: "유휴 제한 시간 (초)",
idleTimeoutHint: "연결 풀 내 유휴 연결의 회수 및 해제에 적용됩니다.",
manualTransactionTimeout: "트랜잭션 유휴 제한 시간 (초)",
manualTransactionTimeoutHint: "수동 트랜잭션에서 이 시간 동안 문이 실행되지 않으면 자동으로 롤백됩니다. 허용 범위: {min}~{max}초.",
keepaliveInterval: "킵얼라이브 간격 (초)",
readOnly: "읽기 전용",
readOnlyHint: "모든 쓰기 작업 차단 (INSERT, UPDATE, DELETE 등)",
@@ -3110,9 +3112,9 @@ export default withEnglishFallback({
rollbackFailed: "트랜잭션은 종료되었지만 롤백을 확인할 수 없습니다. SQL을 다시 실행하기 전에 데이터베이스를 확인하세요.",
commitUnknown: "COMMIT 응답이 손실되었습니다. 트랜잭션이 이미 커밋되었을 수 있습니다. SQL을 다시 실행하기 전에 데이터베이스를 확인하세요.",
controlConflict: "수동 모드에서 트랜잭션 제어와 데이터베이스 컨텍스트를 관리합니다. 스크립트에서 BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS 또는 USE를 제거하세요.",
idleTimeout: "5분간 활동이 없어 트랜잭션이 종료되었습니다. 계속하려면 새 트랜잭션을 시작하세요.",
idleTimeout: "유휴 제한 시간 초과로 트랜잭션이 롤백되었습니다. 계속하려면 새 트랜잭션을 시작하세요.",
executionFailed: "SQL 실행이 실패하여 이 수동 트랜잭션이 종료되었습니다.",
sessionExpired: "수동 트랜잭션이 {timeoutSecs}초 동안 활동이 없어 만료되고 롤백되었습니다. 새 트랜잭션을 자동으로 시작합니다.",
sessionExpired: "수동 트랜잭션이 유휴 제한 시간 초과로 롤백되어 커밋되지 않은 변경 사항이 취소되었습니다. 이 문은 실행되지 않았습니다. 다시 실행하여 새 트랜잭션을 시작하세요.",
},
legacy: "백엔드에서 오류를 반환했습니다.",
unknown: "예상하지 못한 백엔드 오류가 발생했습니다.",
+5 -3
View File
@@ -625,7 +625,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Transação Manual (depende do backend do Agente)",
commit: "Confirmar",
rollback: "Reverter",
txnAutoRolledBack: "Transação revertida automaticamente após 5 minutos de inatividade",
txnAutoRolledBack: "Transação revertida automaticamente por inatividade; as alterações não confirmadas foram descartadas",
autoCommitTxnRolledBack: 'A transação explícita deixada aberta por esta aba foi revertida automaticamente; ative "Manter transações explícitas no autocommit" para mantê-la entre execuções',
autoCommitSessionTxnRolledBack: "Esta conexão está com o autocommit desativado (SET autocommit = 0), então sua transação implícita foi revertida após a execução; o DBX avisa uma vez por conexão",
previewSql: "Visualizar SQL",
@@ -1465,6 +1465,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Conexão",
idleTimeout: "Timeout de Inatividade (segundos)",
idleTimeoutHint: "Aplica-se à recuperação de conexões inativas no pool de conexões.",
manualTransactionTimeout: "Tempo limite de inatividade da transação (segundos)",
manualTransactionTimeoutHint: "Uma transação manual sem nenhuma instrução executada durante esse período é revertida automaticamente. Intervalo permitido: {min}–{max} segundos.",
keepaliveInterval: "Intervalo de keepalive (segundos)",
readOnly: "Somente leitura",
readOnlyHint: "Bloquear todas as operações de escrita (INSERT, UPDATE, DELETE, etc.)",
@@ -3159,9 +3161,9 @@ export default withEnglishFallback({
rollbackFailed: "A transação terminou, mas a reversão não pôde ser confirmada. Verifique o banco de dados antes de executar o SQL novamente.",
commitUnknown: "A resposta de COMMIT foi perdida. A transação pode já ter sido confirmada. Verifique o banco de dados antes de executar o SQL novamente.",
controlConflict: "O modo manual gerencia as transações e o contexto do banco de dados. Remova BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS ou USE do script.",
idleTimeout: "A transação terminou após cinco minutos de inatividade. Inicie uma nova transação para continuar.",
idleTimeout: "A transação foi revertida por inatividade. Inicie uma nova transação para continuar.",
executionFailed: "A execução do SQL falhou e esta transação manual terminou.",
sessionExpired: "A transação manual expirou após {timeoutSecs} segundos de inatividade e foi revertida. Uma nova transação será iniciada automaticamente.",
sessionExpired: "A transação manual expirou por inatividade e foi revertida; as alterações não confirmadas foram descartadas. A instrução não foi executada: execute-a novamente para iniciar uma nova transação.",
},
legacy: "O backend retornou um erro.",
unknown: "Ocorreu um erro inesperado no backend.",
+5 -3
View File
@@ -605,7 +605,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Ручная транзакция (зависит от бэкенда агента)",
commit: "Зафиксировать",
rollback: "Откатить",
txnAutoRolledBack: "Транзакция автоматически откатана после 5 минут бездействия",
txnAutoRolledBack: "Транзакция автоматически откатана из-за простоя; незафиксированные изменения отменены",
autoCommitTxnRolledBack: "Явная транзакция, оставленная открытой этой вкладкой, была автоматически откатана; включите «Сохранять явные транзакции с автофиксацией», чтобы сохранять её между выполнениями",
autoCommitSessionTxnRolledBack: "Это соединение работает с отключённой автофиксацией (SET autocommit = 0), поэтому его неявная транзакция была откатана после выполнения; DBX сообщает об этом один раз на соединение",
previewSql: "Предпросмотр SQL",
@@ -1716,6 +1716,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Подключение",
idleTimeout: "Тайм-аут простоя (секунды)",
idleTimeoutHint: "Применяется для освобождения простаивающих соединений в пуле соединений.",
manualTransactionTimeout: "Тайм-аут простоя транзакции (секунды)",
manualTransactionTimeoutHint: "Ручная транзакция, в которой за это время не выполнялось ни одной инструкции, автоматически откатывается. Допустимый диапазон: {min}–{max} секунд.",
keepaliveInterval: "Интервал keepalive (секунды)",
readOnly: "Только чтение",
readOnlyHint: "Блокировать все операции записи (INSERT, UPDATE, DELETE и т. д.)",
@@ -3260,9 +3262,9 @@ export default withEnglishFallback({
rollbackFailed: "Транзакция завершена, но откат не подтверждён. Проверьте базу данных перед повторным выполнением SQL.",
commitUnknown: "Ответ на COMMIT потерян. Возможно, транзакция уже зафиксирована. Проверьте базу данных перед повторным выполнением SQL.",
controlConflict: "Ручной режим управляет транзакциями и контекстом базы данных. Удалите из скрипта BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS или USE.",
idleTimeout: "Транзакция завершена после пяти минут бездействия. Начните новую транзакцию, чтобы продолжить.",
idleTimeout: "Транзакция откатана из-за простоя. Начните новую транзакцию, чтобы продолжить.",
executionFailed: "Выполнение SQL завершилось ошибкой; эта ручная транзакция завершена.",
sessionExpired: "Ручная транзакция истекла после {timeoutSecs} секунд бездействия и была отменена. Новая транзакция будет запущена автоматически.",
sessionExpired: "Ручная транзакция истекла из-за простоя и была отменена; незафиксированные изменения отброшены. Инструкция не выполнена — запустите её снова, чтобы начать новую транзакцию.",
},
legacy: "Бэкенд вернул ошибку.",
unknown: "Произошла непредвиденная ошибка бэкенда.",
+5 -3
View File
@@ -748,7 +748,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Transaction แบบ Manual (ขึ้นอยู่กับ Backend ของ Agent)",
commit: "Commit",
rollback: "ย้อนกลับ",
txnAutoRolledBack: "Rollback Transaction อัตโนมัติหลังไม่มีการใช้งาน 5 นาที",
txnAutoRolledBack: "Rollback Transaction อัตโนมัติเพราะไม่มีการใช้งาน การเปลี่ยนแปลงที่ยังไม่ Commit ถูกยกเลิก",
salesforceIdentityAdmin: "โปรไฟล์มี “Modify All Data” — ข้ามการรักษาความปลอดภัยระดับฟิลด์และการแชร์ระเบียน",
salesforceIdentityNonAdmin: "โปรไฟล์ไม่มี “Modify All Data” — สิทธิ์ระดับฟิลด์และการแชร์ระเบียนมีผล",
salesforceIdentityUnknownRights: "ไม่สามารถระบุได้ว่าโปรไฟล์มี “Modify All Data” หรือไม่",
@@ -1913,6 +1913,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "การเชื่อมต่อ",
idleTimeout: "Timeout เมื่อไม่มีการใช้งาน (วินาที)",
idleTimeoutHint: "มีผลกับการเรียกคืนการเชื่อมต่อที่ไม่มีการใช้งานภายใน Pool การเชื่อมต่อ",
manualTransactionTimeout: "Timeout ของ Transaction เมื่อไม่มีการใช้งาน (วินาที)",
manualTransactionTimeoutHint: "Transaction แบบ Manual ที่ไม่มีการรันคำสั่งภายในเวลานี้จะถูก Rollback อัตโนมัติ ช่วงที่อนุญาต: {min}–{max} วินาที",
keepaliveInterval: "ช่วง Keepalive (วินาที)",
readOnly: "อ่านอย่างเดียว",
readOnlyHint: "บล็อกการดำเนินการเขียนทั้งหมด (INSERT, UPDATE, DELETE ฯลฯ)",
@@ -3485,9 +3487,9 @@ export default withEnglishFallback({
rollbackFailed: "Transaction จบแล้ว แต่ยืนยัน Rollback ไม่ได้ ตรวจสอบฐานข้อมูลก่อนเรียกใช้ SQL อีกครั้ง",
commitUnknown: "การตอบกลับ COMMIT สูญหาย Transaction อาจ Commit แล้ว ตรวจสอบฐานข้อมูลก่อนเรียกใช้ SQL อีกครั้ง",
controlConflict: "โหมด Manual จัดการการควบคุม Transaction และ Context ฐานข้อมูล นำ BEGIN/COMMIT/ROLLBACK/SAVE, SET IMPLICIT_TRANSACTIONS หรือ USE ออกจาก Script",
idleTimeout: "Transaction จบหลังไม่มีการใช้งานห้านาที เริ่ม Transaction ใหม่เพื่อดำเนินการต่อ",
idleTimeout: "Transaction ถูก Rollback เพราะไม่มีการใช้งาน เริ่ม Transaction ใหม่เพื่อดำเนินการต่อ",
executionFailed: "การเรียกใช้ SQL ล้มเหลวและ Transaction แบบ Manual นี้จบลง",
sessionExpired: "Transaction แบบ Manual หมดอายุหลังไม่มีการใช้งาน {timeoutSecs} วินาทีและถูก Rollback จะเริ่ม Transaction ใหม่โดยอัตโนมัติ",
sessionExpired: "Transaction แบบ Manual หมดเวลาเพราะไม่มีการใช้งานและถูก Rollback การเปลี่ยนแปลงที่ยังไม่ Commit ถูกยกเลิก คำสั่งนี้ยังไม่ได้ถูกรัน รันอีกครั้งเพื่อเริ่ม Transaction ใหม่",
},
legacy: "Backend ส่งข้อผิดพลาดกลับมา",
unknown: "เกิดข้อผิดพลาด Backend ที่ไม่คาดคิด",
+4 -2
View File
@@ -263,7 +263,7 @@ export default withEnglishFallback({
manualTransactionAgent: "Manuel İşlem (Agent arka ucuna bağlıdır)",
commit: "Commit",
rollback: "Rollback",
txnAutoRolledBack: "İşlem, 5 dakikalık hareketsizlik sonrası otomatik olarak geri alındı",
txnAutoRolledBack: "İşlem, hareketsizlik nedeniyle otomatik olarak geri alındı; kaydedilmemiş değişiklikler atıldı",
autoCommitTxnRolledBack: 'Bu sekmenin açık bıraktığı açık işlem otomatik olarak geri alındı; yürütmeler arasında korumak için ayarlardan "Otomatik işlemede açık işlemleri koru" seçeneğini açın',
autoCommitSessionTxnRolledBack: "Bu bağlantı autocommit kapalı (SET autocommit = 0) çalışıyor; bu nedenle örtük işlem yürütmeden sonra geri alındı. DBX bunu bağlantı başına bir kez bildirir",
previewSql: "SQL Önizleme",
@@ -1315,6 +1315,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "Bağlantı",
idleTimeout: "Boşta Kalma Zaman Aşımı (saniye)",
idleTimeoutHint: "Bağlantı havuzundaki boşta kalan bağlantıların geri kazanılması için geçerlidir.",
manualTransactionTimeout: "İşlem Boşta Kalma Zaman Aşımı (saniye)",
manualTransactionTimeoutHint: "Bu süre boyunca hiçbir ifade çalıştırılmayan manuel işlem otomatik olarak geri alınır. İzin verilen aralık: {min}–{max} saniye.",
keepaliveInterval: "Keepalive Aralığı (saniye)",
readOnly: "Salt Okunur",
readOnlyHint: "Tüm yazma işlemlerini engelle (INSERT, UPDATE, DELETE vb.)",
@@ -2727,7 +2729,7 @@ export default withEnglishFallback({
legacyFailure: "Veritabanı agent'ı sınıflandırılmamış bir hata döndürdü.",
},
transaction: {
sessionExpired: "Manuel işlem, {timeoutSecs} saniyelik hareketsizlik sonrası sona erdi ve geri alındı. Otomatik olarak yeni bir işlem başlatılacak.",
sessionExpired: "Manuel işlem boşta kalma süresini aştı ve geri alındı; kaydedilmemiş değişiklikler atıldı. İfade çalıştırılmadı; yeni bir işlem başlatmak için yeniden çalıştırın.",
},
legacy: "Arka uç bir hata döndürdü.",
unknown: "Beklenmeyen bir arka uç hatası oluştu.",
+5 -3
View File
@@ -717,7 +717,7 @@ export default withEnglishFallback({
manualTransactionAgent: "手动事务(取决于 Agent 后端数据库)",
commit: "提交",
rollback: "回滚",
txnAutoRolledBack: "事务已因 5 分钟无操作而自动回滚",
txnAutoRolledBack: "事务因空闲超时已自动回滚,未提交的改动已丢弃。",
salesforceIdentityAdmin: "简档具有“修改所有数据”权限——会绕过字段级安全与记录共享",
salesforceIdentityNonAdmin: "简档不含“修改所有数据”权限——字段权限与记录共享照常生效",
salesforceIdentityUnknownRights: "无法确认简档是否具有“修改所有数据”权限",
@@ -1889,6 +1889,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "当前连接",
idleTimeout: "空闲超时(秒)",
idleTimeoutHint: "作用于连接池内空闲连接的回收释放。",
manualTransactionTimeout: "事务空闲超时(秒)",
manualTransactionTimeoutHint: "手动事务在此时长内没有执行任何语句时将自动回滚,范围 {min}–{max} 秒。",
keepaliveInterval: "保持连接间隔(秒)",
readOnly: "只读模式",
readOnlyHint: "阻止所有写操作(INSERT、UPDATE、DELETE 等)",
@@ -3471,10 +3473,10 @@ export default withEnglishFallback({
rollbackFailed: "事务会话已结束,但无法确认回滚结果;再次执行 SQL 前请核对数据库。",
commitUnknown: "提交响应丢失,事务可能已经提交;再次执行 SQL 前请核对数据库。",
controlConflict: "手动模式统一管理事务与数据库上下文,请移除 SQL 中的 BEGIN/COMMIT/ROLLBACK/SAVE、SET IMPLICIT_TRANSACTIONS 或 USE。",
idleTimeout: "事务已因 5 分钟无操作而结束,请重新执行以开启新事务。",
idleTimeout: "事务因空闲超时已回滚,请重新执行以开启新事务。",
executionFailed: "SQL 执行失败,本次手动事务已结束。",
sessionExpired: "手动事务因 {timeoutSecs} 秒无操作已过期并回滚,DBX 将自动开始新事务。",
sessionExpired: "手动事务因空闲超时已回滚,未提交的改动已丢弃。本次语句未执行,请重新执行以开启新事务。",
},
legacy: "后端返回了错误。",
unknown: "发生了未预期的后端错误。",
+5 -3
View File
@@ -628,7 +628,7 @@ export default withEnglishFallback({
manualTransactionAgent: "手動事務(取決於 Agent 後端資料庫)",
commit: "提交",
rollback: "回溯",
txnAutoRolledBack: "事務已因 5 分鐘無操作而自動回溯",
txnAutoRolledBack: "交易因閒置逾時已自動回滾,未提交的變更已捨棄。",
autoCommitTxnRolledBack: "該分頁遺留的顯式交易已被自動回溯;如需跨多次執行保留交易,可在設定中開啟「自動提交模式下保留顯式交易」",
autoCommitSessionTxnRolledBack: "該連線處於 autocommit=0,本次執行留下的隱式交易已被自動回溯;同一連線只提示一次",
previewSql: "預覽 SQL",
@@ -1475,6 +1475,8 @@ export default withEnglishFallback({
useConnectionQueryTimeout: "目前連線",
idleTimeout: "閒置逾時(秒)",
idleTimeoutHint: "作用於連線池內閒置連線的回收釋放。",
manualTransactionTimeout: "交易閒置逾時(秒)",
manualTransactionTimeoutHint: "手動交易在此時間內未執行任何語句時將自動回滾,範圍 {min}–{max} 秒。",
keepaliveInterval: "保持連線間隔(秒)",
readOnly: "唯讀模式",
readOnlyHint: "阻止所有寫入操作(INSERT、UPDATE、DELETE 等)",
@@ -3163,10 +3165,10 @@ export default withEnglishFallback({
rollbackFailed: "交易工作階段已結束,但無法確認復原結果;再次執行 SQL 前請核對資料庫。",
commitUnknown: "提交回應遺失,交易可能已經提交;再次執行 SQL 前請核對資料庫。",
controlConflict: "手動模式統一管理交易與資料庫上下文,請移除 SQL 中的 BEGIN/COMMIT/ROLLBACK/SAVE、SET IMPLICIT_TRANSACTIONS 或 USE。",
idleTimeout: "交易已因 5 分鐘無操作而結束,請重新執行以開啟新交易。",
idleTimeout: "交易因閒置逾時已回滾,請重新執行以開啟新交易。",
executionFailed: "SQL 執行失敗,本次手動交易已結束。",
sessionExpired: "手動交易因 {timeoutSecs} 秒無操作而過期並回滾,DBX 將自動開始新的交易。",
sessionExpired: "手動交易因閒置逾時已回滾,未提交的變更已捨棄。本次語句未執行,請重新執行以開啟新交易。",
},
legacy: "後端回傳了錯誤。",
unknown: "發生未預期的後端錯誤。",
@@ -38,6 +38,24 @@ describe("connection submit normalization", () => {
expect(invalid).toMatchObject({ query_timeout_secs: 60, idle_timeout_secs: 60, keepalive_interval_secs: 30 });
});
it("clamps the manual transaction idle timeout and falls back to the default for unset values", () => {
const unset = config({});
normalizeConnectionTimeouts(unset, 12, 34);
expect(unset.manual_transaction_timeout_secs).toBe(300);
const tooSmall = config({ manual_transaction_timeout_secs: 5 });
normalizeConnectionTimeouts(tooSmall, 12, 34);
expect(tooSmall.manual_transaction_timeout_secs).toBe(30);
const tooLarge = config({ manual_transaction_timeout_secs: 999_999 });
normalizeConnectionTimeouts(tooLarge, 12, 34);
expect(tooLarge.manual_transaction_timeout_secs).toBe(86400);
const custom = config({ manual_transaction_timeout_secs: 1800 });
normalizeConnectionTimeouts(custom, 12, 34);
expect(custom.manual_transaction_timeout_secs).toBe(1800);
});
it("canonicalizes scope flags and production database names", () => {
const value = config({ db_type: "postgres", one_time: false, read_only: false, save_password: undefined, production_databases: [" app ", "app", ""] });
normalizeConnectionScope(value);
@@ -1,5 +1,5 @@
import { sidebarProductionBadgeScope } from "@/lib/sidebar/sidebarProductionBadge";
import { normalizeIdleTimeoutSecs } from "@/lib/connection/timeoutLimits";
import { normalizeIdleTimeoutSecs, normalizeManualTransactionTimeoutSecs } from "@/lib/connection/timeoutLimits";
import { isSingleDatabase } from "@/lib/database/databaseFeatureSupport";
import { normalizeGlobalConnectTimeoutSecs, normalizeGlobalQueryTimeoutSecs } from "@/stores/settingsStore";
import type { ConnectionConfig } from "@/types/database";
@@ -9,6 +9,7 @@ export function normalizeConnectionTimeouts(config: ConnectionConfig, connectTim
const queryTimeout = Number(config.query_timeout_secs);
config.query_timeout_secs = config.query_timeout_inherit === true ? normalizeGlobalQueryTimeoutSecs(queryTimeoutSecs) : normalizeGlobalQueryTimeoutSecs(queryTimeout);
config.idle_timeout_secs = normalizeIdleTimeoutSecs(config.idle_timeout_secs);
config.manual_transaction_timeout_secs = normalizeManualTransactionTimeoutSecs(config.manual_transaction_timeout_secs);
const keepaliveInterval = Number(config.keepalive_interval_secs);
config.keepalive_interval_secs = Number.isFinite(keepaliveInterval) && keepaliveInterval >= 0 ? keepaliveInterval : 30;
}
@@ -6,6 +6,10 @@ export const DEFAULT_QUERY_TIMEOUT_SECS = 60;
export const MAX_QUERY_TIMEOUT_SECS = 3600;
export const DEFAULT_IDLE_TIMEOUT_SECS = 60;
export const MAX_IDLE_TIMEOUT_SECS = 3600;
// Mirrors dbx-types: idle window of a manual transaction before DBX rolls it back.
export const DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS = 300;
export const MIN_MANUAL_TRANSACTION_TIMEOUT_SECS = 30;
export const MAX_MANUAL_TRANSACTION_TIMEOUT_SECS = 86400;
export function normalizeConnectTimeoutSecs(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_CONNECT_TIMEOUT_SECS;
@@ -22,6 +26,11 @@ export function normalizeIdleTimeoutSecs(value: unknown): number {
return Math.min(MAX_IDLE_TIMEOUT_SECS, Math.round(value));
}
export function normalizeManualTransactionTimeoutSecs(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS;
return Math.min(MAX_MANUAL_TRANSACTION_TIMEOUT_SECS, Math.max(MIN_MANUAL_TRANSACTION_TIMEOUT_SECS, Math.round(value)));
}
export function supportsIdleTimeout(dbType?: DatabaseType): boolean {
return dbType === "mongodb" || dbType === "mysql" || dbType === "doris" || dbType === "starrocks" || dbType === "manticoresearch";
}
@@ -18,6 +18,7 @@ import type {
PluginUiContribution,
PluginWorkbenchContribution,
} from "@/types/database";
import { DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
import { uuid } from "@/lib/common/utils";
import { clonePluginData } from "./pluginData";
@@ -279,6 +280,7 @@ export function buildPluginConnectionConfig(pluginId: string, contribution: Plug
connect_timeout_secs: existing?.connect_timeout_secs || pluginConnectionConnectTimeoutDefault(contribution) || 10,
query_timeout_secs: existing?.query_timeout_secs || 60,
idle_timeout_secs: existing?.idle_timeout_secs || 60,
manual_transaction_timeout_secs: existing?.manual_transaction_timeout_secs || DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS,
keepalive_interval_secs: existing?.keepalive_interval_secs || 30,
read_only: existing?.read_only || false,
save_password: existing?.save_password !== false,
@@ -66,7 +66,7 @@ function expiredTransactionError() {
version: 1 as const,
code: "DBX-TXN-1001",
messageKey: "backendErrors.transaction.sessionExpired",
messageParams: { timeoutSecs: 300 },
messageParams: {},
source: "legacyBackend" as const,
operationOutcome: "not_started" as const,
origin: { subsystem: "database", adapter: "native" },
@@ -111,26 +111,35 @@ describe("queryStore manual transaction expiry recovery", () => {
mocks.saveOpenTabsState.mockResolvedValue(undefined);
});
it("restarts an expired transaction and retries the SQL exactly once", async () => {
it("stops at an expired transaction that held uncommitted work instead of replaying the SQL", async () => {
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockResolvedValueOnce("txn-new");
mocks.executeInManualTransaction.mockResolvedValueOnce(successfulUpdate()).mockRejectedValueOnce(expiredTransactionError()).mockResolvedValueOnce(successfulUpdate());
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP");
// Tabs default to auto-commit; the expiry recovery path only applies to manual transactions.
// Tabs default to auto-commit; the expiry handling only applies to manual transactions.
store.setAutoCommit(tabId, false);
await store.executeTabSql(tabId, "UPDATE USERS SET ACTIVE = 1");
await store.executeTabSql(tabId, "UPDATE USERS SET ACTIVE = 1");
await store.executeTabSql(tabId, "UPDATE USERS SET ACTIVE = 2");
const tab = store.tabs.find((item) => item.id === tabId)!;
// The first UPDATE was rolled back with the old session; running the second one in a
// fresh transaction would silently build on work that no longer exists.
expect(mocks.beginManualTransaction).toHaveBeenCalledOnce();
expect(mocks.executeInManualTransaction).toHaveBeenCalledTimes(2);
expect(tab.txnSessionId).toBeUndefined();
expect(tab.txnAutoRolledBack).toBe(true);
expect(tab.result?.execution_error).toBe(true);
// The user re-runs explicitly and gets a brand new transaction.
await store.executeTabSql(tabId, "UPDATE USERS SET ACTIVE = 2");
expect(mocks.beginManualTransaction).toHaveBeenCalledTimes(2);
expect(mocks.executeInManualTransaction).toHaveBeenCalledTimes(3);
expect(mocks.executeInManualTransaction.mock.calls[1]?.[0]).toBe("txn-old");
expect(mocks.executeInManualTransaction.mock.calls[2]?.[0]).toBe("txn-new");
expect(tab.txnSessionId).toBe("txn-new");
expect(tab.txnAutoRolledBack).toBe(true);
expect(tab.result?.execution_error).not.toBe(true);
expect(tab.result?.affected_rows).toBe(1);
});
@@ -154,7 +163,7 @@ describe("queryStore manual transaction expiry recovery", () => {
expect(tab.txnAutoRolledBack).not.toBe(true);
});
it("keeps the rollback notice for a manual dialect without sticky read-only tracking", async () => {
it("treats an expired transaction as dirty for a manual dialect without sticky read-only tracking", async () => {
mocks.getConnectionConfig.mockReturnValue({
id: "dameng-1",
name: "DM",
@@ -162,8 +171,8 @@ describe("queryStore manual transaction expiry recovery", () => {
database: "DMHR",
query_timeout_secs: 30,
});
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockResolvedValueOnce("txn-new");
mocks.executeInManualTransaction.mockResolvedValueOnce(successfulSelect()).mockRejectedValueOnce(expiredTransactionError()).mockResolvedValueOnce(successfulSelect());
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old");
mocks.executeInManualTransaction.mockResolvedValueOnce(successfulSelect()).mockRejectedValueOnce(expiredTransactionError());
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
@@ -174,10 +183,13 @@ describe("queryStore manual transaction expiry recovery", () => {
await store.executeTabSql(tabId, "SELECT 1 FROM DUAL");
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(tab.txnSessionId).toBe("txn-new");
expect(tab.result?.rows).toEqual([[1]]);
// 不跟踪只读状态的方言无法判断会话是否干净,保留原有提示。
// Without read-only tracking a clean session cannot be told from a dirty one, so
// the statement is not replayed and the rollback is reported.
expect(mocks.beginManualTransaction).toHaveBeenCalledOnce();
expect(mocks.executeInManualTransaction).toHaveBeenCalledTimes(2);
expect(tab.txnSessionId).toBeUndefined();
expect(tab.txnAutoRolledBack).toBe(true);
expect(tab.result?.execution_error).toBe(true);
});
it("normalizes a stale manual tab for a non-transactional database before query dispatch", async () => {
@@ -80,7 +80,7 @@ function expiredTransactionError() {
version: 1 as const,
code: "DBX-TXN-1001",
messageKey: "backendErrors.transaction.sessionExpired",
messageParams: { timeoutSecs: 300 },
messageParams: {},
source: "legacyBackend" as const,
operationOutcome: "not_started" as const,
origin: { subsystem: "database", adapter: "native" },
@@ -363,7 +363,7 @@ describe("queryStore Oracle/OceanBase manual-transaction sticky state (behavior
expect(tab.txnPossiblyDirty).toBe(true);
});
it("resets the sticky state on session-expiry recovery and recomputes from the retry", async () => {
it("resets the sticky state when a dirty session expires and recomputes it on the next run", async () => {
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockResolvedValueOnce("txn-new");
mocks.executeInManualTransaction.mockResolvedValueOnce(dirtyUpdate()).mockRejectedValueOnce(expiredTransactionError()).mockResolvedValueOnce(cleanSelect());
@@ -375,10 +375,36 @@ describe("queryStore Oracle/OceanBase manual-transaction sticky state (behavior
await store.executeTabSql(tabId, "SELECT * FROM EMP");
const tab = store.tabs.find((item) => item.id === tabId)!;
// The dirty session lost its work: the SELECT is not replayed into a fresh transaction.
expect(mocks.executeInManualTransaction).toHaveBeenCalledTimes(2);
expect(tab.txnSessionId).toBeUndefined();
expect(tab.txnPossiblyDirty).not.toBe(true);
expect(tab.txnAutoRolledBack).toBe(true);
await store.executeTabSql(tabId, "SELECT * FROM EMP");
expect(tab.txnSessionId).toBe("txn-new");
expect(tab.txnPossiblyDirty).not.toBe(true);
});
it("silently rebuilds a session that expired before running anything that could be dirty", async () => {
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockResolvedValueOnce("txn-new");
mocks.executeInManualTransaction.mockResolvedValueOnce(cleanSelect()).mockRejectedValueOnce(expiredTransactionError()).mockResolvedValueOnce(cleanSelect());
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = await setupManualTab(store);
await store.executeTabSql(tabId, "SELECT * FROM EMP");
await store.executeTabSql(tabId, "SELECT * FROM EMP");
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(mocks.executeInManualTransaction).toHaveBeenCalledTimes(3);
expect(tab.txnSessionId).toBe("txn-new");
expect(tab.txnPossiblyDirty).not.toBe(true);
expect(tab.txnAutoRolledBack).not.toBe(true);
});
it("does not replay an expired transaction's SQL after its database changes during recovery", async () => {
let finishBegin!: (sessionId: string) => void;
mocks.beginManualTransaction.mockResolvedValueOnce("txn-old").mockImplementationOnce(
+2 -1
View File
@@ -4,7 +4,7 @@ import type { SqlFilePreview } from "@/lib/backend/api";
import { uuid } from "@/lib/common/utils";
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
import { ref, computed, watch, markRaw, reactive } from "vue";
import { DEFAULT_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
import { DEFAULT_QUERY_TIMEOUT_SECS, normalizeManualTransactionTimeoutSecs } from "@/lib/connection/timeoutLimits";
import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import type {
ColumnInfo,
@@ -1818,6 +1818,7 @@ export const useConnectionStore = defineStore("connection", () => {
query_timeout_secs: queryTimeoutInherit ? settingsStore.editorSettings.globalQueryTimeoutSecs : (config.query_timeout_secs ?? DEFAULT_QUERY_TIMEOUT_SECS),
query_timeout_inherit: queryTimeoutInherit,
idle_timeout_secs: config.idle_timeout_secs ?? 60,
manual_transaction_timeout_secs: normalizeManualTransactionTimeoutSecs(config.manual_transaction_timeout_secs),
keepalive_interval_secs: config.keepalive_interval_secs ?? DEFAULT_KEEPALIVE_INTERVAL_SECS,
redis_database_aliases: normalizeRedisDatabaseAliases(config.redis_database_aliases),
redis_key_filter: dbType === "redis" && typeof config.redis_key_filter === "string" ? config.redis_key_filter.trim() || undefined : undefined,
+10 -5
View File
@@ -8653,15 +8653,20 @@ export const useQueryStore = defineStore("query", () => {
} catch (error) {
if (effectiveDbType === "sqlserver" || options?.pagination?.sessionId || manualTransactionRecoveryAttempted || !isManualTransactionSessionExpired(error)) throw error;
if (tab.executionId !== executionId || tab.autoCommit !== false || manualTransactionTargetEpoch(tab) !== executionTargetEpoch) throw error;
// Only a session that ran proven read-only statements lost nothing to
// the idle rollback and can be rebuilt transparently. Otherwise
// uncommitted work was discarded (or we cannot prove it was not), and
// replaying this statement into a fresh transaction would silently
// act on state that no longer exists. Stop here: the outer handler
// clears the dead session and shows the rollback notice, and the
// user decides whether to run the statement again.
if (manualTransactionRollbackNoticeRequired(tab, effectiveDbType)) throw error;
manualTransactionRecoveryAttempted = true;
// A session that only ever ran proven read-only statements lost
// nothing to the idle rollback, so it restarts without the notice.
const rollbackNoticeRequired = manualTransactionRollbackNoticeRequired(tab, effectiveDbType);
// The expired session was discarded by the backend; the replacement
// session starts fresh, so the old sticky state resets with it.
clearTxnPossiblyDirty(tab);
tab.txnSessionId = undefined;
tab.txnAutoRolledBack = rollbackNoticeRequired;
tab.txnAutoRolledBack = false;
queryExecutionLog("info", "manual-txn:expired-recover", { traceId, elapsed: elapsed() });
const refreshedSessionId = await ensureManualTransactionSession(id, executionDatabase, executionSchema, executionCatalog);
if (tab.executionId !== executionId || tab.autoCommit !== false || manualTransactionTargetEpoch(tab) !== executionTargetEpoch) {
@@ -8981,7 +8986,7 @@ export const useQueryStore = defineStore("query", () => {
reportSqlServerTransactionError(tab, e);
} else if (tab.autoCommit === false) {
const errMsg: string = e?.message ?? String(e);
const idleTimeout = /5 minutes of inactivity/i.test(errMsg) || errMsg.includes("5 分钟无操作") || errMsg.includes("已自动回滚");
const idleTimeout = isManualTransactionSessionExpired(e) || errMsg.includes("已自动回滚");
if (idleTimeout) {
// Backend session was removed and rolled back after idle expiry: clear
// the sticky dirty state together with the session. Same rule as the
+2
View File
@@ -174,6 +174,8 @@ export interface ConnectionConfig {
query_timeout_secs?: number;
query_timeout_inherit?: boolean;
idle_timeout_secs?: number;
/** Seconds a manual transaction may sit idle before DBX rolls it back. */
manual_transaction_timeout_secs?: number;
keepalive_interval_secs?: number;
ssl?: boolean;
ca_cert_path?: string;
@@ -184,6 +184,7 @@ fn connection_config(id: &str, database: BenchDatabase) -> Result<ConnectionConf
connect_timeout_secs: 15,
query_timeout_secs: 120,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: std::env::var("DBX_BENCH_SSL").is_ok_and(|value| value.eq_ignore_ascii_case("true")),
ca_cert_path: String::new(),
+1
View File
@@ -209,6 +209,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+1
View File
@@ -951,6 +951,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -396,6 +396,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -672,6 +672,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -760,6 +761,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+1
View File
@@ -2023,6 +2023,7 @@ for line in sys.stdin:
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 30,
ssl: false,
ca_cert_path: String::new(),
@@ -1060,6 +1060,7 @@ mod tests {
connect_timeout_secs: crate::models::connection::default_connect_timeout_secs(),
query_timeout_secs: crate::models::connection::default_query_timeout_secs(),
idle_timeout_secs: crate::models::connection::default_idle_timeout_secs(),
manual_transaction_timeout_secs: crate::models::connection::default_manual_transaction_timeout_secs(),
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+5
View File
@@ -297,6 +297,9 @@ pub struct TransactionSession {
pub connection: Arc<Mutex<TxnConnection>>,
pub pool_key: String,
pub last_activity: std::time::Instant,
/// How long the session may go without a statement before it is rolled back.
/// Fixed at BEGIN from the connection's `manual_transaction_timeout_secs`.
pub idle_timeout: std::time::Duration,
pub busy: bool,
pub snapshot_rotation_safe: bool,
pub connection_id: String,
@@ -7446,6 +7449,7 @@ mod tests {
connect_timeout_secs: default_connect_timeout_secs(),
query_timeout_secs: crate::models::connection::default_query_timeout_secs(),
idle_timeout_secs: crate::models::connection::default_idle_timeout_secs(),
manual_transaction_timeout_secs: crate::models::connection::default_manual_transaction_timeout_secs(),
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -9998,6 +10002,7 @@ sleep 30
connect_timeout_secs: 1,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: interval_secs,
ssl: false,
ca_cert_path: String::new(),
+2
View File
@@ -13691,6 +13691,7 @@ for line in sys.stdin:
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -20918,6 +20919,7 @@ SELECT 1 FROM dual"#
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -3672,6 +3672,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -3761,6 +3762,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -4074,6 +4076,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -12010,6 +12010,7 @@ mod tests {
connect_timeout_secs: 30,
query_timeout_secs: 300,
idle_timeout_secs: 600,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -12090,6 +12091,7 @@ mod tests {
connect_timeout_secs: 30,
query_timeout_secs: 300,
idle_timeout_secs: 600,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+76 -16
View File
@@ -50,8 +50,7 @@ pub const METADATA_POOL_BUSY_ERROR: &str = "DBX metadata pool is busy; please re
pub const MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS: u64 = 300;
pub const MANUAL_TRANSACTION_SESSION_NOT_FOUND_ERROR: &str =
"Transaction session not found or expired; it may have been auto-rolled back due to inactivity";
pub const MANUAL_TRANSACTION_IDLE_ROLLBACK_ERROR: &str =
"Transaction was auto-rolled back due to 5 minutes of inactivity";
pub const MANUAL_TRANSACTION_IDLE_ROLLBACK_ERROR: &str = "Transaction was auto-rolled back due to inactivity";
pub fn is_manual_transaction_session_expired_error(error: &str) -> bool {
error.starts_with(MANUAL_TRANSACTION_SESSION_NOT_FOUND_ERROR) || error == MANUAL_TRANSACTION_IDLE_ROLLBACK_ERROR
@@ -856,6 +855,13 @@ async fn connection_is_mongodb(state: &AppState, connection_id: &str) -> bool {
configs.get(connection_id).is_some_and(|config| config.db_type == DatabaseType::MongoDb)
}
/// Idle window for a new manual transaction on `connection_id`.
pub(crate) async fn manual_transaction_idle_timeout(state: &AppState, connection_id: &str) -> std::time::Duration {
let configs = state.configs.read().await;
let secs = configs.get(connection_id).map(ConnectionConfig::effective_manual_transaction_timeout_secs);
std::time::Duration::from_secs(secs.unwrap_or(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS))
}
async fn connection_database_type(state: &AppState, connection_id: &str) -> Option<DatabaseType> {
let configs = state.configs.read().await;
configs.get(connection_id).map(|config| config.db_type)
@@ -3193,7 +3199,7 @@ async fn request_large_value_with_cancel(
let session = sessions.get_mut(txn_id).ok_or(MANUAL_TRANSACTION_SESSION_NOT_FOUND_ERROR)?;
if session.connection_id != request.connection_id
|| session.busy
|| session.last_activity.elapsed().as_secs() >= MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS
|| session.last_activity.elapsed() >= session.idle_timeout
{
return Err("LOB transaction context expired or busy".to_string());
}
@@ -6299,6 +6305,7 @@ async fn begin_transaction_session(
connection: Arc::new(tokio::sync::Mutex::new(txn_conn)),
pool_key: pool_key.clone(),
last_activity: std::time::Instant::now(),
idle_timeout: manual_transaction_idle_timeout(state, connection_id).await,
busy: false,
snapshot_rotation_safe: !consistent_snapshot,
connection_id: connection_id.to_string(),
@@ -6426,8 +6433,6 @@ pub async fn execute_in_manual_transaction_with_options(
}
return sqlserver_manual_transaction::execute(state, txn_session_id, sql, database, schema, options).await;
}
const TXN_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS);
// Resolve statements and validate before taking the per-session connection
// lock. The session stays visible in the map so close/disconnect cleanup can
// remove it and roll back once the current DB operation releases the lock.
@@ -6533,7 +6538,7 @@ pub async fn execute_in_manual_transaction_with_options(
if session.busy {
return Err("Transaction session is already executing".to_string());
}
if session.last_activity.elapsed() > TXN_IDLE_TIMEOUT {
if session.last_activity.elapsed() > session.idle_timeout {
let session = sessions.remove(txn_session_id).expect("session exists");
Some(session)
} else {
@@ -6730,8 +6735,6 @@ pub(crate) async fn stream_rows_in_manual_transaction_with_cancel<F>(
where
F: FnMut(Vec<Vec<serde_json::Value>>) -> Result<(), String> + Send,
{
const TXN_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS);
let expired_connection = {
let mut sessions = state.transaction_sessions.write().await;
let Some(session) = sessions.get_mut(txn_session_id) else {
@@ -6740,7 +6743,7 @@ where
if session.busy {
return Err("Transaction session is already executing".to_string());
}
if session.last_activity.elapsed() > TXN_IDLE_TIMEOUT {
if session.last_activity.elapsed() > session.idle_timeout {
Some(sessions.remove(txn_session_id).expect("session exists").connection)
} else {
session.busy = true;
@@ -7162,7 +7165,7 @@ async fn execute_manual_txn_external_driver_statement(
}
/// Spawn a background task that removes and rolls back a transaction session
/// after 5 minutes of inactivity. The task does not hold the global lock across
/// after its idle timeout. The task does not hold the global lock across
/// I/O: it briefly checks the map, and if the session exists and is expired,
/// removes it, drops the lock, then rolls back the held connection.
///
@@ -7180,14 +7183,16 @@ fn spawn_txn_idle_watcher_for_sessions(
txn_session_id: String,
) {
tokio::spawn(async move {
const TXN_IDLE_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS);
tokio::time::sleep(TXN_IDLE_TIMEOUT).await;
// The window is fixed per session at BEGIN; a vanished session needs no watcher.
let Some(idle_timeout) = sessions.read().await.get(&txn_session_id).map(|session| session.idle_timeout) else {
return;
};
tokio::time::sleep(idle_timeout).await;
let removed: Option<TransactionSession> = {
let mut guard = sessions.write().await;
match guard.get(&txn_session_id) {
Some(session) if !session.busy && session.last_activity.elapsed() >= TXN_IDLE_TIMEOUT => {
Some(session) if !session.busy && session.last_activity.elapsed() >= session.idle_timeout => {
guard.remove(&txn_session_id)
}
_ => None,
@@ -7198,8 +7203,9 @@ fn spawn_txn_idle_watcher_for_sessions(
let mut conn = session.connection.lock().await;
let _ = rollback_manual_txn_connection(&mut conn).await;
log::info!(
"[query][manual_txn:idle_timeout] session_id={} auto-rolled back after 5 minutes of inactivity",
txn_session_id
"[query][manual_txn:idle_timeout] session_id={} auto-rolled back after {}s of inactivity",
txn_session_id,
session.idle_timeout.as_secs()
);
}
});
@@ -7520,6 +7526,7 @@ mod tests {
connection: Arc::new(tokio::sync::Mutex::new(TxnConnection::Postgres(Box::new(connection)))),
pool_key: "conn-1".to_string(),
last_activity: std::time::Instant::now(),
idle_timeout: std::time::Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS),
busy: false,
snapshot_rotation_safe: true,
connection_id: "conn-1".to_string(),
@@ -8514,6 +8521,55 @@ for line in sys.stdin:
assert!(validate_query_execution_mode(Some(DatabaseType::Postgres), "SELECT 1; SELECT 2", &options).is_err());
}
fn idle_test_session(idle_timeout: Duration) -> TransactionSession {
TransactionSession {
sqlserver: None,
connection: Arc::new(tokio::sync::Mutex::new(TxnConnection::Mysql(None))),
pool_key: "conn-1".to_string(),
last_activity: std::time::Instant::now(),
idle_timeout,
busy: false,
snapshot_rotation_safe: false,
connection_id: "conn-1".to_string(),
database: "test".to_string(),
schema: None,
}
}
#[tokio::test]
async fn idle_watcher_rolls_back_after_the_session_own_timeout() {
let sessions = Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
{
let mut guard = sessions.write().await;
guard.insert("short".to_string(), idle_test_session(Duration::from_millis(50)));
guard.insert("long".to_string(), idle_test_session(Duration::from_secs(3600)));
}
spawn_txn_idle_watcher_for_sessions(Arc::clone(&sessions), "short".to_string());
spawn_txn_idle_watcher_for_sessions(Arc::clone(&sessions), "long".to_string());
tokio::time::sleep(Duration::from_millis(400)).await;
let guard = sessions.read().await;
assert!(!guard.contains_key("short"), "a 50ms session must be reaped long before the 300s default");
assert!(guard.contains_key("long"), "a session with a longer window must survive the same wait");
}
#[tokio::test]
async fn manual_transaction_idle_timeout_follows_connection_config() {
let dir = std::env::temp_dir().join(format!("dbx-txn-timeout-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap();
let state = AppState::new(storage);
let mut config = test_connection_config(DatabaseType::Postgres);
config.manual_transaction_timeout_secs = 1_800;
state.configs.write().await.insert(config.id.clone(), config);
assert_eq!(manual_transaction_idle_timeout(&state, "conn-1").await, Duration::from_secs(1_800));
// An unknown connection keeps the historical five-minute window.
assert_eq!(manual_transaction_idle_timeout(&state, "missing").await, Duration::from_secs(300));
let _ = std::fs::remove_dir_all(dir);
}
fn test_connection_config(db_type: DatabaseType) -> ConnectionConfig {
ConnectionConfig {
oracle_oci_nls_lang: None,
@@ -8546,6 +8602,7 @@ for line in sys.stdin:
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 30,
ssl: false,
ca_cert_path: String::new(),
@@ -11038,6 +11095,7 @@ for line in sys.stdin:
})),
pool_key: pool_key.to_string(),
last_activity: std::time::Instant::now(),
idle_timeout: std::time::Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS),
busy: false,
snapshot_rotation_safe: true,
connection_id: "jdbc-conn".to_string(),
@@ -11670,6 +11728,7 @@ for line in sys.stdin:
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -12278,6 +12337,7 @@ for line in sys.stdin:
})),
pool_key: "agent-conn".to_string(),
last_activity: std::time::Instant::now(),
idle_timeout: std::time::Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS),
busy: false,
snapshot_rotation_safe: true,
connection_id: "agent-conn".to_string(),
@@ -46,7 +46,8 @@ pub mod live_test_hooks {
}
const PREFIX: &str = "sqlserver-txn-";
const ERROR_PREFIX: &str = "DBX_MANUAL_TRANSACTION_ERROR:";
const IDLE: Duration = Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS);
/// How long an ended session's outcome stays queryable for late callers.
const ENDED_SESSION_RETENTION: Duration = Duration::from_secs(MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS);
const MAX_ENDED_SESSIONS: usize = 1024;
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -78,7 +79,7 @@ pub struct EndedSessions {
}
impl EndedSessions {
fn prune(&mut self) {
self.entries.retain(|_, (time, _)| time.elapsed() < IDLE);
self.entries.retain(|_, (time, _)| time.elapsed() < ENDED_SESSION_RETENTION);
}
fn record(&mut self, id: &str, failure: Failure) {
self.prune();
@@ -289,6 +290,7 @@ pub async fn begin(
connection: Arc::new(tokio::sync::Mutex::new(conn)),
pool_key,
last_activity: Instant::now(),
idle_timeout: super::manual_transaction_idle_timeout(state, connection_id).await,
busy: false,
snapshot_rotation_safe: false,
connection_id: connection_id.to_owned(),
@@ -422,7 +424,7 @@ pub async fn execute(
return Err(failure(Kind::Busy, Outcome::Unknown, "The transaction is currently executing"));
}
let context_changed = session.database != database || session.schema.as_deref() != schema;
let expired = session.last_activity.elapsed() >= IDLE;
let expired = session.last_activity.elapsed() >= session.idle_timeout;
session.busy = true;
(
session.connection.clone(),
@@ -669,7 +671,7 @@ pub async fn finish(state: &AppState, id: &str, commit: bool) -> Result<db::Quer
};
let metadata = session.sqlserver.as_ref().expect("SQL Server session metadata");
let mut conn = session.connection.lock().await;
if metadata.lifecycle.cancellation().is_cancelled() || session.last_activity.elapsed() >= IDLE {
if metadata.lifecycle.cancellation().is_cancelled() || session.last_activity.elapsed() >= session.idle_timeout {
let error = cleanup_failure(
state,
id,
@@ -793,14 +795,16 @@ fn spawn_idle_watcher(sessions: Arc<tokio::sync::RwLock<HashMap<String, Transact
if session.busy {
Duration::from_secs(1)
} else {
IDLE.saturating_sub(session.last_activity.elapsed())
session.idle_timeout.saturating_sub(session.last_activity.elapsed())
}
};
tokio::time::sleep(delay).await;
let removed = {
let mut map = sessions.write().await;
match map.get(&id) {
Some(session) if !session.busy && session.last_activity.elapsed() >= IDLE => map.remove(&id),
Some(session) if !session.busy && session.last_activity.elapsed() >= session.idle_timeout => {
map.remove(&id)
}
None => return,
_ => None,
}
@@ -817,7 +821,7 @@ fn spawn_idle_watcher(sessions: Arc<tokio::sync::RwLock<HashMap<String, Transact
Ok(Ok(())) => Failure::new(
Kind::IdleTimeout,
Outcome::RolledBack,
"Transaction rolled back after five minutes of inactivity",
"Transaction rolled back after its idle timeout",
),
Ok(Err(error)) => Failure::new(
Kind::RollbackFailed,
@@ -862,7 +866,7 @@ mod tests {
assert_eq!(ended.entries.len(), MAX_ENDED_SESSIONS);
ended.entries.insert(
"old".into(),
(Instant::now() - IDLE, Failure::new(Kind::IdleTimeout, Outcome::RolledBack, "Idle")),
(Instant::now() - ENDED_SESSION_RETENTION, Failure::new(Kind::IdleTimeout, Outcome::RolledBack, "Idle")),
);
assert!(ended.missing("old").contains("connection_lost"));
assert!(!ended.entries.contains_key("old"));
@@ -959,6 +963,7 @@ for line in sys.stdin:
})),
pool_key: "mock-sqlserver".to_owned(),
last_activity: Instant::now(),
idle_timeout: ENDED_SESSION_RETENTION,
busy: false,
snapshot_rotation_safe: false,
connection_id: "mock-sqlserver".to_owned(),
@@ -910,6 +910,7 @@ mod tests {
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 30,
ssl: false,
ca_cert_path: String::new(),
+1
View File
@@ -3415,6 +3415,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -173,6 +173,7 @@ mod tests {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -1,6 +1,7 @@
use dbx_core::models::connection::{
default_connect_timeout_secs, default_idle_timeout_secs, default_keepalive_interval_secs,
default_query_timeout_secs, default_redis_key_separator, ConnectionConfig, DatabaseType,
default_manual_transaction_timeout_secs, default_query_timeout_secs, default_redis_key_separator, ConnectionConfig,
DatabaseType,
};
use std::str::FromStr;
@@ -36,6 +37,7 @@ fn mysql_config(username: &str, password: &str, database: Option<&str>) -> Conne
connect_timeout_secs: default_connect_timeout_secs(),
query_timeout_secs: default_query_timeout_secs(),
idle_timeout_secs: default_idle_timeout_secs(),
manual_transaction_timeout_secs: default_manual_transaction_timeout_secs(),
keepalive_interval_secs: default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -48,6 +48,7 @@ fn live_postgres_config(
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -44,6 +44,7 @@ fn live_postgres_config(
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -39,6 +39,7 @@ fn live_postgres_config(
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -193,6 +193,7 @@ fn live_connection_config(
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -44,6 +44,7 @@ fn live_postgres_config(
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -39,6 +39,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -43,6 +43,7 @@ fn sqlite_config(id: &str, path: &str) -> ConnectionConfig {
connect_timeout_secs: 15,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -45,6 +45,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -34,6 +34,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -49,6 +49,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
connect_timeout_secs: 10,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -38,6 +38,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> ConnectionConfig {
connect_timeout_secs: 15,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
+1
View File
@@ -105,6 +105,7 @@ async fn public_lob_request_cancels_while_waiting_for_transaction_connection() {
connection: connection.clone(),
pool_key: "lob".into(),
last_activity: Instant::now(),
idle_timeout: std::time::Duration::from_secs(dbx_core::query::MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS),
busy: false,
snapshot_rotation_safe: false,
connection_id: "lob".into(),
+1
View File
@@ -159,6 +159,7 @@ pub fn postgres_test_config(id: &str, port: u16) -> ConnectionConfig {
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
manual_transaction_timeout_secs: 300,
keepalive_interval_secs: 0,
ssl: false,
ca_cert_path: String::new(),
@@ -780,7 +780,7 @@ mod tests {
use super::*;
use crate::models::connection::{
default_connect_timeout_secs, default_idle_timeout_secs, default_keepalive_interval_secs,
default_query_timeout_secs, default_redis_key_separator,
default_manual_transaction_timeout_secs, default_query_timeout_secs, default_redis_key_separator,
};
fn config(db_type: DatabaseType, database: Option<&str>) -> ConnectionConfig {
@@ -815,6 +815,7 @@ mod tests {
connect_timeout_secs: default_connect_timeout_secs(),
query_timeout_secs: default_query_timeout_secs(),
idle_timeout_secs: default_idle_timeout_secs(),
manual_transaction_timeout_secs: default_manual_transaction_timeout_secs(),
keepalive_interval_secs: default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+8 -9
View File
@@ -303,17 +303,17 @@ impl BackendError {
/// Create a retryable envelope for a manual transaction session that DBX
/// already rolled back before the requested SQL was executed.
pub fn from_manual_transaction_session_expired(timeout_secs: u64) -> Self {
///
/// The idle window is a per-connection setting and the session is already
/// gone when this is raised, so the message carries no duration.
pub fn from_manual_transaction_session_expired() -> Self {
let entry = catalog_entry(CatalogCode::TransactionSessionExpired);
let mut params = BTreeMap::new();
params
.insert("timeoutSecs".to_string(), BackendMessageParam::Integer(timeout_secs.min(i64::MAX as u64) as i64));
Self::new(
entry,
BackendErrorSource::LegacyBackend,
BackendErrorOrigin::database(BackendErrorAdapter::Native),
BackendOperationOutcome::NotStarted,
params,
BTreeMap::new(),
None,
Some(diagnostics_for_local("transaction", AgentErrorStage::Execute)),
)
@@ -505,7 +505,6 @@ struct CatalogEntry {
}
const STAGE_PARAM: &[ParamSpec] = &[ParamSpec { name: "stage", kind: ParamKind::String }];
const TRANSACTION_TIMEOUT_PARAM: &[ParamSpec] = &[ParamSpec { name: "timeoutSecs", kind: ParamKind::Integer }];
const NO_PARAMS: &[ParamSpec] = &[];
fn catalog_entry(code: CatalogCode) -> &'static CatalogEntry {
@@ -632,7 +631,7 @@ fn catalog_entry(code: CatalogCode) -> &'static CatalogEntry {
CatalogEntry {
code: "DBX-TXN-1001",
message_key: "backendErrors.transaction.sessionExpired",
params: TRANSACTION_TIMEOUT_PARAM,
params: NO_PARAMS,
help_url: None,
},
),
@@ -1257,13 +1256,13 @@ mod tests {
#[test]
fn manual_transaction_session_expiry_is_retryable_before_execution() {
let error = BackendError::from_manual_transaction_session_expired(300);
let error = BackendError::from_manual_transaction_session_expired();
let payload = serde_json::to_value(&error).unwrap();
assert_eq!(error.code(), "DBX-TXN-1001");
assert_eq!(error.operation_outcome(), BackendOperationOutcome::NotStarted);
assert_eq!(payload["messageKey"], "backendErrors.transaction.sessionExpired");
assert_eq!(payload["messageParams"]["timeoutSecs"], 300);
assert!(payload["messageParams"].as_object().is_none_or(|params| params.is_empty()));
assert_eq!(payload["diagnostics"]["category"], "transaction");
}
+1
View File
@@ -10371,6 +10371,7 @@ mod tests {
connect_timeout_secs: dbx_types::models::connection::default_connect_timeout_secs(),
query_timeout_secs: dbx_types::models::connection::default_query_timeout_secs(),
idle_timeout_secs: dbx_types::models::connection::default_idle_timeout_secs(),
manual_transaction_timeout_secs: dbx_types::models::connection::default_manual_transaction_timeout_secs(),
keepalive_interval_secs: dbx_types::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+49
View File
@@ -187,6 +187,8 @@ pub struct ConnectionConfig {
pub query_timeout_secs: u64,
#[serde(default = "default_idle_timeout_secs")]
pub idle_timeout_secs: u64,
#[serde(default = "default_manual_transaction_timeout_secs")]
pub manual_transaction_timeout_secs: u64,
#[serde(default = "default_keepalive_interval_secs")]
pub keepalive_interval_secs: u64,
#[serde(default)]
@@ -593,6 +595,16 @@ pub fn default_idle_timeout_secs() -> u64 {
60
}
/// Seconds a manual transaction may sit without executing a statement before DBX
/// rolls it back.
pub const DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS: u64 = 300;
pub const MIN_MANUAL_TRANSACTION_TIMEOUT_SECS: u64 = 30;
pub const MAX_MANUAL_TRANSACTION_TIMEOUT_SECS: u64 = 86_400;
pub fn default_manual_transaction_timeout_secs() -> u64 {
DEFAULT_MANUAL_TRANSACTION_TIMEOUT_SECS
}
pub fn default_keepalive_interval_secs() -> u64 {
30
}
@@ -674,6 +686,8 @@ struct ConnectionConfigData {
pub query_timeout_secs: u64,
#[serde(default = "default_idle_timeout_secs")]
pub idle_timeout_secs: u64,
#[serde(default = "default_manual_transaction_timeout_secs")]
pub manual_transaction_timeout_secs: u64,
#[serde(default = "default_keepalive_interval_secs")]
pub keepalive_interval_secs: u64,
#[serde(default)]
@@ -787,6 +801,7 @@ impl From<ConnectionConfigData> for ConnectionConfig {
connect_timeout_secs: data.connect_timeout_secs,
query_timeout_secs: data.query_timeout_secs,
idle_timeout_secs: data.idle_timeout_secs,
manual_transaction_timeout_secs: data.manual_transaction_timeout_secs,
keepalive_interval_secs: data.keepalive_interval_secs,
ssl: data.ssl,
ca_cert_path: data.ca_cert_path,
@@ -996,6 +1011,17 @@ impl ConnectionConfig {
}
}
/// Idle window of a manual transaction. `0` (legacy or unset payloads) falls
/// back to the default instead of rolling back immediately.
pub fn effective_manual_transaction_timeout_secs(&self) -> u64 {
if self.manual_transaction_timeout_secs == 0 {
default_manual_transaction_timeout_secs()
} else {
self.manual_transaction_timeout_secs
.clamp(MIN_MANUAL_TRANSACTION_TIMEOUT_SECS, MAX_MANUAL_TRANSACTION_TIMEOUT_SECS)
}
}
pub fn effective_query_timeout_secs(&self) -> u64 {
if self.query_timeout_secs == 0 {
// An explicit 0 is the UI's "no limit"; a floor must not impose one.
@@ -3188,6 +3214,7 @@ mod tests {
connect_timeout_secs: super::default_connect_timeout_secs(),
query_timeout_secs: default_query_timeout_secs(),
idle_timeout_secs: super::default_idle_timeout_secs(),
manual_transaction_timeout_secs: super::default_manual_transaction_timeout_secs(),
keepalive_interval_secs: super::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
@@ -3721,6 +3748,28 @@ mod tests {
assert_eq!(config.effective_query_timeout_secs(), 0);
}
#[test]
fn manual_transaction_timeout_defaults_when_absent_or_zero_and_is_clamped() {
let mut config = mysql_config("root", "", None);
assert_eq!(config.effective_manual_transaction_timeout_secs(), 300);
// Payloads saved before the setting existed deserialize to the default.
let mut json = serde_json::to_value(&config).unwrap();
json.as_object_mut().unwrap().remove("manual_transaction_timeout_secs");
let legacy: ConnectionConfig = serde_json::from_value(json).unwrap();
assert_eq!(legacy.manual_transaction_timeout_secs, 300);
// 0 must never mean "roll back immediately".
config.manual_transaction_timeout_secs = 0;
assert_eq!(config.effective_manual_transaction_timeout_secs(), 300);
config.manual_transaction_timeout_secs = 1;
assert_eq!(config.effective_manual_transaction_timeout_secs(), super::MIN_MANUAL_TRANSACTION_TIMEOUT_SECS);
config.manual_transaction_timeout_secs = 1_800;
assert_eq!(config.effective_manual_transaction_timeout_secs(), 1_800);
config.manual_transaction_timeout_secs = u64::MAX;
assert_eq!(config.effective_manual_transaction_timeout_secs(), super::MAX_MANUAL_TRANSACTION_TIMEOUT_SECS);
}
#[test]
fn query_timeout_floor_is_scoped_to_spanner() {
let mut config = mysql_config("root", "", None);
+1
View File
@@ -160,6 +160,7 @@ fn credential_target(config: &ConnectionConfig) -> Value {
"connect_timeout_secs",
"query_timeout_secs",
"idle_timeout_secs",
"manual_transaction_timeout_secs",
"keepalive_interval_secs",
"redis_key_separator",
"redis_scan_page_size",
+1
View File
@@ -1092,6 +1092,7 @@ mod tests {
connect_timeout_secs: dbx_core::models::connection::default_connect_timeout_secs(),
query_timeout_secs: dbx_core::models::connection::default_query_timeout_secs(),
idle_timeout_secs: dbx_core::models::connection::default_idle_timeout_secs(),
manual_transaction_timeout_secs: dbx_core::models::connection::default_manual_transaction_timeout_secs(),
keepalive_interval_secs: dbx_core::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+2 -1
View File
@@ -195,7 +195,7 @@ mod tests {
use super::*;
use dbx_core::models::connection::{
default_connect_timeout_secs, default_idle_timeout_secs, default_keepalive_interval_secs,
default_query_timeout_secs, DatabaseType,
default_manual_transaction_timeout_secs, default_query_timeout_secs, DatabaseType,
};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
@@ -233,6 +233,7 @@ mod tests {
connect_timeout_secs: default_connect_timeout_secs(),
query_timeout_secs: default_query_timeout_secs(),
idle_timeout_secs: default_idle_timeout_secs(),
manual_transaction_timeout_secs: default_manual_transaction_timeout_secs(),
keepalive_interval_secs: default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+2 -1
View File
@@ -315,7 +315,7 @@ mod tests {
use dbx_core::connection::AppState;
use dbx_core::models::connection::{
default_connect_timeout_secs, default_idle_timeout_secs, default_keepalive_interval_secs,
default_query_timeout_secs, ConnectionConfig, DatabaseType,
default_manual_transaction_timeout_secs, default_query_timeout_secs, ConnectionConfig, DatabaseType,
};
use dbx_core::persistence::task_history::{TaskRunItemsQuery, TaskRunStatus, TransferObjectSelectionMode};
use dbx_core::transfer::{
@@ -354,6 +354,7 @@ mod tests {
connect_timeout_secs: default_connect_timeout_secs(),
query_timeout_secs: default_query_timeout_secs(),
idle_timeout_secs: default_idle_timeout_secs(),
manual_transaction_timeout_secs: default_manual_transaction_timeout_secs(),
keepalive_interval_secs: default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+1
View File
@@ -266,6 +266,7 @@ mod tests {
connect_timeout_secs: dbx_core::models::connection::default_connect_timeout_secs(),
query_timeout_secs: dbx_core::models::connection::default_query_timeout_secs(),
idle_timeout_secs: dbx_core::models::connection::default_idle_timeout_secs(),
manual_transaction_timeout_secs: dbx_core::models::connection::default_manual_transaction_timeout_secs(),
keepalive_interval_secs: dbx_core::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
+1 -3
View File
@@ -46,9 +46,7 @@ fn manual_transaction_command_error(error: String) -> ManualTransactionCommandEr
return ManualTransactionCommandError::Structured(Box::new(error));
}
if dbx_core::query::is_manual_transaction_session_expired_error(&error) {
ManualTransactionCommandError::Structured(Box::new(BackendError::from_manual_transaction_session_expired(
dbx_core::query::MANUAL_TRANSACTION_IDLE_TIMEOUT_SECS,
)))
ManualTransactionCommandError::Structured(Box::new(BackendError::from_manual_transaction_session_expired()))
} else {
ManualTransactionCommandError::Legacy(error)
}