Architecture of Result Backends
The result backend architecture in this codebase is designed to decouple task execution from result persistence, allowing for a wide variety of storage engines while maintaining a consistent interface for result retrieval. The hierarchy is built on a foundation of base classes that handle serialization, state management, and synchronization logic.
The Core Hierarchy
At the root of the architecture is the Backend class found in celery/backends/base.py. This class defines the fundamental lifecycle of a result, including methods for marking tasks as started, done, or failed. It manages a local LRU cache (self._cache) to optimize repeated lookups and handles the complexities of result serialization using the configured result_serializer.
The architecture branches into two primary paths:
- Generic Backends: Classes like
BaseBackendinherit fromBackendandSyncBackendMixinto provide a standard synchronous interface. - Key-Value Backends: Classes like
BaseKeyValueStoreBackendprovide specialized logic for stores that operate on key-value semantics (e.g., Redis, Memcached, S3).
Synchronous Polling and Mixins
The SyncBackendMixin is a critical component that provides the blocking logic used when a client calls .get() on a result. It implements wait_for and get_many, which use a polling loop to check the backend for a ready state.
# Example of the polling logic in SyncBackendMixin
def wait_for(self, task_id, timeout=None, interval=0.5, no_ack=True, on_interval=None):
self._ensure_not_eager()
time_elapsed = 0.0
while 1:
meta = self.get_task_meta(task_id)
if meta['status'] in states.READY_STATES:
return meta
if on_interval:
on_interval()
time.sleep(interval)
time_elapsed += interval
if timeout and time_elapsed >= timeout:
raise TimeoutError('The operation timed out.')
Key-Value Store Abstractions
The BaseKeyValueStoreBackend simplifies the implementation of new storage engines by standardizing how keys are generated and how results are retrieved in bulk. It defines specific prefixes for different types of metadata:
task_keyprefix: Defaults tocelery-task-meta-group_keyprefix: Defaults tocelery-taskset-meta-chord_keyprefix: Defaults tochord-unlock-
These prefixes can be further customized using a global_keyprefix via result_backend_transport_options. The class also provides a template for get_many using mget (if supported by the underlying store), which significantly reduces network round-trips when joining multiple results.
Implementation Example: Minimal K/V Backend
A concrete implementation of a key-value backend typically only needs to implement get, set, and delete. The following example (adapted from unit tests) demonstrates a minimal implementation:
class SimpleKVBackend(KeyValueStoreBackend):
def __init__(self, app, *args, **kwargs):
self.db = {}
super().__init__(app, *args, **kwargs)
def get(self, key):
return self.db.get(key)
def set(self, key, value):
self.db[key] = value
def delete(self, key):
self.db.pop(key, None)
Reliability and Security Design
The backend architecture incorporates several design choices to ensure data integrity and system security.
Operation Retries
Backend operations are wrapped in _ensure_retryable, which uses an exponential backoff policy. This is controlled by settings like result_backend_always_retry and result_backend_max_retries. This mechanism ensures that transient network issues do not cause task results to be lost.
Result Deduplication
In store_result, the backend checks if a task is already in a SUCCESS state before applying an update. This prevents a race condition where a "lost" worker (due to a network partition) might overwrite a newer, successful result with an older state.
Secure Exception Deserialization
When a task fails, the exception is serialized and stored. To prevent arbitrary code execution during deserialization, exception_to_python performs a strict security check:
# Security check in Backend.exception_to_python
if not isinstance(cls, type) or not issubclass(cls, BaseException):
fake_exc_type = exc_type if exc_module is None else f'{exc_module}.{exc_type}'
raise SecurityError(
f"Expected an exception class, got {fake_exc_type} with payload {exc_msg}")
This ensures that only valid exception classes can be reconstructed from the backend data.
The Null Implementation
When no result backend is configured, the system uses DisabledBackend. This class follows the Null Object pattern, providing the same interface as other backends but raising a NotImplementedError with a descriptive message if any storage or retrieval operation is attempted. This prevents silent failures in applications that expect result persistence.