Nobody has started this yet — be first.
Business impact
TENANT_CACHE_TTL_SECONDS exists to bound how long a resolved tenant/settings lookup can be stale before Redis forgets it and the next request falls back to Postgres for the truth. Support and on-call treat that number as a real upper bound -- "worst case, a change takes effect within ~30 seconds." Right now every cache write actually sets a Redis expiry about 1000x longer than configured (default 30 becomes 30000 seconds -- roughly 8.3 hours). Nothing currently depends on that bound being tight, since settings writes explicitly invalidate the cache -- but the TTL is the only backstop if that invalidation is ever missed, skipped by a crash between commit and cache-clear, or bypassed by a future direct-database operator action. If that ever happens, whoever is debugging "why is this tenant showing stale data" will be told by the code and the docs that the answer is "wait 30 seconds" -- and it will actually be most of a business day.
Problem
set_cached_tenant() sets a Redis TTL roughly 1000x longer than TENANT_CACHE_TTL_SECONDS actually specifies.
Current behavior
set_cached_tenant() reads TENANT_CACHE_TTL_SECONDS (a count of seconds -- the name says so, and every other place in the codebase treats it that way) and passes it to client.set(..., ex=...), which also expects seconds -- but the value has already been multiplied by 1000 somewhere along the way, as if converting to milliseconds for a client that wanted px= instead of ex=. Redis raises no error; it simply accepts the inflated number and expires the key 1000x later than intended.
Expected behavior
A tenant cached with TENANT_CACHE_TTL_SECONDS=N gets a Redis TTL of approximately N seconds, not N * 1000 -- with no other change to the key format, invalidation path, or cache-miss behavior.
Steps to reproduce
Why this matters
This is the classic unit-mismatch bug: two units (seconds, milliseconds) that are both plausible, both integers, and both "just a number" until you look at what the receiving API actually expects. redis-py's .set(..., ex=...) takes seconds; there is a separate px= kwarg for milliseconds. Nothing here raises -- Redis happily accepts an EX of 30000 and just expires the key in 30000 seconds. The bug is silent by construction.
Suggested approach
Look at exactly one line inside set_cached_tenant() in app/tenancy/cache.py. Compare the unit TENANT_CACHE_TTL_SECONDS is named and documented as against what is actually passed to client.set(..., ex=...).
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_01_cache_ttl_unit_bug.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/cache-ttl-unit-bugFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/cache-ttl-unit-bugSubmit 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 inacmeglobexmake seed
*.localhost subdomains resolve to 127.0.0.1 out of the box in curl and every modern browser -- no /etc/hosts editing needed:
curl -s -X POST http://acme.localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
--cookie-jar /tmp/acme.cookies --cookie /tmp/acme.cookies \
-d '{"email": "owner@acme.test", "password": "owner-acme"}'
curl -s http://acme.localhost:5000/api/tenant/settings --cookie /tmp/acme.cookies
For tooling that cannot fake subdomains (some HTTP clients, CI), send X-Tenant-Slug instead -- honored only when TRUST_TENANT_HEADER=true (the Docker dev setup enables it; production does not):
curl -s http://localhost:5000/api/tenant/settings -H "X-Tenant-Slug: acme"
You do not strictly need Docker to work the tickets, but -- unlike a project that can fake its backing services -- this one genuinely needs a real Postgres and Redis, because the whole point is proving isolation against the real database, not a mock. Without Docker: cp .env.example .env, point DATABASE_URL/ADMIN_DATABASE_URL/TEST_DATABASE_URL/TEST_ADMIN_DATABASE_URL/REDIS_URL at a local Postgres/Redis (docker/init-db.sql documents the exact roles/databases the app expects -- notably the saas_admin/BYPASSRLS role migrations need and the plain saas role every request uses), then:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/flask db upgrade
.venv/bin/pytest practice_tickets/tests/test_ticket_07_webhook_signature_raw_body.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
./practice_tickets/run_tickets.sh 07 12 13 # just a few, by number
All 15 tests fail at the start; as you fix each ticket in app/ (or, for TICKET-12, in migrations/versions/0002_row_level_security.py, plus re-applying it), its test -- and only its test -- should flip to passing. make test runs the project's own main suite; several of its tests (RBAC, tenant-spoofing, and billing-webhook tests among them) fail at the start for the same underlying reasons as specific tickets -- fixing those tickets fixes those failures too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.