Anyone use metaprogramming to avoid tons of BG jobs?

After many years of building Ruby web apps with Resque or Sidekiq, I’ve found that the best practice for the contents of a job is to simply defer to another class that does the work (this technique works with however you manage bizlogic):

class ArchiveWidgetJob
  def perform(widget_id)
    widget = Widget.find(widget_id)
    WidgetService.new.archive(widget)
  end
end

Then, over time, you end up with a ton of jobs that just call a method. Anyone ever consider something like:

class RuleThemAllJob
  def perform(id,class_for_id,method,class_for_method)
    object  = Object.const_get(class_for_id).find(id)
    service = Object.const_get(class_for_method).new.send(method,object)
  end
end

(Obviously this could be more robust, but you get the idea)

The advantage that I can see is that you don’t need to make a new job almost ever, as long as you code is relatively consistent in structure (you could imagine a few jobs to handle a few common cases, maybe?)

Disadvantages are that it’s an indirection that is potentially confusing at first + I guess a whole lot of potential for command injection (though if a threat actor has access to your Redis/ValKey, you have bigger problems).

When I’ve done something like the metaprogramming you’re considering, I’ve regretted the lost visibility to the class name. If you want to see the list of jobs being run, you have to query at the parameter level instead of the job.

(That said, I do like the approach of separating the job from actual work. I like to think of jobs like controllers: they might access the database then turn things over to a service to do the work.)

Ah, ok that does jog my memory of a downside the last time I discussed this, because in addition to not knowing the class of what’s actually happening, you lose the ability to tune separate queues for separate jobs.

I guess a compromise would be some codegen based on e.g. a spec file or other formal description

But you can actually dynamically set the class names of the jobs using something like const_set if you really want to query the queues names. at the end, it just depends on how much magic are you willing to add for less boilerplate

Yeah, almost all of our jobs go through a CallableJob class:

class CallableJob < ApplicationJob

  def perform(callable_name, *, **)
    callable_name.constantize.call(*, **)
  end

end

It makes it super easy to background something that maybe started in the foreground. We also have a TrackedJob class that’s more complex that allows us to broadcast to the client-side when it’s completed. We haven’t really had any problems with the meta-programming piece. It’s still easy to grep for where a class name is used, and Honeybadger still separates errors by backtrace.