hermes-rb 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 796451c71d28d43f93373f55f0b0d4e81c260026775c2e65756730aabe3d80ca
4
- data.tar.gz: efa0dcb057ed87302c0c558f17392d06a83db786088f4617c35d3f749bb0a834
3
+ metadata.gz: 104554076c3b4ad46491c12d842e3580fdf8c96ba36eefbfc2f4c66a5ba5f208
4
+ data.tar.gz: a5f1754803e51907feda1164ff25c048731037f4cd75f2a6eaf4db68668b6647
5
5
  SHA512:
6
- metadata.gz: e64fd98f7a5083b81dfd2fa09a491f1a1d6fea938022f29cad594a1132638f41620eb98aa4602e03fae647bc070ccd067e9d82657098a63ab48a05ef5d9d580a
7
- data.tar.gz: 749a9a7d7798c1f76f70ef7f19b1f44d60738806804eac9855f7ba8027fda830c5604fff4465e2f0f80fa9dd441a51d598c2eb304dc091e28bb2ae536ea262d5
6
+ metadata.gz: aea75486bab7ba279b788dc48ea465f3ecb96c7281d9b6cc9bc29443c738ba779fe0951f6d0a8028211cce4f64520556f085cab73aee7ba98bac01af9dc5a973
7
+ data.tar.gz: 10b79dce9ad5c116d6ccb31a6102b1e3732a2b0a73e328b5ef37b421e5407cc8a5cc240e6b68c11b7fd26bd2c06acb579a610ba3708d83515d5c17dc48207f50
@@ -1,4 +1,7 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+ - Implement "Safe Producer" extension allowing to retry delivery later in an automated way when publishing of the message fails.
5
+
3
6
  ## 0.1.0
4
7
  - Initial release
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hermes-rb (0.1.0)
4
+ hermes-rb (0.2.0)
5
5
  activerecord (>= 5)
6
6
  activesupport (>= 5)
7
7
  dry-container (~> 0)
@@ -12,12 +12,12 @@ PATH
12
12
  GEM
13
13
  remote: https://rubygems.org/
14
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)
15
+ activemodel (6.1.1)
16
+ activesupport (= 6.1.1)
17
+ activerecord (6.1.1)
18
+ activemodel (= 6.1.1)
19
+ activesupport (= 6.1.1)
20
+ activesupport (6.1.1)
21
21
  concurrent-ruby (~> 1.0, >= 1.0.2)
22
22
  i18n (>= 1.6, < 2)
23
23
  minitest (>= 5.1)
@@ -30,21 +30,19 @@ GEM
30
30
  json
31
31
  concurrent-ruby (1.1.7)
32
32
  diff-lcs (1.4.4)
33
- dry-configurable (0.11.6)
33
+ dry-configurable (0.12.0)
34
34
  concurrent-ruby (~> 1.0)
35
- dry-core (~> 0.4, >= 0.4.7)
36
- dry-equalizer (~> 0.2)
35
+ dry-core (~> 0.5, >= 0.5.0)
37
36
  dry-container (0.7.2)
38
37
  concurrent-ruby (~> 1.0)
39
38
  dry-configurable (~> 0.1, >= 0.1.3)
40
- dry-core (0.4.10)
39
+ dry-core (0.5.0)
41
40
  concurrent-ruby (~> 1.0)
42
41
  dry-equalizer (0.3.0)
43
42
  dry-inflector (0.2.0)
44
- dry-logic (1.0.8)
43
+ dry-logic (1.1.0)
45
44
  concurrent-ruby (~> 1.0)
46
- dry-core (~> 0.2)
47
- dry-equalizer (~> 0.2)
45
+ dry-core (~> 0.5, >= 0.5)
48
46
  dry-struct (1.3.0)
49
47
  dry-core (~> 0.4, >= 0.4.4)
50
48
  dry-equalizer (~> 0.3)
@@ -62,11 +60,11 @@ GEM
62
60
  bunny (>= 2.15, < 2.16)
63
61
  carrot-top (~> 0.0.7)
64
62
  multi_json (~> 1.14)
65
- i18n (1.8.5)
63
+ i18n (1.8.7)
66
64
  concurrent-ruby (~> 1.0)
67
65
  ice_nine (0.11.2)
68
- json (2.3.1)
69
- minitest (5.14.2)
66
+ json (2.5.1)
67
+ minitest (5.14.3)
70
68
  multi_json (1.15.0)
71
69
  pg (1.2.3)
72
70
  rack (2.2.3)
@@ -87,7 +85,7 @@ GEM
87
85
  rspec-support (~> 3.10.0)
88
86
  rspec-support (3.10.0)
89
87
  timecop (0.9.2)
90
- tzinfo (2.0.3)
88
+ tzinfo (2.0.4)
91
89
  concurrent-ruby (~> 1.0)
92
90
  vcr (5.0.0)
93
91
  zeitwerk (2.4.2)
data/README.md CHANGED
@@ -136,10 +136,14 @@ If you don't care about it, you can leave it empty.
136
136
 
137
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
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.
139
+ 14. `error_notification_service` - an object responding to `capture_exception` method taking one argument (error). 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
140
 
141
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
142
 
143
+ 16. `enable_safe_producer` - a method requiring a job class implementing `enqueue` method that will be responsible for retrying delivery of the event later in case it fails. Check `Safe Event Producer` section for more details.
144
+
145
+ 17. `producer_retryable` - used when `safe_producer` was enabled via (`enable_safe_producer`). By default, it is a method retrying delivery 3 times rescuing from `StandardError` each time. The object responsible for this behavior by default is: `Hermes::Retryable.new(times: 3, errors: [StandardError])`.
146
+
143
147
  ## RPC
144
148
 
145
149
  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.
@@ -226,6 +230,40 @@ It is highly recommended to use a shared database for storing traces. It's not i
226
230
 
227
231
  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
232
 
233
+ ## Safe Event Producer
234
+
235
+ Most likely in your production environment you are going to have a high availability setup with more than one node, probably at least 3. This might seem like there is very little chance that something will go wrong when publishing an event to RabbitMQ and even if it happens, it will be so rare that you will handle any exceptions manually. However, operations like updating Erlang will most likely require a downtime, which will mean that you might have a lot of errors during that period. Not to mention other potential issues, even without scheduled downtime, like the entire cluster being down for random reason or timeouts.
236
+
237
+ In that case, it might be a good idea to have some automated way of dealing with this kind of issues. For that purpose, you can enable a `Safe Event Producer` - by default, it's going to try publishing event 3 times, rescuing twice from `StandardError`, and if it fails after a 3rd time, it's going to use `error_notification_service` to deliver info about the error that ahppened and is going to call `enqueue` method on a specified object.
238
+
239
+ To take advantage of this feature, apply the following logic in the initializer
240
+
241
+ ``` rb
242
+ Hermes.configure do |config|
243
+ config.clock = clock
244
+ config.error_notification_service = Raven
245
+ config.enable_safe_producer(HermesRecoveryJob)
246
+ end
247
+ ```
248
+
249
+ `HermesRecoveryJob` is expected to implement `enqueue` method taking 3 arguments: `event_class_name`, `event_body` and `headers`. What happens in `enqueue` method is up to you. You can, for example, schedule publishing the message in 5 minutes from now. However, the job should call `Hermes::RetryableEventProducer.publish(event_class, event_body, headers)` to properly handle the delivery retry flow. Here is an example job class using Sidekiq:
250
+
251
+ ``` rb
252
+ class HermesRecoveryJob
253
+ include Sidekiq::Worker
254
+
255
+ sidekiq_options queue: :hermes_recovery
256
+
257
+ def self.enqueue(event_class, event_body, origin_headers)
258
+ perform_at(5.minutes.from_now, event_class, event_body, origin_headers)
259
+ end
260
+
261
+ def perform(event_class, event_body, origin_headers)
262
+ Hermes::RetryableEventProducer.publish(event_class, event_body, origin_headers)
263
+ end
264
+ end
265
+ ```
266
+
229
267
  ## Testing
