-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsqlite_util.py
More file actions
72 lines (61 loc) · 2.46 KB
/
Copy pathsqlite_util.py
File metadata and controls
72 lines (61 loc) · 2.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""SQLite lock-contention helpers shared by the LCM engine.
Isolated from ``engine.py`` (WS5 seam): lock-contention detection, bounded
``busy_timeout`` changes, and transaction-preserving savepoints are pure SQLite
concerns with no engine state. Callers keep their own policy constants (for
example the session-end timeout budget).
"""
from __future__ import annotations
import sqlite3
import uuid
from contextlib import contextmanager
from typing import Iterator, List
def _is_sqlite_locked_error(exc: BaseException) -> bool:
"""Return True when an exception chain represents SQLite lock contention."""
seen: set[int] = set()
current: BaseException | None = exc
while current is not None and id(current) not in seen:
seen.add(id(current))
message = str(current).lower()
if isinstance(current, sqlite3.Error) and "locked" in message:
return True
current = current.__cause__ or current.__context__
return False
def _sqlite_busy_timeout_ms(conn: sqlite3.Connection) -> int:
row = conn.execute("PRAGMA busy_timeout").fetchone()
return int(row[0]) if row and row[0] is not None else 0
@contextmanager
def _sqlite_savepoint(conn: sqlite3.Connection) -> Iterator[None]:
"""Isolate helper writes without taking ownership of a caller transaction."""
# UUID hex contains only identifier-safe characters and keeps every nested
# helper's SAVEPOINT name unique with a fixed upper bound on name length.
name = f"lcm_{uuid.uuid4().hex}"
conn.execute(f"SAVEPOINT {name}")
try:
yield
except BaseException:
try:
conn.execute(f"ROLLBACK TO SAVEPOINT {name}")
finally:
conn.execute(f"RELEASE SAVEPOINT {name}")
raise
else:
conn.execute(f"RELEASE SAVEPOINT {name}")
@contextmanager
def _temporary_sqlite_busy_timeout(
connections: List[sqlite3.Connection | None],
timeout_ms: int,
) -> Iterator[None]:
"""Temporarily bound SQLite lock waits for gateway-critical paths."""
bounded_timeout = max(0, int(timeout_ms))
originals: list[tuple[sqlite3.Connection, int]] = []
for conn in connections:
if conn is None:
continue
original = _sqlite_busy_timeout_ms(conn)
conn.execute(f"PRAGMA busy_timeout={bounded_timeout}")
originals.append((conn, original))
try:
yield
finally:
for conn, original in reversed(originals):
conn.execute(f"PRAGMA busy_timeout={original}")