Skip to content

fix: consolidate multiple event loops into a single global async bridge

Bikkumalla Sai Krishna requested to merge upgrade_glab into dev

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:

  1. Global event loop policies were being repeatedly overwritten.
  2. Task context was being lost across dozens of competing threads.
  3. 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 singleton GlobalBridge that manages a single asyncio loop in a dedicated GitLab-Global-Bridge thread.
  • src/infrastructure/gitlab/client.py: Refactored GitLabClient to remove internal loop/thread creation. It now utilizes the global bridge for all sync-to-async operations.
  • src/services/batch/client.py: Refactored the internal GitLabClient to 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 global asyncio policy initialization at the application entry point to ensure stability across Streamlit's thread pool.
  • pyproject.toml: Upgraded glabflow from 0.1.0a4 to 0.1.0a5 to 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

  1. Run the app: uv run streamlit run app.py
  2. Open the app in multiple browser tabs simultaneously.
  3. Log in with different user accounts (if available) or perform concurrent refreshes.
  4. Verify that no "Timeout" or "Event Loop closed" errors appear in the console.
  5. Verify that threading.enumerate() shows only one GitLab-Global-Bridge thread 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

Merge request reports

Loading