hermes-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -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
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hermes-rb.gemspec
4
+ gemspec
@@ -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
@@ -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.
@@ -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).