#171 Delayed Job (revised)
- Download:
- source codeProject Files in Zip (84.4 KB)
- mp4Full Size H.264 Video (23.2 MB)
- m4vSmaller H.264 Video (10.5 MB)
- webmFull Size VP8 Video (10.6 MB)
- ogvFull Size Theora Video (25.6 MB)
구독자들에게 뉴스레터를 발송하는 레일스어플리케이션 웹페이지가 아래에 있습니다. 이 웹페이지에는 뉴스레터 목록이 있으며 “Deliver” 링크를 클릭하면 해당 뉴스레터를 발송하게 됩니다.
“Deliver” 링크를 클릭하여 메일발송을 처리하는데는 오랜 시간이 소요됩니다. 레일스 인스턴스가 메일발송 요청을 처리하고 있는 동안에는 다른 요청을 받을 수 없기 때문에 이것은 좋은 방법이 되지 못합니다. 처리시간이 오래 걸리는 요청이 필요한 경우에는 해당 요청을 백그라운드에서 처리할 수 있도록 해야 합니다. 이렇게 하면 이러한 요청이 즉각적으로 반응할 수 있게 되며 장시간이 소요되는 작업은 백그라운드에서 처리될 것입니다.
레일스에서는 다양한 방법으로 백그라운에서 작업을 처리할 수 있습니다. Delayed Job은 어플리케이션이 사용하는 동일한 데이터베이스를 사용하므로 별도의 추가설정 부분이 없기 때문에 가장 손쉽게 설치해서 사용할 수 있는 방법 중의 하나가 될 수 있습니다. 이것은 간단한 인터페이스를 제공해 주는데, delay
메소드를 통해서 어떠한 메소드라도 호출하면 특정 작업을 백그라운에서 처리할 수 있게 됩니다. 여기에서는 Delayed Job을 이용하여, 뉴스레터발송 작업을 백그라운드로 처리할 것입니다.
Delayed Job 설정하기
Delayed Job 관련 젬들이 몇가지 있는데, 여기서는 어플리케이션에서 ActiveRecord를 사용할 것이기 때문에 delayed_job_active_record
젬을 gemfile에 추가해서 bundle install 명령으로 설치해야 합니다.
source 'http://rubygems.org' gem 'rails', '3.1.3' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.5' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'delayed_job_active_record'
Delayed Job은 DataMapper와 Mongoid와 같은 백엔드 툴을 지원하므로 어플리케이션에서 이들 중에 하나를 사용할 경우에는 이에 관한 정보가 들어 있는 README 파일을 읽어 보기 바랍니다.
Delayed Job 설정을 마무리하기 위해서 제너레이터를 실행해야 합니다. 이것은 작업을 처리하기 위한 테이블을 만들기 위해 마이그레이션 파일을 만들게 되는데 이때, rake db:migrate
명령을 실행해서 해당 테이블을 생성합니다.
$ rails g delayed_job:active_record create script/delayed_job chmod script/delayed_job create db/migrate/20120109185353_create_delayed_jobs.rb
Delayed Job을 설정을 마친 후 실제로 실행하게 되는데, jobs:work
라 불리는 Rake 작업이 이러한 일을 하게 될 것입니다.
$ rake jobs:work [Worker(host:noonoo.home pid:3031)] Starting job worker
어플리케이션에서 Delayed Job 사용하기
Delayed Job 설정후 레일스 어플리케이션을 변경할 수 있게 되는데, “Deliver” 링크를 클릭하면 백그라운드에서 장시간이 소요되는 작업이 처리될 것입니다. 해당 링크를 클릭할 때 NewslettersController
의 deliver
액션이 호출되며, 이때 sleep 10 명령으로 이메일을 전달하는데 10초정도 소요되도록 시뮬레이션할 수 있습니다.
def deliver @newsletter = Newsletter.find(params[:id]) sleep 10 # simulate long newsletter delivery @newsletter.update_attribute(:delivered_at, Time.zone.now) redirect_to newsletters_url, notice: "Delivered newsletter." end
먼저, 실행하는데 장시간이 소요되는 코드를 별도의 메소드로 분리하게 되는데, 이때, 이와 연관이 있는 모델로 옮기는 것이 일반적으로 좋은 생각이라고 할 수 있습니다. 여기에서는 장시간이 소요되는 컨트롤러상의 코드를 Newsletter
모델의 deliver
메소드로 옮길 것입니다.
def deliver @newsletter = Newsletter.find(params[:id]) @newsletter.deliver redirect_to newsletters_url, notice: "Delivered newsletter." end
이제 deliver
메소드를 만들어서 컨트롤러에서 발췌한 코드를 붙여넣을 것입니다. 그리고 코드를 약간 수정하게 되는데, @newsletter
인스턴스 변수를 제거하고 현재의 인스턴스에 대해서 update_attribute
를 호출하도록 합니다.
class Newsletter < ActiveRecord::Base def deliver sleep 10 # simulate long newsletter delivery update_attribute(:delivered_at, Time.zone.now) end end
장시간이 소요되는 작업을 하나의 메소드로 만들었기 때문에 컨트롤러에 있는 delay
메소드를 호출하여 이 작업을 백그라운드에서 쉽게 처리할 수 있게 됩니다.
def deliver @newsletter = Newsletter.find(params[:id]) @newsletter.delay.deliver redirect_to newsletters_url, notice: "Delivering newsletter." end
이 코드는 데이터베이스 테이블 delayed_jobs
에 새로운 레코드를 하나 추가해서 Delayed Job이 newsletter 인스턴스에 대해서 deliver
메소드를 호출하도록 할 것입니다. 이 때 플래시 알림메시지를 변경해서 실제로 일어나는 작업에 대해서 알 수 있도록 했습니다. 이제 “Deliver” 링크를 클릭하면 거의 동시에 페이지가 업데이트되지만 방금 전에 보낸 뉴스레터가 delivered 로 표시되지는 않습니다.
대략 10초 정도 기다린 후에 다시 페이지를 로드하면 해당 뉴스레터가 delivered 로 표시된 것을 확인할 수 있는데, 백그라운드 프로세스가 해당 작업을 처리완료했기 때문입니다.
객체를 더 간단하게 저장하기
Delayed Job은 데이터베이스에 객체들을 저장한 후에 작업을 합니다. delay
를 호출하는 대상 객체 뿐만아니라 메소드 호출시 사용하는 인수들까지도 Delayed Job은 데이터베이스에 YAML 형식으로 시리얼로 저장한 상태에서 백그라운에서 작업을 하게 됩니다. 일반적으로 이것이 문제가 되지 않지만 경험상 Delayed Job을 이용할 때 이러한 작업 대상 객체들을 더 간단하게 만들어 사용하는 것이 더 좋습니다. 예를 들면, newsletter 인스턴스 전체를 사용하는 대신 Newsletter
클래스와 id
값을 아래와 같이 직접 사용할 수 있습니다.
def deliver Newsletter.delay.deliver(params[:id]) redirect_to newsletters_url, notice: "Delivering newsletter." end
이제 인스턴스 대신에 클래스에 대해서 delay
를 호출하여 간단하게 id
값을 넘겨줄 것입니다. 다음에는 Newsletter
클래스에서 deliver
클래스 메소드를 작성하여 해당 id
값을 가진 newsletter를 불러와 deliver
인스턴스 메소드를 호출할 것입니다.
class Newsletter < ActiveRecord::Base def self.deliver(id) find(id).deliver end def deliver sleep 10 # simulate long newsletter delivery update_attribute(:delivered_at, Time.zone.now) end end
이와 같이 추가한 코드량이 많지 않지만 Delay Job의 작업 큐(job queue)를 훨씬 더 간단하게 해 줄 것입니다.
Delay 메소드에 옵션 지정하기
Delayed Job은 delay
메소드에 대해서 여러가지 유용한 옵션을 제공해 줍니다. 그 중 한가지는 큐(queue)라는 옵션이며 해당 큐에 대해서 이름을 지정할 수 있게 해 줍니다. 이것은 여러개의 worker가 각각 다른 큐에 대해서 작업을 할 수 있게 해 줍니다.
Newsletter.delay(queue: "newsletter").deliver(params[:id])
또 다른 유용한 옵션으로 priority
라는 것이 있습니다. 이것은 디폴트로 0
값을 가지지만 더 큰 값을 설정하게 되면 해당 작업이 먼저 처리될 것입니다. 마찬가지로 더 작은 값으로 설정하여 해당작업은 더 나중에 처리될 것입니다. 또한 run_at
옵션을 지정하여 해당 작업의 시작 시점을 명시할 수 있습니다.
Newsletter.delay(queue: "newsletter", priority: 28, run_at: 5.minutes.from_now).deliver(params[:id])
Delayed Job은 다양한 방법으로 작업내용을 큐에 추가할 수 있게 지원합니다. 지금까지 사용한 delay
메소드는 선호되는 접근법이며 대부분의 경우에서 작동합니다. 만약 특정 클래스의 특정 메소드가 비동기적으로 항상 호출되도록 하고자만 한다면 클래스내에 handle_asynchronously
를 사용해서 메소드명을 심볼로 넘겨줄 수 있습니다. 그러면 해당 메소드가 호출될 때마다 Delayed Job이 작동하게 될 것입니다. 또 다른 접근법은 특별한 용도의 커스텀 작업을 정의하는 것인데, 이 때는 특정 작업을 위해 커스텀 클래스를 작성하여 perform
메소드를 정의해 주면 됩니다. 그러면, 다음과 같이, enqueue
메소드를 호출하여 직접 작업을 큐에 추가할 수 있게 됩니다.
class NewsletterJob < Struct.new(:text, :emails) def perform emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) } end end Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))
이 옵션은 더 많은 제어권을 제공해 주지만 대개는 불필요합니다.
실패한 작업을 처리하기
Delayed Job은 또한 실패할 경우에 대한 지원도 빼놓지 않았습니다. 특정 작업이 처리되는 과정에 예외상황이 발생했다고 가정해 보겠습니다. 이러한 경우 해당 작업은 나중에 다시 시도될 것입니다. 그러나 이것이 경우에 따라서는 문제를 야기할 수 있기 때문에 주의를 요합니다. 여기서의 작업내용을 고려할 때, 이메일 발송 중간에 실패하여 다시 발송하게 될 경우, 동일한 사람에게 재차 이메일이 발송될 수 있는 상황이 벌어지게 되므로 이와 같이 비동기적으로 메소드가 한번 이상 호출될 때 발생하는 부작용에 대해서 면밀히 검토해 볼 필요가 있습니다. 여기서는 특정 뉴스레터가 이미 전달된 사람들에 대한 목록을 기록해 둘 필요가 있습니다.
initializer을 만들어 몇가지 설정을 해 두면 이러한 기능을 구현할 수 있습니다.
Delayed::Worker.max_attempts = 5 Delayed::Worker.delay_jobs = !Rails.env.test?
위의 초기화 파일에서, Delayed::Worker
에 대한 옵션을 설정해 두었습니다. 작업이 실패할 경우 해당 작업의 재시도 횟수의 최대치를 설정하였고 현재의 작업환경이 test
일 경우에는 백그라운드에서 처리되지 않도록 설정했습니다.
운영서버에서 Delayed Job 사용하기
지금까지는, rake jobs:work
명령을 실행하여 Delayed Job을 시작해 왔지만, 실제 운영서버에서는 script
폴더에 제공된 delayed_job
스크립트를 사용해야 합니다. 다음과 같이 실행하면 시작할 수 있습니다.
$ script/delayed_job start
만약 개발환경에서 이와 같이 실행할 경우에는 예외가 발생할 수 있는데 이때는 gemfile에 daemons
젬을 추가하여 bundle
명령을 다시 실행할 필요가 있습니다.
gem 'daemons'
이와 같이 작업을 마친 후에 다시 스크립트를 실행하면 다음과 같이 작동해야 합니다.
$ script/delayed_job start delayed_job: process with pid 1672 started.
해당 스크립트의 실행을 중단하고자 할 때는 stop
옵션을 넘겨주면 됩니다.
$ script/delayed_job stop
Delayed Job을 Capistrano와 함께 사용할 경우에는 wiki 페이지를 방문하여 해당 토픽을 읽어보기 바랍니다. 몇가지 recipes가 제공되는데, require "delayed/recipes"
를 추가한 후, Delayed Job을 시작하고 중단하는데 필요한 task를 배포 스크립트(deploy.rb)에 추가합니다.
웹 인터페이스를 이용해서 작업 큐를 모너터링하기를 원한다면 Delayed Job Web gem을 살펴볼 필요가 있습니다. 이 젬은 쉽게 설치해서 사용할 수 있고 작업 큐를 관리하기 위한 멋진 인터페이스를 제공해 줍니다.
Delyed Job은 모든 경우에서 완벽하지는 못합니다. 따라서 Resque와 Beanstalkd과 같은 대체 툴을 고려해야 합니다. 이들에 대한 사용예를 찾아 볼 수 있습니다.