fix: consolidate multiple event loops into a single global async bridge
Merge Request
Overview
This MR implements a robust "Single Global Event Loop" architecture for the GitLab Compliance Checker. It consolidates multiple, fragmented asyncio event loops into a single, persistent background thread managed by a new bridge.py singleton.
What does this MR do and why?
The previous architecture spawned a dedicated background thread and event loop for every GitLabClient instance (essentially per-user or per-session). In a multi-user Streamlit environment, this led to frequent "Timeout context manager should be used inside a task" errors because:
- Global event loop policies were being repeatedly overwritten.
- Task context was being lost across dozens of competing threads.
- Memory and CPU resources were wasted on redundant loop management.
This MR eliminates these issues by ensuring that all GitLab operations—across all users—share the same stable background loop, which is fully compatible with Python 3.11+ asyncio.timeout requirements.
Changes Made
-
src/infrastructure/gitlab/bridge.py[NEW]: Created a singletonGlobalBridgethat manages a singleasyncioloop in a dedicatedGitLab-Global-Bridgethread. -
src/infrastructure/gitlab/client.py: RefactoredGitLabClientto remove internal loop/thread creation. It now utilizes the global bridge for all sync-to-async operations. -
src/services/batch/client.py: Refactored the internalGitLabClientto use the global bridge, ensuring standard behavior across all services. -
src/infrastructure/gitlab/network.py&api_helper.py: Removed duplicate global loops and unified them under the new bridge. -
app.py: Added globalasynciopolicy initialization at the application entry point to ensure stability across Streamlit's thread pool. -
pyproject.toml: Upgradedglabflowfrom0.1.0a4to0.1.0a5to include the latest stability fixes.
Technical Details
The GlobalBridge uses a thread-safe singleton pattern (threading.Lock) to ensure the background loop starts exactly once. Operations are submitted via asyncio.run_coroutine_threadsafe, which is now perfectly synchronized through the global DefaultEventLoopPolicy.
Type of Change
-
🐛 Bug fix (non-breaking change that fixes an issue) -
✨ New feature (non-breaking change that adds functionality) -
💥 Breaking change (fix or feature that would cause existing functionality to change) -
📝 Documentation update -
♻ ️ Refactor (no functional changes) -
⚡ Performance improvement -
🧪 Test update -
🔧 Configuration change -
🚨 Security fix -
🗑 ️ Deprecation (removing deprecated code)
Related Issues / References
- Resolves:
RuntimeError: Timeout context manager should be used inside a task#70 (closed)
Screenshots or Screen Recordings
(User can attach the screenshot showing the loop ID verification if needed)
How to Validate Locally
- Run the app:
uv run streamlit run app.py - Open the app in multiple browser tabs simultaneously.
- Log in with different user accounts (if available) or perform concurrent refreshes.
- Verify that no "Timeout" or "Event Loop closed" errors appear in the console.
- Verify that
threading.enumerate()shows only oneGitLab-Global-Bridgethread regardless of the number of active users.
Testing Done
-
Unit tests updated (Mocked bridge used) -
Manual verification in multi-tab Streamlit sessions. -
Loop ID verification script ( verify_loop.py) passed.
Test Cases Covered:
| Scenario | Expected Result | Status |
|---|---|---|
| Multiple users fetching data | All requests succeed on one loop | |
| Page reload during fetch | Client recovers without loop errors | |
| Simultaneous Batch & UI calls | No thread contention or policy corruption |