Skip to main content

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:

  1. Generic Backends: Classes like BaseBackend inherit from Backend and SyncBackendMixin to provide a standard synchronous interface.
  2. Key-Value Backends: Classes like BaseKeyValueStoreBackend provide 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 to celery-task-meta-
  • group_keyprefix: Defaults to celery-taskset-meta-
  • chord_keyprefix: Defaults to chord-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.