freddy 1.6.0 → 2.2.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 +4 -4
  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 +20 -3
  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 +25 -34
  14. data/lib/freddy/consumers.rb +1 -1
  15. data/lib/freddy/delivery.rb +45 -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 +18 -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 +15 -0
  32. data/spec/integration/tracing_spec.rb +45 -32
  33. data/spec/spec_helper.rb +5 -13
  34. metadata +47 -20
  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
@@ -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.2.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,7 +99,7 @@ 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
@@ -129,12 +124,12 @@ class Freddy
129
124
  # freddy.tap_into 'notifications.*' do |message|
130
125
  # puts "Notification showed #{message.inspect}"
131
126
  # end
132
- def tap_into(pattern, options = {}, &callback)
133
- @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}"
134
129
 
135
130
  Consumers::TapIntoConsumer.consume(
136
131
  thread_pool: Thread.pool(@prefetch_buffer_size),
137
- pattern: pattern,
132
+ patterns: Array(pattern_or_patterns),
138
133
  channel: @connection.create_channel(prefetch: @prefetch_buffer_size),
139
134
  options: options,
140
135
  &callback
@@ -157,15 +152,18 @@ class Freddy
157
152
  # @option options [Integer] :timeout (0)
158
153
  # discards the message after given seconds if nobody consumes it. Message
159
154
  # won't be discarded if timeout it set to 0 (default).
160
- #
155
+ # @option options [String] :compress (nil)
156
+ # - 'zlib' - compresses the payload with zlib
161
157
  # @return [void]
162
158
  #
163
159
  # @example
164
160
  # freddy.deliver 'Metrics', user_id: 5, metric: 'signed_in'
165
161
  def deliver(destination, payload, options = {})
166
162
  timeout = options.fetch(:timeout, 0)
163
+ compression_algorithm = options.fetch(:compress, nil)
167
164
  opts = {}
168
165
  opts[:expiration] = (timeout * 1000).to_i if timeout.positive?
166
+ opts[:content_encoding] = compression_algorithm if compression_algorithm
169
167
 
170
168
  @send_and_forget_producer.produce(destination, payload, opts)
171
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|
@@ -40,4 +40,19 @@ describe 'Tapping into with group identifier' do
40
40
  wait_for { counter == 2 }
41
41
  expect(counter).to eq(2)
42
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
43
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,45 @@ describe 'Tracing' do
131
106
  end
132
107
  end
133
108
 
134
- def active_span
135
- OpenTracing.active_span
109
+ context 'when receiving a broadcast' do
110
+ let(:freddy) { Freddy.build(logger, config) }
111
+ let(:destination) { random_destination }
112
+
113
+ before do
114
+ freddy.tap_into(destination) do
115
+ @deliver_span = current_span_attributes
116
+ end
117
+ end
118
+
119
+ after do
120
+ freddy.close
121
+ end
122
+
123
+ it 'creates a new trace and links it with the sender' do
124
+ initiator_span = nil
125
+ Freddy.tracer.in_span('test') do
126
+ initiator_span = current_span_attributes
127
+ freddy.deliver(destination, {})
128
+ end
129
+ wait_for { @deliver_span }
130
+
131
+ expect(@deliver_span.fetch(:trace_id)).not_to eq(initiator_span.fetch(:trace_id))
132
+
133
+ link = @deliver_span.fetch(:links)[0]
134
+ expect(link.span_context.trace_id).to eq(initiator_span.fetch(:trace_id))
135
+ end
136
+ end
137
+
138
+ def current_span_attributes
139
+ {
140
+ trace_id: current_span.context.trace_id,
141
+ parent_id: current_span.parent_span_id,
142
+ span_id: current_span.context.span_id,
143
+ links: current_span.links || []
144
+ }
145
+ end
146
+
147
+ def current_span
148
+ OpenTelemetry::Trace.current_span
136
149
  end
137
150
  end