본문으로 건너뛰기

Event Catalog

Status

Active

Date

2026-05-15

Owners

  • Platform Backend

Last Verified Commit

(see git log -- docs/architecture/event-catalog.md)

Event Transport Model

servers_v2 uses a shared contracts package and Redis Streams for cross-service domain-event transport.

Common rules:

  • owner service writes domain state and outbox rows in the same DB transaction
  • publisher loops read unpublished outbox rows and append to Redis Streams
  • consumers use inbox-style deduplication or processed-event tracking
  • worker health must reflect successful forward progress, not simple loop survival

Documented Streams

The columns are split into "Currently Emitted" (event types you can expect to find on the live stream) and "Reserved" (event-type constants declared in rgb_contracts/events/*.py and surfaced here so downstream consumers know the namespace, but with no producer wired yet). Adding a reserved event becomes "currently emitted" requires (a) landing a producer that writes the outbox row in the same transaction as the business state change and (b) updating this row.

StreamProducerConsumersCurrently EmittedReserved (constant only)
wallet streamwallet_servicerolling_service + promotion_serviceonly BET_SETTLED_CONFIRMED / BET_ROLLED_BACK_CONFIRMED (see note)BET_SETTLED_CONFIRMED, BET_ROLLED_BACK_CONFIRMED, ROLLING_COMPLETED_CONFIRMED, DEPOSIT_APPROVED, WITHDRAW_APPROVEDBET_AUTHORIZED, PROMOTION_CREDITED, PROMOTION_REVERSED, COUPON_EXPIRED
rolling streamrolling_servicefuture consumers / projectionsROLLING_COMPLETED, ROLLING_CANCELED
player streamplayer_servicefuture consumers / projectionsPLAYER_REGISTEREDVIP_LEVEL_CHANGED
agent streamagent_servicefuture consumers / projectionsAGENT_WITHDRAW_CREATED, AGENT_WITHDRAW_APPROVED, AGENT_WITHDRAW_DECLINED, AGENT_WITHDRAW_PAID, AGENT_WITHDRAW_PAY_DECLINED, AGENT_COUPON_GRANTED, AGENT_COUPON_RECYCLED, AGENT_BALANCE_ADJUSTED

Consumer note. "Currently Emitted" lists what wallet_service produces, not what is consumed. The rolling_service and promotion_service event consumers dispatch only BET_SETTLED_CONFIRMED and BET_ROLLED_BACK_CONFIRMED. ROLLING_COMPLETED_CONFIRMED, DEPOSIT_APPROVED and WITHDRAW_APPROVED are emitted to the stream but have no consumer (producer-only); do not read the table as "rolling/promotion consume DepositApproved".

The wallet Reserved column tracks the four event-type constants declared in servers_v2/shared/contracts/src/rgb_contracts/events/wallet.py but never written to wallet_outbox by any producer. They are not on the live stream and consumers cannot rely on them today. They remain declared so a future producer PR does not need to invent a name. Authoritative producer enumeration is in servers_v2/wallet_service/app/services/wallet_bucket_commands.py / app/api/routes/wallet.py.

Current Consumer Relationships

  • rolling_service
    • consumes wallet events to complete or roll back rolling state
  • promotion_service
    • consumes wallet bet-settlement events to maintain rebate and other promotion projections
  • recon_service
    • currently relies on direct HTTP calls and background polling rather than Redis Streams
  • admin_service
    • currently aggregates via HTTP and scheduler jobs rather than stream consumption

Health Requirements

The following rules are required by current ADRs and worker implementations:

  • a publish loop that repeatedly fails must surface failure to supervision
  • a consumer loop that stops making successful progress must eventually become unhealthy
  • scheduler-backed workers must include job-health readiness, not just process heartbeat

Related ADRs:

  • docs/adr/ADR-003-background-worker-health-must-reflect-forward-progress.md
  • docs/adr/ADR-004-settlement-jobs-must-fail-loudly-on-non-delivery.md

Multi-Brand Payload Requirement

Per ADR-009, every event listed under "Documented Streams" carries brand_id as a field on the shared DomainEvent envelope (servers_v2/shared/contracts/src/rgb_contracts/events/base.py); per-payload schemas (e.g. events/wallet.py) are not modified individually.

Producer-side invariant (enforced at outbox write time): the envelope brand_id MUST equal the owning row's brand_id. The outbox writer asserts this via either an application-level guard (in the worker that persists the outbox row) or a DB-level CHECK constraint that compares outbox.brand_id to the originating row's brand_id. A producer that violates this invariant fails the write and is surfaced to supervision.

Schema versioning: DomainEvent.schema_version bumps from 1 to 2 when brand_id is added to the envelope (Phase 1). Producers emit only schema_version=2 events after Phase 6 deploy. Consumer behavior:

  • schema_version >= 2: read brand_id from envelope; apply MULTI_BRAND_ENFORCEMENT semantics for envelope/target mismatch (observe: log + count + use envelope brand; enforce: reject). Missing or unparseable envelope brand_id is rejected regardless of mode.
  • schema_version == 1: increment event_legacy_schema_total{stream,consumer}, reject, and surface to supervision. The old observe-mode fallback to the seeded default brand was removed by the event-brand-scoping closure.

At Phase 6 producer-deploy, each producer historically emitted one BRAND_AWARE_PUBLISHING_BEGINS sentinel event so consumers could log the exact stream offset where the schema bump took effect. (Removed in T6-E4 / T7-B2: producers no longer emit the sentinel because the schema-version observability counter (event_legacy_schema_total) covers the same alarm surface and the sentinel itself was a no-op in every stream consumer. Historical entries on long-lived streams may still appear in pre-Phase-13 backlogs and are ignored by current consumers.)

In this iteration every consumer is brand-agnostic and processes every event by reading envelope brand_id; per-brand consumer fanout (separate consumer-group per brand) is not introduced.

Known Gaps

  • no stable cross-service event catalog has yet been extracted for every future consumer
  • STREAM_PROMOTION was removed from streams.py (Phase 17) — promotion has no outbound stream contract. If a notification-aimed event surface lands later, re-declare it there with at least one active consumer; do not pre-declare empty.
  • recon currently uses domain-specific worker loops and HTTP integrations rather than a stream contract