사이드킥 큐에서 태스크나 예약작업 삭제하기

들어가며

루비 온 레일스Ruby on Rails에서는 비동기 작업 처리로 사이드킥Sidekiq을 주로 사용합니다. 사이드킥은 등록된 작업을 레디스 큐에 저장해놓고 워커들에서 가져가서 처리하는 방식으로 동작합니다. 실무에서는 의도치 않게 실행되서는 안 되는 작업이 큐나 예약 작업에 등록되는 경우가 종종 있습니다. 이런 경우 사이드킥의 API를 사용해 특정 작업들만 삭제해줄 필요가 있습니다. 이 글에서는 사이드킥 API를 사용해 예약 작업을 가져오고 특정 조건으로 필터링해서 삭제하는 방법을 소개합니다.

샘플 루비 온 레일스 프로젝트 준비

먼저 루비 2.6과 최신 버전의 레일스(6.0)로 샘플 프로젝트를 만들어 보겠습니다.*

* 레일스6에는 자바스크립트 초기화 작업이 포함되어, 적절한 환경이 구축되어있지 않을 경우 초기화가 번거로울 수 있습니다. 여기서는 --skip-javascript 옵션으로 자바스크립트 관련 초기화 작업을 우회했습니다.

$ rails --version
6.0.3
$ rails new --skip-javascript awesome_sidekiq
$ cd awesome_sidekiq

사이드킥과 비동기 처리를 위한 잡 클래스 추가

초기화된 레일스 프로젝트에 sidekiq 의존성을 추가해줍니다. Gemfile에 다음 내용을 추가합니다.

gem 'sidekiq', '~> 6.0.0'

추가한 의존성을 설치해줍니다.

bundle install

다음으로 rails 명령어를 사용해 AwesomeJob을 생성합니다.

$ bundle exec rails g job awesome
Running via Spring preloader in process 8705
      invoke  test_unit
      create    test/jobs/awesome_job_test.rb
      create  app/jobs/awesome_job.rb

app/jobs/awesome_job 파일을 다음과 같이 작성합니다. AwesomeJob이 하는 일은 obj 객체를 받아 출력하는 일입니다.

class AwesomeJob < ApplicationJob
  queue_as :awesome

  def perform(obj)
    pp obj
  end
end

사이드킥 상태를 확인하기 위해 라우터에 sidekiq 대시보드를 마운트 해줍니다. config/routes.rb 파일에 다음 내용을 추가합니다.

require 'sidekiq/web'

Rails.application.routes.draw do
  mount Sidekiq::Web => '/sidekiq'

  # ...
end

이걸로 코드 준비는 끝났습니다. 하지만 사이드킥에 잡을 등록하기 위해서는 태스크를 저장할 레디스 서버가 필요합니다. 여기서는 도커Docker 컨테이너로 레디스 서버를 준비하겠습니다.

$ docker run --name sidekiq_redis -d -p 6399:6379 redis

아래 내용을 config/initializers/sidekiq.rb 파일에 작성합니다.

Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://0.0.0.0:6399/0' }
end

Sidekiq.configure_client do |config|
  config.redis = { url: 'redis://0.0.0.0:6399/0' }
end

이것으로 모든 준비를 마쳤습니다. 레일스 서버를 실행합니다.

$ rails s

이제 0.0.0.0:3000/sidekiq 페이지에 접속하면, 사이드킥 대시보드를 확인할 수 있습니다.

사이드킥 대시보드

사이드킥에 스케줄 잡 추가하고 삭제하기

여기서부터는 레일스 콘솔에서 작업해보도록 하겠습니다.

$ rails c

5분 후에 실행되는 태스크를 하나 추가해보겠습니다. 인자값으로는 {foo: 'bar', time: Time.now}를 넘겨주었습니다.

> AwesomeJob.set(wait: 5.minutes).perform_later({foo: 'bar', time: Time.now})
Enqueued AwesomeJob (Job ID: 4d9a3417-c746-4a07-ab5c-ae2fb4c91bb3) to Sidekiq(awesome) at 2020-05-16 04:19:59 UTC with arguments: {:foo=>"bar", :time=>2020-05-16 13:14:59 +0900}
=> #<AwesomeJob:0x00007fed41838c50 @arguments=[{:foo=>"bar", :time=>2020-05-16 13:14:59 +0900}], @job_id="4d9a3417-c746-4a07-ab5c-ae2fb4c91bb3", @queue_name="awesome", @priority=nil, @executions=0, @exception_executions={}, @scheduled_at=1589602799.83359, @provider_job_id="95b306e3849ffeb3499ca0ee">

