Skip to content

feat(lock_store): make locking backend overridable#6015

Merged
mattatcha merged 9 commits into
mainfrom
matcha/overridable-lock-backend
Jun 4, 2026
Merged

feat(lock_store): make locking backend overridable#6015
mattatcha merged 9 commits into
mainfrom
matcha/overridable-lock-backend

Conversation

@mattatcha
Copy link
Copy Markdown
Collaborator

@mattatcha mattatcha commented Jun 2, 2026

Summary

Allow crewai_core.lock_store to use a custom locking backend instead of the built-in Redis/file selection, without touching call sites.

Key Changes

  • Add set_lock_backend(backend) — register a custom backend; pass None to restore the default.
  • lock() uses the custom backend when one is set, otherwise the unchanged Redis/file logic.
  • A backend is any callable(name, *, timeout) returning a context manager that holds the lock for the with block.
  • Default behavior is byte-for-byte unchanged when no backend is set.

Linear Issues


Note

Low Risk
Additive API with unchanged default locking path; main caveat is unsynchronized backend swaps under concurrent lock use, which is documented for startup-only setup.

Overview
Adds a process-wide pluggable lock backend so callers can swap Redis/file locking for custom implementations (e.g. in-process locks in tests or another distributed service) without changing existing lock() call sites.

set_lock_backend(backend) registers a callable (name, *, timeout) -> context manager; pass None to restore the built-in path. lock() snapshots the current backend before use so a concurrent swap cannot break acquisition, and when a custom backend is set it delegates directly with the raw lock name (the default path still namespaces names via crewai: + hash).

Default Redis vs file selection is unchanged when no backend is registered. Tests add autouse reset of the backend plus coverage that overrides bypass portalocker and clearing the backend restores file locking.

Reviewed by Cursor Bugbot for commit b4c097d. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Pluggable lock backend enabling custom lock implementations to replace and restore the default locking behavior.
  • Documentation
    • Clarified default lock selection: Redis-backed locks when available, otherwise file-based locks.
  • Tests
    • Added tests ensuring custom lock backends are honored and that default behavior is restored between tests.

@linear
Copy link
Copy Markdown

linear Bot commented Jun 2, 2026

OSS-19

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a183ea8-8b63-41cb-8e66-6b49958916bb

📥 Commits

Reviewing files that changed from the base of the PR and between a48fb20 and b4c097d.

📒 Files selected for processing (1)
  • lib/crewai-core/src/crewai_core/lock_store.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai-core/src/crewai_core/lock_store.py

📝 Walkthrough

Walkthrough

This PR adds a runtime-pluggable lock backend to lock_store: a LockBackend type and set_lock_backend() to install or clear a custom context-manager backend. lock() now dispatches to the custom backend when set; otherwise it uses the existing Redis-or-file logic. Tests cover backend override and restoration.

Changes

Pluggable Lock Backend System

Layer / File(s) Summary
Pluggable lock backend contract and configuration
lib/crewai-core/src/crewai_core/lock_store.py
Module docstring documents default Redis-vs-file selection based on REDIS_URL and redis availability. Adds LockBackend type alias, module _backend storage, and set_lock_backend(backend: LockBackend | None) -> None to install or restore the built-in backend.
Custom backend dispatch in lock() function
lib/crewai-core/src/crewai_core/lock_store.py
lock() snapshots the module _backend; if a custom backend is set it calls backend(name, timeout=...) as a context manager to guard the yielded section and returns early; otherwise it falls back to existing Redis/file locking.
Test suite for backend pluggability
lib/crewai/tests/utilities/test_lock_store.py
Test module docstring clarifies scope. Adds an autouse reset_backend fixture to clear custom backend between tests. Adds tests asserting a custom backend is used when set and that set_lock_backend(None) restores the default portalocker-based path.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 A little rabbit did declare,
"Locks that swap without a care!
Redis, files, or your own delight,
Set a backend, lock up tight,
Hop—release—then sleep at night." 🥕🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: making the locking backend overridable through a new pluggable abstraction and set_lock_backend() function.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matcha/overridable-lock-backend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/L label Jun 2, 2026
Comment thread lib/crewai-core/src/crewai_core/lock_store.py Fixed
@github-actions github-actions Bot added size/S and removed size/L labels Jun 4, 2026
mattatcha added 6 commits June 4, 2026 12:24
Allow the centralised lock factory to use a pluggable backend instead of
the hardcoded Redis/file selection. Backends are resolved with precedence
override > CREWAI_LOCK_FACTORY env > built-in default:

- set_lock_backend()/reset_lock_backend() and a scoped lock_backend()
  context manager for programmatic overrides
- CREWAI_LOCK_FACTORY="module:callable" env import-path, resolved lazily
  and cached, with clear errors on malformed or non-callable specs
- LockBackend Protocol documenting the contract (raw name in, context
  manager out; backend owns its namespacing)

