Skip to content

fix: SQLite 'database is locked' under concurrent writes#6474

Merged
Soulter merged 1 commit intoAstrBotDevs:masterfrom
he-yufeng:fix/sqlite-busy-timeout
Mar 17, 2026
Merged

fix: SQLite 'database is locked' under concurrent writes#6474
Soulter merged 1 commit intoAstrBotDevs:masterfrom
he-yufeng:fix/sqlite-busy-timeout

Conversation

@he-yufeng
Copy link
Contributor

@he-yufeng he-yufeng commented Mar 17, 2026

Problem

AstrBot frequently hits (sqlite3.OperationalError) database is locked when multiple async operations (agent responses, metrics inserts, session updates) write to SQLite concurrently. This affects both Windows and Linux deployments and is especially noticeable after importing personas or during rapid conversations.

Root Cause

create_async_engine() in BaseDatabase.__init__ doesn't set a busy timeout:

self.engine = create_async_engine(
    self.DATABASE_URL,
    echo=False,
    future=True,
    # no connect_args → SQLite default timeout is 5s (or 0 depending on driver)
)

SQLite only supports one writer at a time. Without a timeout, the second concurrent write fails immediately instead of waiting for the lock to be released.

Fix

Add connect_args={"timeout": 30} for SQLite engines. This tells the driver to wait up to 30 seconds for the write lock before raising an error — more than enough to handle the typical concurrent write bursts.

Combined with the existing WAL journal mode (set in SQLiteDatabase.initialize()), this provides concurrent reads + serialized writes with proper backpressure.

Fixes #6443

Summary by Sourcery

Bug Fixes:

  • Prevent frequent sqlite3.OperationalError: database is locked errors during concurrent SQLite writes by adding a busy timeout to async engine creation.

The async engine is created without a busy timeout, so concurrent
writes (agent responses, metrics, session updates) fail instantly
with 'database is locked' instead of waiting for the lock.

Add connect_args={'timeout': 30} for SQLite engines so the driver
waits up to 30 seconds for the write lock. Combined with the existing
WAL journal mode, this handles the typical concurrent write bursts
from agent + metrics + session operations.

Fixes AstrBotDevs#6443
@dosubot dosubot bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Mar 17, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves frequent 'database is locked' errors encountered in AstrBot when multiple asynchronous operations attempt to write to SQLite concurrently. The fix introduces a 30-second busy timeout for SQLite database connections, allowing the driver to wait for write locks to be released. This significantly improves the stability and resilience of concurrent database interactions without altering the existing WAL journal mode, ensuring proper backpressure for serialized writes.

Highlights

  • SQLite Database Locking Fix: Implemented a solution to prevent sqlite3.OperationalError: database is locked by configuring a busy timeout for SQLite connections, allowing concurrent write operations to wait for locks instead of failing immediately.
  • Conditional Connection Arguments: Added `connect_args={\
Changelog
  • astrbot/core/db/init.py
    • Added logic to detect SQLite database URLs and conditionally apply connect_args.
    • Introduced a timeout of 30 seconds within connect_args for SQLite engines to handle concurrent write contention.
    • Included detailed comments explaining the necessity and function of the SQLite busy timeout.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Using "sqlite" in self.DATABASE_URL to detect SQLite is a bit fragile; consider checking the URL scheme/driver name (e.g., via SQLAlchemy URL parsing or startswith "sqlite:" / "sqlite+") to avoid accidental matches.
  • The timeout=30 value is currently hardcoded; it might be useful to make this configurable (e.g., via settings/env) so deployments with different concurrency patterns can tune it without code changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `"sqlite" in self.DATABASE_URL` to detect SQLite is a bit fragile; consider checking the URL scheme/driver name (e.g., via SQLAlchemy URL parsing or startswith `"sqlite:"` / `"sqlite+"`) to avoid accidental matches.
- The `timeout=30` value is currently hardcoded; it might be useful to make this configurable (e.g., via settings/env) so deployments with different concurrency patterns can tune it without code changes.

## Individual Comments

### Comment 1
<location path="astrbot/core/db/__init__.py" line_range="41" />
<code_context>
+        # second write is attempted.  Setting timeout=30 tells SQLite to
+        # wait up to 30 s for the lock, which is enough to ride out brief
+        # write bursts from concurrent agent/metrics/session operations.
+        is_sqlite = "sqlite" in self.DATABASE_URL
+        connect_args = {"timeout": 30} if is_sqlite else {}
         self.engine = create_async_engine(
</code_context>
<issue_to_address>
**suggestion:** Use a stricter check for SQLite URLs instead of a substring search.

`"sqlite" in self.DATABASE_URL` can match non-SQLite URLs (e.g., if `sqlite` appears in credentials or query params). Consider parsing the URL and checking the scheme (e.g. via `sqlalchemy.engine.make_url(...).drivername`) or at least using `self.DATABASE_URL.startswith("sqlite")` so the timeout is only applied for real SQLite connections.

Suggested implementation:

```python
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import create_async_engine

