freddy 1.5.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.rubocop.yml +9 -28
  4. data/.ruby-gemset +1 -1
  5. data/.ruby-version +1 -1
  6. data/Gemfile +3 -4
  7. data/README.md +49 -18
  8. data/freddy.gemspec +12 -18
  9. data/lib/freddy/adapters/bunny_adapter.rb +21 -4
  10. data/lib/freddy/adapters.rb +3 -28
  11. data/lib/freddy/consumers/respond_to_consumer.rb +3 -13
  12. data/lib/freddy/consumers/response_consumer.rb +2 -4
  13. data/lib/freddy/consumers/tap_into_consumer.rb +41 -25
  14. data/lib/freddy/consumers.rb +1 -1
  15. data/lib/freddy/delivery.rb +46 -13
  16. data/lib/freddy/encoding.rb +27 -0
  17. data/lib/freddy/payload.rb +4 -34
  18. data/lib/freddy/producers/reply_producer.rb +12 -5
  19. data/lib/freddy/producers/send_and_forget_producer.rb +9 -12
  20. data/lib/freddy/producers/send_and_wait_response_producer.rb +19 -33
  21. data/lib/freddy/producers.rb +1 -1
  22. data/lib/freddy/request_manager.rb +1 -9
  23. data/lib/freddy/tracing.rb +37 -0
  24. data/lib/freddy/version.rb +5 -0
  25. data/lib/freddy.rb +27 -20
  26. data/spec/.rubocop.yml +26 -0
  27. data/spec/freddy/error_response_spec.rb +6 -6
  28. data/spec/freddy/freddy_spec.rb +23 -0
  29. data/spec/freddy/payload_spec.rb +25 -16
  30. data/spec/integration/concurrency_spec.rb +8 -12
  31. data/spec/integration/tap_into_with_group_spec.rb +34 -0
  32. data/spec/integration/tracing_spec.rb +15 -32
  33. data/spec/spec_helper.rb +5 -13
  34. metadata +44 -17
  35. data/.npmignore +0 -8
  36. data/.travis.yml +0 -13
  37. data/lib/freddy/adapters/march_hare_adapter.rb +0 -64
  38. data/lib/freddy/trace_carrier.rb +0 -28
  39. data/spec/freddy/trace_carrier_spec.rb +0 -56
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir[File.dirname(__FILE__) + '/producers/*.rb'].each(&method(:require))
3
+ Dir["#{File.dirname(__FILE__)}/producers/*.rb"].sort.each(&method(:require))
@@ -3,7 +3,7 @@
3
3
  class Freddy
4
4
  class RequestManager
5
5
  def initialize(logger)
6
- @requests = ConcurrentHash.new
6
+ @requests = {}
7
7
  @logger = logger
8
8
  end
9
9
 
@@ -22,13 +22,5 @@ class Freddy
22
22
  def delete(correlation_id)
23
23
  @requests.delete(correlation_id)
24
24
  end
25
-
26
- class ConcurrentHash < Hash
27
- # CRuby hash does not need any locks. Only adding when using JRuby.
28
- if RUBY_PLATFORM == 'java'
29
- require 'jruby/synchronized'
30
- include JRuby::Synchronized
31
- end
32
- end
33
25
  end
