-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: SQLite memory/storage layer unsafe for concurrent multi-agent access #1178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import threading | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Dict, List, Optional, Union, Literal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -189,6 +190,16 @@ def __init__(self, config: Dict[str, Any], verbose: int = 0): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.cfg = config or {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.verbose = verbose | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Thread-local storage for SQLite connections (thread-safe) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local = threading.local() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Write lock for serializing database modifications (thread-safe) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._write_lock = threading.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Connection registry for cleanup across threads (use regular set with careful cleanup) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._all_connections = set() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._connection_lock = threading.Lock() # Protect the connection registry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Set logger level based on verbose | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if verbose >= 5: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.setLevel(logging.INFO) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -247,6 +258,50 @@ def __init__(self, config: Dict[str, Any], verbose: int = 0): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif self.use_rag: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._init_chroma() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _get_stm_conn(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Get thread-local short-term memory SQLite connection.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not hasattr(self._local, 'stm_conn') or self._local.stm_conn is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local.stm_conn = sqlite3.connect( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.short_db, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| check_same_thread=False, # Allow cross-thread cleanup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout=30.0 # 30 second timeout for lock contention | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Configure busy timeout for better contention handling | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local.stm_conn.execute("PRAGMA busy_timeout=30000") # 30 seconds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Enable WAL mode for concurrent read/write without blocking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = self._local.stm_conn.execute("PRAGMA journal_mode=WAL").fetchone() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if result and result[0].upper() != 'WAL': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning(f"WAL mode not enabled for STM, got: {result[0]}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local.stm_conn.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Register connection for cleanup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with self._connection_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._all_connections.add(self._local.stm_conn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self._local.stm_conn | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _get_ltm_conn(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Get thread-local long-term memory SQLite connection.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not hasattr(self._local, 'ltm_conn') or self._local.ltm_conn is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local.ltm_conn = sqlite3.connect( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.long_db, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| check_same_thread=False, # Allow cross-thread cleanup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout=30.0 # 30 second timeout for lock contention | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Configure busy timeout for better contention handling | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local.ltm_conn.execute("PRAGMA busy_timeout=30000") # 30 seconds | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Enable WAL mode for concurrent read/write without blocking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result = self._local.ltm_conn.execute("PRAGMA journal_mode=WAL").fetchone() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if result and result[0].upper() != 'WAL': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning(f"WAL mode not enabled for LTM, got: {result[0]}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._local.ltm_conn.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Register connection for cleanup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with self._connection_lock: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._all_connections.add(self._local.ltm_conn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self._local.ltm_conn | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+261
to
+303
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two methods, def _get_conn(self, db_path: str, conn_attr: str) -> sqlite3.Connection:
"""Get or create a thread-local SQLite connection."""
if not hasattr(self._local, conn_attr) or getattr(self._local, conn_attr) is None:
conn = sqlite3.connect(
db_path,
check_same_thread=False
)
# Enable WAL mode for concurrent read/write without blocking
conn.execute("PRAGMA journal_mode=WAL")
conn.commit()
setattr(self._local, conn_attr, conn)
return getattr(self._local, conn_attr)
def _get_stm_conn(self):
"""Get thread-local short-term memory SQLite connection."""
return self._get_conn(self.short_db, 'stm_conn')
def _get_ltm_conn(self):
"""Get thread-local long-term memory SQLite connection."""
return self._get_conn(self.long_db, 'ltm_conn')
Comment on lines
+283
to
+303
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def close(self) -> None: | |
| """ | |
| Close any open SQLite connections held in this thread's local storage. | |
| This helps avoid leaking file descriptors and keeping database files | |
| open when Memory instances or threads are short-lived (e.g., in tests). | |
| """ | |
| # Close short-term memory connection for this thread, if any | |
| try: | |
| conn = getattr(self._local, "stm_conn", None) | |
| if conn is not None: | |
| conn.close() | |
| self._local.stm_conn = None | |
| except Exception: | |
| # Best-effort cleanup; avoid raising during shutdown | |
| logger.exception("Error while closing short-term memory SQLite connection") | |
| # Close long-term memory connection for this thread, if any | |
| try: | |
| conn = getattr(self._local, "ltm_conn", None) | |
| if conn is not None: | |
| conn.close() | |
| self._local.ltm_conn = None | |
| except Exception: | |
| # Best-effort cleanup; avoid raising during shutdown | |
| logger.exception("Error while closing long-term memory SQLite connection") | |
| def __enter__(self): | |
| """ | |
| Allow Memory to be used as a context manager. | |
| The caller is responsible for using the same thread within the context. | |
| """ | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb) -> None: | |
| """Ensure connections are closed when leaving a context manager block.""" | |
| self.close() | |
| def __del__(self): | |
| """ | |
| Attempt to clean up any open SQLite connections when this instance | |
| is garbage-collected. Errors are suppressed to avoid issues during | |
| interpreter shutdown. | |
| """ | |
| try: | |
| self.close() | |
| except Exception: | |
| pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for Memory cleanup in Agent class
echo "=== Checking for close_connections calls in agent code ==="
rg -n "close_connections|__exit__|with.*Memory" src/praisonai-agents/praisonaiagents/agent/
echo ""
echo "=== Checking how Memory instances are managed ==="
rg -nA5 "_memory_instance" src/praisonai-agents/praisonaiagents/agent/agent.py | head -80
echo ""
echo "=== Checking for any cleanup/destroy methods in Agent ==="
rg -n "def (cleanup|destroy|close|__del__|__exit__)" src/praisonai-agents/praisonaiagents/agent/agent.pyRepository: MervinPraison/PraisonAI
Length of output: 4256
🏁 Script executed:
#!/bin/bash
echo "=== Check for __del__ in Agent class ==="
rg -n "__del__" src/praisonai-agents/praisonaiagents/agent/agent.py
echo ""
echo "=== Check Memory and FileMemory __del__ implementations ==="
rg -n "__del__" src/praisonai-agents/praisonaiagents/memory/
echo ""
echo "=== Check if Memory instances are used as context managers in tests/examples ==="
rg -n "with.*Memory\(" src/praisonai-agents/ | head -20
echo ""
echo "=== Check if close_connections is called anywhere in the codebase ==="
rg -n "\.close_connections\(\)" src/praisonai-agents/ | head -20
echo ""
echo "=== Check the Memory class signature for cleanup patterns ==="
grep -n "def close_connections" src/praisonai-agents/praisonaiagents/memory/memory.py
grep -n "def __del__" src/praisonai-agents/praisonaiagents/memory/memory.pyRepository: MervinPraison/PraisonAI
Length of output: 720
🏁 Script executed:
#!/bin/bash
echo "=== Memory.__del__ implementation ==="
sed -n '1968,1980p' src/praisonai-agents/praisonaiagents/memory/memory.py
echo ""
echo "=== Memory.close_connections implementation ==="
sed -n '1926,1945p' src/praisonai-agents/praisonaiagents/memory/memory.py
echo ""
echo "=== Check if Agent's _memory_instance is ever explicitly cleaned up ==="
rg -n "self._memory_instance\s*=" src/praisonai-agents/praisonaiagents/agent/agent.py | tail -20
</script>Repository: MervinPraison/PraisonAI
Length of output: 2335
Add explicit cleanup for Memory instances in Agent class.
The Agent class creates Memory instances in _init_memory() (lines 3596, 3605, 3609, 3619, 3623, 3626, 3633, 3637, 3642, 3645) without explicit cleanup or context manager usage. While Memory provides __del__ to call close_connections() on garbage collection, relying on GC timing is unreliable for resource management, especially in multi-agent scenarios.
Agent lacks a __del__ or cleanup method to ensure _memory_instance.close_connections() is called before the agent is destroyed. This violates the async-safe execution requirement from the coding guidelines. Consider adding explicit cleanup (either __del__ method in Agent or documented cleanup patterns) to guarantee connections are properly closed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-agents/praisonaiagents/memory/memory.py` around lines 1960 -
1966, Agent creates Memory instances in _init_memory() and never explicitly
closes them; add explicit cleanup to ensure Memory.close_connections() is called
when an Agent is destroyed. Implement a cleanup method on Agent (e.g.,
close_memory or __del__) that checks for self._memory_instance and calls
self._memory_instance.close_connections(); update places that manage Agent
lifecycle to call this cleanup (or implement __aexit__/__exit__ on Agent to use
context manager patterns) to avoid relying on Memory.__del__ and ensure
deterministic resource release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WAL + thread-local connections reduces contention, but concurrent writers can still raise
sqlite3.OperationalError: database is lockedbecause the connection is created with default lock timeout/busy handling. Consider setting a highertimeout=onsqlite3.connect(...)and/orPRAGMA busy_timeout, and optionally adding a small retry/backoff (or an in-process write lock) around write transactions to make multi-agent concurrency robust.