ruby-on-railsruby-on-rails-4actionmailerdelayed-jobactionview

Crash in Rails ActionView related to I18n.fallbacks when invoked from delayed_job


I am trying to send an email from my rails 4 app like so (condensed version from the console):

> ActionMailer::Base.mail(from: 'mail@example.com', to: 'foo@example.com', subject: 'test', body: "Hello, you've got mail!").deliver_later

The mail would be sent by delayed_lob, in my local test setup I trigger it like so:

> Delayed::Job.last.invoke_job

However the job crashes with the following message:

Devise::Mailer#invitation_instructions: processed outbound mail in 56234.1ms
Performed ActionMailer::DeliveryJob from DelayedJob(mailers) in 56880.04ms
TypeError: no implicit conversion of nil into Array
    from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:51:in `concat'
    from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:51:in `block in <class:LookupContext>'
    from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:39:in `initialize_details'
    from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:205:in `initialize'
...

I have looked into the code of lookup_context.rb:51 GitHub, the problem is here:

register_detail(:locale) do
      locales = [I18n.locale]
      locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks  # < crashes here
# from the debugger I got:
# I18n.locale => :de
# I18n.fallbacks => {:en=>[]}

So obviously fallbacks does not contain my locale (:de) which results in a nil exception.

Apparently I18n.fallbacks is not configured correctly.

Question: How can I fix this?


Solution

  • I got the answer with the help of this blog post I found: https://sjoker.net/2013/12/30/delayed_job-and-localization/
    It contains half of what I need. The proposed solution goes like this:

    In order to propagate state from the time of creating a job, that state has to be transferred to the time the job is invoked by storing it on the job object in the database.
    In the blog post, the author only stores the current locale to localize the mail at invitation time. However I also needed to store the fallbacks which required a little bit of serialization.
    Here is my solution:

    # Add a state attributes to delayed_jobs Table
    class AddLocaleToDelayedJobs < ActiveRecord::Migration
      def change
        change_table :delayed_jobs do |t|
          t.string :locale    # will hold the current locale when the job is invoked
          t.string :fallbacks # ...
        end
      end
    end
    
    # store the state when creating the job
    Delayed::Worker.lifecycle.before(:enqueue) do |job|
      # If Locale is not set
      if(job.locale.nil? || job.locale.empty? && I18n.locale.to_s != I18n.default_locale.to_s)
        job.locale    = I18n.locale
        job.fallbacks = I18n.fallbacks.to_json
      end
    end
    
    # retrieve the state when invoking the job
    Delayed::Worker.lifecycle.around(:invoke_job) do |job, &block|
      # Store locale of worker
      savedLocale        = I18n.locale
      savedFallbacks     = I18n.fallbacks
    
      begin
        # Set locale from job or if not set use the default
        if(job.locale.nil?)
          I18n.locale    = I18n.default_locale
        else
          h              = JSON.parse(job.fallbacks, {:symbolize_names => true})
          I18n.fallbacks = h.each { |k, v| h[k] = v.map(&:to_sym) } # any idea how parse this more elegantly?
          I18n.locale    = job.locale
        end
    
        # now really perform the job
        block.call(job)
    
      ensure
        # Clean state from before setting locale
        I18n.locale      = savedLocale
        I18n.fallbacks   = savedFallbacks
      end
    end