-
Notifications
You must be signed in to change notification settings - Fork 4
Publish lifecycle events to routemaster #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 29 commits
5c917b0
f8176e9
0601a2f
e9df9b8
d1cb948
82d1e97
16c13ba
431a46f
25eb951
7e347e9
914c7ee
346f6ff
e8028ac
b8d7b2a
3214b11
79c37aa
d51f58f
c36a5ff
2f3032f
97b1d63
11cf4de
2b94c46
1221947
5609396
ebe8518
b3bcd14
fed4763
f9a97d9
fbb7baa
1c207a4
5fded8e
b42f01c
8afb4e6
e6b6926
fddf7bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
# HEAD (2017-07-28) | ||
|
||
Features: | ||
|
||
- Publish lifecycle events to Routemaster (#19) | ||
|
||
# v1.8.1 (2017-07-27) | ||
|
||
Features: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
## Using the Routemaster Client feature | ||
|
||
[`routemaster-client`](https://github.com/deliveroo/routemaster-client) comes as a dependency of `roo_on_rails` with a basic implementation of lifecycle event publishers. | ||
|
||
This code example assumes that you are using the latest version of the [`roo_on_rails`](roo_on_rails) gem and that you have set the correct environment variables for Routemaster Client to work on your app, as explained in the main [`README.md`](roo_on_rails#routemaster-client) file. | ||
|
||
It also assumes that your app has an API for the resources you want to publish lifecycle events for, with matching routes and an `API_HOST` environment variable set. | ||
|
||
### Setup lifecycle events for your models | ||
|
||
We will most likely want to publish lifecycle events for several models, so to write slightly less code let's create a model concern first: | ||
|
||
```ruby | ||
# app/models/concerns/routemaster_lifecycle_events.rb | ||
require 'roo_on_rails/routemaster/lifecycle_events' | ||
|
||
module RoutemasterLifecycleEvents | ||
extend ActiveSupport::Concern | ||
include RooOnRails::Routemaster::LifecycleEvents | ||
|
||
included do | ||
publish_lifecycle_events | ||
end | ||
end | ||
``` | ||
|
||
Then let's include this concern to the relevant model(s): | ||
|
||
```ruby | ||
# app/models/order.rb | ||
class Order < ApplicationRecord | ||
include RoutemasterLifecycleEvents | ||
|
||
# ... | ||
end | ||
``` | ||
|
||
And another one for the example: | ||
|
||
```ruby | ||
# app/models/rider.rb | ||
class Rider < ApplicationRecord | ||
include RoutemasterLifecycleEvents | ||
|
||
# ... | ||
end | ||
``` | ||
|
||
### Create publishers for lifecycle events | ||
|
||
We have now configured our models to publish lifecycle events to Routemaster, but it won't send anything until you have enabled publishing and created matching publishers. Let's start with creating a `BasePublisher` that we can then inherit from: | ||
|
||
```ruby | ||
# app/publishers/base_publisher.rb | ||
require 'roo_on_rails/routemaster/publisher' | ||
|
||
class BasePublisher < RooOnRails::Routemaster::Publisher | ||
include Rails.application.routes.url_helpers | ||
|
||
def publish? | ||
noop? || model.new_record? || model.previous_changes.any? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could always move this into the base implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds reasonable to me 👍 |
||
end | ||
end | ||
``` | ||
|
||
Then create a publisher for each model with lifecycle events enabled: | ||
|
||
```ruby | ||
# app/publishers/order_publisher.rb | ||
class OrderPublisher < BasePublisher | ||
def url | ||
api_order_url(model, host: ENV.fetch('API_HOST'), protocol: 'https') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could move this into the base implementation too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, no, this one can't really move into the base implementation because sometimes there's an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think this one is best to handle in the individual service apps indeed. |
||
end | ||
end | ||
``` | ||
|
||
and | ||
|
||
```ruby | ||
# app/publishers/rider_publisher.rb | ||
class RiderPublisher < BasePublisher | ||
def url | ||
api_rider_url(model, host: ENV.fetch('API_HOST'), protocol: 'https') | ||
end | ||
end | ||
``` | ||
|
||
### Register the publishers with Routemaster | ||
|
||
The final step is to tell Routemaster that these publishers exist, so that it can listen to their events. We're going to do this in an initialiser: | ||
|
||
```ruby | ||
# config/initilizers/routemaster.rb | ||
require 'roo_on_rails/routemaster/publishers' | ||
|
||
PUBLISHERS = [ | ||
OrderPublisher, | ||
RiderPublisher | ||
].freeze | ||
|
||
PUBLISHERS.each do |publisher| | ||
model_class = publisher.to_s.gsub("Publisher", "").constantize | ||
RooOnRails::Routemaster::Publishers.register(publisher, model_class: model_class) | ||
end | ||
``` | ||
|
||
We should now be all set for our app to publish lifecycle events for `orders` and `riders` onto the event bus, so that other apps can listen to them. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
require 'roo_on_rails/config' | ||
|
||
module RooOnRails | ||
module Railties | ||
class Routemaster < Rails::Railtie | ||
initializer 'roo_on_rails.routemaster' do | ||
next unless Config.routemaster_enabled? | ||
|
||
$stderr.puts 'initializer roo_on_rails.routemaster' | ||
|
||
abort 'Aborting: ROOBUS_URL and ROOBUS_UUID are required' if roobus_credentials_blank? | ||
|
||
require 'routemaster/client' | ||
|
||
::Routemaster::Client.configure do |config| | ||
config.url = roobus_url | ||
config.uuid = roobus_uuid | ||
end | ||
end | ||
|
||
private | ||
|
||
def roobus_credentials_blank? | ||
roobus_url.blank? && roobus_uuid.blank? | ||
end | ||
|
||
def roobus_url | ||
ENV.fetch('ROOBUS_URL') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It strikes me that using ROUTEMASTER_URL and ROUTEMASTER_UUID would probably be cleaner names, but if people are dead set on ROOBUS_ then I guess that's fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just assumed that these were prefixed with Note: It looks like it would just leave There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'd prefer |
||
end | ||
|
||
def roobus_uuid | ||
ENV.fetch('ROOBUS_UUID') | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
require 'active_support/concern' | ||
require 'new_relic/agent' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a require for |
||
require 'roo_on_rails/routemaster/publishers' | ||
|
||
module RooOnRails | ||
module Routemaster | ||
module LifecycleEvents | ||
extend ActiveSupport::Concern | ||
|
||
ACTIVE_RECORD_TO_ROUTEMASTER_EVENT_MAP = { | ||
create: :created, | ||
update: :updated, | ||
destroy: :deleted, | ||
noop: :noop | ||
}.freeze | ||
private_constant :ACTIVE_RECORD_TO_ROUTEMASTER_EVENT_MAP | ||
|
||
def publish_lifecycle_event(event) | ||
publishers = Routemaster::Publishers.for(self, routemaster_event_type(event)) | ||
publishers.each do |publisher| | ||
begin | ||
publisher.publish! | ||
rescue => e | ||
NewRelic::Agent.notice_error(e) | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
def routemaster_event_type(event) | ||
ACTIVE_RECORD_TO_ROUTEMASTER_EVENT_MAP[event].tap do |type| | ||
raise "invalid lifecycle event '#{event}'" unless type | ||
end | ||
end | ||
|
||
%i(create update destroy noop).each do |event| | ||
define_method("publish_lifecycle_event_on_#{event}") do | ||
publish_lifecycle_event(event) | ||
end | ||
end | ||
|
||
module ClassMethods | ||
def publish_lifecycle_events(*events) | ||
events = events.any? ? events : %i(create update destroy) | ||
events.each do |event| | ||
after_commit( | ||
:"publish_lifecycle_event_on_#{event}", | ||
on: event | ||
) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
require 'roo_on_rails/config' | ||
require 'routemaster/client' | ||
|
||
module RooOnRails | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a require for |
||
module Routemaster | ||
class Publisher | ||
attr_reader :model, :event | ||
|
||
def initialize(model, event, client: ::Routemaster::Client) | ||
@model = model | ||
@event = event | ||
@client = client | ||
end | ||
|
||
def publish? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this method always return true? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intention is that it's ready to be overloaded in the clients own publishers There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is that it's |
||
true | ||
end | ||
|
||
def will_publish? | ||
Config.routemaster_publishing_enabled? && publish? | ||
end | ||
|
||
def publish! | ||
return unless will_publish? | ||
@client.send(event, topic, url, data: stringify_keys(data)) | ||
end | ||
|
||
def topic | ||
@model.class.name.tableize | ||
end | ||
|
||
def url | ||
raise NotImplementedError | ||
end | ||
|
||
def data | ||
nil | ||
end | ||
|
||
%i(created updated deleted noop).each do |event_type| | ||
define_method :"#{event_type}?" do | ||
event.to_sym == event_type | ||
end | ||
end | ||
|
||
private | ||
|
||
def stringify_keys(hash) | ||
return hash if hash.nil? || hash.empty? | ||
|
||
hash.each_with_object({}) do |(k, v), h| | ||
h[k.to_s] = v.is_a?(Hash) ? stringify_keys(v) : v | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module RooOnRails | ||
module Routemaster | ||
module Publishers | ||
@publishers = {} | ||
|
||
def self.register(publisher_class, model_class:) | ||
@publishers[model_class] ||= Set.new | ||
@publishers[model_class] << publisher_class | ||
end | ||
|
||
def self.for(model, event) | ||
publisher_classes = @publishers[model.class] | ||
publisher_classes.map { |c| c.new(model, event) } | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we keep the "library usage" v "command features" sections? Users have been confused by the dual nature of
roo_on_rails
(it's both a library/framework complement and a CLI, usable separately).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely! I was a bit confused by this table of content myself, partly because it changed while I was working on this PR. I'll rename "Configuration and usage" to "Library usage" 👍