Создание CSV и загрузка его на S3 после завершения фонового задания

Я предоставляю пользователям возможность загружать чрезвычайно большие объемы данных через CSV. Для этого я использую Sidekiq и переношу задачу в фоновое задание, как только они ее инициируют. Что я сделал в фоновом задании, так это сгенерировал csv, содержащий все нужные данные, сохранил их в /tmp, а затем вызвал save! на моей модели, передавая местоположение файла атрибуту скрепки, который затем отключается и сохраняется в S3.

Все это прекрасно работает локально. Теперь моя проблема связана с Heroku, и его способность хранить файлы в течение короткого времени зависит от того, на каком узле вы находитесь. Моя фоновая работа не может найти сохраненный файл tmp из-за того, как Heroku обрабатывает эти файлы. Думаю, я ищу лучший способ сделать это. Если есть какой-то способ сделать все в памяти, это было бы здорово. Единственная проблема заключается в том, что скрепка ожидает фактический файловый объект в качестве атрибута при сохранении модели. Вот как выглядит моя фоновая работа:

class CsvWorker
  include Sidekiq::Worker

  def perform(report_id)
    puts "Starting the jobz!"
    report = Report.find(report_id)
    items = query_ranged_downloads(report.start_date, report.end_date)

    csv = compile_csv(items)

    update_report(report.id, csv)
  end

  def update_report(report_id, csv)
    report = Report.find(report_id)
    report.update_attributes(csv: csv, status: true)
    report.save!
  end

  def compile_csv(items)
    clean_items = items.compact
    path = File.new("#{Rails.root}/tmp/uploads/downloads_by_title_#{Process.pid}.csv", "w")
    csv_string = CSV.open(path, "w") do |csv|
      csv << ["Item Name", "Parent", "Download Count"]
      clean_items.each do |row|
        if !row.item.nil? && !row.item.parent.nil?
        csv << [
          row.item.name,
          row.item.parent.name,
          row.download_count
          ]
        end
      end
    end

    return path
  end
end

Я пропустил метод запроса для удобства чтения.


person John    schedule 28.08.2012    source источник


Ответы (1)


Я не думаю, что проблема с временным хранилищем файлов Heroku. Предупреждения вокруг этого в основном сосредоточены на фактах, что а) динамометры эфемерны, поэтому все, что вы пишете, может и исчезнет без предварительного уведомления; и б) динамометры взаимозаменяемы, поэтому наличие временных файлов между запросами — это вопрос удачи, когда у вас запущено более одного веб-динамометра. Однако ни в коем случае временные файлы не исчезают во время работы вашего воркера.

Я заметил одну вещь: вы фактически создаете два временных файла с одинаковым именем:

> path = File.new("/tmp/filename", "w")
 => #<File:/tmp/filename> 
> path.fileno
 => 3 
> CSV.open(path, "w") do |csv| csv << %w(foo bar baz); puts csv.fileno end
4
 => nil 

Вы можете изменить строку path =, чтобы просто указать имя файла (вместо того, чтобы открывать его для записи), а затем сделать так, чтобы update_report открывала имя файла для чтения. Я не вникал в то, что делает Paperclip, когда вы даете ему пустой, уже перезаписанный, открытый для записи дескриптор файла, но изменение этого потока вполне может решить проблему.

В качестве альтернативы вы можете сделать это в памяти: сгенерировать CSV как строку и передать ее Paperclip как StringIO. (Paperclip поддерживает некоторые нефайловые объекты, включая StringIO, используя, например, Paperclip::StringioAdapter. ) Попробуйте что-то вроде:

# returns a CSV as a string
def compile_csv(items)
  CSV.generate do |csv|
     # ...
  end
end

def update_report(report_id, csv)
  report = Report.find(report_id)
  report.update_attributes(csv: StringIO.new(csv), status: true)
  report.save!
end
person willglynn    schedule 09.09.2012
comment
Отличный улов! На самом деле я остановился на подходе StringIO. - person John; 09.09.2012
comment
У меня есть TypeError: нет неявного преобразования CSV в String с этим подходом, рельсы 3.2 - person xamenrax; 19.03.2015