Default Redis/file behavior is unchanged when nothing is overridden.
Replace the no-op `...` body with `raise NotImplementedError` to satisfy
the CodeQL ineffectual-statement check while keeping the Protocol
structural-typing only.
Keep the backend overridable via set_lock_backend/reset_lock_backend and
the CREWAI_LOCK_FACTORY env path, but remove the scoped lock_backend()
context manager. It was speculative surface and the only thread-unsafe
piece (racy save/restore of the module global); nothing depends on it.
reset_lock_backend() was just set_lock_backend(None); callers use that
directly. Clearing the override is documented on set_lock_backend.
Reduce the override surface to just set_lock_backend(): lock() uses the
custom backend when one is set, otherwise the unchanged Redis/file default.

Drop the CREWAI_LOCK_FACTORY env import-path, the runtime_checkable
Protocol, the precedence resolver, and the getter — a custom backend is
now any callable(name, *, timeout) -> context manager, registered in
process.
@mattatcha mattatcha force-pushed the matcha/overridable-lock-backend branch from f8f382e to 986dfff Compare June 4, 2026 17:26
@mattatcha mattatcha marked this pull request as ready for review June 4, 2026 17:26
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 986dfff. Configure here.

Comment thread lib/crewai-core/src/crewai_core/lock_store.py
Automatically namespaced to avoid collisions.
timeout: Maximum seconds to wait for the lock before raising.
"""
if _backend is not None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race here

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/crewai-core/src/crewai_core/lock_store.py (1)

37-39: ⚡ Quick win

Consider a Protocol for stricter typing.

The Callable[..., AbstractContextManager[None]] type accepts any arguments, which doesn't enforce the expected backend(name, *, timeout=...) signature documented in the comment. This loses IDE autocomplete and type-checker validation.

🔍 Suggested Protocol-based type definition
-# A backend is called as ``backend(name, timeout=...)`` and returns a context
-# manager that holds the lock while the ``with`` block runs.
-LockBackend = Callable[..., AbstractContextManager[None]]
+# A backend is called as ``backend(name, timeout=...)`` and returns a context
+# manager that holds the lock while the ``with`` block runs.
+from typing import Protocol
+
+class LockBackend(Protocol):
+    """Protocol for custom lock backends."""
+    def __call__(self, name: str, *, timeout: float) -> AbstractContextManager[None]: ...

Note: If you prefer the flexibility of accepting any signature, the current Callable[..., ...] approach is acceptable and you can skip this change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai-core/src/crewai_core/lock_store.py` around lines 37 - 39, The
current LockBackend alias uses Callable[..., AbstractContextManager[None]] which
permits any signature; replace it with a Protocol that enforces the expected
backend(name: str, *, timeout: float | None = ...) ->
AbstractContextManager[None] signature so IDEs and type-checkers can validate
and autocomplete; define a LockBackendProtocol (or similar) implementing
__call__(self, name: str, *, timeout: float | None = ...) ->
AbstractContextManager[None] and update the LockBackend type annotation to
reference that Protocol instead of Callable[..., ...].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai-core/src/crewai_core/lock_store.py`:
- Around line 45-51: Update the docstring of set_lock_backend to state the
intended usage and thread-safety expectations: explain that set_lock_backend
replaces the global _backend for the whole process and is intended as a one-time
initialization (call before spawning threads or concurrent operations), that
changing _backend at runtime is not synchronized and may result in some threads
using the old backend while others use the new one, and that if dynamic runtime
switching is required callers should add their own synchronization (or we could
switch to protecting _backend with a threading.Lock). Refer to set_lock_backend
and the global _backend in the docstring so readers know exactly which symbol is
affected.

---

Nitpick comments:
In `@lib/crewai-core/src/crewai_core/lock_store.py`:
- Around line 37-39: The current LockBackend alias uses Callable[...,
AbstractContextManager[None]] which permits any signature; replace it with a
Protocol that enforces the expected backend(name: str, *, timeout: float | None
= ...) -> AbstractContextManager[None] signature so IDEs and type-checkers can
validate and autocomplete; define a LockBackendProtocol (or similar)
implementing __call__(self, name: str, *, timeout: float | None = ...) ->
AbstractContextManager[None] and update the LockBackend type annotation to
reference that Protocol instead of Callable[..., ...].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9eaa577-4744-46d4-81a4-206198c3c3c9

📥 Commits

Reviewing files that changed from the base of the PR and between aed6923 and 986dfff.

📒 Files selected for processing (2)
  • lib/crewai-core/src/crewai_core/lock_store.py
  • lib/crewai/tests/utilities/test_lock_store.py

Comment thread lib/crewai-core/src/crewai_core/lock_store.py
mattatcha added 2 commits June 4, 2026 12:33
Read the module-global backend once into a local before the None check
and the call, so a concurrent set_lock_backend(None) cannot make lock()
invoke None.
The default namespaces the lock name; custom backends receive it
verbatim. Correct the lock() docstring which implied namespacing always
happens.
@mattatcha mattatcha requested a review from greysonlalonde June 4, 2026 18:02
@mattatcha mattatcha merged commit f3a15a4 into main Jun 4, 2026
56 of 57 checks passed
@mattatcha mattatcha deleted the matcha/overridable-lock-backend branch June 4, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants