Skip to content

Make RedisException the canonical error contract for all public Redis operations - #338

Open
willronchetti wants to merge 2 commits into
masterfrom
fm/dcicutils-redis-error-contract-4w
Open

Make RedisException the canonical error contract for all public Redis operations#338
willronchetti wants to merge 2 commits into
masterfrom
fm/dcicutils-redis-error-contract-4w

Conversation

@willronchetti

Copy link
Copy Markdown
Member

Problem

Redis driver exceptions leak out of dcicutils' public Redis API. Every RedisBase method,
create_redis_client, and every RedisSessionToken operation except store_session_token
propagated redis.exceptions.* straight to the caller, so a consumer that wanted to survive an
unreachable or failing Redis had to import redis-driver exception types itself.

Verified against the pre-change code with an injected redis.exceptions.ConnectionError:

RedisBase.get           -> LEAKS redis.exceptions.ConnectionError
validate_session_token  -> LEAKS redis.exceptions.ConnectionError
delete_session_token    -> LEAKS redis.exceptions.ConnectionError
from_redis              -> LEAKS redis.exceptions.ConnectionError

Change

dcicutils.redis_utils gains a public translate_redis_exceptions decorator (and the
REDIS_DRIVER_EXCEPTIONS tuple it uses). It is applied to:

  • create_redis_client
  • every public RedisBase method: info, set, get, set_expiration, ttl, delete,
    hget, hgetall, hset, hset_multiple, dbsize
  • every RedisSessionToken operation: from_redis, store_session_token,
    validate_session_token, update_session_token, delete_session_token

Driver failures — redis.exceptions.RedisError and redis.exceptions.RedisClusterException
(which deliberately sits outside the RedisError tree) and their subclasses — now raise
RedisException, with the original attached as __cause__ for callers that want to inspect it.

Programmer errors (TypeError, AttributeError, ValueError, KeyError) are deliberately
not translated, so real defects are not masked. Nesting decorated calls is idempotent: an
already-canonical RedisException passes through unwrapped.

Snovault should consume this public error contract rather than importing Redis-driver
exceptions.
RedisException is importable from both dcicutils.redis_utils and
dcicutils.redis_tools; catching it is sufficient for every operation listed above, and
Snovault's configurable Redis session-mode work should not need redis.exceptions at all.

Behavior preserved

  • All signatures and return values are unchanged.
  • Absence stays distinct from failure: validate_session_token still returns False for a
    missing token and raises only when Redis actually fails; from_redis still returns None
    for a missing record.
  • RedisException() still constructs with no arguments.
  • functools.wraps keeps names and docstrings intact for the static lane.

Behavior change (intentional, called out in CHANGELOG)

store_session_token previously caught any Exception and re-raised a bare
RedisException. It now translates only driver failures, so programmer errors propagate
unchanged instead of being masked. The error log line it emitted is preserved.

Tests

New test/test_redis_error_contract.py (164 tests) injects representative connection, timeout,
response, busy-loading and cluster failures into every affected public operation and asserts
RedisException. It also covers cause chaining, non-translation of programmer errors,
unchanged success paths and lifecycle, the absence-vs-failure distinction, and a guard test
that fails if a new RedisBase public method is added without coverage. The tests are
mock-based and need no running redis-server.

165 passed   # new suite + test_misc.py::test_changelog_consistency

flake8 is clean on dcicutils/redis_utils.py, dcicutils/redis_tools.py, and the new test file.
The existing redisdb-backed suites were not run locally: redis_exec in pyproject.toml points
at /usr/local/bin/redis-server, which is not the homebrew prefix on arm64. That is a
pre-existing environment gap and untouched here; CI runs those.

Versioning

Bumped to 8.20.0 (added public symbols) with a matching CHANGELOG.rst entry, per
test_misc.py::test_changelog_consistency.

…ions

Redis driver exceptions leaked out of dcicutils' public Redis API: every
RedisBase method, create_redis_client, and every RedisSessionToken operation
except store_session_token propagated redis.exceptions.* directly, forcing
consumers to import redis-driver exception types to handle an unreachable or
failing Redis.

Add a translate_redis_exceptions decorator in redis_utils and apply it to
create_redis_client, all public RedisBase methods, and all session token
operations (from_redis, store/validate/update/delete_session_token). Driver
failures - redis.exceptions.RedisError and redis.exceptions.RedisClusterException
and their subclasses - now raise RedisException with the original attached as
__cause__. Programmer errors (TypeError, AttributeError, ...) are deliberately
not translated, which narrows store_session_token's previous bare
`except Exception`. Successful behavior is unchanged, and absence stays distinct
from failure: validate_session_token still returns False for a missing token and
from_redis still returns None for a missing record.

Add test/test_redis_error_contract.py, which injects representative connection,
timeout, response, busy-loading and cluster failures into every affected public
operation. The tests are mock-based and need no running redis-server.

Bump to 8.20.0 for the added public symbols.
…aths

The __all__ added to redis_tools narrowed the module's star-import surface for
no benefit: RedisException is already referenced in store_session_token, so the
import stands on its own. Also add success-path assertions for the RedisBase
methods that had error coverage only, including that set_expiration still
passes gt=True through to the driver.
@coveralls

coveralls commented Aug 7, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31202017610

Coverage increased (+0.1%) to 74.762%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 33 of 33 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 15643
Covered Lines: 11695
Line Coverage: 74.76%
Coverage Strength: 0.75 hits per line

💛 - Coveralls

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.

2 participants