Rails에서 SuckerPunch를 이용하여 비동기 작업 처리하기

Rails 구동중에 쓰레드 처리 시 아래처럼 원래 루비 구문인 Thread로 처리할 수 있습니다.

Thread.new do
  Rails.application.executor.wrap do
    system('ping -c 5 127.0.0.1')
    result = Net::HTTP.get(URI.parse("http://127.0.0.1:3000/zfd"))
    puts result
  end
end

다만 이런 쓰레드에 대한 관리를 매번 해줘야한다는 것과 공통적으로 사용되는 쓰레드를 구현하기 위해선 조금 번거로운 부분들이 있습니다. 오늘 이야기드릴 라이브러리는 레일즈에서 쓰레드 사용을 위한 그런 라이브러리입니다.

suckerpunch 입니다.

SuckerPunch

SuckerPunch는 위에서 이야기드린대로 쓰레드 관련 라이브러리이고, 정확히는 단일 Ruby 프로세스에서의 비동기 처리 라이브러리입니다.

https://github.com/brandonhilkert/sucker_punch

단일 어플리케이션 내부에서 Jobs을 생성하고 관리할 수 있기 때문에 Heroku같은 PaaS 환경에서 메모리나 비용적으로 효율적으로 사용할 수 있습니다. 다만 단일 프로세스 위에서 동작하기 때문에 부모가프로세스가 재 시작되면 당연히 초기화됩니다.

해당 라이브러리는 gem 패키지로 gem install 명령또는 Gemfile 이후 bundle install로 설치할 수 있습니다.

Installation

In Shell

gem install suckerpunch

In Gemfile

gem 'sucker_punch', '~> 2.0'

Case Study

설치 후 앱 내부에선 SuckerPunch를 모듈을 로드하여 사용할 수 있습니다. 아래 Rails 코드는 제가 재미삼아 만드는 앱인데 이를 예시로 한번 보겠습니다.

def action
  @log = Log.new(log_params)
  url = log_params["host"]
  hwulTest.new.scan_spider(url, @log)
  # 아래 hwulTest class에서 객체를 생성하고 멤버함수를 실행합니다.
end

# ... 생략 ...

class hwulTest
  include SuckerPunch::Job
  # SuckerPunch => Job 모듈을 가져옵니다.

  def scan_spider(url, obj)
    ActiveRecord::Base.connection_pool.with_connection do
      payload = "http://127.0.0.1:8081/JSON/~~~blahblah"
      res = HTTP.get(payload).body
      res = ActiveSupport::JSON.decode(res)

      p res["scan"]
      data = obj
      data.update(state:true) 
      data.update(data: res["scan"].to_i)
    end
  end
end

해당 컨트롤러에서 action 이 호출됬을 때 레일즈 메인 로직은 원하는 구문 처리 후 정상적으로 결과를 리턴해주고, hwulTest로 정의한 기능은 비동기로 백그라운드 처리됩니다.

With ActiveJob

SuckerPunch는 Rails의 ActiveJob으로도 사용할 수 있습니다. 아래와 같이 gernete로 job을 만들 때 sucker_punch를 지시해주어 만들면 기본적으로 해당 Gem의 기능을 사용할 수 있습니다.

rails g sucker_punch:job logger

Worker and Job

추가적으로 볼만한 옵션은 worker와 job입니다.

worker는 동시성을 의미하며 디폴트는 2로 설정되어 있습니다. 그래서 2개의 worker(thread)가 지정한 job을 처리하게 됩니다.

class LogJob
  include SuckerPunch::Job
  workers 4

  def perform(event)
    Log.new(event).track
  end
end

반대로 Job은 Queue Size를 의미합니다. 그래서 max_jobs를 지정해주면 최대 저장할 수 있는 job의 갯수, 즉 Queue의 크기를 제한할 수 있습니다.

class LogJob
  include SuckerPunch::Job
  max_jobs 10

  def perform(event)
    Log.new(event).track
  end
end