BUG-HUNT: [concurrency] ActorRegistry.ensure_built_in_actors() is not thread-safe, potentially creating duplicate actors under concurrent calls #7382

Open
opened 2026-04-10 18:38:56 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: [concurrency] ActorRegistry.ensure_built_in_actors() has no locking, allowing multiple threads to create duplicate built-in actors simultaneously

Severity Assessment

  • Impact: Multiple threads calling ActorRegistry.ensure_built_in_actors() concurrently (e.g., during startup or when multiple CLI commands run in parallel) can each create actors simultaneously, potentially:
    1. Creating duplicate database entries for the same actor
    2. Setting different "default" actors based on race conditions
    3. Triggering database constraint violations (if the DB enforces uniqueness)
  • Likelihood: Medium — happens when multiple threads or processes initialize actors simultaneously, which is common in web server or worker pool contexts
  • Priority: High

Location

  • File: src/cleveragents/actor/registry.py
  • Function/Class: ActorRegistry.ensure_built_in_actors()
  • Lines: ~70-130

Description

The ensure_built_in_actors() method performs a check-then-act sequence without any locking:

  1. Gets list of configured providers
  2. For each provider, creates/upserts an actor via _actor_service.upsert_actor()
  3. Checks if a default actor is set
  4. If not, sets the default

This entire sequence is not protected by any lock. In a multi-threaded environment:

  1. Thread A calls ensure_built_in_actors(), gets providers
  2. Thread B calls ensure_built_in_actors(), gets providers
  3. Both threads try to upsert the same built-in actor simultaneously
  4. Database constraint violations or silent duplicate creation can occur
  5. Both threads check if a default actor is set (none is)
  6. Both threads race to set the default actor

Additionally, many other methods in ActorRegistry call ensure_built_in_actors() at the start (add(), get(), remove(), set_default_actor(), etc.). This means the initialization check is done on every call without any coordination between threads.

Evidence

def ensure_built_in_actors(self) -> list[Actor]:
    """Generate built-in actors from configured providers if missing."""
    # No lock here!
    configured: list[ProviderInfo] = (
        self._provider_registry.get_configured_providers()
    )
    if not configured:
        return []

    actors: list[Actor] = []
    # No lock protecting these upsert calls from concurrent calls:
    for info in configured:
        # ...
        actor = self._actor_service.upsert_actor(...)  # Race condition!
        actors.append(actor)

    if not self._actor_service.get_default_actor() and actors:
        # Race: two threads could both check here and both set default!
        ...
        self._actor_service.set_default_actor(preferred.name)  # Race!

Compare with PluginManager.activate_plugin() and McpClient.start() which both use proper locking for similar initialization patterns.

Expected Behavior

ensure_built_in_actors() should be idempotent and thread-safe. The typical pattern is to use a threading.Lock with a "initialized" flag:

def __init__(self, ...):
    self._built_in_init_lock = threading.Lock()
    self._built_ins_initialized = False

def ensure_built_in_actors(self) -> list[Actor]:
    if self._built_ins_initialized:
        return self._actor_service.list_actors()  # Fast path
    
    with self._built_in_init_lock:
        if self._built_ins_initialized:  # Double-check under lock
            return self._actor_service.list_actors()
        # ... do initialization ...
        self._built_ins_initialized = True

Actual Behavior

ensure_built_in_actors() has no locking. Concurrent calls can create duplicate entries or race conditions when setting the default actor.

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_, 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] ActorRegistry.ensure_built_in_actors() has no locking, allowing multiple threads to create duplicate built-in actors simultaneously ### Severity Assessment - **Impact**: Multiple threads calling `ActorRegistry.ensure_built_in_actors()` concurrently (e.g., during startup or when multiple CLI commands run in parallel) can each create actors simultaneously, potentially: 1. Creating duplicate database entries for the same actor 2. Setting different "default" actors based on race conditions 3. Triggering database constraint violations (if the DB enforces uniqueness) - **Likelihood**: Medium — happens when multiple threads or processes initialize actors simultaneously, which is common in web server or worker pool contexts - **Priority**: High ### Location - **File**: `src/cleveragents/actor/registry.py` - **Function/Class**: `ActorRegistry.ensure_built_in_actors()` - **Lines**: ~70-130 ### Description The `ensure_built_in_actors()` method performs a check-then-act sequence without any locking: 1. Gets list of configured providers 2. For each provider, creates/upserts an actor via `_actor_service.upsert_actor()` 3. Checks if a default actor is set 4. If not, sets the default This entire sequence is not protected by any lock. In a multi-threaded environment: 1. Thread A calls `ensure_built_in_actors()`, gets providers 2. Thread B calls `ensure_built_in_actors()`, gets providers 3. Both threads try to upsert the same built-in actor simultaneously 4. Database constraint violations or silent duplicate creation can occur 5. Both threads check if a default actor is set (none is) 6. Both threads race to set the default actor Additionally, many other methods in `ActorRegistry` call `ensure_built_in_actors()` at the start (`add()`, `get()`, `remove()`, `set_default_actor()`, etc.). This means the initialization check is done on every call without any coordination between threads. ### Evidence ```python def ensure_built_in_actors(self) -> list[Actor]: """Generate built-in actors from configured providers if missing.""" # No lock here! configured: list[ProviderInfo] = ( self._provider_registry.get_configured_providers() ) if not configured: return [] actors: list[Actor] = [] # No lock protecting these upsert calls from concurrent calls: for info in configured: # ... actor = self._actor_service.upsert_actor(...) # Race condition! actors.append(actor) if not self._actor_service.get_default_actor() and actors: # Race: two threads could both check here and both set default! ... self._actor_service.set_default_actor(preferred.name) # Race! ``` Compare with `PluginManager.activate_plugin()` and `McpClient.start()` which both use proper locking for similar initialization patterns. ### Expected Behavior `ensure_built_in_actors()` should be idempotent and thread-safe. The typical pattern is to use a `threading.Lock` with a "initialized" flag: ```python def __init__(self, ...): self._built_in_init_lock = threading.Lock() self._built_ins_initialized = False def ensure_built_in_actors(self) -> list[Actor]: if self._built_ins_initialized: return self._actor_service.list_actors() # Fast path with self._built_in_init_lock: if self._built_ins_initialized: # Double-check under lock return self._actor_service.list_actors() # ... do initialization ... self._built_ins_initialized = True ``` ### Actual Behavior `ensure_built_in_actors()` has no locking. Concurrent calls can create duplicate entries or race conditions when setting the default actor. ### 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-11 00:21:05 +00:00
Author
Owner

Issue triaged by project owner:

  • State: Verified
  • Priority: Critical — ActorRegistry.ensure_built_in_actors() is not thread-safe. Race condition in actor initialization.
  • Milestone: v3.5.0 (M6: Autonomy Hardening) — Actor registry thread safety is required for parallel execution
  • Story Points: 3 (M) — Thread safety fix
  • MoSCoW: Must Have — Thread-safe actor registry is required for autonomous operation

Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

Issue triaged by project owner: - **State**: Verified - **Priority**: Critical — ActorRegistry.ensure_built_in_actors() is not thread-safe. Race condition in actor initialization. - **Milestone**: v3.5.0 (M6: Autonomy Hardening) — Actor registry thread safety is required for parallel execution - **Story Points**: 3 (M) — Thread safety fix - **MoSCoW**: Must Have — Thread-safe actor registry is required for autonomous operation --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
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#7382
No description provided.