refactor(server): drop redis stream + worker role (#1792)

The Redis Stream `billing-events` + `worker` Railway role +
advisory-lock poller layered together didn't actually buy us reliability
— `debitFlux` swallowed XADD failures, leaving the door open to "balance
updated, ledger row never written". Collapse the whole thing back to:
`creditFlux` and `debitFlux` write `flux_transaction` ledger rows inline
within the same DB transaction that mutates `user_flux`, and `(user_id,
request_id)` remains the partial unique index that keeps retries safe.

Concrete changes:
- Inline ledger inserts in `BillingService.{debitFlux, creditFlux,
creditFluxFromStripeCheckout, creditFluxFromInvoice}`; drop `billingMq`
and `publishEvent` plumbing entirely.
- `routes/openai/v1` writes `llm_request_log` synchronously via the
existing `requestLogService`; the duplicate `llm-request-log.ts` service
module is removed.
- `bin/run-worker.ts`, `libs/mq/*`,
`services/billing/billing-events.ts`,
`services/billing/billing-consumer-handler.ts`, and matching tests are
deleted. CLI now exposes only `api`.
- `BILLING_EVENTS_*` env vars and the `DEFAULT_BILLING_EVENTS_STREAM`
helper are dropped; `docker-compose.yml` no longer ships a worker
service.
- `docs/ai-context/{workers-and-runtime, billing-architecture,
redis-boundaries-and-pubsub, data-model-and-state,
architecture-overview, README}.md`, `CLAUDE.md`, and the existing
verification docs are updated to describe the single-process synchronous
pipeline.

Tests: 29 files / 247 cases pass. Production deployments need to drop
the worker Railway service after this lands.
This commit is contained in:
RainbowBird
2026-05-08 21:14:01 +08:00
parent eba047328a
commit f8d1fa7a64
38 changed files with 1217 additions and 1854 deletions
+21 -28
View File
@@ -3,10 +3,8 @@ import type Redis from 'ioredis'
import type { AuthInstance } from './libs/auth'
import type { Database } from './libs/db'
import type { Env } from './libs/env'
import type { MqService } from './libs/mq'
import type { OtelInstance } from './libs/otel'
import type { FluxGrantBatchService } from './services/admin-flux-grant-batch/flux-grant-batch-service'
import type { BillingEvent } from './services/billing/billing-events'
import type { AdminFluxGrantsService } from './services/admin-flux-grants'
import type { BillingService } from './services/billing/billing-service'
import type { FluxMeter } from './services/billing/flux-meter'
import type { CharacterService } from './services/characters'
@@ -15,6 +13,7 @@ import type { ConfigKVService } from './services/config-kv'
import type { FluxService } from './services/flux'
import type { FluxTransactionService } from './services/flux-transaction'
import type { ProviderService } from './services/providers'
import type { RequestLogService } from './services/request-log'
import type { StripeService } from './services/stripe'
import type { UserDeletionService } from './services/user-deletion'
import type { HonoEnv } from './types/hono'
@@ -41,7 +40,7 @@ import { createRedis } from './libs/redis'
import { resolveRequestAuth } from './libs/request-auth'
import { sessionMiddleware } from './middlewares/auth'
import { otelMiddleware } from './middlewares/otel'
import { createAdminFluxGrantBatchRoutes } from './routes/admin/flux-grant-batches'
import { createAdminFluxGrantsRoutes } from './routes/admin/flux-grants'
import { createAuthRoutes } from './routes/auth'
import { createCharacterRoutes } from './routes/characters'
import { createChatWsHandlers } from './routes/chat-ws'
@@ -50,8 +49,7 @@ import { createFluxRoutes } from './routes/flux'
import { createV1CompletionsRoutes } from './routes/openai/v1'
import { createProviderRoutes } from './routes/providers'
import { createStripeRoutes } from './routes/stripe'
import { createFluxGrantBatchService } from './services/admin-flux-grant-batch/flux-grant-batch-service'
import { createBillingMq } from './services/billing/billing-events'
import { createAdminFluxGrantsService } from './services/admin-flux-grants'
import { createBillingService } from './services/billing/billing-service'
import { createFluxMeter } from './services/billing/flux-meter'
import { createCharacterService } from './services/characters'
@@ -78,9 +76,9 @@ interface AppDeps {
fluxTransactionService: FluxTransactionService
stripeService: StripeService
billingService: BillingService
fluxGrantBatchService: FluxGrantBatchService
adminFluxGrantsService: AdminFluxGrantsService
ttsMeter: FluxMeter
billingMq: MqService<BillingEvent>
requestLogService: RequestLogService
configKV: ConfigKVService
redis: Redis
env: Env
@@ -213,7 +211,7 @@ export async function buildApp(deps: AppDeps) {
/**
* V1 routes for official provider.
*/
.route('/api/v1/openai', createV1CompletionsRoutes(deps.fluxService, deps.billingService, deps.configKV, deps.billingMq, deps.ttsMeter, deps.redis, deps.env, deps.otel?.genAi))
.route('/api/v1/openai', createV1CompletionsRoutes(deps.fluxService, deps.billingService, deps.configKV, deps.requestLogService, deps.ttsMeter, deps.redis, deps.env, deps.otel?.genAi))
/**
* Flux routes.
@@ -226,10 +224,10 @@ export async function buildApp(deps: AppDeps) {
.route('/api/v1/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.redis, deps.otel?.revenue))
/**
* Admin routes — guarded by ADMIN_USER_IDS allowlist. v1 only includes
* batch-based promo flux grant operations.
* Admin routes — guarded by `ADMIN_EMAILS` allowlist + verified email.
* v1 only includes synchronous one-shot promo flux grants.
*/
.route('/api/admin/flux-grant-batches', createAdminFluxGrantBatchRoutes(deps.fluxGrantBatchService, deps.env))
.route('/api/admin/flux-grants', createAdminFluxGrantsRoutes(deps.adminFluxGrantsService, deps.env))
/**
* Catch-all 404 in JSON. Replaces hono's default `text/html` "404 Not
@@ -330,13 +328,6 @@ export async function createApp() {
build: ({ dependsOn }) => createConfigKVService(dependsOn.redis),
})
const billingMq = injeca.provide('services:billingMq', {
dependsOn: { redis, env: parsedEnv },
build: ({ dependsOn }) => createBillingMq(dependsOn.redis, {
stream: dependsOn.env.BILLING_EVENTS_STREAM,
}),
})
const emailService = injeca.provide('services:email', {
dependsOn: { env: parsedEnv },
build: ({ dependsOn }) => createEmailService({
@@ -429,13 +420,16 @@ export async function createApp() {
})
const billingService = injeca.provide('services:billing', {
dependsOn: { db, redis, billingMq, configKV, otel },
build: ({ dependsOn }) => createBillingService(dependsOn.db, dependsOn.redis, dependsOn.billingMq, dependsOn.configKV, dependsOn.otel?.revenue),
dependsOn: { db, redis, configKV, otel },
build: ({ dependsOn }) => createBillingService(dependsOn.db, dependsOn.redis, dependsOn.configKV, dependsOn.otel?.revenue),
})
const fluxGrantBatchService = injeca.provide('services:adminFluxGrantBatch', {
dependsOn: { db },
build: ({ dependsOn }) => createFluxGrantBatchService(dependsOn.db),
const adminFluxGrantsService = injeca.provide('services:adminFluxGrants', {
dependsOn: { db, billingService },
build: ({ dependsOn }) => createAdminFluxGrantsService({
db: dependsOn.db,
billingService: dependsOn.billingService,
}),
})
const ttsMeter = injeca.provide('services:ttsMeter', {
@@ -468,9 +462,8 @@ export async function createApp() {
requestLogService,
stripeService,
billingService,
fluxGrantBatchService,
adminFluxGrantsService,
ttsMeter,
billingMq,
configKV,
redis,
env: parsedEnv,
@@ -487,9 +480,9 @@ export async function createApp() {
fluxTransactionService: resolved.fluxTransactionService,
stripeService: resolved.stripeService,
billingService: resolved.billingService,
fluxGrantBatchService: resolved.fluxGrantBatchService,
adminFluxGrantsService: resolved.adminFluxGrantsService,
ttsMeter: resolved.ttsMeter,
billingMq: resolved.billingMq,
requestLogService: resolved.requestLogService,
configKV: resolved.configKV,
redis: resolved.redis,
env: resolved.env,