백그라운드 잡

NATS JetStream 큐, 워커, 크론.

느리거나, 불안정하거나, 재시도가 필요한 작업은 요청 경로가 아니라 워커에서 실행됩니다.

계약(contract) 크레이트

job_queue는 서버↔워커 경계 전체를 담당합니다: 페이로드 구조체, 스트림/서브젝트/ 컨슈머 이름, 그리고 멱등한 스트림 생성. 서버는 bridge/worker_client/를 통해 발행하고(직렬화 → 발행 → JetStream ack 대기), 워커는 핸들러에서 동일한 페이로드 타입을 재수출합니다. 두 바이너리 모두 시작 시 initialize_all_streams를 실행하므로, 어느 프로세스가 먼저 기동하든 새로 띄운 NATS에서도 문제없이 동작합니다.

현재 잡: 트랜잭셔널 이메일(MJML 템플릿을 한 번만 렌더링해 캐시하고, minijinja 변수는 HTML 이스케이프 처리), 사용자 인덱싱 및 배치 방식의 사용자 리인덱싱 (배치가 스스로 다음 배치를 큐에 넣으며 임시 인덱스를 구축한 뒤 원자적으로 교체), 그리고 OAuth 아바타 처리(SSRF 방어가 적용된 fetch → 미디어 프로세서 → 콘텐츠 주소 기반 R2 업로드).

컨슈머 엔진

모든 컨슈머는 durable JetStream pull 컨슈머이며, 실패 처리 의미론이 이미 정립된 하나의 제네릭 엔진으로 구동됩니다:

  • 재시도 — 명시적 백오프(1/2/4/8/16초); 실패한 메시지는 Nak 처리되어 총 6회까지 재전달됩니다.
  • 데드레터 큐 — 영구적으로 실패했거나 파싱 불가능한 메시지는 출처 헤더와 함께 DLQ 스트림(14일 보존)으로 재발행됩니다. 원본 메시지는 DLQ 사본이 서버로부터 ack된 후에만 삭제되므로, 사본이 어디에도 없는 상태로 잡이 유실되는 일은 없습니다.
  • 하트비트 + 타임아웃 백스톱 — 오래 걸리는 핸들러는 진행 중(in-progress) ack를 전송해 실행 도중 재전달되지 않으며, 핸들러 전체에 시간 제한(기본 300초, 리인덱스 배치는 900초)이 걸려 있어 외부 호출이 멈춰도 동시성 슬롯을 영구히 점유할 수 없습니다.
  • 패닉 격리 — 핸들러가 패닉하더라도 큐를 멈추게 하는 대신 일반 실패 경로를 따라 처리됩니다.
  • 메시지 단위 중복 제거 — 멱등하지 않은 핸들러(이메일, 아바타 처리, 리인덱싱)는 ack 전에 처리한 스트림 시퀀스를 noeviction Redis에 기록하므로, ack 유실 후 재전달되더라도 다시 실행되지 않고 건너뜁니다.
  • 설정 전파 — 컨슈머는 create-or-update 의미론으로 생성되므로, 튜닝 변경 사항이 재배포 시 기존 durable에도 실제로 반영됩니다.

슈퍼바이저는 종료되거나 패닉한 컨슈머를 재시작합니다. SIGTERM을 받으면 워커는 메시지 pull을 중단하고, 처리 중인 핸들러를 드레인한 뒤(최대 30초), 크론 스케줄러를 종료해 어떤 잡도 분산 락을 잡은 채 방치되지 않도록 합니다.

크론

tokio-cron-scheduler를 사용하며, Redis 락(SET NX + lua 하트비트 연장; TTL 만료가 락 해제를 처리해 클록 스큐를 흡수)으로 다중 인스턴스 안전성을 확보합니다:

스케줄 하는 일
cleanup 토 04:00 배치 삭제: 만료된 그룹 멤버십, 역할, 밴; 90일이 지난 알림
sitemap 일 03:00 사이트맵을 생성해 R2에 업로드
view-count flush 매분 버퍼링된 게시판 조회수를 원자적으로 드레인(HGETALL + DEL)해 Postgres에 반영 — 드레인 자체가 상호 배제이므로 락이 필요 없음

잡 추가하기

  1. job_queue에 페이로드와 스트림/서브젝트/컨슈머 이름을 정의합니다.
  2. worker/src/jobs/…에 핸들러와 run_consumer를 작성하고, 워커의 main.rs에 컨슈머 종류를 등록합니다.
  3. server/src/bridge/worker_client/에 발행 함수를 추가합니다. 사용자가 실패를 확인해야 하는 경우(가입 이메일)에는 인라인으로 호출하고, best-effort인 경우 (인덱싱, 알림)에는 커밋 후 tokio::spawn 래퍼로 호출합니다.

Built by levish. The source code is available on GitHub.

Batteries included.

enko