winfred.nadeau

Great software speaks for itself.

Reactor

A Sidekiq-driven pub/sub layer for your ruby app

View on GitHub

Hired was written with “log every relevant user click for analysis later” in mind from day one. It began as little ActiveRecord::Base#create calls sprinkled across the controller layer like so:

def create
  employment = Employment.new params[:employment]
  if employment.save
    action_event :employment_created
    redirect_to profile_path
  else
    render :new
  end
end

def action_event(name, options)
  Activity.create options.merge(actor: current_user, type: name.to_s.camelize)
end

The example action above would create a record in the ‘activities’ table with these columns

type: 'EmploymentCreated'
actor_id: 123

Later on a different problem comes around: we need to send transactional emails using templates in our database written by our team to the relevant users for any arbitrary set of email template <-> event combinations. Previously we had been binding specific hard-coded templates using ActionMailer#deliver calls throughout both model and controller layers, like any good rails app should do. It was then we realized that we already had logical bindings to “this kind of thing happening within our app” all over the controller layer disguised as “Activity.create” calls, so this is where we made our cut to allow for more flexible growth patterns in the design.

Each of these lines was refactored to pass through a new pub/sub layer that would allow us to bind arbitrary blocks of code and database-driven transactional emails as well as create a log of the activity.


def action_event(name, options)
  Reactor::Event.publish name, options.merge(actor: current_user)
end


# The first wildcard event listener

class ActivityLogger
 include Reactor::Subscribable

 on_event '*' do |event|
   Activity.create type: event.name.to_s.camelize, actor_id: event.actor_id,
     target_id: event.target_id, target_type: event.target_type
 end
end


# The first resource-driven event listener
### a subscribers table with type column for STI,
### an on_event column to describe event subscription,
### and other data necessary (like email_template_id to bind to, event party)

class CustomizableEmail < Reactor::Subscriber
  # executed in context of a specific "fire this email template
  # to recipient on this event" record in the database
  on_fire do
    email_template.send_to!(event.actor)
  end
end

# The first event-driven ActionMailer
### The event block and mailer action meld into one

class EventMailer < ActionMailer::Base
  include Reactor::EventMailer

  on_event :asked_question do |event|
    @question = event.target
    developer = @question.developer
    mail subject: 'Question For You',
         to: developer.email
  end
end

# with corresponding partial
--- app/views/event_mailer/asked_question.haml
  Hi #{@question.developer.name},

  = @question.body
---

The process jumps to Sidekiq as soon as Reactor::Event.publish is called by default, but in-memory (request blocking) subscribers can be defined as well as a potential code-refactoring pattern that can help encapsulate complex state management bindings.

We’ve been using Reactor for about a year now and it runs very reliably through Sidekiq. It’s definitely still a bit of an experiment, as it is painfully easy for pub/sub to go dwanky. There are some interesting trade-offs with regard to inserting a whole layer into a rails app like this.

Pros

Cons