230
268
 
231
269
  ### RSpec useful stuff
@@ -395,7 +433,7 @@ RSpec.describe HermesHandlerJob do
395
433
  end
396
434
  ```
397
435
 
398
- ## Deployment and managing consumerzs
436
+ ## Deployment and managing consumers
399
437
 
400
438
  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
439
 
@@ -18,6 +18,11 @@ require "hermes/distributed_trace_repository"
18
18
  require "hermes/dependencies_container"
19
19
  require "hermes/null_error_notification_service"
20
20
  require "hermes/database_error_handler"
21
+ require "hermes/retryable"
22
+ require "hermes/retryable_event_producer"
23
+ require "hermes/producer_error_handler"
24
+ require "hermes/producer_error_handler/null_handler"
25
+ require "hermes/producer_error_handler/safe_handler"
21
26
  require "active_support"
22
27
  require "active_support/core_ext/string"
23
28
  require "active_record"
@@ -3,6 +3,22 @@ module Hermes
3
3
  attr_reader :trace_context
4
4
  private :trace_context
5
5
 
6
+ def self.trace_id_key
7
+ "X-B3-TraceId"
8
+ end
9
+
10
+ def self.span_id_key
11
+ "X-B3-SpanId"
12
+ end
13
+
14
+ def self.parent_span_id_key
15
+ "X-B3-ParentSpanId"
16
+ end
17
+
18
+ def self.sampled_key
19
+ "X-B3-Sampled"
20
+ end
21
+
6
22
  def initialize(trace_context)
7
23
  @trace_context = trace_context
8
24
  end
@@ -13,10 +29,10 @@ module Hermes
13
29
 
14
30
  def to_h
15
31
  {
16
- "X-B3-TraceId" => trace_context.trace,
17
- "X-B3-ParentSpanId" => trace_context.parent_span,
18
- "X-B3-SpanId" => trace_context.span,
19
- "X-B3-Sampled" => ""
32
+ self.class.trace_id_key => trace_context.trace,
33
+ self.class.parent_span_id_key => trace_context.parent_span,
34
+ self.class.span_id_key => trace_context.span,
35
+ self.class.sampled_key => ""
20
36
  }
21
37
  end
22
38
  end
@@ -3,7 +3,8 @@ module Hermes
3
3
  attr_accessor :adapter, :clock, :hutch, :application_prefix, :logger,
4
4
  :background_processor, :enqueue_method, :event_handler, :rpc_call_timeout,
5
5
  :instrumenter, :distributed_tracing_database_uri, :distributed_tracing_database_table,
6
- :distributes_tracing_mapper, :database_error_handler, :error_notification_service
6
+ :distributes_tracing_mapper, :database_error_handler, :error_notification_service, :producer_error_handler,
7
+ :producer_error_handler_job_class, :producer_retryable
7
8
 
8
9
  def configure_hutch
9
10
  yield hutch
@@ -38,7 +39,7 @@ module Hermes
38
39
  end
39
40
 
40
41
  def distributes_tracing_mapper=(mapper)
41
- raise ArgumentError.new("mapper must espond to :call method") if !mapper.respond_to?(:call)
42
+ raise ArgumentError.new("mapper must respond to :call method") if !mapper.respond_to?(:call)
42
43
  @distributes_tracing_mapper = mapper
43
44
  end
44
45
 
@@ -54,6 +55,24 @@ module Hermes
54
55
  @database_error_handler || Hermes::DatabaseErrorHandler.new(error_notification_service: error_notification_service)
55
56
  end
56
57
 
58
+ def producer_error_handler
59
+ @producer_error_handler || Hermes::ProducerErrorHandler::NullHandler
60
+ end
61
+
62
+ def producer_retryable
63
+ @producer_retryable || Hermes::Retryable.new(times: 3, errors: [StandardError])
64
+ end
65
+
66
+ def enable_safe_producer(producer_error_handler_job_class)
67
+ self.producer_error_handler_job_class = producer_error_handler_job_class
68
+
69
+ @producer_error_handler = Hermes::ProducerErrorHandler::SafeHandler.new(
70
+ job_class: producer_error_handler_job_class,
71
+ error_notifier: error_notification_service,
72
+ retryable: producer_retryable
73
+ )
74
+ end
75
+
57
76
  class HutchConfig
58
77
  attr_accessor :uri
59
78
  end
@@ -64,5 +64,17 @@ module Hermes
64
64
  def self.database_error_handler
65
65
  config.database_error_handler
66
66
  end
67
+
68
+ def self.producer_error_handler
69
+ config.producer_error_handler
70
+ end
71
+
72
+ def self.producer_retryable
73
+ config.producer_retryable
74
+ end
75
+
76
+ def self.objects_resolver
77
+ Object
78
+ end
67
79
  end
68
80
  end
@@ -2,8 +2,8 @@ module Hermes
2
2
  class EventProducer
3
3
  extend Forwardable
4
4
 
5
- attr_reader :publisher, :serializer, :distributed_trace_repository, :config
6
- private :publisher, :serializer, :distributed_trace_repository, :config
5
+ attr_reader :publisher, :serializer, :distributed_trace_repository, :producer_error_handler, :config
6
+ private :publisher, :serializer, :distributed_trace_repository, :producer_error_handler, :config
7
7
 
8
8
  def self.publish(event, properties = {}, options = {})
9
9
  build.publish(event, properties, options)
@@ -14,19 +14,23 @@ module Hermes
14
14
  publisher: Hermes::DependenciesContainer["publisher"],
15
15
  serializer: Hermes::DependenciesContainer["serializer"],
16
16
  distributed_trace_repository: Hermes::DependenciesContainer["distributed_trace_repository"],
17
+ producer_error_handler: Hermes::DependenciesContainer["producer_error_handler"],
17
18
  config: Hermes::DependenciesContainer["config"]
18
19
  )
19
20
  end
20
21
 
21
- def initialize(publisher:, serializer:, distributed_trace_repository:, config:)
22
+ def initialize(publisher:, serializer:, distributed_trace_repository:, producer_error_handler:, config:)
22
23
  @publisher = publisher
23
24
  @serializer = serializer
24
25
  @distributed_trace_repository = distributed_trace_repository
26
+ @producer_error_handler = producer_error_handler
25
27
  @config = config
26
28
  end
27
29
 
28
30
  def publish(event, properties = {}, options = {})
29
- publish_event(event, properties, options).tap { store_trace(event) }
31
+ producer_error_handler.call(event) do
32
+ publish_event(event, properties, options).tap { store_trace(event) }
33
+ end
30
34
  end
31
35
 
32
36
  private
@@ -0,0 +1,4 @@
1
+ module Hermes
2
+ module ProducerErrorHandler
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module Hermes
2
+ module ProducerErrorHandler
3
+ class NullHandler
4
+ def self.call(*)
5
+ yield
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Hermes
2
+ module ProducerErrorHandler
3
+ class SafeHandler
4
+ attr_reader :job_class, :error_notifier, :retryable
5
+ private :job_class, :error_notifier, :retryable
6
+
7
+ def initialize(job_class:, error_notifier:, retryable:)
8
+ @job_class = job_class
9
+ @error_notifier = error_notifier
10
+ @retryable = retryable
11
+ end
12
+
13
+ def call(event)
14
+ retryable.perform { yield }
15
+ rescue => error
16
+ error_notifier.capture_exception(error)
17
+ job_class.enqueue(event.class.name, event.as_json, event.origin_headers)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,7 +1,6 @@
1
1
  require "hermes/publisher/hutch_adapter"
2
2
  require "hermes/publisher/in_memory_adapter"
3
3
 
4
-
5
4
  module Hermes
6
5
  class PublisherFactory
7
6
  def self.build(adapter)
@@ -1,5 +1,5 @@
1
1
  module Hermes
2
2
  module Rb
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -0,0 +1,27 @@
1
+ module Hermes
2
+ class Retryable
3
+ attr_reader :times, :errors, :before_retry
4
+ private :times, :errors, :before_retry
5
+
6
+ def initialize(times:, errors: [], before_retry: ->(_error) {})
7
+ @times = times
8
+ @errors = errors
9
+ @before_retry = before_retry
10
+ end
11
+
12
+ def perform
13
+ executed = 0
14
+ begin
15
+ executed += 1
16
+ yield
17
+ rescue *errors => error
18
+ if executed < times
19
+ before_retry.call(error)
20
+ retry
21
+ else
22
+ raise error
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ module Hermes
2
+ class RetryableEventProducer
3
+ def self.publish(event_class, event_body, origin_headers)
4
+ new(
5
+ objects_resolver: Hermes::DependenciesContainer["objects_resolver"],
6
+ event_producer: Hermes::DependenciesContainer["event_producer"]
7
+ ).publish(event_class, event_body, origin_headers)
8
+ end
9
+
10
+ attr_reader :objects_resolver, :event_producer
11
+ private :objects_resolver, :event_producer
12
+
13
+ def initialize(objects_resolver:, event_producer:)
14
+ @objects_resolver = objects_resolver
15
+ @event_producer = event_producer
16
+ end
17
+
18
+ def publish(event_class, event_body, origin_headers)
19
+ event = objects_resolver.const_get(event_class).new(event_body.deep_symbolize_keys)
20
+ event.origin_headers = origin_headers.except(Hermes::B3PropagationModelHeaders.span_id_key)
21
+ event_producer.publish(event)
22
+ end
23
+ end
24
+ end
@@ -12,7 +12,7 @@ module Hermes
12
12
  end
13
13
 
14
14
  def trace
15
- @trace ||= origin_event_headers.fetch("X-B3-TraceId", SecureRandom.hex(32))
15
+ @trace ||= origin_event_headers.fetch(Hermes::B3PropagationModelHeaders.trace_id_key, SecureRandom.hex(32))
16
16
  end
17
17
 
18
18
  def span
@@ -20,7 +20,7 @@ module Hermes
20
20
  end
21
21
 
22
22
  def parent_span
23
- origin_event_headers.fetch("X-B3-SpanId", nil)
23
+ origin_event_headers.fetch(Hermes::B3PropagationModelHeaders.span_id_key, nil)
24
24
  end
25
25
 
26
26
  def service
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hermes-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Galanciak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-01 00:00:00.000000000 Z
11
+ date: 2021-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-struct
@@ -214,12 +214,17 @@ files:
214
214
  - lib/hermes/logger.rb
215
215
  - lib/hermes/null_error_notification_service.rb
216
216
  - lib/hermes/null_instrumenter.rb
217
+ - lib/hermes/producer_error_handler.rb
218
+ - lib/hermes/producer_error_handler/null_handler.rb
219
+ - lib/hermes/producer_error_handler/safe_handler.rb
217
220
  - lib/hermes/publisher.rb
218
221
  - lib/hermes/publisher/hutch_adapter.rb
219
222
  - lib/hermes/publisher/in_memory_adapter.rb
220
223
  - lib/hermes/publisher_factory.rb
221
224
  - lib/hermes/rb.rb
222
225
  - lib/hermes/rb/version.rb
226
+ - lib/hermes/retryable.rb
227
+ - lib/hermes/retryable_event_producer.rb
223
228
  - lib/hermes/rpc_client.rb
224
229
  - lib/hermes/serializer.rb
225
230
  - lib/hermes/support/matchers/publish_async_message.rb
@@ -247,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
252
  - !ruby/object:Gem::Version
248
253
  version: '0'
249
254
  requirements: []
250
- rubygems_version: 3.1.4
255
+ rubygems_version: 3.1.2
251
256
  signing_key:
252
257
  specification_version: 4
253
258
  summary: A messenger of gods, delivering them via RabbitMQ with a little help from