This document describes the current stable version of Celery (5.7). For development docs, go here.

Source code for celery.backends.cache

"""Memcached and in-memory cache result backend."""
from kombu.utils.encoding import bytes_to_str, ensure_bytes
from kombu.utils.objects import cached_property

from celery.exceptions import ImproperlyConfigured
from celery.utils import deprecated
from celery.utils.functional import LRUCache
from celery.utils.log import get_logger

from .base import KeyValueStoreBackend

__all__ = ('CacheBackend',)

logger = get_logger(__name__)

# Memcached reads a longer expiry as an absolute Unix time.
MAX_RELATIVE_EXPIRES = 30 * 24 * 60 * 60

W_CHORD_EXPIRES_TOO_LONG = """\
result_chord_expires is over 30 days, which memcached reads as an absolute \
time, so chord keys will expire at once.\
"""

REQUIRES_BACKEND = """\
The Memcached backend requires the 'pymemcache' library. \
The 'pylibmc' and 'python-memcached' libraries are no longer used.\
"""

UNKNOWN_BACKEND = """\
The cache backend {0!r} is unknown,
Please use one of the following backends instead: {1}\
"""

# Options only supported by pylibmc, dropped before creating the client.
PYLIBMC_ONLY_OPTIONS = ('behaviors', 'binary')

# Defaults applied to the pymemcache client unless set in
# cache_backend_options: wait for the server reply so that failed
# writes raise, and don't block forever on an unresponsive server.
DEFAULT_CLIENT_OPTIONS = {
    'default_noreply': False,
    'connect_timeout': 5.0,
    'timeout': 5.0,
}

# Global shared in-memory cache for in-memory cache client
# This is to share cache between threads
_DUMMY_CLIENT_CACHE = LRUCache(limit=5000)


def get_memcache_client():
    """Get pymemcache client factory."""
    try:
        from pymemcache.client.base import Client
        from pymemcache.client.hash import HashClient
        from pymemcache.client.retrying import RetryingClient
    except ImportError:
        raise ImproperlyConfigured(REQUIRES_BACKEND)

    def ClientFactory(servers=None, **kwargs):
        """Create a pymemcache client with optional retry support.

        Args:
            servers: List of server addresses
            **kwargs: Additional options including:
                - retry_attempts: Number of retry attempts (enables RetryingClient)
                - retry_delay: Delay between retries in seconds
                - retry_for: List of exceptions to retry for
                - do_not_retry_for: List of exceptions to not retry for
                - behaviors, binary: Ignored for backward compatibility
                  with pylibmc
        """
        # Remove pylibmc-specific options for backward compatibility
        for option in PYLIBMC_ONLY_OPTIONS:
            if option in kwargs:
                kwargs.pop(option)
                deprecated.warn(
                    description=f'The {option!r} cache backend option',
                    removal='6.0',
                    alternative='It is only supported by pylibmc and is ignored.',
                    stacklevel=3,
                )
        kwargs = {**DEFAULT_CLIENT_OPTIONS, **kwargs}

        # Extract retry-related options
        retry_attempts = kwargs.pop('retry_attempts', None)
        retry_delay = kwargs.pop('retry_delay', None)
        retry_for = kwargs.pop('retry_for', None)
        do_not_retry_for = kwargs.pop('do_not_retry_for', None)

        # Normalize/validate servers (CacheBackend passes a list, but guard other callers)
        if servers is None:
            servers = []
        elif isinstance(servers, str):
            servers = [servers]
        elif (
            isinstance(servers, tuple)
            and len(servers) == 2
            and isinstance(servers[0], str)
            and isinstance(servers[1], int)
        ):
            # Allow passing a single server address as (host, port)
            servers = [servers]
        elif not isinstance(servers, (list, tuple)):
            raise ImproperlyConfigured(
                f"Memcache servers must be a sequence of server addresses, got {type(servers)}: {servers!r}"
            )

        # Filter out empty entries (e.g. memcache:// or trailing ';')
        servers = [s for s in servers if s]
        if not servers:
            raise ImproperlyConfigured("No memcache servers provided")

        # Use HashClient for multiple servers, base Client for single server
        if len(servers) > 1:
            base_client = HashClient(servers, **kwargs)
        else:
            server = servers[0]
            if isinstance(server, (str, tuple)):
                base_client = Client(server, **kwargs)
            else:
                raise ImproperlyConfigured(
                    f"Memcache server address must be a string or tuple, got {type(server)}: {server!r}"
                )

        # Wrap with RetryingClient if retry options are specified
        if retry_attempts is not None:
            retry_kwargs = {'attempts': retry_attempts}
            if retry_delay is not None:
                retry_kwargs['retry_delay'] = retry_delay
            if retry_for is not None:
                retry_kwargs['retry_for'] = retry_for
            if do_not_retry_for is not None:
                retry_kwargs['do_not_retry_for'] = do_not_retry_for
            return RetryingClient(base_client, **retry_kwargs)

        return base_client

    return ClientFactory, bytes_to_str


