ADR-0023: Live Sync Batch Processing¶
Umbra 의 Live Sync 는 Discord Gateway 이벤트를 실시간 1:1 처리하지 않고 배치로 모아서 주기적으로 DB 에 반영한다.
Status¶
Accepted
- Decided at — 2026-04-13
- Decided by — Pablo
Context¶
Umbra 의 Recovery 기능은 "길드의 현재 상태" 가 항상 우리 DB 에 반영되어 있어야 한다. Discord Gateway 에서 멤버 추가/삭제, 역할 변경, 채널 생성 등 이벤트가 초당 수십~수백 건씩 발생할 수 있다.
순진한 구현 옵션:
- 이벤트당 즉시 UPSERT — 각 이벤트마다 DB INSERT/UPDATE
- 이벤트 로그 + 주기 배치 — 이벤트를 Redis 에 적재, N초마다 배치로 DB 반영
- 이벤트 스트림 + 컨슈머 — Kafka 같은 스트림 + 컨슈머 (인프라 과함)
현실 관찰:
- 대량 이벤트 폭주 — 길드 owner 가 역할을 재배열하면 수백 건이 수 초 내 발생
- DB 쓰기 부담 — 즉시 UPSERT 는 connection pool 고갈 위험
- 중복 병합 가능 — 같은 멤버가 역할 A 추가 → B 제거 → A 제거면 최종 상태만 반영하면 됨
- 실시간 요구 낮음 — Live Sync 는 복구 시점 기준 최대 수 초 오차 허용 (분 단위 아니면 문제없음)
Decision¶
Live Sync 는 이벤트를 버퍼링한 뒤 배치로 DB 반영 한다.
흐름¶
- Bot 프로세스의 Discord 핸들러가 이벤트 수신
- 이벤트를 Redis sorted set / list 에 적재 (key:
sync:guild:{guild_id}:buffer) - Worker 의 asynq cron (5초 주기) 이 길드별 버퍼 조회
- 버퍼 내 이벤트를 merge 하여 최종 상태 계산 (같은 aggregate 에 대한 중복 이벤트 제거)
- 단일 트랜잭션으로 DB 반영
- 버퍼 비우기
설정¶
- Batch interval — 5초 (기본), 길드별 조정 가능
- Batch size cap — 1000 events (초과 시 즉시 flush)
- High-watermark flush — 버퍼가 500 events 초과 시 즉시 flush
선택 근거:
- DB 쓰기 부하 ↓ — 1000 이벤트가 5초 내 몰려도 1 트랜잭션으로 처리
- 이벤트 병합 가능 — 같은 멤버의 역할 추가→제거 중복 → 최종 상태만 반영
- rate limit 여유 — Discord 호출은 필요 시에만 (대부분은 DB 작업)
- 단순 구조 — 이벤트 스트림 인프라 없이 Redis + asynq 로 구현
Consequences¶
Positive¶
- 대량 이벤트 폭주 상황에서도 DB 안정
- 중복 이벤트가 자동 병합되어 쓰기 횟수 ↓
- Live Sync 가 느려도 Bot/API 에 영향 없음 (비동기)
- 구현 단순 (Redis + asynq 이미 사용 중)
Negative¶
- DB 반영까지 최대 5초 지연 (실시간 아님)
- Redis 버퍼 장애 시 이벤트 손실 가능 (Redis 는 RDB/AOF 지원으로 완화)
- 배치 실패 시 재시도 정책 필요
Neutral¶
- Restore workflow 가 시작될 때는 Live Sync 를 일시 중지 (Temporal signal 로)
- 복구 직후 Live Sync 재개 시 버퍼는 비움 (과거 이벤트가 새 상태를 덮어쓰는 것 방지)
Event buffering format¶
Redis 에 저장되는 이벤트 형식:
Key: sync:guild:{guild_id}:buffer
Value: List or Sorted Set of events
Event: {
"event_type": "GUILD_MEMBER_UPDATE",
"timestamp": "2026-04-13T00:00:00Z",
"payload": {...}
}
Merge logic¶
Batch 처리 시 이벤트는 aggregate ID 기반으로 병합:
for each aggregate_id in buffer:
events = filter by aggregate_id
final_state = fold events (apply in order)
upsert final_state into DB
예: 멤버 A 의 이벤트가 ADD_ROLE(X) → ADD_ROLE(Y) → REMOVE_ROLE(X) 이면 최종 상태는 roles=[Y]. 3번의 UPDATE 대신 1번의 UPDATE.
Pause / resume¶
Restore workflow 시작 시 Live Sync 는 일시 중지된다.
sequenceDiagram
participant Restore
participant LiveSync
participant Redis
Restore->>LiveSync: Signal pause
LiveSync->>Redis: Mark buffer as paused
Note over Restore: Restore executes
Restore->>LiveSync: Signal resume
LiveSync->>Redis: Clear buffer (복구 후 상태가 최신)
LiveSync->>LiveSync: Resume batch processing
복구 후 버퍼를 비우는 이유: 복구 직후 DB 상태가 "최신" 이므로, 복구 실행 중 쌓인 이벤트(구 상태 기준)를 반영하면 DB 가 뒤틀림.
Alternatives considered¶
Alternative 1: 이벤트당 즉시 UPSERT¶
Pros
- 실시간성 (지연 0)
- 구현 단순
Cons
- 대량 이벤트 시 DB 부하 폭증
- 중복 UPSERT 발생 (역할 변경 후 철회 등)
- Connection pool 고갈 위험
Why rejected — 결제 SaaS 규모에서 DB 안전성이 우선.
Alternative 2: Kafka 이벤트 스트림¶
Pros
- 고처리량
- 재생 가능 (재처리 시)
Cons
- 인프라 추가 (Kafka 운영)
- MVP 에 과함
Why rejected — Redis + asynq 로 90% 이점 확보 가능.
Alternative 3: PostgreSQL LISTEN/NOTIFY + buffer¶
Pros
- PostgreSQL 네이티브
Cons
- 메시지 크기 제한 (8KB)
- 장애 시 손실
Why rejected — Redis 가 적합.
Alternative 4: 이벤트 중복 제거 없이 모두 UPSERT¶
Pros
- 단순
Cons
- 쓰기 횟수가 이벤트 수만큼 발생
- 배치의 이점이 반감
Why rejected — 병합이 결정적 이점.
Compliance¶
- Live Sync 구현은
engine/recovery/subdomain/sync/에 위치 - Bot 프로세스는 이벤트를 Redis 에 enqueue, DB 접근 금지 (쓰기 경로 단일화)
- Worker 의 asynq cron 이 배치 처리 주체
- 배치 실패 시 asynq 재시도 정책 (3회 시도)
- 메트릭: 버퍼 크기, 배치 지연, 처리 실패율 모니터링
Revisit triggers¶
- 5초 지연이 Recovery 품질에 문제가 되면 interval 단축
- 특정 길드의 이벤트 TPS 가 배치 한계 초과 시 샤딩 (guild_id 해시 기반)
- 손실 허용 불가 수준의 요구사항이 생기면 Kafka 전환