34
26
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Freddy
4
+ module Tracing
5
+ # NOTE: Make sure you finish the span youself.
6
+ def self.span_for_produce(exchange, routing_key, payload, correlation_id: nil, timeout_in_seconds: nil)
7
+ destination = exchange.name
8
+ destination_kind = exchange.type == :direct ? 'queue' : 'topic'
9
+
10
+ attributes = {
11
+ 'payload.type' => (payload[:type] || 'unknown').to_s,
12
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_SYSTEM => 'rabbitmq',
13
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_RABBITMQ_ROUTING_KEY => routing_key,
14
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_DESTINATION => destination,
15
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_DESTINATION_KIND => destination_kind,
16
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_OPERATION => 'send'
17
+ }
18
+
19
+ attributes['freddy.timeout_in_seconds'] = timeout_in_seconds if timeout_in_seconds
20
+
21
+ if correlation_id
22
+ attributes[OpenTelemetry::SemanticConventions::Trace::MESSAGING_CONVERSATION_ID] = correlation_id
23
+ end
24
+
25
+ Freddy.tracer.start_span(
26
+ ".#{routing_key} send",
27
+ kind: OpenTelemetry::Trace::SpanKind::PRODUCER,
28
+ attributes: attributes
29
+ )
30
+ end
31
+
32
+ def self.inject_tracing_information_to_properties!(properties)
33
+ properties[:headers] ||= {}
34
+ OpenTelemetry.propagation.inject(properties[:headers])
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Freddy
4
+ VERSION = '2.1.0'
5
+ end
data/lib/freddy.rb CHANGED
@@ -3,9 +3,11 @@
3
3
  require 'json'
4
4
  require 'thread/pool'
5
5
  require 'securerandom'
6
- require 'opentracing'
6
+ require 'opentelemetry'
7
+ require 'opentelemetry/semantic_conventions'
8
+ require_relative './freddy/version'
7
9
 
8
- Dir[File.dirname(__FILE__) + '/freddy/*.rb'].each(&method(:require))
10
+ Dir["#{File.dirname(__FILE__)}/freddy/*.rb"].sort.each(&method(:require))
9
11
 
10
12
  class Freddy
11
13
  FREDDY_TOPIC_EXCHANGE_NAME = 'freddy-topic'
@@ -26,22 +28,15 @@ class Freddy
26
28
  # @return [Freddy]
27
29
  #
28
30
  # @example
29
- # Freddy.build(Logger.new(STDOUT), user: 'thumper', pass: 'howdy')
30
- def self.build(logger = Logger.new(STDOUT), max_concurrency: DEFAULT_MAX_CONCURRENCY, **config)
31
- OpenTracing.global_tracer ||= OpenTracing::Tracer.new
32
-
31
+ # Freddy.build(Logger.new($stdout), user: 'thumper', pass: 'howdy')
32
+ def self.build(logger = Logger.new($stdout), max_concurrency: DEFAULT_MAX_CONCURRENCY, **config)
33
33
  connection = Adapters.determine.connect(config)
34
34
  new(connection, logger, max_concurrency)
35
35
  end
36
36
 
37
- # @deprecated Use {OpenTracing.active_span} instead
38
- def self.trace
39
- OpenTracing.active_span
40
- end
41
-
42
- # @deprecated Use OpenTracing ScopeManager instead
43
- def self.trace=(trace)
44
- OpenTracing.scope_manager.activate(trace) if OpenTracing.active_span != trace
37
+ # @private
38
+ def self.tracer
39
+ @tracer ||= OpenTelemetry.tracer_provider.tracer('freddy', Freddy::VERSION)
45
40
  end
46
41
 
47
42
  def initialize(connection, logger, max_concurrency)
@@ -70,7 +65,7 @@ class Freddy
70
65
  # Received message as a ruby hash with symbolized keys
71
66
  # @yieldparam [#success, #error] handler
72
67
  # Handler for responding to messages. Use handler#success for successful
73
- # respone and handler#error for error response.
68
+ # response and handler#error for error response.
74
69
  #
75
70
  # @return [#shutdown]
76
71
  #
@@ -104,13 +99,22 @@ class Freddy
104
99
  # consuming them. It is useful for general messages that two or more clients
105
100
  # are interested.
106
101
  #
107
- # @param [String] pattern
102
+ # @param [String] pattern_or_patterns
108
103
  # the destination pattern. Use `#` wildcard for matching 0 or more words.
109
104
  # Use `*` to match exactly one word.
110
105
  # @param [Hash] options
111
106
  # @option options [String] :group
112
107
  # only one of the listeners in given group will receive a message. All
113
108
  # listeners will receive a message if the group is not specified.