추가된 결과는 사이드킥 대시보드의 Scheduled 메뉴에서 확인할 수 있습니다. 따로 사이드킥 워커를 실행해놓지 않았기 때문에 5분이 지나도 이 작업이 실행되지는 않습니다.

사이드킥 대시보드에서 추가된 작업을 확인할 수 있습니다

그렇다면 이번에는 이 작업을 객체로 가져온 다음에 삭제해보도록 하겠습니다.

sq = Sidekiq::ScheduledSet.new
sq.each {|j| puts j.value }
# {"retry":true,"queue":"awesome", ...

sq 변수는 모든 예약 작업을 가지고 있습니다. 이 변수는 이터레이터를 지원해서 each와 같은 메서드를 사용하면 모든 예약 작업의 내용을 확인하는 게 가능합니다. 잡은 사이드킥의 Sidekiq::SortedEntry 클래스이며, 잡의 내용은 value 속성에 담겨있습니다. 이 값은 레디스에 JSON 문자열로 저장되어있기 때문에 객체로 접근하기 위해서는 먼저 JSON으로 파싱을 해주어야합니다.

sq.each {|j| puts JSON.parse(j.value)['queue'] }
# awesome

파싱한 결과에서 queue 속성을 확인해보면 awesome이 출력됩니다. JSON을 파싱하는 대신 args 메서드를 사용해도 결과는 같습니다.

sq.each {|j| j.args[0]['arguments'][0]['queue'] }
# awesome

앞서 잡에 넘겨준 인자값을 확인해봅니다. 조금 까다롭습니다만, 데이터 형식을 쫓아가면 넘겨준 값을 찾을 수 있습니다.

sq.each {|j| puts j.args[0]['arguments'][0]['foo'] }
# bar

args는 배열로 되어있습니다. 그 안에 다시 arguments 안에 넘겨준 인자값들이 배열로 저장되어있습니다. 첫 번째 인자의 foo 속성을 확인하면 bar가 들어있는 것을 확인할 수 있습니다.

큐의 이름이나 인자값들의 내용을 통해서 삭제하고자하는 작업을 필터링 할 수 있습니다.

간단한 예제를 위해 먼저 100개의 작업을 추가해보겠습니다.

1.upto(100).each do |i|
  AwesomeJob.set(wait: 1.hour).perform_later({id: i})
end

sq = Sidekiq::ScheduledSet.new
sq.count
# 101

101개의 예약 작업이 쌓여있는 것을 확인할 수 있습니다.

id가 55번인 작업만 골라서 삭제해보겠습니다.

sq.each do |j|
  id = j.args[0]['arguments'][0]['id']
  j.delete if id == 55
end

sq.count
# 100

사이드킥 대시보드에서도 확인해볼 수 있습니다.

id가 55인 작업이 삭제된 것을 확인할 수 있습니다.

이번에는 id 값이 80 이상인 잡들만 삭제해보겠습니다.

sq.each do |j|
  id = j.args[0]['arguments'][0]['id']
  j.delete if id && id >= 80
end

sq.count
# 79

사이드킥 대시보드에서도 확인할 수 있습니다.

id가 80 이상인 잡이 삭제된 것을 확인할 수 있습니다.

사이드킥 대시보드에도 잡을 삭제하는 기능이 추가되어서 한 2개의 작업을 삭제하는 경우에는 이 방법을 써도 무방합니다. 하지만 실무에서는 수만개 이상의 작업에서 조건에 맞는 작업만을 삭제하려면 사이드킥의 API를 활용하는 게 유용합니다.

큐에 있는 작업을 삭제하기

큐에 있는 작업은 다음과 같이 가져올 수 있습니다.

Sidekiq::Queue.new("queue_name")

필터링해서 삭제하는 방법은 스케줄 잡과 같습니다.

리트라이 셋과 데드 셋도 가져올 수 있습니다.

# 리트라이 셋에 저장된 작업들
Sidekiq::RetrySet.new

# 데드 셋에 저장된 작업들
Sidekiq::DeadSet.new

이외에도 사이드킥 API를 사용해 다양한 작업들을 수행할 수 있으니 더 자세한 내용은 다음 문서를 참고해주세요.