What I learnt today from reading gems' code

… and check why 5600+ Rails engineers read also this

What I learnt today from reading gems’ code

Today I was working on chillout.io client and while I was debugging some parts, I had a look at some Ruby gems. This is always an interesting experience because you can learn how other developers design their API and how different it can be from your approach.

Sidekiq

So here are some interesting bits from sidekiq code.

Sidekiq::Client initializer

module Sidekiq
  class Client
    def initialize(redis_pool=nil)
      @redis_pool = redis_pool ||
      Thread.current[:sidekiq_via_pool] ||
      Sidekiq.redis_pool
    end
  end
end

Quoting the documentation:

Sidekiq::Client normally uses the default Redis pool but you may pass a custom ConnectionPool if you want to shard your Sidekiq jobs across several Redis instances…

I generally don’t like globals as a gem consumer but sometimes they are convenient and provide the convention over configuration magical feeling.

The nice thing about this global is that you don’t need to use it. It is easily overridable with such constructor. If you have specific requirements, your own connection pool, special redis connection, multiple clients and multiple connections etc, etc, you can still get the work done.

Sidekiq::Client.new(ConnectionPool.new { Redis.new })

Delegating class methods

Going further with global which you don’t need to use.

module Sidekiq
  class Client
    def push(item)
      # ...
    end

    def self.push(item)
      new.push(item)
    end
  end
end

With this code, instead of

Sidekiq::Client.new().push(
  'queue' => 'one',
  'class' => MyWorker,
  'args'  => ['do_it']
)

you can do

Sidekiq::Client.push(
  'queue' => 'one',
  'class' => MyWorker,
  'args'  => ['do_it']
)

Again. No one forces you to use the class method. If for any reason, the first approach works better than the second, if you need to have a new instance with specific constructor arguments, do it. Sidekiq can handle both.

Sidekiq.redis_pool

module Sidekiq
  def self.redis_pool
    @redis ||= Sidekiq::RedisConnection.create
  end

  def self.redis=(hash)
    @redis = if hash.is_a?(ConnectionPool)
      hash
    else
      Sidekiq::RedisConnection.create(hash)
    end
  end
end

This redis=(hash) setter can handle a Hash with redis configuration options or a Sidekiq::ConnectionPool instance.

yielding for configuration

module Sidekiq
  def self.server?
    defined?(Sidekiq::CLI)
  end

  def self.configure_server
    yield self if server?
  end

  def self.server_middleware
    @server_chain ||= default_server_middleware
    yield @server_chain if block_given?
    @server_chain
  end

  def self.default_server_middleware
    Middleware::Chain.new
  end
end

Quoting the documentation:

Sidekiq has a similar notion of middleware to Rack: these are small bits of code that can implement functionality. Sidekiq breaks middleware into client-side and server-side.

  • Server-side middleware runs ‘around’ job processing.
  • Client-side middleware runs before the pushing of the job to Redis and allows you to modify/stop the job before it gets pushed.

So the sidekiq client is the app (usually a Rails app) responsible for pushing jobs and scheduling them.

Sidekiq server is the worker process that execute on a different machine for processing jobs in the background.

Sidekiq needs to know which mode it is in, and it needs to have the ability to have different configurations for both of them. Especially considering that usually it is the same Rails application running either in client mode (http application server such as puma or unicorn) or server mode (worker process executed with sidekiq command).

The configuration can be set such as:

Sidekiq.configure_server do |config|
  config.redis = { namespace: 'myapp', size: 25 }
  config.server_middleware do |chain|
    chain.add MyServerHook
  end
end
Sidekiq.configure_client do |config|
  config.redis = { namespace: 'myapp', size: 1 }
end

So the configure_server method yields the block only when the if-statement evaluates we are in a server process. It uses block for lazy configuration. It is not evaluated when unnecessary (in the client).

server_middleware yields for nicer readability, I believe. Especially in the case of many middlewares.

BTW. chillout.io client uses a middleware to schedule sending metrics when a background job is done.

ActiveSupport

ActiveSupport::TaggedLogging

ActiveSupport::TaggedLogging wraps any standard Logger object to provide tagging capabilities.

logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
logger.tagged('BCX') { logger.info 'Stuff' }           # Logs "[BCX] Stuff"
logger.tagged('BCX', "Jason") { logger.info 'Puff' }   # Logs "[BCX] [Jason] Puff"

There is one method which brought my attention:

module ActiveSupport
  module TaggedLogging
    def flush
      clear_tags!
      super if defined?(super)
    end
  end
end

I’ve never seen this super if defined?(super) but it turns out it is useful to dynamically figure out if the ancestor defined given method and you should call it or this is the first module/class in inheritance chain which defines it.

class Fool
  def foo
    puts "foo from Fool"
  end
end

module Baron
  def bar
    puts "bar from Baron"
  end
end

module Bazinga
  def baz
    puts "baz from Bazinga"
    super if defined?(super)
  end
end

module Freddy
  def fred
    puts "fred from Freddy"
    super if defined?(super)
  end
end

class Powerful < Fool
  include Baron
  prepend Freddy

  def foo
    puts "foo from Powerful"
    super if defined?(super)
  end

  def bar
    puts "bar from Powerful"
    super if defined?(super)
  end

  def baz
    puts "baz from Powerful"
  end

  def fred
    puts "fred from Powerful"
  end

  def qux
    puts "qux from Powerful"
    super if defined?(super)
  end

  def corge
    puts "corge from Powerful"
    super
  end
end

p = Powerful.new
p.extend(Bazinga)

# inheritance
p.foo
# foo from Powerful
# foo from Fool

# module included in class
p.bar
# bar from Powerful
# bar from Baron

# object extended with module
p.baz
# baz from Bazinga
# baz from Powerful

# module prepended in class
p.fred
# fred from Freddy
# fred from Powerful

# nothing
p.qux
# qux from Powerful

# without `if defined?(super)`
p.corge
# corge from Powerful
# NoMethodError: super: no superclass method `corge' for #<Powerful:0x000000015e8390>

self.new in a module

Also, check this out.

module ActiveSupport
  module TaggedLogging
    def self.new(logger)
      logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new
      logger.formatter.extend Formatter
      logger.extend(self)
    end
  end
end

new is not used to create a new instance of TaggedLogging (after all it is a module, not a class) that would delegate to the logger as one could expect based on the API. Instead it extends the logger object with itself.

You might also like