2

An interesting design choice/discussion has cropped up in code review, and I would like to understand more about proposed solutions. The original review includes re-factoring of a messy piece of code which I cannot share, but I think focussing on the problems with the original code is skewing opinions about what a good solution should look like.

We have a Ruby/Sinatra web application with many Sequel ORM classes, as well as separate modules that perform tasks. Some of the tasks are slow and can be run asynchronously, so we offload them from the web server via a jobs mechanism. This uses a beanstalkd queue to pass parameters of the task from web process to worker process.

It is the API design of the job scheduler that is under discussion.

For example, we might have methods as follows:

module Foo
  def self.slow_generic_task params
  end
end

class Bar < Sequel::Model
  def slow_instance_method params
  end
end

The two approaches for a job scheduler we are considering are:


1. Job interface only invokes class or module methods

This simplifies the method signature and serialisation of jobs in the queue. The job interface looks like this:

Job.schedule( Foo, :slow_generic_task, params )
Job.schedule( Bar, :slow_instance_method, id, params )

and the serialisation of jobs in the queue is simple. However, it requires for each target that is an instance method that we add a adaptor method:

class Bar < Sequel::Model
  def self.slow_instance_method id, params
    self[id].slow_instance_method params
  end
end

Concerns with this approach are:

  • Requires adding this "shim" class method as an implied part of the API (so it hasn't really simplified the API so much as moved some of the complexity out of syntax and into design).

  • There does not seem to be a good reason to have an "instantiate by id and do something with the instance" method on a class other than to fulfil the contract implied by the Job scheduler design.


2. Job interface is extended to disambiguate singleton calls from ORM instance methods

This involves having an interface to job system that allows disambiguation. E.g.

Job.schedule_singleton( Foo, :slow_generic_task, params )
Job.schedule_instance( Bar, :slow_instance_method, id, params )

or we could accept an instance of Bar and disambiguate inside the schedule function:

Job.schedule( Foo, :slow_generic_task, params )
Job.schedule( bar, :slow_instance_method, params )

(that is how the original code being re-factored did it). Either way, the serialisation of the jobs in the queue, and handling them in the worker process that reads the queue has become more complex.

The good thing about this approach is that no modifications to Bar are required. Any ORM-based object instance method we want to defer to separate worker process can be handled by this interface as-is.

Concerns with this approach are:

  • It requires additional logic or additional methods to disambiguate types of object being passed to the scheduler.

  • It requires a more complex serialisation in the jobs queue.

  • It requires additional logic at the other end inside the worker code to decide whether to construct the target object as ORM or call directly as a singleton method.


I of course have an opinion, but have tried to present this neutrally.

My question(s): Are any of the concerns or advantages of the above approaches - or any additional analysis - serious enough to make a clear decision for a "better" design? Are there better alternatives we have been blinded to by focussing on the differences between these two approaches?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.