Ruby TempFile behaviour among different classes

Viewed 261

Our processing server works mainly with TempFiles as it makes things easier on our side: no need to take care of deleting them as they get garbage collected or handle name collisions, etc.

Lately, we are having problems with TempFiles getting GCed too early in the process. Specially with one of our services that will convert a Foo file from a url to some Bar file and upload it to our servers.

For sake of clarity I added bellow a case scenario in order to make discussion easier and have an example at hand.

This workflow does the following:

  1. Get a url as parameter
  2. Download the Foo file as a TempFile
  3. Duplicate it to a new TempFile
  4. Download the related assets to TempFiles
  5. Link the related assets into the local dup TempFile
  6. Convert the Foo to Bar format
  7. Upload it to our server

At times the conversion fail and everything points to the fact that our local Foo file is pointing to related assets that have been created and GCed before the conversion.

My two questions:

  1. Is it possible that my TempFiles get GCed too early? I read about Ruby GCed system it was very conservative to avoid those scenarios.

  2. How can I avoid this from happening? I could try to save all related assets from download_and_replace_uri(node) and passing them as a return to keep it alive while the instance of ConvertService is still existing. But I'm not sure if this would solve it.

myfile.foo

{
  "buffers": [
    { "uri": "http://example.com/any_file.jpg" },
    { "uri": "http://example.com/any_file.png" },
    { "uri": "http://example.com/any_file.jpmp3" }
  ]
}

main.rb

  ConvertService.new('http://example.com/myfile.foo')

ConvertService

class ConvertService
  def initialize(url)
    @url = url
    @bar_file = Tempfile.new
  end

  def call
    import_foo
    convert_foo
    upload_bar
  end

  private

  def import_foo
    @foo_file = ImportService.new(@url).call.edited_file
  end

  def convert_foo
    `create-bar "#{@foo_file.path}" "#{@bar_file.path}"`
  end

  def upload_bar
    UploadBarService.new(@bar_file).call
  end
end

ImportService

class ImportService
  def initialize(url)
    @url = url
    @edited_file ||= Tempfile.new
  end

  def call
    download
    duplicate
    replace
  end

  private

  def download
    @original = DownloadFileService.new(@url).call.file
  end

  def duplicate
    FileUtils.cp(@original.path, @edited_file.path)
  end

  def replace
    file = File.read(@edited_file.path)
    json = JSON.parse(file, symbolize_names: true)
    json[:buffers]&.each do |node| 
      node[:uri] = DownloadFileService.new(node[:uri]).call.file.path
    end
    write_to_disk(@edited_file.path, json.to_json)
  end
end

DownloadFileService

module Helper
  class DownloadFileService < ApplicationHelperService
    def initialize(url)
      @url = url
      @file = Tempfile.new
    end

    def call
      uri = URI.parse(@url)
      Net::HTTP.start(
        uri.host, 
        uri.port, 
        use_ssl: uri.scheme == 'https'
      ) do |http|
        response = http.request(Net::HTTP::Get.new(uri.path))
        @file.binmode
        @file.write(response.body)
        @file.flush
      end
    end
  end
end

UploadBarService

module Helper
  class UploadBarService < ApplicationHelperService
    def initialize(file)
      @file = file
    end

    def call
      HTTParty.post('http://example.com/upload', body: { file: @file })
      # NOTE: End points returns the url for the uploaded file
    end
  end
end
1 Answers

Because of the complexity of your code and missing parts which may be obfuscated to us, the simple answer to your problem is to insure that your tempfile instance objects remain in memory throughout the lifecycle in which they are needed, otherwise they will get garbage collected immediately, removing the tempfile from the file system, and will lead to the the missing tempfile state you've encountered.

The Ruby Document for Tempfile states "When a Tempfile object is garbage collected, or when the Ruby interpreter exits, its associated temporary file is automatically deleted."

As per comments, others may find this conversation helpful when running into this problem.

Related