BUG-HUNT: [concurrency] Thread-unsafe global ProviderRegistry singleton in get_provider_registry() #7320

Open
opened 2026-04-10 16:47:29 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Concurrency — Thread-Unsafe Global Provider Registry

Severity Assessment

  • Impact: Multiple threads could each create their own ProviderRegistry instance, discover providers independently, and non-deterministically overwrite the global _registry. This can lead to provider discovery inconsistencies in multi-threaded server environments.
  • Likelihood: Medium — any async server startup or parallel test scenario will hit this
  • Priority: Medium

Location

  • File: src/cleveragents/providers/registry.py
  • Function/Class: get_provider_registry()
  • Lines: 750–765

Description

The module-level _registry singleton is initialized lazily inside get_provider_registry() without any lock. Two concurrent callers who both see _registry is None will each create a new ProviderRegistry, and whichever thread assigns last wins. This means some callers may get a reference to a ProviderRegistry that was immediately overwritten and is no longer the global singleton.

The same pattern repeats with reset_provider_registry() — but the real danger is the lazy init race.

Evidence

# src/cleveragents/providers/registry.py lines 750-765
_registry: ProviderRegistry | None = None  # ← module-level, no lock

def get_provider_registry(settings: Settings | None = None) -> ProviderRegistry:
    global _registry
    if _registry is None or settings is not None:   # ← Thread A and B both see None
        _registry = ProviderRegistry(settings)       # ← Both create new registry
    return _registry                                 # ← One gets discarded

Expected Behavior

The global registry should be created exactly once under concurrent access.

Actual Behavior

Under concurrent startup, multiple ProviderRegistry objects are created; the last writer wins.

Suggested Fix

_registry: ProviderRegistry | None = None
_registry_lock = threading.Lock()

def get_provider_registry(settings: Settings | None = None) -> ProviderRegistry:
    global _registry
    if settings is not None:
        with _registry_lock:
            _registry = ProviderRegistry(settings)
        return _registry
    if _registry is None:
        with _registry_lock:
            if _registry is None:
                _registry = ProviderRegistry(settings)
    return _registry

Category

concurrency

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor

## Bug Report: Concurrency — Thread-Unsafe Global Provider Registry ### Severity Assessment - **Impact**: Multiple threads could each create their own `ProviderRegistry` instance, discover providers independently, and non-deterministically overwrite the global `_registry`. This can lead to provider discovery inconsistencies in multi-threaded server environments. - **Likelihood**: Medium — any async server startup or parallel test scenario will hit this - **Priority**: Medium ### Location - **File**: `src/cleveragents/providers/registry.py` - **Function/Class**: `get_provider_registry()` - **Lines**: 750–765 ### Description The module-level `_registry` singleton is initialized lazily inside `get_provider_registry()` without any lock. Two concurrent callers who both see `_registry is None` will each create a new `ProviderRegistry`, and whichever thread assigns last wins. This means some callers may get a reference to a `ProviderRegistry` that was immediately overwritten and is no longer the global singleton. The same pattern repeats with `reset_provider_registry()` — but the real danger is the lazy init race. ### Evidence ```python # src/cleveragents/providers/registry.py lines 750-765 _registry: ProviderRegistry | None = None # ← module-level, no lock def get_provider_registry(settings: Settings | None = None) -> ProviderRegistry: global _registry if _registry is None or settings is not None: # ← Thread A and B both see None _registry = ProviderRegistry(settings) # ← Both create new registry return _registry # ← One gets discarded ``` ### Expected Behavior The global registry should be created exactly once under concurrent access. ### Actual Behavior Under concurrent startup, multiple `ProviderRegistry` objects are created; the last writer wins. ### Suggested Fix ```python _registry: ProviderRegistry | None = None _registry_lock = threading.Lock() def get_provider_registry(settings: Settings | None = None) -> ProviderRegistry: global _registry if settings is not None: with _registry_lock: _registry = ProviderRegistry(settings) return _registry if _registry is None: with _registry_lock: if _registry is None: _registry = ProviderRegistry(settings) return _registry ``` ### Category concurrency ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor
HAL9000 added this to the v3.5.0 milestone 2026-04-10 19:05:02 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#7320
No description provided.