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.