📋

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_jobsON 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)

📋 solid_queue_jobs GitHub ↗
queue_name, class_name, arguments, priority, scheduled_at, finished_at
ON DELETE CASCADE
ready_executions
실행 대기
scheduled_executions
예약 대기
🔄
claimed_executions
처리 중
failed_executions
실패
🔒
blocked_executions
동시성 대기
🔑
semaphores
동시성 제어
핵심: <strong>status 컬럼</strong> 대신 <strong>상태별 테이블 분리</strong>
각 테이블에 최적화된 인덱스 = 빠른 폴링

잡 라이프사이클

MyJob.perform_later(args) Job.rb ↗
solid_queue_jobs INSERT + after_create :prepare_for_execution
즉시 실행?
예약?
동시성 제한?
ready_executions
scheduled_executions
blocked_executions
Dispatcher 가 scheduled → ready로 이동 dispatcher.rb ↗
Worker: claim (FOR UPDATE SKIP LOCKED)
ready → claimed (트랜잭션)
worker.rb ↗
ActiveJob::Base.execute(arguments)
성공
finished_at 갱신
실패
failed_executions

3가지 프로세스 타입

⚙️
Worker
ready_executions 폴링
claim (SKIP LOCKED)
스레드 풀에서 실행
worker.rb ↗
📡
Dispatcher
scheduled_executions 폴링
due된 잡 → ready로 이동
동시성 유지보수
dispatcher.rb ↗
🔁
Scheduler
cron 스케줄 확인
반복 작업 enqueue
중복 실행 방지
scheduler.rb ↗

핵심 포인트

1

GitHub에서 rails/solid_queue 저장소 열기

2

db/migrate/ → 10개 테이블 스키마 확인 (상태별 테이블 분리 패턴)

3

lib/solid_queue/worker.rb → 폴링 루프와 claim 로직 분석

4

lib/solid_queue/dispatcher.rb → 예약 잡 디스패치 로직 분석

5

app/models/solid_queue/job.rb → enqueue 흐름 확인

6

app/models/solid_queue/ready_execution.rb → FOR UPDATE SKIP LOCKED 확인

7

app/models/solid_queue/claimed_execution.rb → 실행/성공/실패 처리

8

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 별도)

사용 사례

이메일 발송: UserMailer.welcome.deliver_later 데이터 처리: CsvImportJob.perform_later(file) 정기 작업: Scheduler로 매일 자정 리포트 생성 동시성 제어: 같은 사용자의 결제 잡을 순차 실행 Sidekiq → Solid Queue 마이그레이션 설치형 앱에서 Redis 없이 백그라운드 잡 처리