hermes-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +17 -0
- data/Changelog.md +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +418 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/hermes-rb.gemspec +43 -0
- data/lib/hermes-rb.rb +1 -0
- data/lib/hermes.rb +47 -0
- data/lib/hermes/b_3_propagation_model_headers.rb +23 -0
- data/lib/hermes/base_event.rb +44 -0
- data/lib/hermes/configuration.rb +62 -0
- data/lib/hermes/consumer_builder.rb +86 -0
- data/lib/hermes/database_error_handler.rb +14 -0
- data/lib/hermes/dependencies_container.rb +68 -0
- data/lib/hermes/distributed_trace.rb +11 -0
- data/lib/hermes/distributed_trace_repository.rb +41 -0
- data/lib/hermes/event_handler.rb +57 -0
- data/lib/hermes/event_processor.rb +41 -0
- data/lib/hermes/event_producer.rb +59 -0
- data/lib/hermes/logger.rb +35 -0
- data/lib/hermes/null_error_notification_service.rb +6 -0
- data/lib/hermes/null_instrumenter.rb +5 -0
- data/lib/hermes/publisher.rb +48 -0
- data/lib/hermes/publisher/hutch_adapter.rb +37 -0
- data/lib/hermes/publisher/in_memory_adapter.rb +20 -0
- data/lib/hermes/publisher_factory.rb +18 -0
- data/lib/hermes/rb.rb +8 -0
- data/lib/hermes/rb/version.rb +5 -0
- data/lib/hermes/rpc_client.rb +118 -0
- data/lib/hermes/serializer.rb +25 -0
- data/lib/hermes/support/matchers/publish_async_message.rb +30 -0
- data/lib/hermes/trace_context.rb +47 -0
- data/untitled +0 -0
- metadata +255 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 796451c71d28d43f93373f55f0b0d4e81c260026775c2e65756730aabe3d80ca
|
4
|
+
data.tar.gz: efa0dcb057ed87302c0c558f17392d06a83db786088f4617c35d3f749bb0a834
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e64fd98f7a5083b81dfd2fa09a491f1a1d6fea938022f29cad594a1132638f41620eb98aa4602e03fae647bc070ccd067e9d82657098a63ab48a05ef5d9d580a
|
7
|
+
data.tar.gz: 749a9a7d7798c1f76f70ef7f19b1f44d60738806804eac9855f7ba8027fda830c5604fff4465e2f0f80fa9dd441a51d598c2eb304dc091e28bb2ae536ea262d5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
---
|
2
|
+
language: ruby
|
3
|
+
cache: bundler
|
4
|
+
rvm:
|
5
|
+
- 2.7.2
|
6
|
+
env:
|
7
|
+
- HUTCH_URI="amqp://guest:guest@localhost:5672"
|
8
|
+
- HUTCH_ENABLE_HTTP_API_USE=false
|
9
|
+
dist: xenial
|
10
|
+
addons:
|
11
|
+
apt:
|
12
|
+
packages:
|
13
|
+
- rabbitmq-server
|
14
|
+
services:
|
15
|
+
- rabbitmq
|
16
|
+
- postgresql
|
17
|
+
before_install: gem install bundler -v 2.1.4
|
data/Changelog.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
hermes-rb (0.1.0)
|
5
|
+
activerecord (>= 5)
|
6
|
+
activesupport (>= 5)
|
7
|
+
dry-container (~> 0)
|
8
|
+
dry-struct (~> 1)
|
9
|
+
hutch (~> 1.0)
|
10
|
+
request_store (~> 1)
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: https://rubygems.org/
|
14
|
+
specs:
|
15
|
+
activemodel (6.1.0)
|
16
|
+
activesupport (= 6.1.0)
|
17
|
+
activerecord (6.1.0)
|
18
|
+
activemodel (= 6.1.0)
|
19
|
+
activesupport (= 6.1.0)
|
20
|
+
activesupport (6.1.0)
|
21
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
22
|
+
i18n (>= 1.6, < 2)
|
23
|
+
minitest (>= 5.1)
|
24
|
+
tzinfo (~> 2.0)
|
25
|
+
zeitwerk (~> 2.3)
|
26
|
+
amq-protocol (2.3.2)
|
27
|
+
bunny (2.15.0)
|
28
|
+
amq-protocol (~> 2.3, >= 2.3.1)
|
29
|
+
carrot-top (0.0.7)
|
30
|
+
json
|
31
|
+
concurrent-ruby (1.1.7)
|
32
|
+
diff-lcs (1.4.4)
|
33
|
+
dry-configurable (0.11.6)
|
34
|
+
concurrent-ruby (~> 1.0)
|
35
|
+
dry-core (~> 0.4, >= 0.4.7)
|
36
|
+
dry-equalizer (~> 0.2)
|
37
|
+
dry-container (0.7.2)
|
38
|
+
concurrent-ruby (~> 1.0)
|
39
|
+
dry-configurable (~> 0.1, >= 0.1.3)
|
40
|
+
dry-core (0.4.10)
|
41
|
+
concurrent-ruby (~> 1.0)
|
42
|
+
dry-equalizer (0.3.0)
|
43
|
+
dry-inflector (0.2.0)
|
44
|
+
dry-logic (1.0.8)
|
45
|
+
concurrent-ruby (~> 1.0)
|
46
|
+
dry-core (~> 0.2)
|
47
|
+
dry-equalizer (~> 0.2)
|
48
|
+
dry-struct (1.3.0)
|
49
|
+
dry-core (~> 0.4, >= 0.4.4)
|
50
|
+
dry-equalizer (~> 0.3)
|
51
|
+
dry-types (~> 1.3)
|
52
|
+
ice_nine (~> 0.11)
|
53
|
+
dry-types (1.4.0)
|
54
|
+
concurrent-ruby (~> 1.0)
|
55
|
+
dry-container (~> 0.3)
|
56
|
+
dry-core (~> 0.4, >= 0.4.4)
|
57
|
+
dry-equalizer (~> 0.3)
|
58
|
+
dry-inflector (~> 0.1, >= 0.1.2)
|
59
|
+
dry-logic (~> 1.0, >= 1.0.2)
|
60
|
+
hutch (1.0.0)
|
61
|
+
activesupport (>= 4.2, < 7)
|
62
|
+
bunny (>= 2.15, < 2.16)
|
63
|
+
carrot-top (~> 0.0.7)
|
64
|
+
multi_json (~> 1.14)
|
65
|
+
i18n (1.8.5)
|
66
|
+
concurrent-ruby (~> 1.0)
|
67
|
+
ice_nine (0.11.2)
|
68
|
+
json (2.3.1)
|
69
|
+
minitest (5.14.2)
|
70
|
+
multi_json (1.15.0)
|
71
|
+
pg (1.2.3)
|
72
|
+
rack (2.2.3)
|
73
|
+
rake (13.0.1)
|
74
|
+
request_store (1.5.0)
|
75
|
+
rack (>= 1.4)
|
76
|
+
rspec (3.10.0)
|
77
|
+
rspec-core (~> 3.10.0)
|
78
|
+
rspec-expectations (~> 3.10.0)
|
79
|
+
rspec-mocks (~> 3.10.0)
|
80
|
+
rspec-core (3.10.0)
|
81
|
+
rspec-support (~> 3.10.0)
|
82
|
+
rspec-expectations (3.10.0)
|
83
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
84
|
+
rspec-support (~> 3.10.0)
|
85
|
+
rspec-mocks (3.10.0)
|
86
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
87
|
+
rspec-support (~> 3.10.0)
|
88
|
+
rspec-support (3.10.0)
|
89
|
+
timecop (0.9.2)
|
90
|
+
tzinfo (2.0.3)
|
91
|
+
concurrent-ruby (~> 1.0)
|
92
|
+
vcr (5.0.0)
|
93
|
+
zeitwerk (2.4.2)
|
94
|
+
|
95
|
+
PLATFORMS
|
96
|
+
ruby
|
97
|
+
|
98
|
+
DEPENDENCIES
|
99
|
+
bundler
|
100
|
+
hermes-rb!
|
101
|
+
pg
|
102
|
+
rake
|
103
|
+
rspec
|
104
|
+
timecop
|
105
|
+
vcr
|
106
|
+
|
107
|
+
BUNDLED WITH
|
108
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Karol Galanciak
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,418 @@
|
|
1
|
+
# Hermes
|
2
|
+
|
3
|
+
Hermes - a messenger of gods, delivering them via RabbitMQ with a little help from [hutch](https://github.com/gocardless/hutch).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'hermes-rb'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install hermes-rb
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
First, define an initializer, for example `config/initializers/hermes.rb`
|
24
|
+
|
25
|
+
``` rb
|
26
|
+
Rails.application.config.to_prepare do
|
27
|
+
event_handler = Hermes::EventHandler.new
|
28
|
+
|
29
|
+
Hermes.configure do |config|
|
30
|
+
config.adapter = Rails.application.config.async_messaging_adapter
|
31
|
+
config.application_prefix = "my_app"
|
32
|
+
config.background_processor = HermesHandlerJob
|
33
|
+
config.enqueue_method = :perform_async
|
34
|
+
config.event_handler = event_handler
|
35
|
+
config.clock = Time.zone
|
36
|
+
config.instrumenter = Instrumenter
|
37
|
+
config.configure_hutch do |hutch|
|
38
|
+
hutch.uri = ENV.fetch("HUTCH_URI")
|
39
|
+
end
|
40
|
+
config.distributed_tracing_database_uri = ENV.fetch("DISTRIBUTED_TRACING_DATABASE_URI", nil)
|
41
|
+
config.error_notification_service = Raven
|
42
|
+
end
|
43
|
+
|
44
|
+
event_handler.handle_events do
|
45
|
+
handle Events::Example::Happened, with: Example::HappenedHandler
|
46
|
+
|
47
|
+
handle Events::Example::SyncCallHappened, with: Example::SyncCallHappenedHandler, async: false
|
48
|
+
end
|
49
|
+
|
50
|
+
# if you care about distributed tracing
|
51
|
+
if Hermes.configuration.store_distributed_traces?
|
52
|
+
Hermes::DistributedTrace.establish_connection(Hermes.configuration.distributed_tracing_database_uri)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Hutch::Logging.logger = Rails.logger if !Rails.env.test? && !Rails.env.development?
|
57
|
+
```
|
58
|
+
|
59
|
+
Note that not all options are required (could be the case if the application is just a producer or just a consumer).
|
60
|
+
|
61
|
+
1. `adapter` - messages can be either delivered via RabbitMQ or in-memory adapter (useful for testing). Most likely you will want to make it based on the environment, that's why it's advisable to use `Rails.application.config.async_messaging_adapter` and define `async_messaging_adapter` on `config` object in `development.rb`, `test.rb` and `production.rb` files. The recommended setup is to assign `config.async_messaging_adapter = :in_memory` for test ENV and `config.async_messaging_adapter = :hutch` for production and development ENVs.
|
62
|
+
2. `application_prefix` - identifier for this application. **ABSOLUTELY NECESSARY** unless you want to have competing queues with different applications (hint: most likely you don't want that).
|
63
|
+
|
64
|
+
3 and 4. `background_processor` and `enqueue_method`. By design, Hermes is supposed to use Hutch workers to fetch the messages from RabbitMQ and process them in some background jobs framework. `background_processor` refers to the name of the class for the job and `enqueue_method` is the method name that will be called when enqueuing the job. This method must accept three arguments: `event_class`, `body` and `headers`. Here is an example for Sidekiq:
|
65
|
+
|
66
|
+
``` rb
|
67
|
+
class HermesHandlerJob
|
68
|
+
include Sidekiq::Worker
|
69
|
+
|
70
|
+
sidekiq_options queue: :critical
|
71
|
+
|
72
|
+
def perform(event_class, body, headers)
|
73
|
+
Hermes::EventProcessor.call(event_class, body, headers)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
If you know what you are doing, you don't necessarily have to process things in the background. As long as the class implements the expected interface, you can do anything you want.
|
79
|
+
|
80
|
+
5. `event_handler` - an instance of event handler/storage, just use what is shown in the example.
|
81
|
+
6. `clock` - a clock object that is time-zone aware, implementing `now` method.
|
82
|
+
7. `configure_hutch` - a way to specify `hutch uri`, basically the URI for RabbitMQ.
|
83
|
+
8. `event_handler.handle_events` - that's how you declare events and their handlers. The event handler is an object that responds to `call` method and takes `event` as an argument. All events should ideally be subclasses of `Hermes::BaseEvent`
|
84
|
+
|
85
|
+
This class inherits from `Dry::Struct`, so getting familiar with [dry-struct gem](https://dry-rb.org/gems/dry-struct/) would be beneficial. Here is an example event:
|
86
|
+
|
87
|
+
``` rb
|
88
|
+
class Payment::MarkedAsPaid < Hermes::BaseEvent
|
89
|
+
attribute :payment_id, Types::Strict::Integer
|
90
|
+
attribute :cents, Types::Strict::Integer
|
91
|
+
attribute :currency, Types::Strict::String
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
To keep things clean, you might want to prefix the namespace with `Events`:
|
96
|
+
|
97
|
+
``` rb
|
98
|
+
class Events::Payment::MarkedAsPaid < Hermes::BaseEvent
|
99
|
+
attribute :payment_id, Types::Strict::Integer
|
100
|
+
attribute :cents, Types::Strict::Integer
|
101
|
+
attribute :currency, Types::Strict::String
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
In both cases, the routing key will be the same (`Events` prefix is dropped) and will resolve to `payment.marked_as_paid`
|
106
|
+
|
107
|
+
To avoid unexpected problems, don't use restricted names for attribtes such as `meta`, `routing_key`, `origin_headers`, `origin_body`, `trace_context`, `version`.
|
108
|
+
|
109
|
+
You can also specify whether the event should be processed asynchronously using `background_processor` (default behavior) or synchronously. If you want the event to be processed synchronously, e.g. when doing RPC, use `async: false` option.
|
110
|
+
|
111
|
+
9. `rpc_call_timeout` - a timeout for RPC calls, defaults to 10 seconds. Can be also customized per instance of RPC Client (covered later). Optional.
|
112
|
+
|
113
|
+
10. `instrumenter` - instrumenter object responding to `instrument` method taking one string argument, one optional hash argument and a block.
|
114
|
+
|
115
|
+
For example:
|
116
|
+
|
117
|
+
``` rb
|
118
|
+
module Instrumenter
|
119
|
+
extend ::NewRelic::Agent::MethodTracer
|
120
|
+
|
121
|
+
def self.instrument(name, payload = {})
|
122
|
+
ActiveSupport::Notifications.instrument(name, payload) do
|
123
|
+
self.class.trace_execution_scoped([name]) do
|
124
|
+
yield if block_given?
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
If you don't care about it, you can leave it empty.
|
132
|
+
|
133
|
+
11. `distributed_tracing_database_uri` - If you want to enable distributed tracing, specify Postgres database URI. Optional.
|
134
|
+
|
135
|
+
12. `distributed_tracing_database_table` - Table name for storing traces, by default it's `hermes_distributed_traces`. Optional.
|
136
|
+
|
137
|
+
13. `distributes_tracing_mapper` - an object responding to `call` method taking one argument (a hash of attributes) that has to return a hash as well. This hash will be used for assigning attributes when creating `Hermes::DistributedTrace`. The default mapper just returns the original hash. You can use it if you want to remove, for example, some sensitive info from the event's body. Optional.
|
138
|
+
|
139
|
+
14. `error_notification_service` - an object responding to `capture_exception` method taking one argument (error). Used when storing distributed traces, its interface is based on `Raven` from [Sentry Raven](https://github.com/getsentry/sentry-ruby/tree/master/sentry-raven). By default `Hermes::NullErrorNotificationService` is used, which does nothing. Optional.
|
140
|
+
|
141
|
+
15. `database_error_handler` - `an object responding to `call` method taking one argument (error). Used when storing distributed traces. By default it uses `Hermes::DatabaseErrorHandler` which depends on `error_notification_service`, so in most cases, you will probably want to just configure `error_notification_service`. Optional.
|
142
|
+
|
143
|
+
## RPC
|
144
|
+
|
145
|
+
If you want to handle RPC call, you need to add `rpc: true` flag. Keep in mind that RPC requires a synchronous processing and response, so you also need to set `async: false`. The routing key and correlation ID will be resolved based on the message that is published by the client. The payload that is sent back will be what event handler reutrns, so it might be a good idea to just return a hash so that you can operate on JSON easily.
|
146
|
+
|
147
|
+
## Publishing
|
148
|
+
|
149
|
+
To publish an async event call `Hermes::Publisher`:
|
150
|
+
|
151
|
+
``` rb
|
152
|
+
Hermes::EventProducer.publish(event)
|
153
|
+
```
|
154
|
+
|
155
|
+
`event` is an instance of a subclass of `Events::BaseEvent`.
|
156
|
+
|
157
|
+
If you want to perform a synchronous RPC call, use `Hermes::RpcClient`:
|
158
|
+
|
159
|
+
``` rb
|
160
|
+
parsed_response_hash = Hermes::RpcClient.call(event)
|
161
|
+
```
|
162
|
+
|
163
|
+
You can also use an explicit initializer and provide custom `rpc_call_timeout`:
|
164
|
+
|
165
|
+
``` rb
|
166
|
+
parsed_response_hash = Hermes::RpcClient.new(rpc_call_timeout: 10).call(event)
|
167
|
+
```
|
168
|
+
|
169
|
+
If the request timeouts, `Hermes::RpcClient::RpcTimeoutError` will be raised.
|
170
|
+
|
171
|
+
## Distributed Tracing (experimental feature, the interface might change in the future)
|
172
|
+
|
173
|
+
If you want to take advantage of distributed tracing, you need to specify `distributed_tracing_database_uri` in the config and in many cases that will be enough, although there are some cases where some extra code will be required to properly use it.
|
174
|
+
|
175
|
+
If you have a "standard" flow, which means producing events and then consuming them in the jobs specified by `background_processor` and publishing other events from the same class, then you don't need to do anything extra as things will be handled out-of-box. In such scenario, at least two `Hermes::DistributedTrace` will be created (one for producer, and the rest for consumers and then potential other traces if the consumer also published some events).
|
176
|
+
|
177
|
+
However, if you enqueue some job inside the job specified by `background_processor`, you will need to do something extra:
|
178
|
+
|
179
|
+
1. You need to pass `origin_headers` as an argument to the job to have headers available. You can extract them inside the handler from the event by calling `event.origin_headers`
|
180
|
+
2. When processing the job, you will need to assign these headers to `Hermes`:
|
181
|
+
|
182
|
+
``` rb
|
183
|
+
Hermes.origin_headers = origin_headers
|
184
|
+
```
|
185
|
+
|
186
|
+
These `origin_headers` will be stored in `RequestStore.store` (it uses [request_store](https://github.com/steveklabnik/request_store)).
|
187
|
+
|
188
|
+
Traces are also stored for RPC calls. For a single RPC, there will be traces:
|
189
|
+
1. Client (the actual RPC call)
|
190
|
+
2. Server (processing the request)
|
191
|
+
3. Client (processing the response) - that one uses a special internal event to keep the consistency: `ResponseEvent`, which stores `response_body` as a hash.
|
192
|
+
|
193
|
+
|
194
|
+
You will also need to create an appropriate database table:
|
195
|
+
|
196
|
+
``` rb
|
197
|
+
create_table(:hermes_distributed_traces) do |t|
|
198
|
+
t.string "trace", null: false
|
199
|
+
t.string "span", null: false
|
200
|
+
t.string "parent_span"
|
201
|
+
t.string "service", null: false
|
202
|
+
t.text "event_class", null: false
|
203
|
+
t.text "routing_key", null: false
|
204
|
+
t.jsonb "event_body", null: false, default: []
|
205
|
+
t.jsonb "event_headers", null: false, default: []
|
206
|
+
t.datetime "created_at", precision: 6, null: false
|
207
|
+
t.datetime "updated_at", precision: 6, null: false
|
208
|
+
|
209
|
+
t.index ["created_at"], name: "index_hermes_distributed_traces_on_created_at", using: :brin
|
210
|
+
t.index ["trace"], name: "index_hermes_distributed_traces_on_trace"
|
211
|
+
t.index ["span"], name: "index_hermes_distributed_traces_on_span"
|
212
|
+
t.index ["service"], name: "index_hermes_distributed_traces_on_service"
|
213
|
+
t.index ["event_class"], name: "index_hermes_distributed_traces_on_event_class"
|
214
|
+
t.index ["routing_key"], name: "index_hermes_distributed_traces_on_routing_key"
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
Some important attributes to understand which will be useful during potential debugging:
|
219
|
+
|
220
|
+
1. `trace` - ID of the trace - all events from the same saga will have the same value (and that's why it's important to properly deal with `origin_headers`).
|
221
|
+
2. `span` - ID of the operation.
|
222
|
+
3. `parent span` - span value of the previous operation from the previous service.
|
223
|
+
4. `service` - name of the service where the given event occured, based on `application_prefix`,
|
224
|
+
|
225
|
+
It is highly recommended to use a shared database for storing traces. It's not ideal, but the benefits of storing traces in a single DB shared by the applications outweigh the disadvantages in many cases.
|
226
|
+
|
227
|
+
Since distributed tracing is a secondary feature, all exceptions coming from the database are rescued. It is highly recommended to provide `error_notification_service` to be notified about these errors. If you are not happy with that behavior and you would prefer to have errors raised, you can implement your own `database_error_handler` where you can re-raise the exception.
|
228
|
+
|
229
|
+
## Testing
|
230
|
+
|
231
|
+
### RSpec useful stuff
|
232
|
+
|
233
|
+
Put this inside `rails_helper`. Note that it requires `webmock` and `sidekiq`.
|
234
|
+
|
235
|
+
``` rb
|
236
|
+
def execute_jobs_inline
|
237
|
+
original_active_job_adapter = ActiveJob::Base.queue_adapter
|
238
|
+
ActiveJob::Base.queue_adapter = :inline
|
239
|
+
Sidekiq::Testing.inline! do
|
240
|
+
yield
|
241
|
+
end
|
242
|
+
ActiveJob::Base.queue_adapter = original_active_job_adapter
|
243
|
+
end
|
244
|
+
|
245
|
+
config.around(:example, :inline_jobs) do |example|
|
246
|
+
execute_jobs_inline { example.run }
|
247
|
+
end
|
248
|
+
|
249
|
+
class ActiveRecord::Base
|
250
|
+
mattr_accessor :shared_connection
|
251
|
+
|
252
|
+
def self.connection
|
253
|
+
shared_connection.presence || retrieve_connection
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
config.after(:each) do
|
258
|
+
Hermes::Publisher.instance.reset
|
259
|
+
end
|
260
|
+
|
261
|
+
config.before(:each, :with_rabbit_mq) do
|
262
|
+
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
|
263
|
+
|
264
|
+
stub_request(:get, "http://127.0.0.1:15672/api/exchanges")
|
265
|
+
stub_request(:get, "http://127.0.0.1:15672/api/bindings")
|
266
|
+
|
267
|
+
hutch_publisher = Hermes::Publisher::HutchAdapter.new
|
268
|
+
Hermes::Publisher.instance.current_adapter = hutch_publisher
|
269
|
+
|
270
|
+
@worker_thread = Thread.new do
|
271
|
+
Hutch.connect
|
272
|
+
|
273
|
+
worker = Hutch::Worker.new(Hutch.broker, Hutch.consumers, Hutch::Config.setup_procs)
|
274
|
+
worker.run
|
275
|
+
end
|
276
|
+
|
277
|
+
sleep 0.2
|
278
|
+
end
|
279
|
+
|
280
|
+
config.after(:each, :with_rabbit_mq) do |example|
|
281
|
+
@worker_thread.kill
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
To run integrations specs (with real RabbitMQ process), use `inline_jobs` and `with_rabbit_mq` meta flags.
|
286
|
+
|
287
|
+
#### Example integration spec with RabbitMQ
|
288
|
+
|
289
|
+
``` rb
|
290
|
+
require "rails_helper"
|
291
|
+
|
292
|
+
RSpec.describe "Example Event Test", :with_rabbit_mq, :inline_jobs do
|
293
|
+
describe "when Events::Example::Happened is published" do
|
294
|
+
subject(:publish_event) { Hermes::EventProducer.publish(event) }
|
295
|
+
|
296
|
+
let(:event) { Events::Example::Happened.new(event_params) }
|
297
|
+
let(:event_params) do
|
298
|
+
{
|
299
|
+
name: name
|
300
|
+
}
|
301
|
+
end
|
302
|
+
let(:name) { "hermes" }
|
303
|
+
|
304
|
+
it "calls Example::HappenedHandler" do
|
305
|
+
expect(Example::HappenedHandler).to receive(:call)
|
306
|
+
.with(instance_of(Events::Example::Happened)).and_call_original
|
307
|
+
|
308
|
+
publish_event
|
309
|
+
sleep 0.2 # since this is an async action, some delay will be required, either with a simple way like this, or you may want to go with something more complex to not put ugly `sleep` here
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
```
|
315
|
+
|
316
|
+
### Matchers
|
317
|
+
|
318
|
+
E.g. in `spec/supports/matchers/publish_async_message`:
|
319
|
+
|
320
|
+
``` rb
|
321
|
+
require "hermes/support/matchers/publish_async_message"
|
322
|
+
```
|
323
|
+
|
324
|
+
And then use it in the following way:
|
325
|
+
|
326
|
+
``` rb
|
327
|
+
expect {
|
328
|
+
call
|
329
|
+
}.to publish_async_message(routing_key_of_the_expected_event).with_event_payload(expected_event_payload)
|
330
|
+
```
|
331
|
+
|
332
|
+
Note that `expected_event_payload` does not contain extra `meta` key that is added by Hermes publisher, it's just a symbolized hash with the result of the serialization of the event.
|
333
|
+
|
334
|
+
### Example test of HermesHandlerJob
|
335
|
+
|
336
|
+
``` rb
|
337
|
+
require "rails_helper"
|
338
|
+
|
339
|
+
RSpec.describe HermesHandlerJob do
|
340
|
+
it { is_expected.to be_processed_in :critical }
|
341
|
+
|
342
|
+
describe "#perform" do
|
343
|
+
subject(:perform) { described_class.new.perform(EventClassForTestingHermesHandlerJob.to_s, payload, headers) }
|
344
|
+
|
345
|
+
let(:configuration) { Hermes.configuration }
|
346
|
+
let(:event_handler) { Hermes::EventHandler.new }
|
347
|
+
let(:payload) do
|
348
|
+
{
|
349
|
+
"bookingsync" => "hermes"
|
350
|
+
}
|
351
|
+
end
|
352
|
+
let(:headers) do
|
353
|
+
{}
|
354
|
+
end
|
355
|
+
class EventClassForTestingHermesHandlerJob < Hermes::BaseEvent
|
356
|
+
attribute :bookingsync, Types::Strict::String
|
357
|
+
end
|
358
|
+
class HandlerForEventClassForTestingHermesHandlerJob
|
359
|
+
def self.event
|
360
|
+
@event
|
361
|
+
end
|
362
|
+
|
363
|
+
def self.call(event)
|
364
|
+
@event = event
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
before do
|
369
|
+
event_handler.handle_events do
|
370
|
+
handle EventClassForTestingHermesHandlerJob, with: HandlerForEventClassForTestingHermesHandlerJob
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
around do |example|
|
375
|
+
original_event_handler = configuration.event_handler
|
376
|
+
|
377
|
+
Hermes.configure do |config|
|
378
|
+
config.event_handler = event_handler
|
379
|
+
end
|
380
|
+
|
381
|
+
example.run
|
382
|
+
|
383
|
+
Hermes.configure do |config|
|
384
|
+
config.event_handler = original_event_handler
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
it "calls proper handler with a given event" do
|
389
|
+
perform
|
390
|
+
|
391
|
+
expect(HandlerForEventClassForTestingHermesHandlerJob.event).to be_a(EventClassForTestingHermesHandlerJob)
|
392
|
+
expect(HandlerForEventClassForTestingHermesHandlerJob.event.bookingsync).to eq "hermes"
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
## Deployment and managing consumerzs
|
399
|
+
|
400
|
+
Hermes is just an extra layer on top of [hutch](https://github.com/gocardless/hutch), refer to Hutch's docs for more info about dealing with the workers and deployment.
|
401
|
+
|
402
|
+
## CircleCI config for installing RabbitMQ
|
403
|
+
|
404
|
+
Use `- image: brandembassy/rabbitmq:latest`
|
405
|
+
|
406
|
+
## Development
|
407
|
+
|
408
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
409
|
+
|
410
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
411
|
+
|
412
|
+
## Contributing
|
413
|
+
|
414
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/BookingSync/hermes-rb.
|
415
|
+
|
416
|
+
## License
|
417
|
+
|
418
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|