DEV Community

Matías Hernán García
Matías Hernán García

Posted on

Feature Flagging Asynchronous Workers

In the project we are working on, we heavily use Feature Flags to gradually roll new functionality. Also, it let us turn them off in case something is causing problems.

Since all of our business code gets executed asynchronously, I recently found myself repeating this chunk of code in my workers.

class BaseWorker
  include Sidekiq::Worker
end

class SendNotificationEmailWorker < BaseWorker
  def perform
    return unless FeatureFlags.enabled?(:send_emails)

    # do work
  end
end

This code is simple and it works, but what if I don't want to repeat the feature flag check in every worker I define?

Well, as always, we can leverage some Ruby concepts to encapsulate this behaviour and avoid repeating ourselves.
First of all, let's define how we want this to work:

class SendNotificationEmailWorker < BaseWorker
  feature_flagged(:send_emails)

  def perform
    # do work
  end
end

As you see, I want to call a class method feature_flagged that receives a flag key and that it avoids calling SendNotificationEmailWorker#perform in case it's turned off.

Before showing you the code, should understand the following concepts:

  1. How the Ruby Ancestor chain works
  2. Difference between include and prepend
  3. The super keyword
  4. Anonymous modules
  5. Class variables
class BaseWorker
  include Sidekiq::Worker

  class << self
    def feature_flagged(feature_flag)
      class_attribute :feature_flag
      self.feature_flag = feature_flag

      anonymous_module = Module.new do
        def perform(*args)
          unless ::FeatureFlags.enabled?(feature_flag)
            logger.info("Skipping execution since #{feature_flag} is turned off")
            return
          end

          super
        end
      end

      prepend anonymous_module
    end
  end
end

class SendNotificationEmailWorker < BaseWorker
  feature_flagged(:send_emails)

  def perform
    # work
  end
end

As you see:

  1. I created an Anonymous Module that defines the #perform method.
  2. The Anonymous Module is prepended to SendNotificationEmailWorker so it's invoked before SendNotificationEmailWorker#perform. By doing this, we can control if SendNotificationEmailWorker#perform should be invoked depending on the feature toggle.
  3. If it's on, we call SendNotificationEmailWorker#perform by doing super. Calling super without arguments guarantees that all arguments are passed by to the next implementation.
  4. In order for the module to be able to access the feature_flag value, we needed to define it somehow. Since I am in a Rails app I used class_attribute

Declares an attribute whose value is inheritable by subclasses. Subclasses can change their own value and it will not impact parent class.

Now, let's analyze the pros and cons of this code.

Pros

  1. DRY. No more repeating in all your workers' perform method return unless FeatureFlags.enabled?(feature_flag).
  2. When looking at a class definition, you will identify quicker if it depends on a feature flag or not. For example:
class SendNotificationEmailWorker < BaseWorker
  feature_flagged(:send_emails)

  def perform
    # send_email
  end
end

versus

class SendNotificationEmailWorker < BaseWorker
  def perform
    return unless FeatureFlags.enabled?(:send_emails)
  end
end

Cons

  1. Anonymous Modules are less performant than non-anonymous ones. If you don't abuse from the feature you shouldn't really worry about it but it's something to consider when introducing them.
  2. This doesn't stop your worker from getting queued and executed. If you are concerned about performance or consuming your queue storage when you know that you won't execute the code maybe you should focus on trying to not queue it at all.
  3. As mentioned in the class_attribute definition, once the feature_flagged method is called, a class variable feature_flag value will be defined. This means that all the subclasses that inherit from the class that called this method will have that value although it can be overridden. In our case this is fine.

Top comments (0)