Repository navigation
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/dashboard/api/backups.py" line_range="119" />
<code_context>
+ # run on the event loop thread that also serves WebChat and the platform adapters.
return await _run(
- lambda: service.list_backups(page=page, page_size=page_size),
+ lambda: asyncio.to_thread(service.list_backups, page=page, page_size=page_size),
prefix="获取备份列表失败",
)
</code_context>
<issue_to_address>
**Backup listing fails during concurrent changes**
When a backup is renamed or deleted after its manifest is read but before `os.stat`, `os.stat` raises `FileNotFoundError`, and `_run` returns an error for the entire listing instead of returning the remaining backups.
Handle `FileNotFoundError` for an archive that disappears during the scan and skip that archive.
Also at `astrbot/dashboard/api/backups.py:132`.
</issue_to_address>
### Comment 2
<location path="astrbot/dashboard/api/backups.py" line_range="119" />
<code_context>
+ # run on the event loop thread that also serves WebChat and the platform adapters.
return await _run(
- lambda: service.list_backups(page=page, page_size=page_size),
+ lambda: asyncio.to_thread(service.list_backups, page=page, page_size=page_size),
prefix="获取备份列表失败",
)
</code_context>
<issue_to_address>
**Upload cleanup never starts**
When the cleanup task is unset or has stopped when a list endpoint runs, `asyncio.to_thread` runs `list_backups` in a worker, where `ensure_cleanup_task_started` calls `asyncio.create_task` without a running event loop; the caught `RuntimeError` leaves the coroutine unawaited and cleanup does not start, so polling emits a `RuntimeWarning` and expired upload chunks remain.
Start the cleanup task on the event-loop thread before running the backup scan in a worker.
Also at `astrbot/dashboard/api/backups.py:132`.
</issue_to_address>Sourcery assessment
Approval pending. 2 findings to address first.
Blocking findings: astrbot/dashboard/api/backups.py:119, astrbot/dashboard/api/backups.py:119
|
Both findings checked; the second one was a regression in this PR and is fixed. Upload cleanup never starts — valid, fixed in try:
self._cleanup_task = asyncio.create_task(self._cleanup_expired_sessions())
except RuntimeError:
pass
service.ensure_cleanup_task_started()
return await _run(
lambda: asyncio.to_thread(service.list_backups, page=page, page_size=page_size),
prefix="获取备份列表失败",
)Added a test that pins this by thread identity (no sleeps): With the fix, the same file: and the surrounding suites:
Backup listing fails during concurrent changes — real, but pre-existing and out of scope here. |
Symptom
Opening the dashboard Backup page (or polling
GET /api/v1/backups) runs the whole backup-directory scan —os.listdir,os.stat, and azipfile.ZipFile()open plusread("manifest.json")for every stored backup — directly on the asyncio event loop thread. With several backups on disk this freezes the loop for the duration of the read: WebChat SSE streams stall, plugin Web handlers queue up, and AstrBot's own lag monitor (astrbot/core/utils/event_loop_diagnostics.py:15, threshold 15 s) fires exactly on this kind of stall.Trigger input
Any authenticated dashboard user hitting the list endpoint, e.g. with three archives present in
data/backups:The dashboard polls this endpoint whenever the Backup tab is open, so the stall repeats.
Root cause
BackupService.list_backupsis a synchronous method (astrbot/dashboard/services/backup_service.py:183) that performs blocking filesystem work per file:os.listdirat line 188 and a real ZIP open + manifest read viaget_backup_manifestat line 196 (zipfile.ZipFile(zip_path, "r")at line 174).Both routes pass it to
_runas a plain callable:and
_run→run_maybe_async(astrbot/dashboard/async_utils.py:17) simply calls the lambda and only awaits the result if it happens to be awaitable. A sync callable therefore executes inline on the event loop thread — there is no thread hop anywhere in the path.Sibling dashboard code already treats this pattern as the bug it is:
stat_service.py:231(await asyncio.to_thread(self.storage_cleaner.get_status)),logs.py:96,bot_cleanup_service.py:148all offload sync service calls before awaiting them.Fix
Route both list endpoints through
asyncio.to_thread, matching the established pattern in the dashboard layer. No signature, schema, or route surface changes, so the generated dashboard API client is unaffected.asyncio.to_threadis available from Python 3.9, so this stays 3.10-compatible and cross-platform. The thread hop is asserted on thread identity, not on any timing/sleep, so the test is deterministic.Red (test added, source unchanged at
origin/master)Green
Regression check
Repo-wide
ruff format --check .(526 files already formatted) andruff check .(All checks passed) both pass.Prepared with an AI coding agent.
Summary by Sourcery
Offload backup manifest scanning from both dashboard list endpoints while preserving event-loop cleanup scheduling.
Bug Fixes:
Enhancements:
Tests: