|
9 | 9 | from threading import Lock |
10 | 10 |
|
11 | 11 | logger = logging.getLogger("trackio") |
12 | | -if not logger.handlers: |
13 | | - logging.basicConfig() |
14 | | - logger.setLevel(logging.INFO) |
15 | 12 |
|
16 | 13 | try: |
17 | 14 | import fcntl |
@@ -49,17 +46,10 @@ def _configure_sqlite_pragmas(conn: sqlite3.Connection) -> None: |
49 | 46 | if override in _JOURNAL_MODE_WHITELIST: |
50 | 47 | journal = override.upper() |
51 | 48 | elif os.environ.get("SYSTEM") == "spaces": |
52 | | - journal = "DELETE" |
| 49 | + journal = "MEMORY" |
53 | 50 | else: |
54 | 51 | journal = "WAL" |
55 | | - result = conn.execute(f"PRAGMA journal_mode = {journal}").fetchone() |
56 | | - logger.info( |
57 | | - "SQLite pragmas: requested journal_mode=%s, actual=%s, SYSTEM=%s, TRACKIO_DIR=%s", |
58 | | - journal, |
59 | | - result[0] if result else "unknown", |
60 | | - os.environ.get("SYSTEM"), |
61 | | - os.environ.get("TRACKIO_DIR"), |
62 | | - ) |
| 52 | + conn.execute(f"PRAGMA journal_mode = {journal}") |
63 | 53 | conn.execute("PRAGMA synchronous = NORMAL") |
64 | 54 | conn.execute("PRAGMA temp_store = MEMORY") |
65 | 55 | conn.execute("PRAGMA cache_size = -20000") |
@@ -274,44 +264,37 @@ def _init_db_tables(db_path: Path, project: str) -> None: |
274 | 264 | conn.commit() |
275 | 265 |
|
276 | 266 | @staticmethod |
277 | | - def _wait_for_writable_dir( |
278 | | - directory: Path, retries: int = 10, delay: float = 1.0 |
279 | | - ) -> None: |
280 | | - probe = directory / ".trackio_probe" |
281 | | - for attempt in range(retries): |
282 | | - try: |
283 | | - probe.write_bytes(b"ok") |
284 | | - probe.unlink(missing_ok=True) |
285 | | - return |
286 | | - except OSError as e: |
287 | | - logger.warning( |
288 | | - "mount not ready: attempt %d/%d (%s), retrying in %.1fs", |
289 | | - attempt + 1, |
290 | | - retries, |
291 | | - e, |
292 | | - delay, |
293 | | - ) |
294 | | - time.sleep(delay) |
295 | | - delay = min(delay * 2, 10.0) |
296 | | - raise OSError(f"Directory {directory} is not writable after {retries} attempts") |
297 | | - |
298 | | - @staticmethod |
299 | | - def init_db(project: str) -> Path: |
| 267 | + def init_db(project: str, _retries: int = 10, _delay: float = 1.0) -> Path: |
300 | 268 | """ |
301 | 269 | Initialize the SQLite database with required tables. |
| 270 | + Retries on transient I/O errors (e.g. FUSE mount not yet ready). |
302 | 271 | Returns the database path. |
303 | 272 | """ |
304 | 273 | SQLiteStorage._ensure_hub_loaded() |
305 | 274 | db_path = SQLiteStorage.get_project_db_path(project) |
306 | 275 | db_path.parent.mkdir(parents=True, exist_ok=True) |
307 | | - SQLiteStorage._wait_for_writable_dir(db_path.parent) |
308 | | - logger.info( |
309 | | - "init_db: project=%s, db_path=%s, db_exists=%s", |
310 | | - project, |
311 | | - db_path, |
312 | | - db_path.exists(), |
313 | | - ) |
314 | | - SQLiteStorage._init_db_tables(db_path, project) |
| 276 | + for attempt in range(_retries): |
| 277 | + try: |
| 278 | + SQLiteStorage._init_db_tables(db_path, project) |
| 279 | + return db_path |
| 280 | + except sqlite3.OperationalError as e: |
| 281 | + msg = str(e).lower() |
| 282 | + is_transient = "disk i/o error" in msg or "readonly" in msg |
| 283 | + if not is_transient or attempt >= _retries - 1: |
| 284 | + raise |
| 285 | + logger.warning( |
| 286 | + "init_db failed (%s), retrying in %.1fs", |
| 287 | + e, |
| 288 | + _delay, |
| 289 | + ) |
| 290 | + if db_path.exists() and db_path.stat().st_size == 0: |
| 291 | + try: |
| 292 | + with open(db_path, "wb"): |
| 293 | + pass |
| 294 | + except OSError: |
| 295 | + pass |
| 296 | + time.sleep(_delay) |
| 297 | + _delay = min(_delay * 2, 10.0) |
315 | 298 | return db_path |
316 | 299 |
|
317 | 300 | @staticmethod |
|
0 commit comments