Repository navigation
Feat/executor cleanup cursor #1014
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
c881042
945f53d
4d6ee68
d2009d5
9fd481a
901eb6d
80c0962
84baf74
528b257
1c93564
1a105e1
bda9beb
2f14578
03d6bb8
0ce6adc
bbed9ce
055413d
5f5f056
026d5c2
450cb37
f4f8162
d4d798f
909982d
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 |
|---|---|---|
|
|
@@ -2,10 +2,11 @@ | |
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from contextlib import contextmanager | ||
| from typing import Generator | ||
| from contextlib import asynccontextmanager, contextmanager | ||
| from typing import AsyncGenerator, Generator, Optional | ||
|
|
||
| from sqlalchemy import create_engine | ||
| from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine | ||
|
Comment on lines
8
to
+9
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. 🧩 Analysis chain🏁 Script executed: cat -n backend/app/db/session.py | head -80Repository: wecode-ai/Wegent Length of output: 3408 🏁 Script executed: # Check SQLAlchemy version in requirements
fd -e txt -e in | xargs grep -l -i sqlalchemy 2>/dev/null | head -5Repository: wecode-ai/Wegent Length of output: 85 🏁 Script executed: # Check requirements files for SQLAlchemy version
rg -i "sqlalchemy" --type txt -A 1 -B 1Repository: wecode-ai/Wegent Length of output: 182 🏁 Script executed: # Check if there are any other requirements files with SQLAlchemy
find . -name "requirements*.txt" -o -name "pyproject.toml" -o -name "setup.py" | xargs grep -l -i sqlalchemy 2>/dev/nullRepository: wecode-ai/Wegent Length of output: 224 🏁 Script executed: # Check SQLAlchemy version in backend pyproject.toml
cat backend/pyproject.toml | grep -A 2 -B 2 sqlalchemyRepository: wecode-ai/Wegent Length of output: 450 🏁 Script executed: # Check if asyncmy and aiosqlite are listed as dependencies
rg "asyncmy|aiosqlite" backend/pyproject.tomlRepository: wecode-ai/Wegent Length of output: 81 🏁 Script executed: # Check if aiosqlite is a dependency anywhere
rg "aiosqlite" . --type toml --type txtRepository: wecode-ai/Wegent Length of output: 112 🌐 Web query:
💡 Result: In SQLAlchemy 2.0, use make_url to create a URL object from a string, URL.create for programmatic construction, URL.set to modify an existing URL object (including setting drivername), and URL.render_as_string to get the string representation. Example workflow: from sqlalchemy.engine import make_url, URL # From string url = make_url("postgresql://user:pass@localhost/db") # Create new url2 = URL.create(drivername="mysql+pymysql", host="example.com", database="mydb") # Set drivername on existing url3 = url.set(drivername="postgresql+psycopg2") # Render as string url_str = url3.render_as_string(hide_password=False) print(url_str) # postgresql+psycopg2://user:pass@localhost/db URL objects are immutable; set returns a new object with modifications. drivername is the dialect+driver part (e.g., "postgresql+psycopg2"). Citations:
🏁 Script executed: # Check where _get_async_database_url is used
rg "_get_async_database_url" backend/Repository: wecode-ai/Wegent Length of output: 191 Use SQLAlchemy URL parsing for async driver conversion. The fallback on line 74 creates invalid dialect strings. For example, a URL like Safer conversion from sqlalchemy import create_engine
+from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
@@
def _get_async_database_url() -> str:
"""Convert sync DATABASE_URL to async driver URL."""
- url = SQLALCHEMY_DATABASE_URL
- if "pymysql" in url:
- return url.replace("pymysql", "asyncmy")
- elif url.startswith("mysql://"):
- return url.replace("mysql://", "mysql+asyncmy://")
- elif "asyncmy" in url:
- return url
- elif url.startswith("sqlite"):
- return url.replace("sqlite://", "sqlite+aiosqlite://")
- return url.replace("mysql+", "mysql+asyncmy+", 1)
+ url = make_url(SQLALCHEMY_DATABASE_URL)
+ if url.drivername.startswith("mysql"):
+ url = url.set(drivername="mysql+asyncmy")
+ elif url.drivername.startswith("sqlite"):
+ url = url.set(drivername="sqlite+aiosqlite")
+ return url.render_as_string(hide_password=False)🤖 Prompt for AI Agents |
||
| from sqlalchemy.ext.declarative import declarative_base | ||
| from sqlalchemy.orm import Session, sessionmaker | ||
|
|
||
|
|
@@ -54,6 +55,78 @@ def _create_engine(): | |
| # Sync session factory | ||
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
|
|
||
| # Async database support (lazily initialized) | ||
| _async_engine: Optional[AsyncEngine] = None | ||
| _AsyncSessionLocal: Optional[sessionmaker] = None | ||
|
|
||
|
|
||
| def _get_async_database_url() -> str: | ||
| """Convert sync DATABASE_URL to async driver URL.""" | ||
| url = SQLALCHEMY_DATABASE_URL | ||
| if "pymysql" in url: | ||
| return url.replace("pymysql", "asyncmy") | ||
| elif url.startswith("mysql://"): | ||
| return url.replace("mysql://", "mysql+asyncmy://") | ||
| elif "asyncmy" in url: | ||
| return url | ||
| elif url.startswith("sqlite"): | ||
| return url.replace("sqlite://", "sqlite+aiosqlite://") | ||
| return url.replace("mysql+", "mysql+asyncmy+", 1) | ||
|
|
||
|
|
||
| def _create_async_engine() -> AsyncEngine: | ||
| """Create async database engine.""" | ||
| async_url = _get_async_database_url() | ||
| if async_url.startswith("sqlite"): | ||
| return create_async_engine(async_url, pool_pre_ping=True) | ||
| return create_async_engine( | ||
| async_url, | ||
| pool_pre_ping=True, | ||
| pool_size=10, | ||
| max_overflow=20, | ||
| pool_timeout=30, | ||
| pool_recycle=3600, | ||
| ) | ||
|
|
||
|
|
||
| def get_async_engine() -> AsyncEngine: | ||
| """Get the async database engine, initializing if needed.""" | ||
| global _async_engine | ||
| if _async_engine is None: | ||
| _async_engine = _create_async_engine() | ||
| return _async_engine | ||
|
|
||
|
|
||
| def get_async_session_factory() -> sessionmaker: | ||
| """Get the async session factory, initializing if needed.""" | ||
| global _AsyncSessionLocal | ||
| if _AsyncSessionLocal is None: | ||
| _AsyncSessionLocal = sessionmaker( | ||
| bind=get_async_engine(), | ||
| class_=AsyncSession, | ||
| autocommit=False, | ||
| autoflush=False, | ||
| expire_on_commit=False, | ||
| ) | ||
| return _AsyncSessionLocal | ||
|
|
||
|
|
||
| # Convenience alias for direct use | ||
| def AsyncSessionLocal() -> AsyncSession: | ||
| """Create a new async session.""" | ||
| factory = get_async_session_factory() | ||
| return factory() | ||
|
|
||
|
|
||
| async def get_async_db() -> AsyncGenerator[AsyncSession, None]: | ||
| """Async database session dependency for FastAPI.""" | ||
| session = AsyncSessionLocal() | ||
| try: | ||
| yield session | ||
| finally: | ||
| await session.close() | ||
|
|
||
|
|
||
| # Import Base from shared package for consistency | ||
| # All models should use the same Base for proper relationship resolution | ||
| from app.db.base import Base | ||
|
|
||
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:
Repository: wecode-ai/Wegent
Length of output: 5086
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 9742
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 4165
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 2901
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 454
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 2867
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 1139
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 832
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 639
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 950
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 489
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 1437
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 1389
🏁 Script executed:
# Look at test files that use the distributed_lock or async lock methods cat backend/tests/services/test_jobs_cleanup.pyRepository: wecode-ai/Wegent
Length of output: 1549
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 42
🏁 Script executed:
# Look at the actual test file that patches the lock head -60 backend/tests/services/test_jobs_cleanup.pyRepository: wecode-ai/Wegent
Length of output: 1549
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 6319
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 42
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 183
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 42
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 934
Async Redis client lacks ping verification and has cross-event-loop binding vulnerability.
The
_async_redis_clientis cached on the module-level singleton, but unlike the syncredis_clientproperty (which validates connectivity viaping()), the async property never verifies that Redis is reachable. If Redis is down at startup, every call toacquire_async/extend_asyncwill silently hang for the fullsocket_connect_timeout(5 seconds) instead of failing open.Additionally,
redis.asyncio.Redisinstances bind their internal connection pool to whatever event loop first issues a command. While current usage is safe—cleanup_workerruns continuously in the single uvicorn event loop—the pattern is fragile. If async lock methods are called from different event loop contexts in the future (e.g., throughasyncio.run()or a separate scheduler thread), operations will fail withRuntimeError: Task ... attached to a different loop.Recommended fixes:
await client.ping()in the async property initialization (mirroring the sync version) to fail fast if Redis is unreachable.app/core/cache.pypattern) or cache-per-loop usingid(asyncio.get_running_loop())as key.🧰 Tools
🪛 Ruff (0.15.10)
[warning] 273-273: Do not catch blind exception:
Exception(BLE001)
🤖 Prompt for AI Agents