fix: clear deleted knowledge bases from sessions#8259
Conversation
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- In
_remove_kb_from_session_configs, consider defensively handling the case wheresp.session_get(None, "kb_config")returnsNoneor a non-iterable to avoid aTypeErrorwhen iterating overprefs.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `_remove_kb_from_session_configs`, consider defensively handling the case where `sp.session_get(None, "kb_config")` returns `None` or a non-iterable to avoid a `TypeError` when iterating over `prefs`.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request implements a cleanup mechanism to remove deleted knowledge base IDs from session configurations. It introduces a new method _remove_kb_from_session_configs in the KnowledgeBaseRoute and updates the delete_kb endpoint to trigger this cleanup, accompanied by new unit tests. Feedback highlights that this business logic should ideally reside in the KnowledgeBaseManager for better architectural consistency and points out a potential race condition in the update loop that could lead to data loss during concurrent updates.
| return self.core_lifecycle.kb_manager | ||
|
|
||
| @staticmethod | ||
| async def _remove_kb_from_session_configs(kb_id: str) -> int: |
There was a problem hiding this comment.
The logic for cleaning up session configurations when a knowledge base is deleted is business logic that should ideally reside in the KnowledgeBaseManager (in astrbot/core/knowledge_base/kb_mgr.py) rather than the API route. Moving it to the core manager ensures that the cleanup happens regardless of how the deletion is triggered (e.g., via a CLI, a scheduled task, or another internal component), maintaining data consistency across the system.
| value = pref.value.get("val") if isinstance(pref.value, dict) else None | ||
| if not isinstance(value, dict): | ||
| continue | ||
|
|
||
| kb_ids = value.get("kb_ids") | ||
| if not isinstance(kb_ids, list) or kb_id not in kb_ids: | ||
| continue | ||
|
|
||
| new_value = { | ||
| **value, | ||
| "kb_ids": [item for item in kb_ids if item != kb_id], | ||
| } | ||
| await sp.session_put(pref.scope_id, "kb_config", new_value) | ||
| updated += 1 |
There was a problem hiding this comment.
There is a potential race condition here. The code iterates over a pre-fetched list of session configurations (prefs). Because the loop contains await calls (specifically sp.session_put), the event loop can process other tasks between iterations. If a session's configuration is updated by another coroutine after the initial session_get but before the loop reaches that specific session, the concurrent update will be overwritten and lost.
To mitigate this, you should fetch the latest value for each session configuration inside the loop before applying the changes. Note that if you apply this suggestion, you will also need to update the unit test mock for session_get in tests/test_kb_import.py to handle non-null umo values.
| value = pref.value.get("val") if isinstance(pref.value, dict) else None | |
| if not isinstance(value, dict): | |
| continue | |
| kb_ids = value.get("kb_ids") | |
| if not isinstance(kb_ids, list) or kb_id not in kb_ids: | |
| continue | |
| new_value = { | |
| **value, | |
| "kb_ids": [item for item in kb_ids if item != kb_id], | |
| } | |
| await sp.session_put(pref.scope_id, "kb_config", new_value) | |
| updated += 1 | |
| # Fetch the latest value for this specific session to avoid overwriting concurrent updates | |
| value = await sp.session_get(pref.scope_id, "kb_config") | |
| if not isinstance(value, dict): | |
| continue | |
| kb_ids = value.get("kb_ids") | |
| if not isinstance(kb_ids, list) or kb_id not in kb_ids: | |
| continue | |
| new_value = { | |
| **value, | |
| "kb_ids": [item for item in kb_ids if item != kb_id], | |
| } | |
| await sp.session_put(pref.scope_id, "kb_config", new_value) | |
| updated += 1 |
References
- In a single-threaded asyncio event loop, synchronous functions (code blocks without 'await') are executed atomically and will not be interrupted by other coroutines. Therefore, they are safe from race conditions when modifying shared state within that block.
c501840 to
5ce1775
Compare
|
Updated to address the review feedback:
Validation:
|
Summary
Fixes #8251.
When a knowledge base is deleted, remove that KB id from every session-level
kb_config.kb_idsentry. This avoids stale per-session bindings continuing to point at a deleted KB whenunique_sessionis enabled.This intentionally keeps the rest of each session KB config, such as
top_k, unchanged. If the deleted KB was the only binding,kb_idsbecomes an empty list so retrieval stops cleanly for that session.To verify
Summary by Sourcery
Ensure deleting a knowledge base also removes its ID from all stored session knowledge base configurations to prevent stale bindings.
Bug Fixes:
Tests: