JASON CLARK: All righty. I think, by my clock,
it's about 1:40. A couple more people
are coming on in, but we'll get things started.
My name's Jason Clark. I'm an engineer at
New
Relic. I work specifically on the New Relic
RPM
gem, which some of you may have installed
in
your applications. So that's actually gonna
play in a
little bit in some of the examples that I'm
gonna use later on. But you don't need to
know too much about how that works.
The first, so, I, I've got to start off
with a, an admission to something that I've
always
wanted to do when presenting in front of a
group of people, and I figured that RailsConf
is
probably just about the best opportunity.
So, if you'll
just bear with me for a second. Just hang
on.
So at New Relic, we have a support hero
who is responsible for handling the escalations.
So any
time that something gets too complicated for
support, they
send it to the support hero. And my wife
heard that, and she went, if you're a hero,
you need a cape. And so she made me
this lovely hand-embroidered Ruby emblem cape.
And I've always
wanted to give a presentation wearing this.
So, if
that's OK. Little unprofessional, maybe, but,
you know. I'm
just gonna go for it, so.
AUDIENCE: Look in the sky! It's a bird!
J.C.: All right. That feels good. Thank you.
Thank
you for humoring me. All right.
So what we're here for, here to talk about
today is about something that I call evented
patterns
in Ruby. So let's start off with a little
bit of a story. This is a little bit
of a fabrication, but it might be something
that
some of you have experienced before.
So, imagine that you have created an application.
Maybe
it's something like, something basic, for
doing tracking of
your cycling statistics. And all of a sudden,
your
application really takes off. It gets on the
social
media. People find out about it and, you know,
you, you start getting this influx of users
that
you've got to deal with. As your traffic scales,
you've got to do things like tuning your web
server, right. You, you pick your, your Unicorn
or
your Puma and you get that set up. You
tune your system. And while your traffic is
growing,
there's a similar sort of growth that goes
on
in your code.
Your controllers start out life kind of like
this.
They start out pretty simple, you know. We
just
create our user when somebody signs up. But
then,
as your application grows and you get more
stake
holders and your company's, you know, doubling
every two
months, all of a sudden, you've got other
things
that you need to add in there, right. You,
maybe you want to send welcome emails, so
you,
you end up, you know, getting your action
mailer
going there, and cram that into, into your
action
when someone signs up.
Oh, and then maybe it's really important that
our
business people be able to do analytics. They
want
to know about who's signing up and track data,
and there's some other place that we want
to
push that data. Oh, and then the sales people,
they really want this to be integrated to
their
sales system, and so they want stuff to get
pushed over to their CRM. And maybe, oh, we
want to do some sort of posting out to
the social networks. So we have a background
job
that takes care of that. You know, this sort
of growth happens in some of those critical
points
in your infrastructure. The spots where interesting
things happen
in the domain of your application.
Now, that's kind of, can get to be a
mess, you know. This, over time, I mean this
is a short example and slightly fabricated,
but I'm
sure you all have, you know, I'm sure all
of your controllers are, like, twelve lines
or less
for an action, right? Nobody has these long
methods
that go on and do tons and tons of
things.
You can end up with a massive snarl. You
can end up with controllers that are very
difficult
to test, difficult to reason about, and they
go
on and on handling all of these different
things.
So what we're gonna talk about is a pattern
of using events to handle that and to take
those key parts of your infrastructure and
break them
apart into smaller bits.
So, first off, we're gonna talk about what
I
actually mean when I say events, the pattern
that
we're talking about. We'll talk about how
that can
be used in the coupling of your application,
both
internally within your app and as it relates
to
other libraries that you might be using.
We'll take a look at a couple of mechanisms
that you can use to institute this sort of
system. And then we'll talk about some of
the
responsibilities and sort of how you should
think about
this type of evented system within your app.
And
keep things flexible and performant.
So, first up, the pattern. So, when I say
events, you know, there's lots of different
people that
think lots of different things when I say
that.
Like, some folks that this was gonna be about
event machine. It's not about event machine.
So what,
what sort of things pop to mind when I
say evented programming?
AUDIENCE: Asynchronous.
J.C.: K.
AUDIENCE: ActiveSupport notifications.
J.C.: Visual Basic event handlers? Anybody?
AUDIENCE: Nice!
J.C.: I'm sorry to, no. I shouldn't do that.
No.
A lot of people probably think node. Think
asynchronous
sorts of callbacks. But that's not necessarily
the core
of the type of event that I'm gonna describe
here today. This, these events are not necessarily
asynchronous.
They're not necessarily about IO or about
distributing things.
They're just a basic pattern to decoupled
pieces of
your application.
So it starts off, the main term that I'll
use. Somebody knows that something interesting
happens in your
system, I'm going to refer to it as a
notifier. There's a set of subscribers that
also exist.
And those are the pieces of your application
that
care about that event that just happened.
And then
there's some sort of eventing in the middle.
Something
that dispatches those events from the notifier
to all
of the subscribers.
Now, this is really pretty basic, and you
might
wonder, like, why aren't these just method
calls? Well,
it's because of that eventing system that
we can
get the decoupling. It allows for the notifier
and
the subscriber to not necessarily know directly
about each
other. Those classes don't have to interact,
directly, without
that intermediary between them.
What's the relationship to the, to callbacks,
you might
say as well. There's, you know, at ActiveRecord,
we
have before and after hooks and around hooks
to
do all sorts of different things. Well, this
is
similar, but it's a little different from
what we're
gonna describe, because of that line, where
we have
to derive from ActiveRecord::Base. That's
a really tight coupling.
And what I'm gonna describe doesn't put any
requirements
on your class, except that it interacts with
the
event dispatcher as a collaborator.
So that's the basics of what I mean by
evented patterns. So let's take a look, now,
at
how it can influence coupling. So the first
example
that I'm gonna draw is about internal coupling.
So
when I say that, I mean, things that are
within the scope of your application, the
code that
you own and that you've written.
So, working for New Relic, the gem that I
work on, you install it in your application
and
it spins up, starts monitoring performance
information, and then
sends it back every minute to New Relic so
we can give you pretty graphs and alerting
and
all sorts of insight into what's going on
in
your app.
When this gem was originally created, we would
load
things up when the app starts and we read
a little config file to say what types of
things we want to be doing. You know, there's
features that you can turn on and turn off
and configure. And we knew the story about
what
this configuration was when the application
had successfully gotten
run.
Well, later on, we added a feature. And that
feature was server-side configuration. So
the application now connects
to our server and gets back some additional
set
of configuration values. And what this means
is that
the point in time at which we could assume
that the application was actually configured
changed. If we
had put a bunch of logic directly into our
app start up, like right there in the initialize
where things start running, we now didn't
have the
right answer for what the configuration was.
We have
to wait until that connect happens in the
background
before we can do that.
So let's take a look at what happened with
that connect method. So we make a call out
to the server and, you know, this is obviously
a little stripped down. There's error handling
in real
life. But we get back some sort of configuration
from the server. It tells us what those settings
are supposed to be. We pass that along to
finish set up and then finish set up starts
looking like a little bit of a mess. There's
a whole bunch of different concerns here,
right. We've
got something about naming rules. Something
about cross-application tracing.
Javascript. Some sort of beacon, like. It's
not important
what all of the inner details of all of
these pieces are. But none of them are really
related. The only thing about them is that
they
care that the configuration is ready. They're
wanting to
respond to that event that happened, that
the config
is set.
Now, one of the outgrowths of the code being
laid out like this is that we would like
to be able to write some simple tests. Some
unit tests, like DHH tells us we shouldn't,
that
look kind of like this. We run our given
set up against a chosen config and then we
can see the results of what came out of
that.
But, in practice, what we end up having to
do is we end up having to stub out
things. Like those different collaborators
that were in there.
The cross application tracing. If we don't
care about
it in this test, but we still need to
make sure that that code path can run without
tripping anything up or crashing because things
aren't filled
out the way that it needs it to be.
Things can get even worse than this. Now that
beacon line down on the bottom, you know,
we're
constructing a brand new object. Who, who
knows what
that thing might be doing internally. You
know? I
mean, nobody's ever written code like this,
right, that
does something bad like that from a constructor
for
an object. That doesn't happen. But, you know,
we
end up having to do things like stubbing out
new. Make it so that objects don't do interactions
and things that we don't really want.
Well, what if we could take this whole method
and just get rid of it in the form
that it's in and instead we notify that an
event has happened. We tell our event system
that
the system is now fully configured. We are
ready
to go. Everything's logged. We can head on.
Now this has some really nice side-effects.
One of
them is that beacon code. Now, in initialize,
you
still see the code where we're making this
HTTP
callout. But it's not wrapped in this subscribe
block.
So it means that that call is only going
to happen when the event system lets us know
that this configuration event has occurred.
That provides a
buffer. You can safely create a beacon now
and
it's not going to immediately go hit the network.
The tests surround the finished set up method
become
very slim and very straightforward as well.
All the
finish set up is responsible for is firing
that
event. And we check that that event gets dispatched
correctly and that's all that we need to unit
test at this level. And unit testing for the
individual effects that occur, moves over
and gets done
in the place that responds to that event.
The
thing that is gonna subscribe to the event
now
takes the responsibility for testing that.
Now, this doesn't cover everything. There
is a need
for an integration test. You do want something
that
sees that we wire up to the right events.
But you probably have tests that are gonna
exercise
those things anyways, if this isn't a critical
path
of your application.
So events allow us, within our own code, to
have more focused tests around those individual
pieces, more
independence between the classes. The configuration
in the finished
set up no longer has to know anything about
the Javascript instrumentor, the cross-application
tracer, the beacon. All
of that is pried apart.
And we found that this also starts to kind
of create a language within your application.
A domain
language, if you will. Because these are the
important
events that happen for us. We care, as the
New Relic gem, that we are configured. That
is
a critical thing for us. And there's now a
name for that. It's not just a bunch of
stuff that's tacked on to the end of one
of our methods.
It's not all roses, though. There is an issue
if you have some sort of ordering that's required
in the things that respond to those events.
And
actually we've had a couple of situations
where we've
been refactoring code and trying to pull it
into
event handlers and discovered that there were
dependencies between
different subscribers that aren't really handled
by a really
simple event system like this.
So, that's something that you have to watch
out
for. If there's a lot of that sort of
ordering, you're gonna have to deal with that
somewhere
in your code.
It can also make debugging and reasoning about
things
a little harder. It does spread some of those
responsibilities out. Now you don't see one
method that
lists all of the things that we do on
connect. You have to go search for things
that
subscribe to those events. But in the right
cases
I feel like it can buy you things with
the other benefits it brings along that outweigh
that.
Lastly, it is a performance consideration.
You know, I
would not use this sort of structure in a
tight loop that's gonna get called a million
times
a second. You know. If it's something like
we're
responding to a configuration event, like,
that we are
now ready to go or somebody just signed up.
I mean, it'd be great if you had a
million people signing up a minute, but reality
is
it's not happening as frequently as that.
So, being
aware of how this will impact your performance
is
really critical.
Internal coupling, like within the code that
you write
yourself, though, is not the only place where
you
can get bound to different things. So in,
in
the New Relic gem, we have a lot of
different functionality that we instrument
that work through middleware.
So we do Javascript insertion for doing end
user
timings. So as your request is going back
out,
we'll modify your html and put things in there.
We have error collection. So if there are
unhandled
exceptions that occur during your call, at
the rack
level, we will actually gather those up and
see
that those happened and report them back.
And we do things with HTTP requests between
the
applications if both ends of them are running
New
Relic. And we need to modify headers, HTTP
headers
that are on those requests. And all of these
separate things fit for us very well within
middlewares.
But, that makes kind of an assumption that
everything
that we want to apply that functionality to
is
built on top of rack. And while that's a
pretty good assumption for a lot of things
in
the Ruby space, we do support things that
aren't
necessarily configured that way.
We also want to avoid people having to set
things up. There are certain cases where maybe
you
have to manually add the things that we've
created,
and if we have seven different middlewares
that you
need to add for all this different functionality,
that's
kind of problematic from a configuration view
point.
So our solution to this was that we standardized
on having one middleware that we commonly
will install.
And we use events to actually do the dispatch
from there. So you'll see this is just a
very basic middleware. When a request comes
in and
hits call, we notify on the before and pass
along the environment that we got. We call
through
to the app inside and notify after and then
return the results.
So this allows us to be able to plug
things in that maybe are not shaped like rack.
We could use some of that code, like in
the cross application tracing. If we had some
web
server that was not rack-based or some configuration
that
was not gonna run through middleware, we could
still
have it generate a before call from the right
point and handle that with this same code,
without
having to deal with trying to create middlewares
or
insert something at a point that doesn't really
fit.
Now, just for the middleware case, you know,
this
probably could have been solved with some
sort of
composition. But, for us, it was actually
a really
powerful technique for keeping our code decoupled
from the
library that we were building on top of. That
looser coupling depends a lot on what sort
of
library and what your interactions with it
are, you
know. You're maintaining, basically, kind
of a compatibility layer.
And the more code that you have to put
into that layer, the more you should probably
think
about whether you're approaching it correctly.
Obviously you can't
do something like this with ActiveRecord,
it's just not
gonna be plausible. But if you have a small
number of points where you interact with another
third-party
library, applying events at those boundaries
can be a
way that you can keep some separation between
your
code and the library that you're dealing with.
So that's coupling. That's sort of the primary
motivator
behind using an evented pattern is to decouple
the
pieces of your application and allow for those
to
be looser and not as away of what's on
the other end of either firing or responding
to
an event.
So, let's talk about what it actually takes
to
implement this. What does this look like in
practice?
So I have created a small gem. This is
sort of an abstraction from what we do in
the New Relic gem. We don't use it directly
and I am not suggesting this for production
use.
But it's a really good example of how simple
it is to implement something like this in
Ruby.
So there's one primary class. The SimpleEvents::Notifier.
When we
initialize that, it's gonna be the central
thing that
keeps track of all the events. And everybody's
gonna
talk to this notifier object. So we hold onto
a hash, which is where we're gonna store those
handlers from the people that subscribe to
us.
Subscribing gets done. Passing in just a key
for
what the event is. We typically use symbols
within
our application. Some similar things that
we'll see later
use strings. And then hands in a block, which
is the code for it to run at that
point that that event gets fired.
And then it's just a simple matter of keeping
track of that list of handlers. Each event
may
have multiple handlers, so we make sure the
array
is there and add that handler onto the list.
Now there's one other line here that we're
dealing
with and that's checking for runaway subscriptions.
That has to do with the fact that for
our usage, there is a limited number of things
that should be subscribing to events. This
is not
something where every single web request that
comes through
subscribes and hooks itself in. And we'll
talk a
little bit later on about some of the negative
side effects you can have if it's happening.
So, for us, if more than a hundred event
subscribers have subscribed to a given event,
we actually
have a bug. And so we check for that
and make sure to warn that that's going on.
Your mileage may vary depending on the structure
that
you choose to use.
Notifying is a little more complicated than
subscribing, but
not too much more. And we'll step through
it
here. So first off, we check whether the events
collection has any handlers for the particular
key that
we just passed in. And this is one of
the really nice aspects of taking this simple
approach.
There's no central registry. There's nothing
to set up.
And event, I can create a handler for an
event that doesn't exist or I can start firing
and event that nobody handles, and it all
just
gracefully falls out. It doesn't cause any
problems if
there aren't handlers wired up for things.
If we do find that there are event handlers,
we simply iterate across those and call passing
along
the arguments that we received on the event.
For
our particular use in New Relic, we trap errors
and will log and swallow any exceptions that
happen
as a result of our direct event handlers.
Now,
this is kind of an artifact of the sort
of gem that we are. We're there in the
background and these events are things for
the agent
and for us sending our data back.
And the worst possible thing that we could
do
is crash an application. So we trap everything
that
goes through. If you were doing this in an
application setting, you may want to log.
You may
also want to raise those errors and fail quickly
in the case that one of your notifiers has
something go wrong. But your mileage, you
know, you
can choose what the right policy is for how
you're using events and what setting you're
putting those
in.
So that's it. Like, that's an entire eventing
system
is about fifty lines of Ruby, which allows
you
to decouple those different pieces of your
application. But
the fact of the matter is that you might
not have to even write that system yourself
if
you're using Rails. ActiveSupport::Notifications
have been baked in from
I think about the 3.0 version of Rails. And
it's a system that's very much like what we've
described here.
I'll abbreviate in the slides a little because
ActiveSupport::Notifications
gets really, really long in the code, so,
we'll
scrunch that down a little bit. But all sorts
of things flow through this evented system
any time
a Rails request is occurring. So we get things
about the dispatch at the top level. We get
things about the controller and what it's
doing. We
get notifications about SQL calls. We get
notifications about
view and template rendering. Like, tons and
tons of
events are streaming through this ActiveSupport::Notification
system all of
the time.
Some common things that you might be familiar
with,
like the log, the debug log that you get
of your queries, that's running through ActiveSupport::Notifications
that are
fired by ActiveRecord. So what's this look
like?
It looks very similar to what we've seen before.
So subscribing to an event, you give it the
name, you give it a block, and then you
can see the form of what one of these
events was that came through. It'll hand it
to
us. It's got a name. It's got an identifier.
And then it's got this hash that's sort of
a payload of data that comes along with it.
ActiveSupport also provides a simple wrapper
for handling that
payload and handling those events when they're
in a
very common form. So most of the things that
Rails sends out, they're gonna have a name.
They're
gonna have a duration and a payload in a
standard format. So this class kind of wraps
up
that access and makes it a little neater.
On the notifying side, the method that you
call
is instrument. So if you want to fire an
event and have that be sent out to the
system, you just give it the string for the
name, give it whatever payload for the data,
and
you're off and running.
There's also a nice form of it that it
has that will wrap your code in a block,
so you can do something in there, and what
that'll do is that'll tack a duration on.
So
it'll time what's going on. And this is one
of the key things that ActiveSupport::Notifications
are used for
within Rails is timing how long those different
things
take. Those timings that you see in your debug
output are all coming from things being wrapped
by
this sort of instrument call.
As with all things Rails, there's a lot more
richness as well under the surface. You can
do
subscriptions by regular expressions. If events
are nested, the
ActiveSupport::Notifications system keeps
track of that and will take
care of letting you know so you could look
and see whether an event was occurring within
another
event, and you can go crazy if that's happening
too much, but.
There are temporary subscriptions and unsubscribe,
which is useful
in certain scenarios where, maybe you do want
to
have something hook into the ActiveSupport::Notifications
for the course
of a request. But you don't want to have
that handler hanging around and responding
for the rest
of the lifetime of your application.
And there's some really great examples that
you can
find in Rails itself. So the log subscriber
class,
which is the thing that spits out a lot
of your development log, is a great example
of
how you can hook into that underlying infrastructure.
So that's the mechanism. That's how you can
build
events in or use a library that you're probably
already using to get a lot of these things
done.
Let's talk a little bit more in detail about
the different parties that are involved here,
and what
they're responsible for and considerations
there. So notifiers have
a lot of responsibility. They're, they're
kind of the
key points in your application. You should
be, in
this scheme, you would be adding events for
the
things that matter the most.
And on that, I think that naming is a
really critical thing around this to get the
benefit
that you're looking for. So in the example
that
I gave up front, you'll, you probably didn't
notice,
but I named the event configured, to say,
hey,
we are in a state where we are fully
configured.
I didn't name it connect to server or finish
with the set up or, you know, binding it
to the mechanism of what it was. I couched
it in the terms of what mattered to my
application. I think that that's something
that's really powerful
if you do this right. If you, it allows
for changes in the location, like in our case,
that change of when we were configured. It
moved
from just plain application start up to a
point
in time in the future when we may hear
back from a server. And who knows when that
configured point might change in the future?
We're not bound by the event name to some
particular method or point in time. We can
change
that and still have it make sense within the
domain of what we've created.
It's also important to consider what your
payload is.
What data you're sending along from the notifier.
So
Rails follows the convention with ActiveSupport::Notifications
that I would
definitely encourage, of sending along primarily
a hash of
different values that you can pipe through.
I learned
this the hard way the second time that I
needed to add a bit of data onto one
of my events. And the mechanism, as we saw,
just sort of percolates the arguments along
that we've
given it.
Well, if you add another argument, all those
other
handlers downstream need to get updated. So
do it
from the start to make these things flexible.
Pass
your payloads along as a hash that you can
then add additional data that old event handlers
don't
need to care about and that new things can
pick up along the way.
On that topic of the payload as well, if
you're writing some sort of internal application,
you know,
you can put whatever you want into these payloads.
But as a third party, you may want to
be cautious if you're having other people,
other gems,
and other codes subscribe to these events
to try
to keep your payloads as primitive as you
can.
Maybe even all the way down to just base
types.
The advantage here is that you are, if you
put one of your internal classes as something
that
you're packing around in this event, and then
broadcast
it out to the world for everybody to use,
you now don't know who might be getting access
to those objects and using them. And depending
on
what sort of usage you've got with your, your
app, it can be difficult to make changes to
things that maybe you consider to be internal
details.
If you turn things into primitives, you know,
it,
they're just strings and hashes and numbers,
those are
pretty safe, right. You can maintain that
mapping from
your internal objects to those primitives
and not have
to worry about who might be changed when you
refactor.
So the other end of things is the subscribers.
I think it's really important to consider,
carefully, who
the subscribers are for a given event. This
pattern
doesn't fit everywhere. This is not something
where every
single thing that happens in your application
should be
an event. And if you're writing an event,
and
there's only one thing that's gonna handle
it, you
might be misapplying this pattern. You might
not get
the benefit that you need, there.
A lot of times, you'll find that there is
some natural home, some place within your
app that
is already there that is the right place to
be subscribing to that event and responding
to what's
happening. But if you don't, it's an, there's
some
interesting ideas around how you can create
what that
location is. So there's this gem that I've
run
into, it's called mutations. It's sort of
building command
objects that represent different actions that
happen in your
system.
Now, you know, you might recognize something
very like
this from earlier in the morning, but it can
be an interesting idea if you have key pieces
of your system where a workflow or something
that
occurs is really important. You can write
something, a
command, that encapsulates what the event
is, what that
action is that needs to be taken. And this
can be something that helps in those cases
that
I described, where there may be implicit ordering.
There
may be ordering between different components.
You could create
a command that wraps those up in the right
ordering to be applied and then have that
be
what responds to the events.
Probably one of the biggest difficulties that
you can
run into with an evented pattern is, if there's
too much nesting, if this gets overused, and
events
trigger events and trigger events on all sorts
of
different things, it can become very difficult.
And the
main advice that I have there is that this,
this technique works best, I think, at the
critical
lynch points of your application. The things
that you
really care about the most to be providing,
that
sort of visibility and language around.
And another thing to be aware of with the
implementation that we've done here. I've
had a number
of poeple come up afterwards and point out,
they're
like, it's all still synchronous. It's all
still just
running. I mean, this is just splitting up
where
those things are. And that's totally true.
This is
not as described something that will make
your application
parallel or asynchronous in how that event
handling happens.
But it might give you good points in time
when you could decide to push things off.
Things
could go to background jobs or things could
get
dispatched to all sorts of other places if
you
wanted.
It's also important with subscribers to watch
out for
leaks. There is, when you create a block,
so
like, if this subscriber was created for every
web
request, the fact that we have that block
potentially
holds onto a reference to this object and
keeps
it alive past a point in time when you
might have thought that it was gonna go away.
I've seen this not just in Ruby but in
a number of different languages with these
sorts of
evented patterns with handlers that get subscribed,
where they
can hold references to objects that you might
not
be expecting them too. And so you gotta watch
out for that to keep things from leaking.
So those are some ideas around the responsibilities
that
your subscribers and your notifiers should
have. And I
hope that maybe this has opened your eyes
a
little bit to some ideas about how you might
be able to partition your app differently.
What are
those responsibilities and things that happen
during your app
that would be worth having an event or worth
giving that name to.
And as you can see that it's a pretty
simple thing to implement that can give you
a
lot of benefit down the road. Thank you.