Skip to content

Commit 30ef8f9

Browse files
Greg Beechmezis
Greg Beech
authored andcommitted
Publish lifecycle events to routemaster (#19)
* Rough outline of event publishing, no tests yet Has an ActiveRecord module, a basic publisher implementation, and a registry for publishers. Still needs a bunch more work, but this should be a slightly cleaner version of what’s in orderweb geared towards more complex publishing and multi-publishing. * Simplify publisher registration and rename mixing * Tidy up some of the publisher code Addresses the review comments and makes things a bit tidier. * Add helper methods to check the event type * Add publishers spec * Add routemaster-client to dependencies * Add tests for Routemaster::Publisher * Add test example for #publish! * Try with method stubs instead * Update instructions in README * Not sure how these changes slipped through * Update CHANGELOG.md * Move RM client config to RooOnRails and add further documentation * Require Routemaster and refactor config_spec * Fix empty array issue and add Routemaster integration test * Fix the integration tests * Add lifecycle method for each verb and add tests * Refactor LifecycleEvents and add tests * Introduce ROUTEMASTER_PUBLISHING_ENBALED env var * Update documentation to reflect latest changes * Small tweaks to LifecycleEvents spec * Fix tiny typo in the README * Check if create, update or noop event before publishing * Rename ROOBUS_ to ROUTEMASTER_ * Unblock CodeClimate * Fix CodeClimate issue * Fix Routemaster integration test * Address PR comments
1 parent 197ac44 commit 30ef8f9

16 files changed

+565
-69
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# HEAD (2017-08-07)
2+
3+
Features:
4+
5+
- Publish lifecycle events to Routemaster (#19)
6+
17
# v1.8.1 (2017-07-27)
28

39
Features:

README.md

+20-12
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@
1616
**Table of Contents**
1717

1818
- [Installation](#installation)
19-
- [Usage](#usage)
20-
- [Library features](#library-features)
21-
- [New Relic configuration](#new-relic-configuration)
22-
- [Rack middleware](#rack-middleware)
23-
- [Database configuration](#database-configuration)
24-
- [Sidekiq](#sidekiq)
25-
- [HireFire Workers](#hirefire-workers)
26-
- [Logging](#logging)
27-
- [Google Oauth](#google-oauth)
19+
- [Library usage](#library-usage)
20+
- [New Relic configuration](#new-relic-configuration)
21+
- [Rack middleware](#rack-middleware)
22+
- [Database configuration](#database-configuration)
23+
- [Sidekiq](#sidekiq)
24+
- [HireFire (for Sidekiq workers)](#hirefire-for-sidekiq-workers)
25+
- [Logging](#logging)
26+
- [Google OAuth authentication](#google-oauth-authentication)
27+
- [Routemaster Client](#routemaster-client)
2828
- [Command features](#command-features)
29+
- [Usage](#usage)
30+
- [Description](#description)
2931
- [Contributing](#contributing)
3032
- [License](#license)
3133

@@ -57,9 +59,7 @@ And then execute:
5759

5860
Then re-run your test suite to make sure everything is shipshape.
5961

60-
## Usage
61-
62-
## Configuration and usage
62+
## Library usage
6363

6464
### New Relic configuration
6565

@@ -199,6 +199,14 @@ application.
199199

200200
A simple but secure example is detailed in `README.google_oauth2.md`.
201201

202+
### Routemaster Client
203+
204+
When `ROUTEMASTER_ENABLED` is set to `true` we attempt to configure [`routemaster-client`](https://github.com/deliveroo/routemaster-client) on your application. In order for this to happen you need to set the following environment variables:
205+
206+
* `ROUTEMASTER_URL` – the full URL of your Routemaster application (mandatory)
207+
* `ROUTEMASTER_UUID` – the UUID of your application, e.g. `logistics-dashboard` (mandatory)
208+
209+
If you then want to enable the publishing of events onto the event bus, you need to set `ROUTEMASTER_PUBLISHING_ENABLED` to `true` and implement publishers as needed. An example of how to do this is detailed in [`README.routemaster_client.md`](README.routemaster_client.md).
202210

203211
## Command features
204212

README.routemaster_client.md

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
## Using the Routemaster Client feature
2+
3+
[`routemaster-client`](https://github.com/deliveroo/routemaster-client) comes as a dependency of `roo_on_rails` with a basic implementation of lifecycle event publishers.
4+
5+
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.
6+
7+
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.
8+
9+
### Setup lifecycle events for your models
10+
11+
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:
12+
13+
```ruby
14+
# app/models/concerns/routemaster_lifecycle_events.rb
15+
require 'roo_on_rails/routemaster/lifecycle_events'
16+
17+
module RoutemasterLifecycleEvents
18+
extend ActiveSupport::Concern
19+
include RooOnRails::Routemaster::LifecycleEvents
20+
21+
included do
22+
publish_lifecycle_events
23+
end
24+
end
25+
```
26+
27+
Then let's include this concern to the relevant model(s):
28+
29+
```ruby
30+
# app/models/order.rb
31+
class Order < ApplicationRecord
32+
include RoutemasterLifecycleEvents
33+
34+
# ...
35+
end
36+
```
37+
38+
And another one for the example:
39+
40+
```ruby
41+
# app/models/rider.rb
42+
class Rider < ApplicationRecord
43+
include RoutemasterLifecycleEvents
44+
45+
# ...
46+
end
47+
```
48+
49+
### Create publishers for lifecycle events
50+
51+
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:
52+
53+
```ruby
54+
# app/publishers/base_publisher.rb
55+
require 'roo_on_rails/routemaster/publisher'
56+
57+
class BasePublisher < RooOnRails::Routemaster::Publisher
58+
include Rails.application.routes.url_helpers
59+
60+
# Add your method overrides here if needed
61+
end
62+
```
63+
64+
Then create a publisher for each model with lifecycle events enabled:
65+
66+
```ruby
67+
# app/publishers/order_publisher.rb
68+
class OrderPublisher < BasePublisher
69+
def url
70+
api_order_url(model, host: ENV.fetch('API_HOST'), protocol: 'https')
71+
end
72+
end
73+
```
74+
75+
and
76+
77+
```ruby
78+
# app/publishers/rider_publisher.rb
79+
class RiderPublisher < BasePublisher
80+
def url
81+
api_rider_url(model, host: ENV.fetch('API_HOST'), protocol: 'https')
82+
end
83+
end
84+
```
85+
86+
### Register the publishers with Routemaster
87+
88+
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:
89+
90+
```ruby
91+
# config/initilizers/routemaster.rb
92+
require 'roo_on_rails/routemaster/publishers'
93+
94+
PUBLISHERS = [
95+
OrderPublisher,
96+
RiderPublisher
97+
].freeze
98+
99+
PUBLISHERS.each do |publisher|
100+
model_class = publisher.to_s.gsub("Publisher", "").constantize
101+
RooOnRails::Routemaster::Publishers.register(publisher, model_class: model_class)
102+
end
103+
```
104+
105+
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.

lib/roo_on_rails.rb

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ module RooOnRails
1212
require 'roo_on_rails/railties/sidekiq'
1313
require 'roo_on_rails/railties/rake_tasks'
1414
require 'roo_on_rails/railties/google_oauth'
15+
require 'roo_on_rails/railties/routemaster'
1516
end

lib/roo_on_rails/config.rb

+8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ def google_auth_controller
2525
ENV.fetch('GOOGLE_AUTH_CONTROLLER')
2626
end
2727

28+
def routemaster_enabled?
29+
enabled? 'ROUTEMASTER_ENABLED', default: false
30+
end
31+
32+
def routemaster_publishing_enabled?
33+
enabled? 'ROUTEMASTER_PUBLISHING_ENABLED', default: false
34+
end
35+
2836
private
2937

3038
ENABLED_PATTERN = /\A(YES|TRUE|ON|1)\Z/i

lib/roo_on_rails/default.env

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ NEW_RELIC_DEVELOPER_MODE=false
1515
NEW_RELIC_LICENSE_KEY=override-me
1616
RACK_SERVICE_TIMEOUT=10
1717
RACK_WAIT_TIMEOUT=30
18+
ROUTEMASTER_URL=''
19+
ROUTEMASTER_UUID=''
20+
ROUTEMASTER_ENABLED=false
21+
ROUTEMASTER_PUBLISHING_ENABLED=false
1822
SIDEKIQ_ENABLED=true
1923
SIDEKIQ_THREADS=25
2024
SIDEKIQ_DATABASE_REAPING_FREQUENCY=10
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require 'roo_on_rails/config'
2+
3+
module RooOnRails
4+
module Railties
5+
class Routemaster < Rails::Railtie
6+
initializer 'roo_on_rails.routemaster' do
7+
next unless Config.routemaster_enabled?
8+
9+
$stderr.puts 'initializer roo_on_rails.routemaster'
10+
11+
abort 'Aborting: ROUTEMASTER_URL and ROUTEMASTER_UUID are required' if bus_details_missing?
12+
13+
require 'routemaster/client'
14+
15+
::Routemaster::Client.configure do |config|
16+
config.url = routemaster_url
17+
config.uuid = routemaster_uuid
18+
end
19+
end
20+
21+
private
22+
23+
def bus_details_missing?
24+
routemaster_url.blank? || routemaster_uuid.blank?
25+
end
26+
27+
def routemaster_url
28+
ENV.fetch('ROUTEMASTER_URL')
29+
end
30+
31+
def routemaster_uuid
32+
ENV.fetch('ROUTEMASTER_UUID')
33+
end
34+
end
35+
end
36+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
require 'active_support/concern'
2+
require 'new_relic/agent'
3+
require 'roo_on_rails/routemaster/publishers'
4+
5+
module RooOnRails
6+
module Routemaster
7+
module LifecycleEvents
8+
extend ActiveSupport::Concern
9+
10+
ACTIVE_RECORD_TO_ROUTEMASTER_EVENT_MAP = {
11+
create: :created,
12+
update: :updated,
13+
destroy: :deleted,
14+
noop: :noop
15+
}.freeze
16+
private_constant :ACTIVE_RECORD_TO_ROUTEMASTER_EVENT_MAP
17+
18+
def publish_lifecycle_event(event)
19+
publishers = Routemaster::Publishers.for(self, routemaster_event_type(event))
20+
publishers.each do |publisher|
21+
begin
22+
publisher.publish!
23+
rescue => e
24+
NewRelic::Agent.notice_error(e)
25+
end
26+
end
27+
end
28+
29+
private
30+
31+
def routemaster_event_type(event)
32+
ACTIVE_RECORD_TO_ROUTEMASTER_EVENT_MAP[event].tap do |type|
33+
raise "invalid lifecycle event '#{event}'" unless type
34+
end
35+
end
36+
37+
%i(create update destroy noop).each do |event|
38+
define_method("publish_lifecycle_event_on_#{event}") do
39+
publish_lifecycle_event(event)
40+
end
41+
end
42+
43+
module ClassMethods
44+
def publish_lifecycle_events(*events)
45+
events = events.any? ? events : %i(create update destroy)
46+
events.each do |event|
47+
after_commit(
48+
:"publish_lifecycle_event_on_#{event}",
49+
on: event
50+
)
51+
end
52+
end
53+
end
54+
end
55+
end
56+
end
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
require 'roo_on_rails/config'
2+
require 'routemaster/client'
3+
4+
module RooOnRails
5+
module Routemaster
6+
class Publisher
7+
attr_reader :model, :event
8+
9+
def initialize(model, event, client: ::Routemaster::Client)
10+
@model = model
11+
@event = event
12+
@client = client
13+
end
14+
15+
def publish?
16+
noop? || @model.new_record? || @model.previous_changes.any?
17+
end
18+
19+
def will_publish?
20+
Config.routemaster_publishing_enabled? && publish?
21+
end
22+
23+
def publish!
24+
return unless will_publish?
25+
@client.send(@event, topic, url, data: stringify_keys(data))
26+
end
27+
28+
def topic
29+
@model.class.name.tableize
30+
end
31+
32+
def url
33+
raise NotImplementedError
34+
end
35+
36+
def data
37+
nil
38+
end
39+
40+
%i(created updated deleted noop).each do |event_type|
41+
define_method :"#{event_type}?" do
42+
@event.to_sym == event_type
43+
end
44+
end
45+
46+
private
47+
48+
def stringify_keys(hash)
49+
return hash if hash.nil? || hash.empty?
50+
51+
hash.each_with_object({}) do |(k, v), h|
52+
h[k.to_s] = v.is_a?(Hash) ? stringify_keys(v) : v
53+
end
54+
end
55+
end
56+
end
57+
end
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module RooOnRails
2+
module Routemaster
3+
module Publishers
4+
@publishers = {}
5+
6+
def self.register(publisher_class, model_class:)
7+
@publishers[model_class] ||= Set.new
8+
@publishers[model_class] << publisher_class
9+
end
10+
11+
def self.for(model, event)
12+
publisher_classes = @publishers[model.class]
13+
publisher_classes.map { |c| c.new(model, event) }
14+
end
15+
end
16+
end
17+
end

roo_on_rails.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
3535
spec.add_runtime_dependency 'omniauth-google-oauth2'
3636
spec.add_runtime_dependency 'faraday'
3737
spec.add_runtime_dependency 'faraday_middleware'
38+
spec.add_runtime_dependency 'routemaster-client'
3839

3940
spec.add_development_dependency 'bundler', '~> 1.13'
4041
spec.add_development_dependency 'rake', '~> 10.0'

0 commit comments

Comments
 (0)