Solid Queue (Rails)
Redis 없이 DB만으로 — Rails 8 기본 백그라운드 잡 시스템
GitHub: rails/solid_queue
Solid Queue는 37시그널스가 만든 DB 기반 ActiveJob 백엔드로, Rails 8부터 기본 포함됩니다. "Redis가 정말 필요한가?"라는 질문에서 시작된 프로젝트입니다.
핵심 아이디어
상태를 컬럼이 아닌 테이블로 관리합니다.
하나의 jobs 테이블에 status 컬럼을 두는 대신, 잡의 상태마다 별도 테이블을 사용합니다. DHH의 "상태를 레코드로 관리하라"는 원칙이 인프라 레벨에서도 적용된 것입니다.
DB 스키마 (10개 테이블)
solid_queue_jobs # 중앙 잡 테이블 (class_name, arguments, queue_name)
├── solid_queue_ready_executions # 실행 대기 중인 잡
├── solid_queue_scheduled_executions # 예약된 미래 잡
├── solid_queue_claimed_executions # 워커가 가져간 잡 (처리 중)
├── solid_queue_failed_executions # 실패한 잡 (에러 메시지 포함)
├── solid_queue_blocked_executions # 동시성 제한으로 대기 중
├── solid_queue_semaphores # 동시성 제어 세마포어
├── solid_queue_processes # 등록된 워커/디스패처 프로세스
├── solid_queue_recurring_executions # 반복 작업 실행 이력
├── solid_queue_recurring_tasks # cron 같은 반복 작업 정의
└── solid_queue_pauses # 일시정지된 큐 이름
모든 execution 테이블은 solid_queue_jobs에 ON DELETE CASCADE FK로 연결됩니다.
잡 라이프사이클
Enqueue
│
├─ 즉시 실행 가능 ──→ ready_executions (대기)
├─ 예약된 시간 ────→ scheduled_executions (예약)
└─ 동시성 제한 ────→ blocked_executions (대기)
│
Worker가 폴링 ←─────────┘
│
├─ claim (FOR UPDATE SKIP LOCKED)
│ └─→ claimed_executions (처리 중)
│
├─ 성공 ──→ finished_at 갱신 (또는 레코드 삭제)
└─ 실패 ──→ failed_executions (에러 저장)
1. Enqueue (잡 등록)
# ActiveJob에서 perform_later 호출 시
SolidQueue::Job.enqueue(class_name, arguments, queue_name, ...)
# → after_create :prepare_for_execution 콜백 발동
# → 즉시 실행 가능하면 ReadyExecution 생성
# → 미래 시간이면 ScheduledExecution 생성
2. Dispatch (스케줄 → 대기)
# Dispatcher가 주기적으로 폴링
ScheduledExecution.dispatch_next_batch(batch_size)
# → scheduled_at <= now인 잡을 FOR UPDATE SKIP LOCKED으로 선택
# → ReadyExecution으로 이동
3. Claim & Execute (잡 실행)
# Worker가 주기적으로 폴링
ReadyExecution.claim(queues, limit, process_id)
# → FOR UPDATE SKIP LOCKED으로 비차단 선택
# → ClaimedExecution 생성 (트랜잭션)
# → 스레드 풀에서 ActiveJob::Base.execute 실행
# → 성공: finished! / 실패: FailedExecution 생성
3가지 프로세스 타입
| 프로세스 | 역할 |
|---|---|
| Worker | ready_executions 폴링 → 잡 claim → 스레드 풀에서 실행 |
| Dispatcher | scheduled_executions 폴링 → due된 잡을 ready_executions로 이동 |
| Scheduler | cron 스케줄 기반 반복 작업 enqueue |
모두 Processes::Poller를 상속하여 poll → sleep → poll 루프를 실행합니다.
실행 방식:
ForkSupervisor — 프로덕션: 자식 프로세스로 fork
AsyncSupervisor — 개발: 같은 프로세스에서 스레드로 실행 (Puma 플러그인)
핵심 설계 결정
FOR UPDATE SKIP LOCKED
여러 워커가 동시에 잡을 가져가도 서로 블로킹하지 않습니다. 이미 다른 워커가 lock한 행은 건너뛰고 다음 행을 가져갑니다. SQLite에서는 graceful fallback.
상태 = 별도 테이블
status 컬럼 대신 테이블 분리로 각 상태별 최적 인덱스를 가질 수 있습니다. 폴링 쿼리가 단순해지고 성능이 좋습니다.
트랜잭션 안전
enqueue_after_transaction_commit?가 true를 반환하여, 감싸는 트랜잭션이 커밋된 후에만 잡이 보입니다.
구조 다이어그램
잡 상태별 테이블 분리 (ERD)
각 테이블에 최적화된 인덱스 = 빠른 폴링
잡 라이프사이클
3가지 프로세스 타입
핵심 포인트
GitHub에서 rails/solid_queue 저장소 열기
db/migrate/ → 10개 테이블 스키마 확인 (상태별 테이블 분리 패턴)
lib/solid_queue/worker.rb → 폴링 루프와 claim 로직 분석
lib/solid_queue/dispatcher.rb → 예약 잡 디스패치 로직 분석
app/models/solid_queue/job.rb → enqueue 흐름 확인
app/models/solid_queue/ready_execution.rb → FOR UPDATE SKIP LOCKED 확인
app/models/solid_queue/claimed_execution.rb → 실행/성공/실패 처리
lib/solid_queue/supervisor.rb → Fork vs Async 프로세스 관리
장점
- ✓ Redis 불필요 — DB만으로 완전한 잡 시스템
- ✓ Rails 8 기본 — 별도 설치 없이 바로 사용
- ✓ ActiveJob 호환 — 기존 잡 코드 변경 없이 전환
- ✓ FOR UPDATE SKIP LOCKED — 워커 간 비차단 경쟁
- ✓ 동시성 제어 내장 — 세마포어 기반 concurrency limit
- ✓ Puma 플러그인 — 개발 시 별도 프로세스 불필요
단점
- ✗ Redis 기반 Sidekiq보다 처리량(throughput)이 낮을 수 있음
- ✗ DB 부하 증가 — 잡이 많으면 DB에 쓰기 부담
- ✗ SQLite에서 SKIP LOCKED 미지원 (graceful fallback)
- ✗ 대규모 서비스에서는 여전히 Sidekiq/Redis가 유리
- ✗ Web UI가 Sidekiq보다 덜 성숙 (Mission Control 별도)