Skip to content

Commit 957f979

Browse files
committed
fix(core): resolve SQLite 'database is locked' errors in concurrent environments
- Switched to BEGIN IMMEDIATE for manual transactions to ensure write lock acquisition at start. - Implemented _execute_with_retry with exponential backoff for write operations. - Set PRAGMA synchronous=NORMAL for better reliability in WAL mode. - Resolves CI failures in multi-process concurrency tests.
1 parent 02ebd2a commit 957f979

1 file changed

Lines changed: 17 additions & 3 deletions

File tree

  • src/ledgermind/core/stores/semantic_store

src/ledgermind/core/stores/semantic_store/meta.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
import sqlite3
33
import logging
4+
import time
45
from typing import List, Dict, Any, Optional, Tuple
56
from datetime import datetime
67
from contextlib import contextmanager
@@ -28,11 +29,10 @@ def __init__(self, db_path: str):
2829

2930
def _init_db(self):
3031
self._conn.execute("PRAGMA journal_mode=WAL")
31-
self._conn.execute("PRAGMA synchronous=OFF") # Restored from v3.0.3 for performance
32+
self._conn.execute("PRAGMA synchronous=NORMAL")
3233
self._conn.execute("PRAGMA busy_timeout=60000")
3334
self._conn.execute("PRAGMA cache_size=-64000") # 64MB cache
3435
self._conn.execute("PRAGMA temp_store=MEMORY")
35-
self._conn.execute("PRAGMA mmap_size=30000000000")
3636
# Ensure FTS5 is available or fallback
3737
try:
3838
self._conn.execute("SELECT 1")
@@ -146,6 +146,20 @@ def _init_db(self):
146146
self.rollback_transaction()
147147
raise
148148

149+
def _execute_with_retry(self, sql: str, params: tuple = ()):
150+
"""Executes a query with built-in retry logic for 'database is locked' errors."""
151+
max_retries = 5
152+
retry_delay = 0.5
153+
for i in range(max_retries):
154+
try:
155+
return self._conn.execute(sql, params)
156+
except sqlite3.OperationalError as e:
157+
if "database is locked" in str(e) and i < max_retries - 1:
158+
logger.warning(f"Database locked, retrying {i+1}/{max_retries}...")
159+
time.sleep(retry_delay * (i + 1))
160+
continue
161+
raise
162+
149163
def upsert(self, fid: str, target: str, status: str, kind: str, timestamp: datetime,
150164
title: str = "", superseded_by: Optional[str] = None, namespace: str = "default",
151165
content: str = "", keywords: str = "", confidence: float = 1.0, context_json: str = "{}",
@@ -155,7 +169,7 @@ def upsert(self, fid: str, target: str, status: str, kind: str, timestamp: datet
155169
# Ensure timestamp is in ISO format string
156170
ts_str = timestamp.isoformat() if hasattr(timestamp, 'isoformat') else str(timestamp)
157171

158-
self._conn.execute("""
172+
self._execute_with_retry("""
159173
INSERT INTO semantic_meta (fid, target, title, status, kind, timestamp, superseded_by, namespace, content, keywords, confidence, context_json, phase, vitality, reinforcement_density, stability_score, coverage, link_count)
160174
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
161175
ON CONFLICT(fid) DO UPDATE SET

0 commit comments

Comments
 (0)