Export data to zip files on background using sidekiq in Rails

Mở đầu

Bài viết này mình sẽ giải quyết yêu cầu với đầu vào là một hệ thống có databases cần export ra file và nén trong thư mục zip, tất cả xử lý ở background.

Ví dụ chúng ta có một bảng posts và cần xuất dữ liệu ra file data_feed.txt và nén trong data_feed.zip.

Bảng posts và chúng ta xuất dữ liệu ra cần một vài trường cụ thể, và ở ví dụ bài viết này mình sẽ xuất ra các trường như sau:

id, title, content, user_name, created_at. Và các cột này cách nhau bởi dấu tab.

Tới đây với yêu cầu bài toán vậy mình sẽ tiến hành sử dụng sidekiq gem trong Rails để xử lý background. Nén file mình sử dụng gem rubyzip để thao tác với zip files.

Cài đặt sidekiq

Cài đặt và sử dụng sidekiq cũng đơn giản. Chúng ta add gem vào Gemfile và chạy bundle install

# Gemfile

gem 'sidekiq'

Cài đặt gem

$ bundle install

Để sử dụng sidekiq chúng ta cần cài đặt redis nữa. Cài đặt redis đơn giản như sau:

wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make

sudo cp src/redis-server /usr/local/bin/
sudo cp src/redis-cli /usr/local/bin/

Chạy redis-server và khởi động sidekiq là xong.

# Khởi động redis
$ redis-server

# Khởi động sidekiq trong rails root app

$ bundle exec sidekiq

Export data

Tới đây mình sẽ tạo một worker để làm nhiệm vụ export data vào zip file như yêu cầu ban đầu.

Tạo ExportDataWorker

$ rails g sidekiq:worker ExportData # will create app/workers/export_data_worker.rb
class ExportDataWorker
  include Sidekiq::Worker

  def perform
    # do something
  end
end

Sử dụng gem 'rubyzip'

# Gemfile

gem 'rubyzip'

Cài đặt gem

$ bundle install

Export data to zip files

Chúng ta quay lại export_data_worker.rb

class ExportDataWorker
  include Sidekiq::Worker

  def perform
    attributes = %w(id title content user_name created_at)
    headers = attributes.join("\t") << "\n"

    buffer = Zip::OutputStream.write_buffer do |out|
      # Ghi vào file `data_feed.txt`
      out.put_next_entry("#{Rails.root}/public/data_feed.txt")

      # Ghi nội dung dòng header vào file
      out.write headers

      # Ghi data vào file
      Post.includes(:user).find_in_batches(batch_size: 10000) do |group|
        group.each do |post|
          row_body = attributes.map{|a| post.send("export_#{a}")}.join("\t") << "\n"

          out.write row_body
        end
      end
    end

    File.open("#{Rails.root}/public/data_feed.zip", "wb") {|f| f.write(buffer.string) }
  end
end

Ở trên mình sử dụng hàm post.send("export_#{a}") để có thể customize giá trị đầu ra như mong muốn.

Như vậy mình cần viết một số hàm trong post.rb như sau:

# Post

def export_id
  id
end

def export_title
  title.titleize
end

def export_content
  content
end

def export_user_name
  user.name
end

def export_created_at
  created_at
end

Cuối cùng chúng ta có thể chạy worker đó trong background.

Chạy worker bất đồng bộ

ExportDataWorker.perform_async

Chạy worker sau khi gọi lệnh 5 phút.

ExportDataWorker.perform_in 5.minutes

Và nhiều cấu hình chạy nền khác xem ở https://github.com/mperham/sidekiq/wiki/Getting-Started

Sidekiq worker trong Heroku

Nếu bạn deploy trong heroku thì chỉ cần cài thêm addons redistogo.

Chạy lệnh sau để cấu hình biến môi trường cho redis

$ heroku config:set REDIS_PROVIDER=REDISTOGO_URL

Tạo Procfile tại root directory trong main app và thêm dòng sau:

# Procfile

worker: bundle exec sidekiq -c 3

Tham số -cconcurrency. Với heroku app host hobby thì mình cần cấu hình giá trị concurrency thấp như vậy thôi.

Hết rồi. Cảm ơn các bạn theo dõi bài viết nhé. 😃