mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
4.4 KiB
4.4 KiB
phase, title, status, priority, effort, dependencies
| phase | title | status | priority | effort | dependencies | |
|---|---|---|---|---|---|---|
| 4 | Firestore KVStore + per-module prefixing | pending | P1 | 4h |
|
Phase 04: Firestore KVStore + per-module prefixing
Overview
Implement FirestoreKVStore against the KVStore interface from Phase 03. One Firestore collection per module (<module>), each KV entry one document. Test against the local Firestore emulator. Provide an in-memory fake for module-level unit tests so they don't need the emulator.
Requirements
- Functional:
Get/GetJSON/Put/PutJSON/Delete/Listwork against Firestore. Per-module isolation via collection name. JSON values stored asvaluefield on the document. - Non-functional: P50 read ≤80ms warm, ≤500ms cold. Connection reused across requests via package-level
*firestore.Client. Free-tier-aware: avoidQuery.GetAllon hot paths.
Architecture
internal/storage/
├── firestore_kv.go ← FirestoreKVStore impl
├── firestore_client.go ← package-level client (lazy init, project ID from env)
└── firestore_kv_test.go ← runs against emulator if FIRESTORE_EMULATOR_HOST set
Firestore document shape:
collection: <moduleName>
document id: <key> ← URL-safe key (rejects `/` per Firestore rules)
fields:
value: bytes | string | map ← raw bytes for Put, JSON-marshaled struct for PutJSON
updatedAt: timestamp
List(prefix) uses collection.Where(firestore.DocumentID(), ">=", prefix).Where(firestore.DocumentID(), "<", prefixSuccessor(prefix)).
Related Code Files
- Create:
internal/storage/firestore_kv.go,firestore_client.go,firestore_kv_test.go - Modify:
cmd/server/main.goto initialize Firestore client, pass to module Deps - Modify:
internal/modules/dispatcher.goDeps construction - Create:
Makefiletargettest-emulator(start emulator, run tests)
Implementation Steps
- Add dep:
go get cloud.google.com/go/firestore. firestore_client.go: singletonfunc Client(ctx) (*firestore.Client, error)readingGOOGLE_CLOUD_PROJECTfrom env. Reuse across requests.firestore_kv.go:- Struct
FirestoreKVStore { c *firestore.Client; collection string }. Get(ctx, key):c.Collection(collection).Doc(key).Get(ctx). Mapcodes.NotFound→ErrNotFound. Returnvaluefield as bytes.Put(ctx, key, val):Doc(key).Set(ctx, map{"value": val, "updatedAt": time.Now()}).GetJSON/PutJSON: marshal/unmarshal viaencoding/json.Delete:Doc(key).Delete(ctx).List(prefix):Where(DocumentID >= prefix).Where(DocumentID < successor). Iterator → slice of doc IDs.
- Struct
- Key validation: reject
/, empty string, length >1500 bytes (Firestore limit). firestore_kv_test.go: skip ifFIRESTORE_EMULATOR_HOSTnot set. Round-trip Put/Get/Delete/List/PutJSON/GetJSON/NotFound.- Update
internal/storage/memory_kv.goto supportList(prefix)symmetrically (iterate map keys). - Update
cmd/server/main.go:- Init Firestore client at startup.
- For each module, pass
Prefixed(NewFirestoreKVStore(client, module.Name), module.Name)(collection name = module name = prefix; equivalent to single-collection prefixing). - Actually: drop the
Prefixedwrapper for Firestore — collection itself isolates.Prefixedonly used withMemoryKVfor tests.
- Add
Makefile:firestore-emulator: gcloud emulators firestore start --host-port=localhost:8085andtest: FIRESTORE_EMULATOR_HOST=localhost:8085 go test ./....
Success Criteria
- All KV ops round-trip against emulator
- In-memory fake matches Firestore semantics for List ordering + ErrNotFound
- Two modules writing to same key name → no collision (verified by test)
go test ./internal/storage/...green
Risk Assessment
- Risk: Firestore document IDs reject
/but JS keys may contain them (e.g. nested loldle state). Mitigation: encode/→_in Put, decode on Get. Document the mapping. Or use base64 for arbitrary keys. - Risk: 50k reads/day hard cap. Listing leaderboards on every request hits this fast. Mitigation: cache hot reads in process memory with 5-minute TTL — free for warm instance, costs 0 reads.
- Risk: Emulator behavior diverges from prod (e.g. timestamp resolution, indexes). Mitigation: smoke a 50-key Put/List against real Firestore at end of phase.
Rollback
Drop the Firestore client init, revert to MemoryKV for all modules. Modules continue working but lose persistence.