```

```python
        # SQLite only supports a single writer at a time.  Without a busy
        # timeout the driver raises "database is locked" instantly when a
        # second write is attempted.  Setting timeout=30 tells SQLite to
        # wait up to 30 s for the lock, which is enough to ride out brief
        # write bursts from concurrent agent/metrics/session operations.
        url = make_url(self.DATABASE_URL)
        is_sqlite = url.drivername.startswith("sqlite")
        connect_args = {"timeout": 30} if is_sqlite else {}

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

# second write is attempted. Setting timeout=30 tells SQLite to
# wait up to 30 s for the lock, which is enough to ride out brief
# write bursts from concurrent agent/metrics/session operations.
is_sqlite = "sqlite" in self.DATABASE_URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Use a stricter check for SQLite URLs instead of a substring search.

"sqlite" in self.DATABASE_URL can match non-SQLite URLs (e.g., if sqlite appears in credentials or query params). Consider parsing the URL and checking the scheme (e.g. via sqlalchemy.engine.make_url(...).drivername) or at least using self.DATABASE_URL.startswith("sqlite") so the timeout is only applied for real SQLite connections.

Suggested implementation:

from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import create_async_engine
        # SQLite only supports a single writer at a time.  Without a busy
        # timeout the driver raises "database is locked" instantly when a
        # second write is attempted.  Setting timeout=30 tells SQLite to
        # wait up to 30 s for the lock, which is enough to ride out brief
        # write bursts from concurrent agent/metrics/session operations.
        url = make_url(self.DATABASE_URL)
        is_sqlite = url.drivername.startswith("sqlite")
        connect_args = {"timeout": 30} if is_sqlite else {}

@dosubot dosubot bot added the area:core The bug / feature is about astrbot's core, backend label Mar 17, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses the database is locked error with SQLite by introducing a busy timeout. The implementation is clear and directly solves the problem of concurrent writes. I've provided one suggestion to make the logic for detecting an SQLite connection more robust, which will prevent potential issues if other database URLs happen to contain 'sqlite' in their string.

Comment on lines +41 to +42
is_sqlite = "sqlite" in self.DATABASE_URL
connect_args = {"timeout": 30} if is_sqlite else {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For improved robustness and conciseness, these lines can be combined. Using startswith("sqlite") is more reliable than checking for containment with in. This change prevents misidentifying a non-SQLite database that might have "sqlite" in its name or path, which could lead to errors if that database driver does not support the timeout connection argument.

Suggested change
is_sqlite = "sqlite" in self.DATABASE_URL
connect_args = {"timeout": 30} if is_sqlite else {}
connect_args = {"timeout": 30} if self.DATABASE_URL.startswith("sqlite") else {}

@Soulter Soulter changed the title Fix SQLite 'database is locked' under concurrent writes fix: SQLite 'database is locked' under concurrent writes Mar 17, 2026
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 17, 2026
@Soulter Soulter merged commit acbc515 into AstrBotDevs:master Mar 17, 2026
6 of 8 checks passed
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 19, 2026
…6474)

The async engine is created without a busy timeout, so concurrent
writes (agent responses, metrics, session updates) fail instantly
with 'database is locked' instead of waiting for the lock.

Add connect_args={'timeout': 30} for SQLite engines so the driver
waits up to 30 seconds for the write lock. Combined with the existing
WAL journal mode, this handles the typical concurrent write bursts
from agent + metrics + session operations.

Fixes AstrBotDevs#6443
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend lgtm This PR has been approved by a maintainer size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]多会话/会话规则并发写入时,保存对话历史会触发 database is locked

2 participants