Skip to content

Commit 2c3a940

Browse files
feat(scheduled_jobs): add scheduler database support and improve SQLite configuration, get around the write lock when scheduler and Runner wants to write to the same DB
Introduced a new `scheduler_url` field in `DbSettings` for APScheduler job storage, allowing separate database management. Updated the `engine.py` to utilize the new scheduler database and removed unused SQLite connection settings. Enhanced the `scheduler.py` to create a dedicated engine for the scheduler database and ensure proper SQLite configuration for foreign key support. Updated logging to use a private logger for better encapsulation.
1 parent 83e743f commit 2c3a940

File tree

4 files changed

+36
-15
lines changed

4 files changed

+36
-15
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ reports/
166166
.DS_Store
167167
/chat
168168
/askui_chat.db
169-
/askui_chat.db-shm
170-
/askui_chat.db-wal
169+
/askui_scheduler.db
171170
.cache/
172171

173172
bom.json

src/askui/chat/api/db/engine.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from sqlite3 import Connection as SQLite3Connection
33
from typing import Any
44

5-
from sqlalchemy import Engine, create_engine, event
5+
from sqlalchemy import create_engine, event
66

77
from askui.chat.api.dependencies import get_settings
88

@@ -14,12 +14,8 @@
1414
engine = create_engine(settings.db.url, connect_args=connect_args, echo=echo)
1515

1616

17-
@event.listens_for(Engine, "connect")
17+
@event.listens_for(engine, "connect")
1818
def set_sqlite_pragma(dbapi_conn: SQLite3Connection, connection_record: Any) -> None: # noqa: ARG001
1919
cursor = dbapi_conn.cursor()
2020
cursor.execute("PRAGMA foreign_keys=ON")
21-
# WAL mode - allows concurrent readers + writer
22-
cursor.execute("PRAGMA journal_mode=WAL")
23-
# Busy timeout - prevents SQLite from hanging on locked databases
24-
cursor.execute("PRAGMA busy_timeout=10000") # 10 seconds
2521
cursor.close()

src/askui/chat/api/scheduled_jobs/scheduler.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,32 @@
66
"""
77

88
import logging
9+
from sqlite3 import Connection as SQLite3Connection
910
from typing import Any
1011

1112
from apscheduler import AsyncScheduler
1213
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
14+
from sqlalchemy import create_engine, event
1315

14-
from askui.chat.api.db.engine import engine
16+
from askui.chat.api.dependencies import get_settings
1517

16-
logger = logging.getLogger(__name__)
18+
_logger = logging.getLogger(__name__)
1719

18-
# Module-level singleton data store (similar to engine pattern)
19-
_data_store: Any = SQLAlchemyDataStore(engine_or_url=engine)
20+
# Module-level settings for scheduler database
21+
_settings = get_settings()
22+
_connect_args = {"check_same_thread": False}
23+
_echo = _logger.isEnabledFor(logging.DEBUG)
24+
25+
# Separate engine for scheduler database
26+
scheduler_engine = create_engine(
27+
_settings.db.scheduler_url,
28+
connect_args=_connect_args,
29+
echo=_echo,
30+
)
31+
32+
33+
# Module-level singleton data store using separate scheduler database
34+
_data_store: Any = SQLAlchemyDataStore(engine_or_url=scheduler_engine)
2035

2136
# Module-level singleton scheduler instance
2237
# - max_concurrent_jobs=1: only one job runs at a time (sequential execution)
@@ -27,6 +42,13 @@
2742
)
2843

2944

45+
@event.listens_for(scheduler_engine, "connect")
46+
def set_sqlite_pragma(dbapi_conn: SQLite3Connection, connection_record: Any) -> None: # noqa: ARG001
47+
cursor = dbapi_conn.cursor()
48+
cursor.execute("PRAGMA foreign_keys=ON")
49+
cursor.close()
50+
51+
3052
async def start_scheduler() -> None:
3153
"""
3254
Start the scheduler to begin processing jobs.
@@ -38,10 +60,10 @@ async def start_scheduler() -> None:
3860
await scheduler.__aenter__()
3961
# Then start background processing of jobs
4062
await scheduler.start_in_background()
41-
logger.info("Scheduler started in background")
63+
_logger.info("Scheduler started in background")
4264

4365

4466
async def shutdown_scheduler() -> None:
4567
"""Shut down the scheduler gracefully."""
4668
await scheduler.__aexit__(None, None, None)
47-
logger.info("Scheduler shut down")
69+
_logger.info("Scheduler shut down")

src/askui/chat/api/settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ class DbSettings(BaseModel):
1818
default_factory=lambda: f"sqlite:///{(Path.cwd().absolute() / 'askui_chat.db').as_posix()}",
1919
description="Database URL for SQLAlchemy connection",
2020
)
21+
scheduler_url: str = Field(
22+
default_factory=lambda: f"sqlite:///{(Path.cwd().absolute() / 'askui_scheduler.db').as_posix()}",
23+
description="Database URL for APScheduler job storage",
24+
)
2125
auto_migrate: bool = Field(
2226
default=True,
2327
description="Whether to run migrations automatically on startup",
2428
)
2529

26-
@field_validator("url")
30+
@field_validator("url", "scheduler_url")
2731
@classmethod
2832
def validate_sqlite_url(cls, v: str) -> str:
2933
"""Ensure only synchronous SQLite URLs are allowed."""

0 commit comments

Comments
 (0)