Nobody has started this yet — be first.
Business impact
This whole project's stated purpose is doing the Flask app-factory pattern correctly, not the common broken shortcut -- db = SQLAlchemy() at module scope, bound per-app via init_app(), specifically so multiple independent app instances (a test app, a CLI-only app, a worker) never share state. redis_client was built to look like it follows the same pattern, but it does not: it's a single object holding a single _client attribute, and creating a second Flask app overwrites that attribute out from under the first app. In production this bites anywhere more than one app instance can be alive in the same process at once -- multi-tenant test runners, a management script that spins up create_app() internally while a request is mid-flight in the same process, or this project's own test suite under certain parallel test configurations. It is the exact bug class the app-factory pattern is supposed to make impossible.
Problem
RedisExtension.init sets self._client = None, and init_app(app) reassigns self._client on the same shared RedisExtension instance every time it is called -- there is nothing keyed by which app is asking, unlike db, whose per-app scoping is handled correctly by Flask-SQLAlchemy itself.
Current behavior
Creating a second app via create_app() and calling init_app() reassigns the single module- level redis_client's _client attribute, so the first app's subsequent redis_client calls silently talk to the second app's Redis connection instead of its own.
Expected behavior
Each app instance's Redis calls should always talk to the Redis connection that app was configured with, regardless of how many other app instances get created afterward in the same process.
Steps to reproduce
Why this matters
This doesn't require any concurrency or threading to reproduce -- it's a pure object-identity bug, visible from purely sequential code, which is part of what makes it dangerous: it is easy to believe "we are not running multiple apps concurrently, so this is fine" and miss that the bug fires the instant a second app object exists at all, active or not. Compare this to how db/Migrate are scoped: Flask-SQLAlchemy's db.session is a scoped_session keyed by Flask's own app/request context, so two db-using apps in the same process never see each other's sessions. redis_client needs the equivalent: state keyed per-app, not one shared mutable attribute.
Suggested approach
Look at how Flask itself recommends storing per-app extension state -- app.extensions (a dict Flask provides on every app instance specifically for this) is the idiomatic place. Store each app's Redis client keyed by the app object itself (or on app.extensions["redis_client"]), and have calls go through current_app to find the right one, rather than through a single shared self._client attribute on the RedisExtension instance.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_17_redis_shared_state.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/redis-extension-shared-stateFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/redis-extension-shared-stateSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker-entrypoint.shcurl -X POST http://localhost:5000/api/urls \
-H "Content-Type: application/json" \
-d '{"target_url": "https://anthropic.com", "custom_slug": "claude"}'
# -> {"slug": "claude", "short_url": "http://localhost:5000/claude", "expires_at": null}
curl -i http://localhost:5000/claude # 302 redirect, records a click
make seed # or: docker compose exec web flask seed-db
If something is already listening on 5432/6379 locally, uncomment POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env to remap the host side — the app always talks to db/redis over the compose network regardless. Run production-style (gunicorn, no dev server, no bind mount) with docker compose -f docker-compose.prod.yml up --build. GET /healthz checks both Postgres and Redis connectivity.
You do not need Docker running at all to work the tickets — the test suite uses TestingConfig (SQLite in-memory via StaticPool) and fakeredis instead of a real Postgres/Redis, so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_01_base62_roundtrip.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
./practice_tickets/run_tickets.sh 05 13 16 # just a few, by number
All 20 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ (via make test) runs the project's own main suite — some ticket bugs (TICKET-05, 07, 08, 09, 14 among others) also break assertions in that suite, since they are real bugs in shared code paths; fixing the ticket fixes those too, not a separate task.
Level 4
Refactor architecture
Improve structure without changing behaviour.