FEAT: Added Redis Caching
This commit is contained in:
@@ -23,10 +23,12 @@ catalogue search — see below) and the Swagger UI. Migration `0001_init` still
|
||||
creates nothing (it predates the domain and exists only because
|
||||
`internal/db/migrate.go` embeds `migrations/*.sql`, which will not compile
|
||||
against an empty directory); `0002_create_videos_table` is where the schema
|
||||
actually starts, and `0003`–`0005` add what the search needs. It now has an `internal/services` (SQS only) and an
|
||||
actually starts, and `0003`–`0005` add what the search needs. It now has an `internal/services` (SQS and Redis) and an
|
||||
`internal/consumers`, and a task role scoped to that one queue — receive,
|
||||
delete, get-attributes, and nothing else. It cannot publish back onto the
|
||||
topic: it is a subscriber, not a participant.
|
||||
topic: it is a subscriber, not a participant. The Redis client needs nothing
|
||||
from that role: ElastiCache is reached over the Redis protocol on the private
|
||||
network, so the security group is what grants access, not IAM.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -49,7 +51,7 @@ never had a v1.
|
||||
|
||||
```bash
|
||||
cd discovery
|
||||
go run . # serves on :8080 (requires DB_* plus AWS_REGION/CATALOGUE_EVENTS_QUEUE_URL — no S3/MediaConvert)
|
||||
go run . # serves on :8080 (requires DB_* plus AWS_REGION/CATALOGUE_EVENTS_QUEUE_URL/REDIS_ADDR — no S3/MediaConvert)
|
||||
go run . migrate # applies pending DB migrations, then exits (no HTTP server)
|
||||
go build ./...
|
||||
go vet ./...
|
||||
@@ -58,7 +60,8 @@ go vet ./...
|
||||
Its OpenAPI spec is generated into `discovery/docs` by the same `swag init`
|
||||
invocation as `cms`, run from `discovery/`. In `docker-compose.yml` it is a
|
||||
service of its own on `127.0.0.1:8080`, wired to the `discovery` database and
|
||||
role LocalStack provisions, plus the AWS_* vars its consumer needs — and, like
|
||||
role LocalStack provisions, plus the AWS_* vars its consumer needs and
|
||||
`REDIS_ADDR` pointing at the `redis` container — and, like
|
||||
`cms`, started in server mode only, so a freshly created local database needs
|
||||
`docker exec discovery ./discovery migrate` once. Until that runs, the consumer
|
||||
logs `relation "videos" does not exist` per announcement and leaves them on the
|
||||
@@ -124,6 +127,41 @@ browse path (no title term) is a true index seek — that is what
|
||||
`videos_recent_idx` is for, and it takes a 200k-row browse from ~15 ms to
|
||||
~0.03 ms.
|
||||
|
||||
#### Search cache (Redis / ElastiCache)
|
||||
|
||||
Because of that caveat, the search reads through a Redis cache before it
|
||||
touches Postgres — `handlers.cachedSearch`/`cacheSearch` around the repository
|
||||
call, storing the `api.SearchResults` a page serialises to. `REDIS_ADDR` points
|
||||
at the ElastiCache node `infrastructure/main.go` provisions (a `redis`
|
||||
container under compose). Locally it takes a repeated ranked search from
|
||||
~6.8 ms to ~0.5 ms.
|
||||
|
||||
- **The key is `discovery:search:v1:` + a canonical JSON encoding of
|
||||
`{t: title, c: categories, l: limit, p: cursor}`.** The cursor *is* the page
|
||||
identity — keyset paging has no page number to key on. `limit` is in the key
|
||||
because the same title/categories/cursor at a different limit is a different
|
||||
set of rows. JSON rather than pasted-together separators so a category name
|
||||
containing `,` or `|` cannot be mistaken for a boundary. The title is
|
||||
lowercased (`tsQueryFor` folds case anyway) but the **categories are not**:
|
||||
`categories && $2` compares them verbatim, so `News` and `news` really are
|
||||
different searches. Categories are sorted and deduped — `&&` means "any of
|
||||
these", so order never changed the answer — and never nil, so `"categories":
|
||||
[]` and an omitted member share an entry.
|
||||
- **TTL is 60 s and nothing invalidates on write.** The consumer writes rows
|
||||
continuously and would have to know which cached pages a new title belongs
|
||||
on — for a ranked search, every page it outranks. Expiry is cheaper and
|
||||
bounds staleness to roughly the announcement's own delivery lag.
|
||||
- **Only 200s are cached**, so a malformed cursor still reaches the repository
|
||||
and is still a 400.
|
||||
- **Every cache failure is a miss.** A Get/Set error is logged and the search
|
||||
is answered from Postgres; the service also *starts* with an unreachable
|
||||
cache, warning rather than panicking as the DB and SQS assertions do — a slow
|
||||
read side beats no read side. Each call is bounded by
|
||||
`services.cacheOperationTimeout` (100 ms, retries off), because an unbounded
|
||||
cache miss on a dead node costs the dial timeout *plus* the query it was
|
||||
avoiding. A total cache outage adds ~200 ms per search (a failed read and a
|
||||
failed write), not seconds.
|
||||
|
||||
`cms`, `discovery` and `infrastructure` hold no test files of their own. The
|
||||
only tests in the repo are the black-box BDD scenarios in `tests/` — see below.
|
||||
|
||||
@@ -353,6 +391,18 @@ on boot if any required var is empty.
|
||||
backups — intentionally minimal). Each app (`cms`, `discovery`) gets its
|
||||
own login role and same-named database via the `postgresql` provider
|
||||
(`newServiceDatabase`), so services never share DB credentials.
|
||||
- **ElastiCache**: one `cache.t4g.micro` Redis node (`search-cache`, engine
|
||||
7.1, single-AZ, no replica, no snapshots) that `discovery` answers repeated
|
||||
catalogue searches from. Its contents are derivable from Postgres by
|
||||
definition, so there is nothing to back up. Its security group admits 6379
|
||||
from `ecs-service-sg` **only** — narrower than the database's, which also
|
||||
admits the deployer's IP for the `postgresql` provider; there is nothing to
|
||||
administer here from a laptop. The endpoint is the single node's address
|
||||
(`CacheNodes[0]`, not `ConfigurationEndpoint` — that is a Memcached thing),
|
||||
exported as `searchCacheAddress` and injected as `REDIS_ADDR`. Skipped under
|
||||
LocalStack, where docker-compose runs a plain `redis:7-alpine` container
|
||||
instead: ElastiCache picks its own endpoint, and compose needs a literal
|
||||
`REDIS_ADDR` before anything is provisioned.
|
||||
- **ECS Fargate**: one cluster (`app-cluster`), one ALB *per service* (each
|
||||
gets its own DNS name rather than sharing a load balancer on different
|
||||
ports). `deployFargateService(...)` is the shared helper building a
|
||||
@@ -402,7 +452,9 @@ on boot if any required var is empty.
|
||||
- `discovery-task-role` — the `discovery` container's own AWS identity, and
|
||||
its only one: receive/delete/get-attributes on
|
||||
`discovery-catalogue-events`. No S3, no MediaConvert, and no `sns:Publish`
|
||||
— it consumes the catalogue, it does not add to it.
|
||||
— it consumes the catalogue, it does not add to it. The search cache is
|
||||
absent from it on purpose: ElastiCache is reached over the Redis protocol
|
||||
inside the VPC, so `search-cache-sg` is the grant, not IAM.
|
||||
- `mediaconvert-service-role` — trusted by `mediaconvert.amazonaws.com`,
|
||||
not by ECS; the role MediaConvert itself assumes (passed as
|
||||
`CreateJobInput.Role`) to read `raw-uploads-bucket` and write
|
||||
|
||||
Reference in New Issue
Block a user