fix: 防止 Worker Timeout 與改進錯誤訊息

- 加入 Oracle call_timeout (55s) 防止慢查詢阻塞 Gunicorn worker
- 改進前端 fetchWithTimeout 錯誤訊息,顯示超時的 URL
- 區分超時錯誤與網路錯誤,便於問題診斷

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-27 21:01:46 +08:00
parent 3d4db4f530
commit 42dd06b2c0
3 changed files with 25 additions and 7 deletions

View File

@@ -181,6 +181,7 @@ def get_db_connection():
"""Create a direct oracledb connection.
Used for operations that need direct cursor access.
Includes call_timeout to prevent long-running queries from blocking workers.
"""
try:
conn = oracledb.connect(
@@ -189,7 +190,10 @@ def get_db_connection():
retry_count=1, # Retry once on connection failure
retry_delay=1, # 1s delay between retries
)
logger.debug("Direct oracledb connection established")
# Set call timeout to 55 seconds (must be less than Gunicorn's 60s worker timeout)
# This prevents queries from blocking workers indefinitely
conn.call_timeout = 55000 # milliseconds
logger.debug("Direct oracledb connection established (call_timeout=55s)")
return conn
except Exception as exc:
ora_code = _extract_ora_code(exc)

View File

@@ -764,7 +764,11 @@
async function fetchWithTimeout(url, timeout = API_TIMEOUT, externalSignal = null) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeout);
// If external signal is provided, abort when it fires
if (externalSignal) {
@@ -782,9 +786,12 @@
if (externalSignal && externalSignal.aborted) {
throw error; // Re-throw AbortError for external cancellation
}
throw new Error('Request timeout');
if (timedOut) {
throw new Error(`Request timeout after ${timeout/1000}s: ${url}`);
}
throw error; // Other abort (shouldn't happen, but be safe)
}
throw error;
throw new Error(`Network error for ${url}: ${error.message}`);
}
}

View File

@@ -951,7 +951,11 @@
async function fetchWithTimeout(url, timeout = API_TIMEOUT, externalSignal = null) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeout);
// If external signal is provided, abort when it fires
if (externalSignal) {
@@ -969,9 +973,12 @@
if (externalSignal && externalSignal.aborted) {
throw error; // Re-throw AbortError for external cancellation
}
throw new Error('Request timeout');
if (timedOut) {
throw new Error(`Request timeout after ${timeout/1000}s: ${url}`);
}
throw error; // Other abort (shouldn't happen, but be safe)
}
throw error;
throw new Error(`Network error for ${url}: ${error.message}`);
}
}