def import_best_memcache():
    """Deprecated: use :func:`get_memcache_client` instead."""
    deprecated.warn(description='import_best_memcache()', removal='6.0',
                    alternative='Use get_memcache_client() instead.')
    try:
        import pymemcache
    except ImportError:
        raise ImproperlyConfigured(REQUIRES_BACKEND)
    return False, pymemcache, bytes_to_str


def get_best_memcache(*args, **kwargs):
    """Deprecated: use :func:`get_memcache_client` instead."""
    deprecated.warn(description='get_best_memcache()', removal='6.0',
                    alternative='Use get_memcache_client() instead.')
    return get_memcache_client()


class DummyClient:

    def __init__(self, *args, **kwargs):
        self.cache = _DUMMY_CLIENT_CACHE

    def get(self, key, *args, **kwargs):
        return self.cache.get(key)

    def get_multi(self, keys):
        cache = self.cache
        return {k: cache[k] for k in keys if k in cache}

    def set(self, key, value, *args, **kwargs):
        self.cache[key] = value

    def delete(self, key, *args, **kwargs):
        self.cache.pop(key, None)

    def incr(self, key, delta=1):
        return self.cache.incr(key, delta)

    def touch(self, key, expire):
        pass


backends = {
    'memcache': get_memcache_client,
    'memcached': get_memcache_client,
    'pylibmc': get_memcache_client,  # Backward compatibility
    'pymemcache': get_memcache_client,
    'memory': lambda: (DummyClient, ensure_bytes),
}


[docs] class CacheBackend(KeyValueStoreBackend): """Cache result backend.""" servers = None supports_autoexpire = True supports_native_join = True implements_incr = True def __init__(self, app, expires=None, backend=None, options=None, url=None, **kwargs): options = {} if not options else options super().__init__(app, **kwargs) self.url = url self.options = dict(self.app.conf.cache_backend_options, **options) self.backend = url or backend or self.app.conf.cache_backend if self.backend: self.backend, _, servers = self.backend.partition('://') self.servers = servers.rstrip('/').split(';') self.expires = self.prepare_expires(expires, type=int) try: self.Client, self.key_t = backends[self.backend]() except KeyError: raise ImproperlyConfigured(UNKNOWN_BACKEND.format( self.backend, ', '.join(backends))) self._encode_prefixes() # rencode the keyprefixes if (self._chord_expires is not None and self.chord_expires > MAX_RELATIVE_EXPIRES): logger.warning(W_CHORD_EXPIRES_TOO_LONG)
[docs] def get(self, key): return self.client.get(key)
[docs] def mget(self, keys): return self.client.get_multi(keys)
[docs] def set(self, key, value): return self.client.set(key, value, self.expires or 0)
[docs] def delete(self, key): return self.client.delete(key)
def _apply_chord_incr(self, header_result_args, body, **kwargs): chord_key = self.get_key_for_chord(header_result_args[0]) self.client.set(chord_key, 0, self.chord_expires or 0) return super()._apply_chord_incr( header_result_args, body, **kwargs)
[docs] def incr(self, key): return self.client.incr(key, 1)
[docs] def expire(self, key, value): return self.client.touch(key, value or 0)
@cached_property def client(self): return self.Client(self.servers, **self.options) def __reduce__(self, args=(), kwargs=None): kwargs = {} if not kwargs else kwargs servers = ';'.join(self.servers) backend = f'{self.backend}://{servers}/' kwargs.update( {'backend': backend, 'expires': self.expires, 'options': self.options}) return super().__reduce__(args, kwargs)
[docs] def as_uri(self, *args, **kwargs): """Return the backend as an URI. This properly handles the case of multiple servers. """ servers = ';'.join(self.servers) return f'{self.backend}://{servers}/'