Skip to content

Feat/acronym search 03 bulk upload endpoint - #6

Open
borkarsaish65 wants to merge 7 commits into
ELEVATE-Project:release-2.1.0from
borkarsaish65:feat/acronym-search-03-bulk-upload-endpoint
Open

Feat/acronym search 03 bulk upload endpoint#6
borkarsaish65 wants to merge 7 commits into
ELEVATE-Project:release-2.1.0from
borkarsaish65:feat/acronym-search-03-bulk-upload-endpoint

Conversation

@borkarsaish65

Copy link
Copy Markdown

No description provided.

Adds Alembic with a no-op baseline revision for the pre-existing
unmanaged translations table, plus a migration that creates
acronym_mapping (id PK, expansions as JSONB array, description,
composite index on acronym+is_active) and seeds it from
data/acronyms.csv, now the real 599-acronym dataset.
get_expansion() checks Redis first, falls back to Postgres on miss,
writes through to cache; warm_cache() pre-populates every active
acronym at startup. Redis pieces (cache_client.py, redis_client.py)
stay acronym-agnostic; only acronym_service.py knows about acronyms,
JSON-encoding the expansions array for the string-only cache layer.
get_expansion() only handled a cache miss (Redis says "not found"),
not a cache error (Redis unreachable/timing out) — the latter raised
straight out of the function before Postgres was ever tried, so a
Redis outage broke every acronym-detected search instead of just
degrading it. Now both the cache read and the write-through are
guarded: any Redis error logs a warning and falls back to Postgres,
matching the fallback behavior main.py's startup path already
assumes exists.
POST /api/acronyms/bulk, gated behind a shared-secret X-Internal-Token
header (INTERNAL_API_TOKEN env var, no default). Upserts a batch of
{acronym, expansion, is_active} rows in a single transaction — a row
that's blank after trimming, or an in-batch duplicate acronym (Postgres
can't affect the same row twice in one ON CONFLICT statement), is
collected as a per-row error instead of failing the whole batch, with
last-value-wins for duplicates. Invalidates the Redis cache for every
acronym actually upserted so the next lookup re-fetches the new value.

Third slice of the acronym-search feature (see ACRONYM_SEARCH_PLAN.md).

# Conflicts:
#	app/services/acronym_service.py
Was blank, which read as easy to miss when setting up a new
environment. Placeholder, not a real token — each env still needs its
own generated value per the comment above it.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e4ddd75c-2974-4100-b884-96e4119878f7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Three issues found in review:
- invalidate_cache() (the bulk-upload endpoint's fallback when the
  post-commit cache refresh fails) had no error handling of its own,
  so the same Redis outage that triggered the fallback made the
  fallback raise too — turning a fully successful, already-committed
  upload into an opaque 500 for the caller. Now swallows and logs.
- Bulk upload decoded the file as strict utf-8, which leaves a
  spreadsheet-exported BOM attached to the first header name
  ("acronym"), silently failing every row's acronym validation.
  Switched to utf-8-sig, which strips a BOM if present and is
  otherwise identical to utf-8.
- The shared Redis connection never forwarded REDIS_PASSWORD, so any
  environment with a password-protected Redis would have every
  acronym cache operation fail with AuthenticationError. Also
  normalizes a blank password to None rather than "", since redis-py
  treats an empty string as a real credential to AUTH with.

Adds tests/test_acronym_bulk_upload.py (BOM handling, cache-outage
500 regression) and commits tests/test_acronym_service.py, which was
still sitting uncommitted from PR2's review.
…loop

Two more issues found in review:
- bulk_upsert() only checked non-empty acronym/expansions before the
  single multi-row INSERT. An acronym longer than the acronym_mapping
  column's 32-char limit reached Postgres, raised
  StringDataRightTruncation, and rolled back the WHOLE batch —
  including every otherwise-valid row — contradicting the endpoint's
  own promise that one bad row doesn't fail the batch. Now validated
  per-row (length read off the model, not hardcoded) before the
  insert, alongside the existing non-empty check.
- bulk_upload_acronyms is async def, but bulk_upsert() and warm_cache()
  are both fully synchronous, blocking I/O (sync SQLAlchemy Session,
  sync Redis client) with no actual awaits — warm_cache() alone does
  up to ~600 sequential blocking Redis writes. Called directly, this
  stalls the single-threaded event loop for its full duration, so
  every other in-flight request (search, health checks) queues up
  behind one acronym upload. warm_cache() converted from async def to
  a plain (honestly synchronous) function; both it and bulk_upsert()
  are now run via starlette.concurrency.run_in_threadpool from the
  endpoint and from app/main.py's startup lifespan. Verified live: 3
  concurrent health checks each returned in ~20-30ms while a real
  upload (with its full cache refresh) was in flight, with no
  stacking delay.

Tests added for both; two existing startup-lifespan tests updated
from AsyncMock to Mock since warm_cache is no longer a coroutine.
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.

1 participant