Spike: persistent, multi-instance-safe storage — recommend Redis, prototype GigService - #191
Conversation
…totype GigService
Every domain service in this backend stores state in a process-local Map,
so it's lost on restart and diverges across instances behind a load
balancer. This inventories every in-memory store found, evaluates
Postgres/DynamoDB/Redis against their access patterns, and recommends
Redis: it's already a hard dependency (NonceStoreService/RateLimitGuard
already use it with a graceful in-memory fallback), and every inventoried
access pattern is a point lookup, small index, or time-ordered range scan
that maps directly onto Redis strings/sets/sorted sets, so adopting it
elsewhere is consistent with existing practice rather than new
infrastructure.
Prototypes the recommendation against GigService (the newest/smallest
domain service, per the issue): gigs persist as SET gig:{id}, with sorted
sets for creation-order listing and open-gig expiry lookups and a set per
creator for the by-creator index, all written atomically via MULTI. Falls
back to the original in-memory Map when Redis is unavailable, matching
NonceStoreService's convention, logged at error level since this fallback
reintroduces the exact divergence this spike exists to fix.
Distributed locking for the sweep workers is out of scope — already
tracked independently in trustflow-protocol#182 — this only confirms the recommended
persistence layer doesn't conflict with it.
Full write-up: backend/PERSISTENT_STORAGE_SPIKE.md.
Follow-up implementation issues filed and linked from trustflow-protocol#181: trustflow-protocol#187 (Escrow),
trustflow-protocol#188 (UserProfile), trustflow-protocol#189 (IpfsPinning), trustflow-protocol#190 (remaining worker/saga
stores).
Closes trustflow-protocol#181
Thanks — this is an excellent, thorough spike and a very clean prototype. I appreciate the inventory, the trade-off reasoning, and the GigService implementation that falls back to the in-memory Map when Redis is unavailable. Tests, lint, and build passing is great. A few required changes before I merge:
Minor nits / suggestions:
Overall: I'm strongly +1 on the direction and the recommendation to use Redis for these patterns. Address items 1–4 (docs, integration test, metrics, and transaction verification), push an update, and I will approve/merge. |
…dis CI smoke test Responds to maintainer review on trustflow-protocol#191: - GigService.onModuleInit() now refuses to start in production (NODE_ENV=production) without a configured Redis client, instead of silently engaging the in-memory fallback and diverging across instances. Non-production environments keep the fallback so the app still runs without a local Redis. - assertTransactionOk() inspects MULTI/EXEC's per-command results array (and treats a null exec() result the same way), since ioredis only rejects the whole call on a queue-time error — a runtime failure in one queued command otherwise surfaces as a [Error, null] entry while exec() still resolves, which the code previously treated as success. - Every fallback now increments gig_persistence_fallback_total via the existing MetricsService, exposed at GET /metrics. - Add gig.service.redis-integration.spec.ts, exercising GigService against a real Redis (no mocking) to validate MULTI/EXEC atomicity and sorted-set/set semantics the mock can't faithfully reproduce. Gated on REDIS_URL and skipped (not failed) when unset. CI now runs a redis:7-alpine service container and sets REDIS_URL for the test step so this runs there; verified locally against a real container before pushing. - Expand PERSISTENT_STORAGE_SPIKE.md: document the fallback/fail-fast behavior and its production implications, add alerting guidance for the new metric, expand the durability/sizing notes, and add an owner/priority table for the follow-up issues (also reflected on trustflow-protocol#187-trustflow-protocol#190 themselves).
|
Thanks for the thorough review — pushed a follow-up commit addressing all 6 required items:
Also took the minor nits: fallback is now environment-gated (fails app startup in production if Redis isn't configured; still falls back in dev/test), and added a "running it locally" subsection to §4 with copy-pasteable commands for both the with- and without-Redis paths. Full CI suite (lint, format, tsc, tests including the new integration suite, build) verified clean locally with a real Redis container before pushing. |
Thanks — I appreciate the quick follow-up and the thoroughness here. I reviewed the updates and, CI passes, this looks ready to merge. What I checked and am happy to see:
Two final asks before I merge:
If those two small items are added (or you prefer to follow up with a quick patch), Looks good to me and ready to merge. Thanks again — this moves us forward on a safe, well-tested path to persistent, multi-instance-safe storage. |
…od requirement - CI's redis:7-alpine service container already has a --health-cmd gate on job startup, but add an explicit "Wait for Redis" retry step right before the test step too, as a belt-and-suspenders guard against intermittent flakiness the maintainer flagged as a risk. - Add a one-line callout to PERSISTENT_STORAGE_SPIKE.md's header noting the new production requirement introduced by this PR: REDIS_URL must be configured under NODE_ENV=production, or GigService now refuses to start rather than silently falling back to per-instance memory.
|
Both addressed:
Pushed as a830557. Thanks again for the review — let me know if there's anything else before merge. |
meshackyaro
left a comment
There was a problem hiding this comment.
Approved — great work.
This is a thorough, well-reasoned spike and a clean prototype. CI is green and I reviewed the updates: the spike doc, the configurable fallback (disabled by default in prod), the real-Redis integration smoke test in CI, the fallback metric, MULTI/EXEC transaction checks and tests, ops notes on durability/replication/SPOF, and the follow-up issues with owners/priorities.
Thanks for the excellent work.
Summary
Closes #181
Every domain service in this backend —
EscrowService,GigService,UserProfileService,IpfsPinningService,NonceStoreService, plus several worker/saga stores found during the inventory — keeps its state in a process-localMap. This spike:REDIS_URLis documented as required in.env.example), andNonceStoreService/RateLimitGuardalready establish a working, tested pattern for it in this exact codebase — every inventoried access pattern (point lookups, small indices, time-ordered range scans) maps directly onto Redis strings/sets/sorted sets, so this is consistent with existing practice, not new infrastructure.EscrowServiceas a deliberate exception: it's money-adjacent/audit-sensitive, so the spike leaves the Redis-vs-relational call open rather than defaulting it, and hands that decision to its own follow-up issue.GigService(the newest/smallest domain service, per the issue's own suggestion): gigs now persist asSET gig:{id}, with sorted sets for creation-order listing and open-gig expiry lookups, and a set per creator for the by-creator index — all written atomically viaMULTI. Falls back to the original in-memoryMapwhen Redis is unavailable, matchingNonceStoreService's convention, logged aterrorlevel since that fallback reintroduces the exact divergence this spike exists to fix.ioredisonly, consistent with the rest of the repo's Redis tests), Redis durability/backup configuration unverified, single-instance Redis is a SPOF, andIpfsPinningService's raw-contentBuffermap is a poor fit for Redis as general KV storage.Full write-up:
backend/PERSISTENT_STORAGE_SPIKE.md.Follow-up implementation issues filed and linked from #181:
EscrowService(Redis vs. relational decision left open)UserProfileServiceIpfsPinningService's pin registryDisputeSagaService,EscrowReconciliationStateStore,EventProcessorService,LedgerCursorService,MigrationStateStore)Test plan
npm run lint:check— clean (pre-existing warnings only, unrelated to this change)npm run format:check— cleannpx tsc --noEmit— cleannpm run test:ci— 356/356 tests passing (GigService's suite runs its full behavior twice — once against a mocked Redis client, once against the in-memory fallback — plus a dedicated Redis-failure test)npm run build— clean