Rails crono를 이용하여 스케줄링하기(Scheduling with crono on Rails)

보통 유닉스 기반 PC에선 스케줄링을 위해 crontab 또는 at를 사용하고, 윈도우에선 서비스에 등록하는식으로 사용합니다. 이를 위해선 시스템에서 crontab 같은 도구 사용이 가능해야하며, PaaS 이상의 추상화가 이루어진 플랫폼에선 사용하기 어려운 경우가 대다수입니다.

오늘은 Rails에서 crono를 이용해서 Application 단에서 스케줄링하는 방법에 대해 이야기할까 합니다.

crono - What is? How to Use?

crono는 루비 Gem으로 제공되는 스케줄링 관련 라이브러리입니다. 이름에서 느껴지듯이 crontab같은 느낌의 라이브러리이지요. 보통은 crontab을 직접 제어하는 경우가 많은데 crono의 경우는 자체적으로 스케줄링을 관리하고 처리하는 방식으로 동작합니다.

우선 젬 파일에 추가해주고 bundle install로 설치해줍시다.

Gemfile

gem 'crono'

설치가 완료되면 rails에서 crono를 사용할 수 있어집니다. 아래 명령으로 rails 앱에서 crono를 자동으로 세팅할 수 있습니다.

rails generate crono:install

명령을 수행하면 config/cronotab.rb 파일과 db 스키마 생성 파일까지 2개 파일이 생성됩니다.

Running via Spring preloader in process 64394
      create  config/cronotab.rb
      create  db/migrate/20190509001743_create_crono_jobs.rb

우선 원래 필수적인 작업은 아닌데, 특정 레일즈 버전에서 발생하는 에러가 있어서 아래 방식으로 변경해주시면 좋습니다. ActiveRecord 상속받을 떄 뒤에 버전정보 명시가 필요합니다.

Migrate 수정

class CreateCronoJobs < ActiveRecord::Migration[5.2] # 뒤에 버전정보 붙여줘야함
  def self.up
    create_table :crono_jobs do |t|
      t.string    :job_id, null: false
      t.text      :log, limit: 1073741823 # LONGTEXT for MySQL
      t.datetime  :last_performed_at
      t.boolean   :healthy
      t.timestamps null: false
    end
    add_index :crono_jobs, [:job_id], unique: true
  end

  def self.down
    drop_table :crono_jobs
  end
end

안할 시 ActiveRecord::Migration is not supported. 관련 에러 발생하며 해당 에러는 레일즈 공통 이슈입니다. 아래 링크 참고해주세요. https://github.com/RolifyCommunity/rolify/issues/444

자 이제 rake로 디비 마이그레이션을 해줍시다.

rake db:migrate

그러면 기본적인 사용 준비는 모두 완료되었습니다.

In the Code

많이 쓰게될 것 같은 부분은 아까 위에서 생성된 crontab.rb에서 정의하거나 jobs에 별도로 정의하는 것이지 않을까 싶습니다.

config/crontab.rb

require 'rake'

Rails.app_class.load_tasks

class Test
  def perform
    #Rake::Task['crono:hello'].invoke
    p "abcd"
  end
end

Crono.perform(Test).every 5.seconds        # 5초마다 위의 Test 클래스의 perform 메소드 실행
Crono.perform(Cronjobb).every 5.seconds    # 5초마다 ActiveJob의 Cronjobb class의 perform 메소드 실행

Job이 될 class를 하나 만들어주시고 메소드로 perform을 만든 후 하위에 실제 처리할 로직을 넣어줍니다. 이후 Cfrono.perform(Class이름) 형태로 Job을 등록할 수 있으며 하위 메소드를 통해 주기나 기간, 반복 여부 등을 지정할 수 있습니다.

패턴 자체는 단순해서 딱 보시면 아 어떤식으로 넣는구나(suggest 보시면 더 확실해지죠) 싶고, 인자값으로 좀 더 특별한 로직 수행이 가능해집니다.

Crono.perform(TestJob).every 2.days, at: {hour: 15, min: 30}   # 2일에 한번씩, 15:30분에
Crono.perform(TestJob).every 1.week, on: :monday, at: "15:30"  # 1주일에 한번 월요일 15:30분에

등등 구현할 수 있는 방식은 많습니다. 그리고.. 만약 처리해야하는 Job에서 ActiveJob 사용이 필요하다면 jobs 로 만들어서 ActiveJob을 상속받아 사용하시면 됩니다.

jobs/cronjobb.rb (ActiveJob 사용하는 경우)

class Cronjobb < ActiveJob::Base
  def perform
    p "abcd"
  end
end

다 구현했으면 bundle 명령으로 구동이 가능합니다. 이때 Rails 서버는 Run중이여야 하고, 특이점으론 자동 시작을 위해서 Rails init 부분에 system() 등으로 넣어주면 동일한 요청이 엄청 생길 수 있기 때문에(재귀적으로 호출이 일어나죠….) 혹시나 저런 방식으로 사용하시더라도 정말 조심히 사용해야할 것 같습니다.

Run

bundle exec crono

실행하면 주기적으로 잘 반복되는걸 확인할 수 있습니다. ** **Output

bundle exec crono                      
I, [2019-05-09T22:01:37.122510 #7616]  INFO -- : Loading Crono 1.1.2
I, [2019-05-09T22:01:37.122576 #7616]  INFO -- : Running in ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
I, [2019-05-09T22:01:37.122594 #7616]  INFO -- : Jobs:
I, [2019-05-09T22:01:37.122766 #7616]  INFO -- : 'Test' with rule 'every 5 seconds' next time will perform at 2019-05-09 22:01:37 +0900
I, [2019-05-09T22:01:37.122835 #7616]  INFO -- : 'Cronjobb' with rule 'every 5 seconds' next time will perform at 2019-05-09 22:01:37 +0900
I, [2019-05-09T22:01:37.122921 #7616]  INFO -- : Perform Test
"abcd"
I, [2019-05-09T22:01:37.123211 #7616]  INFO -- : Finished Test in 0.00 seconds
I, [2019-05-09T22:01:37.123274 #7616]  INFO -- : Perform Cronjobb
"abcd"
I, [2019-05-09T22:01:37.127520 #7616]  INFO -- : Finished Cronjobb in 0.00 seconds
I, [2019-05-09T22:01:42.125759 #7616]  INFO -- : Perform Test
"abcd"
I, [2019-05-09T22:01:42.126101 #7616]  INFO -- : Perform Cronjobb
I, [2019-05-09T22:01:42.126212 #7616]  INFO -- : Finished Test in 0.00 seconds
"abcd"