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:
- How the Ruby Ancestor chain works
- Difference between
include
andprepend
- The
super
keyword - Anonymous modules
- 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:
- I created an Anonymous Module that defines the
#perform
method. - The Anonymous Module is prepended to
SendNotificationEmailWorker
so it's invoked beforeSendNotificationEmailWorker#perform
. By doing this, we can control ifSendNotificationEmailWorker#perform
should be invoked depending on the feature toggle. - If it's on, we call
SendNotificationEmailWorker#perform
by doingsuper
. Callingsuper
without arguments guarantees that all arguments are passed by to the next implementation. - 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
- DRY. No more repeating in all your workers' perform method
return unless FeatureFlags.enabled?(feature_flag)
. - 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
- 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.
- 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.
- As mentioned in the
class_attribute
definition, once thefeature_flagged
method is called, a class variablefeature_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)