Background

I wanted to do a quick review of setting up background jobs for a Sinatra app I have deployed with Heroku. My web app serves as a music archive where I can upload songs for reference. Naturally, I wanted the site to be able to handle lossless audio formats so I could have acccess to both the lossless and lossy formats. This is where Resque comes in. Resque is a Redis-backed Ruby library for creating job queues, and Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. In my example, I set up a jobs queue for transcoding my lossless audio formats to mp3 using ffmpeg. In this post, I’ll show you how I got Sinatra, ffmpeg, Resque, and Redis working together in Heroku.

RESTful Music Archive

A RESTful music archive with Sinatra that handles lossless and lossy audio formats.

Source: https://github.com/jescriba/music-archive

Setting up Resque

After installing and requiring the resque gem, I added some uploading logic to my route POST to artists/:id/songs that checks if the uploaded file format is of type audio/x-aiff among other lossless formats.

References: POST to artists/:id/songs Upload Song

To queue up the transcoding logic, I call: Resque.enqueue(SongTranscoder, upload_params) after the initial upload. The rest of the logic lives inside my SongTranscoder and Rakefile. Resque will automatically call the class perform method. In this method, I download the lossless file from s3 then shell out to ffmpeg to transcode to mp3.

  def self.perform(upload_params)
    s3 = Aws::S3::Resource.new(region: 'us-west-1')

    # Ensure we are working w/ symbols
    upload_params = upload_params.reduce({}) { |memo, (k, v)| memo.merge({ k.to_sym => v}) }
    extension = File.extname(upload_params[:lossless_url])
    # Download s3 lossless to local
    upload_params[:tempfile_path] = "/app/tmp/temp#{extension}"
    download_cmd_str = "curl -o #{upload_params[:tempfile_path]} #{upload_params[:lossless_url]}"
    download_cmd =  `#{download_cmd_str}`
    tempfile = File.new(upload_params[:tempfile_path], "r")

    # transcode to V0 mp3
    temp_fi_dirname = File.dirname(upload_params[:tempfile_path])
    lossy_file_path = "#{temp_fi_dirname}/#{upload_params[:song_name]}.mp3"
    cmd = "ffmpeg -i #{upload_params[:tempfile_path]} -codec:a libmp3lame -qscale:a 0 #{lossy_file_path}"
    `#{cmd}`
    lossy_file = File.new(lossy_file_path, "r")

    # upload lossy
    lossy_object_path = upload_params[:lossy_url].sub(upload_params[:base_url], "")
    s3_lossy_object = s3.bucket(BUCKET).object(lossy_object_path)

    # upload
    s3_lossy_object.upload_file(lossy_file, acl: 'public-read')
    s3_lossy_object.copy_to("#{s3_lossy_object.bucket.name}/#{s3_lossy_object.key}",
                            :metadata_directive => "REPLACE",
                            :acl => "public-read",
                            :content_type => "audio/mpeg",
                            :content_disposition => "attachment; filename='#{upload_params[:song_name]}.mp3'")

    # Clean up lossy temp file
    FileUtils.rm(lossy_file_path)

    # Clean up - delete temp file
    FileUtils.rm([upload_params[:tempfile_path]])
  end

References: SongTranscoder

Setting up Resque Server

Resque comes with an easy to set up server that’ll be helpful for monitoring jobs.

In my config.ru file, I map the /resque route to the Resque server:

require 'resque/server'
require_relative 'main'
require_relative 'globals'

$stdout.sync = true

map "/resque" do
  Resque::Server.use Rack::Auth::Basic do |username, password|
    [username, password] == [Globals::AUTH_USER, Globals::AUTH_PASSWORD]
  end

  run Resque::Server
end

map "/" do
  run Main
end

Configuring Heroku

Add Worker Dyno

Next I added a worker dyno for running the queue of jobs from the Rakefile:

QUEUE=* RACK_ENV=production bundle exec rake resque:work

Add Redis To Go

In my Rakefile and Globals, I set up Resque per rack environment to use different redis servers:

if ENV["RACK_ENV"] != 'production'
  Resque.redis = 'localhost:6379'
else
  uri = URI.parse(ENV["REDISTOGO_URL"])
  Resque.redis = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
end

Note: I set REDISTOGO_URL in my Heroku environment variables to that specified by the Heroku Redis To Go Add-On.

Add Ffmpeg Build Pack

Lastly, to get ffmpeg to actually work on Heroku, I added an ffmpeg build-pack.