On the Existence of Struct::Group in Rails

I ran into a really strange case yesterday while working to move an app from bj to delayed_job. I won’t spend much time going into the details about why we’re making this switch, but suffice to say that we had a problem similar to the one GitHub describes in their blog post. The problem is that bj reloads the entire Rails stack for every request, which is terribly inefficient. Imagine if you had to restart your web browser every time you went to a new page or submitted a form. You’d be paying a “startup tax” to launch your browser with every single request. It doesn’t make sense architecturally, and it absolutely kills your CPU. The delayed_job plugin operates by leaving a single Rails instance open and available for processing requests asynchronously. It’s proven to be much faster in my limited testing.

In making the move to delayed_job, I checked out the readme, which suggests structuring things like so:

class NewsletterJob < Struct.new(:text, :emails)
  def perform
    emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
  end    
end

Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))

The idea here is that you can use a Struct to quickly create a class with a method named perform. When you enqueue a job for later, the perform method will be called with the parameters you provided. However cool this may be, it introduces a really interesting gotcha that I ran into almost immediately.

If your app has a Group model, you won’t be able to use it within your perform method.

Why is that? Because of the way that Ruby namespaces work, the etc module, and the fact that something called Struct::Group already exists in your Rails app.

Perhaps a code example will help explain how this could happen:

require 'etc' # in Rails, rails/railties/lib/rails/mongrel_server requires 'etc' 

class Group
  def foo
    puts "hello"
  end
end

class WTF < Struct.new(:whatever)
  def foo
   Group.new.foo
  end
end

Group.new.foo
WTF.new.foo

# OUTPUT # 
# hello
# NoMethodError: undefined method ‘foo’ for #

The Group.new.foo call will work as expected, but the WTF.new.foo call will fail because it’s calling the foo method on Struct::Group, which (surprisingly enough) exists, and doesn’t have a method named foo. It exists because Rails has required the ‘etc’ module. This creates a couple of Structs on your behalf, which is the source of our problem.

Luckily, there’s an easy workaround. If you prefix your calls to Group with two colons, you’ll get access to the Group class that you expect. In our example, the perform method in WTF would be changed like so:

class WTF < Struct.new(:whatever)
  def foo
   ::Group.new.foo
  end
end

Totally weird. I know.

Published by

Trevor Turk

A chess-playing machine of the late 18th century, promoted as an automaton but later proved a hoax.

5 thoughts on “On the Existence of Struct::Group in Rails”

  1. Thanks for the heads-up.

    How is

    class NewsletterJob < Struct.new(:text, :emails)

    end

    different from

    class NewsletterJob

    attr_accessor :text, :emails

    end

    ?

  2. Not weird at all, it's just a scoping issue… if you're asking for Group inside of a scope that has it defined, it will return the first one instead of the one outside of the first scope checked (which is usually always the local scope).

    When you prefix two colons like ::Group you're actually saying Object::Group which tells Ruby to look in the scope of the Object class which will see the right class because the other one is only local inside of the Struct class object.

    If I can clarify further, let me know.

    Matt

  3. Maybe I should say… surprising? In my case, I didn't expect that calling Group.find from within a Struct would be any different from calling it elsewhere in my app. The fact that Struct::Group already existed came from way out in left field, to me at least.

Comments are closed.