109
+ # @option options [Boolean] :durable
110
+ # Should the consumer queue be durable? Default is `false`. This option can
111
+ # be used only in combination with option `:group`.
112
+ # @option options [Boolean] :on_exception
113
+ # Defines consumer's behaviour when the callback fails to process a message
114
+ # and raises an exception. Can be one of `:ack`, `:reject` or `:requeue`.
115
+ # `:ack` simply acknowledges the message and re-raises the exception. `:reject`
116
+ # rejects the message without requeueing it. `:requeue` rejects the message with
117
+ # `requeue` flag.
114
118
  #
115
119
  # @yield [message] Yields received message to the block
116
120
  #
@@ -120,12 +124,12 @@ class Freddy
120
124
  # freddy.tap_into 'notifications.*' do |message|
121
125
  # puts "Notification showed #{message.inspect}"
122
126
  # end
123
- def tap_into(pattern, options = {}, &callback)
124
- @logger.debug "Tapping into messages that match #{pattern}"
127
+ def tap_into(pattern_or_patterns, options = {}, &callback)
128
+ @logger.debug "Tapping into messages that match #{pattern_or_patterns}"
125
129
 
126
130
  Consumers::TapIntoConsumer.consume(
127
131
  thread_pool: Thread.pool(@prefetch_buffer_size),
128
- pattern: pattern,
132
+ patterns: Array(pattern_or_patterns),
129
133
  channel: @connection.create_channel(prefetch: @prefetch_buffer_size),
130
134
  options: options,
131
135
  &callback
@@ -148,15 +152,18 @@ class Freddy
148
152
  # @option options [Integer] :timeout (0)
149
153
  # discards the message after given seconds if nobody consumes it. Message
150
154
  # won't be discarded if timeout it set to 0 (default).
151
- #
155
+ # @option options [String] :compress (nil)
156
+ # - 'zlib' - compresses the payload with zlib
152
157
  # @return [void]
153
158
  #
154
159
  # @example
155
160
  # freddy.deliver 'Metrics', user_id: 5, metric: 'signed_in'
156
161
  def deliver(destination, payload, options = {})
157
162
  timeout = options.fetch(:timeout, 0)
163
+ compression_algorithm = options.fetch(:compress, nil)
158
164
  opts = {}
159
165
  opts[:expiration] = (timeout * 1000).to_i if timeout.positive?
166
+ opts[:content_encoding] = compression_algorithm if compression_algorithm
160
167
 
161
168
  @send_and_forget_producer.produce(destination, payload, opts)
162
169
  end
data/spec/.rubocop.yml ADDED
@@ -0,0 +1,26 @@
1
+ require: rubocop-rspec
2
+ inherit_from: ../.rubocop.yml
3
+
4
+ RSpec/ExampleLength:
5
+ Enabled: no
6
+
7
+ RSpec/MultipleExpectations:
8
+ Enabled: no
9
+
10
+ RSpec/MessageSpies:
11
+ Enabled: no
12
+
13
+ RSpec/VerifiedDoubles:
14
+ Enabled: no
15
+
16
+ RSpec/InstanceVariable:
17
+ Enabled: no
18
+
19
+ RSpec/NestedGroups:
20
+ Enabled: no
21
+
22
+ RSpec/DescribeClass:
23
+ Enabled: no
24
+
25
+ RSpec/MultipleMemoizedHelpers:
26
+ Enabled: no
@@ -13,10 +13,10 @@ describe Freddy::ErrorResponse do
13
13
  end
14
14
 
15
15
  describe '#message' do
16
- subject { error.message }
16
+ subject(:message) { error.message }
17
17
 
18
18
  it 'uses error type as a message' do
19
- is_expected.to eq('SomeError')
19
+ expect(message).to eq('SomeError')
20
20
  end
21
21
  end
22
22
  end
@@ -31,10 +31,10 @@ describe Freddy::ErrorResponse do
31
31
  end
32
32
 
33
33
  describe '#message' do
34
- subject { error.message }
34
+ subject(:message) { error.message }
35
35
 
36
36
  it 'uses error type as a message' do
37
- is_expected.to eq('SomeError: extra info')
37
+ expect(message).to eq('SomeError: extra info')
38
38
  end
39
39
  end
40
40
  end
@@ -49,10 +49,10 @@ describe Freddy::ErrorResponse do
49
49
  end
50
50
 
51
51
  describe '#message' do
52
- subject { error.message }
52
+ subject(:message) { error.message }
53
53
 
54
54
  it 'uses default error message as a message' do
55
- is_expected.to eq('Use #response to get the error response')
55
+ expect(message).to eq('Use #response to get the error response')
56
56
  end
57
57
  end
58
58
  end
@@ -49,6 +49,29 @@ describe Freddy do
49
49
  expect(processed_after_timeout).to be(true)
50
50
  end
51
51
  end
52
+
53
+ context 'with compress' do
54
+ it 'compresses the payload' do
55
+ expect(Freddy::Encoding).to receive(:compress).with(anything, 'zlib').and_call_original
56
+
57
+ freddy.tap_into(destination) { |msg| @tapped_message = msg }
58
+ freddy.deliver(destination, payload, compress: 'zlib')
59
+ default_sleep
60
+
61
+ wait_for { @tapped_message }
62
+ expect(@tapped_message).to eq(payload)
63
+ end
64
+ end
65
+
66
+ context 'without compress' do
67
+ it 'does not compress the payload' do
68
+ freddy.tap_into(destination) { |msg| @tapped_message = msg }
69
+ deliver
70
+
71
+ wait_for { @tapped_message }
72
+ expect(@tapped_message).to eq(payload)
73
+ end
74
+ end
52
75
  end
53
76
 
54
77
  context 'when making a synchronized request' do
@@ -2,19 +2,28 @@ require 'spec_helper'
2
2
 
3
3
  describe Freddy::Payload do
4
4
  describe '#dump' do
5
- it 'serializes time objects as iso8601 format strings' do
6
- expect(dump(time: Time.utc(2016, 1, 4, 20, 18)))
7
- .to eq('{"time":"2016-01-04T20:18:00Z"}')
8
- end
9
-
10
- it 'serializes time objects in an array as iso8601 format strings' do
11
- expect(dump(time: [Time.utc(2016, 1, 4, 20, 18)]))
12
- .to eq('{"time":["2016-01-04T20:18:00Z"]}')
13
- end
14
-
15
- it 'serializes time objects in a nested hash as iso8601 format strings' do
16
- expect(dump(x: { time: Time.utc(2016, 1, 4, 20, 18) }))
17
- .to eq('{"x":{"time":"2016-01-04T20:18:00Z"}}')
5
+ context 'with a given Ruby engine' do
6
+ let(:ts) do
7
+ RUBY_ENGINE == 'jruby' ? '{"time":"2016-01-04T20:18:00Z"}' : '{"time":"2016-01-04T20:18:00.000000Z"}'
8
+ end
9
+ let(:ts_array) do
10
+ RUBY_ENGINE == 'jruby' ? '{"time":["2016-01-04T20:18:00Z"]}' : '{"time":["2016-01-04T20:18:00.000000Z"]}'
11
+ end
12
+
13
+ it 'serializes time objects as iso8601 format strings' do
14
+ expect(dump(time: Time.utc(2016, 1, 4, 20, 18)))
15
+ .to eq(ts)
16
+ end
17
+
18
+ it 'serializes time objects in an array as iso8601 format strings' do
19
+ expect(dump(time: [Time.utc(2016, 1, 4, 20, 18)]))
20
+ .to eq(ts_array)
21
+ end
22
+
23
+ it 'serializes time objects in a nested hash as iso8601 format strings' do
24
+ expect(dump(x: { time: Time.utc(2016, 1, 4, 20, 18) }))
25
+ .to eq("{\"x\":#{ts}}")
26
+ end
18
27
  end
19
28
 
20
29
  it 'serializes date objects as iso8601 format strings' do
@@ -33,17 +42,17 @@ describe Freddy::Payload do
33
42
  end
34
43
 
35
44
  it 'serializes datetime objects as iso8601 format strings' do
36
- expect(dump(datetime: DateTime.new(2016, 1, 4, 20, 18))) # rubocop:disable Style/DateTime
45
+ expect(dump(datetime: DateTime.new(2016, 1, 4, 20, 18)))
37
46
  .to eq('{"datetime":"2016-01-04T20:18:00+00:00"}')
38
47
  end
39
48
 
40
49
  it 'serializes datetime objects in an array as iso8601 format strings' do
41
- expect(dump(datetime: [DateTime.new(2016, 1, 4, 20, 18)])) # rubocop:disable Style/DateTime
50
+ expect(dump(datetime: [DateTime.new(2016, 1, 4, 20, 18)]))
42
51
  .to eq('{"datetime":["2016-01-04T20:18:00+00:00"]}')
43
52
  end
44
53
 
45
54
  it 'serializes datetime objects in a nested hash as iso8601 format strings' do
46
- expect(dump(x: { datetime: DateTime.new(2016, 1, 4, 20, 18) })) # rubocop:disable Style/DateTime
55
+ expect(dump(x: { datetime: DateTime.new(2016, 1, 4, 20, 18) }))
47
56
  .to eq('{"x":{"datetime":"2016-01-04T20:18:00+00:00"}}')
48
57
  end
49
58
 
@@ -10,21 +10,17 @@ describe 'Concurrency' do
10
10
 
11
11
  it 'supports multiple requests in #respond_to' do
12
12
  freddy1.respond_to 'Concurrency1' do |_payload, msg_handler|
13
- begin
14
- freddy1.deliver_with_response 'Concurrency2', msg: 'noop'
15
- result2 = freddy1.deliver_with_response 'Concurrency3', msg: 'noop'
16
- msg_handler.success(result2)
17
- rescue Freddy::InvalidRequestError => e
18
- msg_handler.error(e.response)
19
- end
13
+ freddy1.deliver_with_response 'Concurrency2', msg: 'noop'
14
+ result2 = freddy1.deliver_with_response 'Concurrency3', msg: 'noop'
15
+ msg_handler.success(result2)
16
+ rescue Freddy::InvalidRequestError => e
17
+ msg_handler.error(e.response)
20
18
  end
21
19
 
22
20
  freddy2.respond_to 'Concurrency2' do |_payload, msg_handler|
23
- begin
24
- msg_handler.success(from: 'Concurrency2')
25
- rescue Freddy::InvalidRequestError => e
26
- msg_handler.error(e.response)
27
- end
21
+ msg_handler.success(from: 'Concurrency2')
22
+ rescue Freddy::InvalidRequestError => e
23
+ msg_handler.error(e.response)
28
24
  end
29
25
 
30
26
  freddy3.respond_to 'Concurrency3' do |_payload, msg_handler|
@@ -10,6 +10,11 @@ describe 'Tapping into with group identifier' do
10
10
 
11
11
  after { [deliverer, responder1, responder2].each(&:close) }
12
12
 
13
+ it 'raises an exception if option :durable is provided without group' do
14
+ expect { responder1.tap_into(destination, durable: true) }
15
+ .to raise_error(RuntimeError)
16
+ end
17
+
13
18
  it 'receives a message once' do
14
19
  msg_counter = Hamster::MutableSet[]
15
20
 
@@ -21,4 +26,33 @@ describe 'Tapping into with group identifier' do
21
26
  default_sleep
22
27
  expect(msg_counter.count).to eq(1)
23
28
  end
29
+
30
+ it 'can requeue message on exception' do
31
+ counter = 0
32
+
33
+ responder1.tap_into(destination, group: arbitrary_id, on_exception: :requeue) do
34
+ counter += 1
35
+ raise 'error' if counter == 1
36
+ end
37
+
38
+ deliverer.deliver(destination, {})
39
+
40
+ wait_for { counter == 2 }
41
+ expect(counter).to eq(2)
42
+ end
43
+
44
+ it 'taps into multiple topics' do
45
+ destination2 = random_destination
46
+ counter = 0
47
+
48
+ responder1.tap_into([destination, destination2], group: arbitrary_id) do
49
+ counter += 1
50
+ end
51
+
52
+ deliverer.deliver(destination, {})
53
+ deliverer.deliver(destination2, {})
54
+
55
+ wait_for { counter == 2 }
56
+ expect(counter).to eq(2)
57
+ end
24
58
  end
@@ -1,13 +1,8 @@
1
1
  require 'spec_helper'
2
- require 'opentracing_test_tracer'
3
2
 
4
3
  describe 'Tracing' do
5
- let(:tracer) { OpenTracingTestTracer.build(logger: logger) }
6
4
  let(:logger) { spy }
7
5
 
8
- before { OpenTracing.global_tracer = tracer }
9
- after { OpenTracing.global_tracer = nil }
10
-
11
6
  context 'when receiving a traced request' do
12
7
  let(:freddy) { Freddy.build(logger, config) }
13
8
  let(:freddy2) { Freddy.build(logger, config) }
@@ -18,21 +13,13 @@ describe 'Tracing' do
18
13
  before do
19
14
  freddy.respond_to(destination) do |_payload, msg_handler|
20
15
  msg_handler.success(
21
- trace_initiator: {
22
- trace_id: active_span.context.trace_id,
23
- parent_id: active_span.context.parent_id,
24
- span_id: active_span.context.span_id
25
- },
16
+ trace_initiator: current_span_attributes,
26
17
  current_receiver: freddy.deliver_with_response(destination2, {})
27
18
  )
28
19
  end
29
20
 
30
21
  freddy2.respond_to(destination2) do |_payload, msg_handler|
31
- msg_handler.success(
32
- trace_id: active_span.context.trace_id,
33
- parent_id: active_span.context.parent_id,
34
- span_id: active_span.context.span_id
35
- )
22
+ msg_handler.success(current_span_attributes)
36
23
  end
37
24
  end
38
25
 
@@ -75,31 +62,19 @@ describe 'Tracing' do
75
62
  before do
76
63
  freddy.respond_to(destination) do |_payload, msg_handler|
77
64
  msg_handler.success({
78
- trace_initiator: {
79
- trace_id: active_span.context.trace_id,
80
- parent_id: active_span.context.parent_id,
81
- span_id: active_span.context.span_id
82
- }
65
+ trace_initiator: current_span_attributes
83
66
  }.merge(freddy.deliver_with_response(destination2, {})))
84
67
  end
85
68
 
86
69
  freddy2.respond_to(destination2) do |_payload, msg_handler|
87
70
  msg_handler.success(
88
- previous_receiver: {
89
- trace_id: active_span.context.trace_id,
90
- parent_id: active_span.context.parent_id,
91
- span_id: active_span.context.span_id
92
- },
71
+ previous_receiver: current_span_attributes,
93
72
  current_receiver: freddy2.deliver_with_response(destination3, {})
94
73
  )
95
74
  end
96
75
 
97
76
  freddy3.respond_to(destination3) do |_payload, msg_handler|
98
- msg_handler.success(
99
- trace_id: active_span.context.trace_id,
100
- parent_id: active_span.context.parent_id,
101
- span_id: active_span.context.span_id
102
- )
77
+ msg_handler.success(current_span_attributes)
103
78
  end
104
79
  end
105
80
 
@@ -131,7 +106,15 @@ describe 'Tracing' do
131
106
  end
132
107
  end
133
108
 
134
- def active_span
135
- OpenTracing.active_span
109
+ def current_span_attributes
110
+ {
111
+ trace_id: current_span.context.trace_id,
112
+ parent_id: current_span.parent_span_id,
113
+ span_id: current_span.context.span_id
114
+ }
115
+ end
116
+
117
+ def current_span
118
+ OpenTelemetry::Trace.current_span
136
119
  end
137
120
  end