Skip to content

feat: raise BackendUnavailableError when the server cannot be reached (#260) - #464

Merged
allen0099 merged 1 commit into
masterfrom
claude/charming-gauss-dyvsj7
Oct 14, 2026
Merged

allen0099 merged 1 commit into
masterfrom
claude/charming-gauss-dyvsj7

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Fixes #260

Summary

On a transport failure, each network backend raised its own client's exceptions. To handle "the cache is down" around CacheManager, CacheLock or a backend call, you had to know which backend was configured and import redis or pymemcache exceptions.

What changes

  • New BackendUnavailableError(CacheXError), exported from fastapi_cachex.
  • The Redis and Memcached backends wrap their I/O methods (get, set, delete, delete_many, the atomic primitives, increment, clear*, get_all_keys, get_cache_data) with backends/_unavailable.py. A transport failure is raised again as BackendUnavailableError, chained to the original. Transport failures are:
    • Redis: redis-py ConnectionError (and its subclasses), TimeoutError, and a raw OSError.
    • Memcached: socket errors (OSError: refused, reset, timed out), MemcacheUnexpectedCloseError, and HashClient's "All servers seem to be down" while a server is out of rotation.
  • Other errors pass through unchanged: bad input, MemcacheClientError such as an item too large (Handle Memcached "object too large" consistently outside @cache #261), and the existing CacheXError cases. The memory backend never raises it.

Not a breaking change. This answers the issue's first open question. The class raised is built once per original type and inherits from both BackendUnavailableError and that type, so:

  • A refused Memcached connection is also a ConnectionRefusedError and OSError, with the same errno. A Redis one is also a redis.exceptions.ConnectionError.
  • Existing except redis.exceptions.ConnectionError, except OSError and except MemcacheError clauses keep catching it. The existing suite passes unchanged, including the tests that expect ConnectionRefusedError.
  • The runtime classes are named BackendUnavailableError in tracebacks and pickle correctly through __reduce__.

Left as is

Docs (English and zh-TW): a new "When the server is unavailable" section in BACKENDS.md with an example, and links from the Memcached and fail-fast sections. Changelog fragment: 260.added.md.

As in #463, the docs say "before 0.5.1", assuming this ships in the next patch release.

Checklist

  • The issue above is assigned to me (maintainer's own issue)

  • Tests under tests/ cover the change. tests/backends/test_unavailable.py needs no server; it points at a port nothing listens on. It covers:

    • a Redis refusal, and a Memcached refusal followed by the server out of rotation (exact class, both bases, __cause__, errno)
    • CacheManager propagating the error
    • other errors passing through unchanged
    • a nested call translated only once
    • one runtime class per type
    • a pickle round trip

    Disabling the translation fails the three backend tests.

  • A changelog fragment: changelog.d/260.added.md

  • uv run pre-commit run --all-files and uv run pytest --cov pass:

    • pytest: 1251 passed, 192 skipped, 93.10% coverage
    • with a live Redis: 1380 passed (Memcached was not available locally, so its live suite is left to CI)
    • mypy strict, mypy tests, mypy scripts and both zensical build --strict runs pass

Generated by Claude Code

…#260)

Each network backend raised its own client's exceptions on a transport
failure, so handling "the cache is down" meant knowing which backend is
configured and importing redis or pymemcache exceptions.

The Redis and Memcached backends' methods now re-raise a refused or
reset connection, a timeout, and pymemcache's "All servers seem to be
down" as BackendUnavailableError (a CacheXError), chained to the
original. The class raised is built per original type and inherits from
both, so it is also a redis.exceptions.ConnectionError, an OSError and
so on: existing except clauses keep catching it. It pickles. Other
errors (bad input, an item too large) pass through unchanged, and
@cache still fails open on every backend error.

Closes #260
@allen0099
allen0099 merged commit 687f69e into master Oct 14, 2026
14 checks passed
@allen0099
allen0099 deleted the claude/charming-gauss-dyvsj7 branch October 14, 2026 08:29
@allen0099 allen0099 mentioned this pull request Oct 14, 2026
18 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Raise a backend-independent BackendUnavailableError for transport failures

1 participant