freddy 1.7.0 → 2.0.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.
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require 'oj'
5
- rescue LoadError
6
- require 'symbolizer'
7
- require 'json'
8
- end
3
+ require 'oj'
9
4
 
10
5
  class Freddy
11
6
  class Payload
@@ -20,7 +15,7 @@ class Freddy
20
15
  end
21
16
 
22
17
  def self.json_handler
23
- @json_handler ||= defined?(Oj) ? OjAdapter : JsonAdapter
18
+ @json_handler ||= OjAdapter
24
19
  end
25
20
 
26
21
  class OjAdapter
@@ -35,31 +30,5 @@ class Freddy
35
30
  Oj.dump(payload, DUMP_OPTIONS)
36
31
  end
37
32
  end
38
-
39
- class JsonAdapter
40
- def self.parse(payload)
41
- # MRI has :symbolize_keys, but JRuby does not. Not adding it at the
42
- # moment.
43
- Symbolizer.symbolize(JSON.parse(payload))
44
- end
45
-
46
- def self.dump(payload)
47
- JSON.dump(serialize_time_objects(payload))
48
- end
49
-
50
- def self.serialize_time_objects(object)
51
- if object.is_a?(Hash)
52
- object.reduce({}) do |hash, (key, value)|
53
- hash.merge(key => serialize_time_objects(value))
54
- end
55
- elsif object.is_a?(Array)
56
- object.map(&method(:serialize_time_objects))
57
- elsif object.is_a?(Time) || object.is_a?(Date)
58
- object.iso8601
59
- else
60
- object
61
- end
62
- end
63
- end
64
33
  end
65
34
  end
@@ -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))
@@ -10,17 +10,24 @@ class Freddy
10
10
  @exchange = channel.default_exchange
11
11
  end
12
12
 
13
- def produce(destination, payload, properties)
14
- if (span = OpenTracing.active_span)
15
- span.set_tag('message_bus.destination', destination)
16
- end
13
+ def produce(routing_key, payload, properties)
14
+ span = Tracing.span_for_produce(
15
+ @exchange,
16
+ routing_key,
17
+ payload,
18
+ correlation_id: properties[:correlation_id]
19
+ )
17
20
 
18
21
  properties = properties.merge(
19
- routing_key: destination,
22
+ routing_key: routing_key,
20
23
  content_type: CONTENT_TYPE
21
24
  )
25
+ Tracing.inject_tracing_information_to_properties!(properties)
22
26
 
23
27
  @exchange.publish Payload.dump(payload), properties
28
+ ensure
29
+ # We won't wait for a reply. Just finish the span immediately.
30
+ span.finish
24
31
  end
25
32
  end
26
33
  end
@@ -11,19 +11,15 @@ class Freddy
11
11
  @topic_exchange = channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
12
12
  end
13
13
 
14
- def produce(destination, payload, properties)
15
- span = OpenTracing.start_span("freddy:notify:#{destination}",
16
- tags: {
17
- 'message_bus.destination' => destination,
18
- 'component' => 'freddy',
19
- 'span.kind' => 'producer' # Message Bus
20
- })
14
+ def produce(routing_key, payload, properties)
15
+ span = Tracing.span_for_produce(@topic_exchange, routing_key, payload)
21
16
 
22
17
  properties = properties.merge(
23
- routing_key: destination,
18
+ routing_key: routing_key,
24
19
  content_type: CONTENT_TYPE
25
20
  )
26
- OpenTracing.global_tracer.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, TraceCarrier.new(properties))
21
+ Tracing.inject_tracing_information_to_properties!(properties)
22
+
27
23
  json_payload = Payload.dump(payload)
28
24
 
29
25
  # Connection adapters handle thread safety for #publish themselves. No
@@ -33,8 +29,6 @@ class Freddy
33
29
  ensure
34
30
  # We don't know how many listeners there are and we do not know when
35
31
  # this message gets processed. Instead we close the span immediately.
36
- # Listeners should use FollowsFrom to add trace information.
37
- # https://github.com/opentracing/specification/blob/master/specification.md
38
32
  span.finish
39
33
  end
40
34
  end
@@ -12,7 +12,6 @@ class Freddy
12
12
  @request_manager = RequestManager.new(@logger)
13
13
 
14
14
  @exchange = @channel.default_exchange
15
- @topic_exchange = @channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
16
15
 
17
16
  @channel.on_no_route do |correlation_id|
18
17
  @request_manager.no_route(correlation_id)
@@ -24,43 +23,37 @@ class Freddy
24
23
  @response_consumer.consume(@channel, @response_queue, &method(:handle_response))
25
24
  end
26
25
 
27
- def produce(destination, payload, timeout_in_seconds:, delete_on_timeout:, **properties)
26
+ def produce(routing_key, payload, timeout_in_seconds:, delete_on_timeout:, **properties)
28
27
  correlation_id = SecureRandom.uuid
29
28
 
30
- span = OpenTracing.start_span("freddy:request:#{destination}",
31
- tags: {
32
- 'component' => 'freddy',
33
- 'span.kind' => 'client', # RPC
34
- 'payload.type' => payload[:type] || 'unknown',
35
- 'message_bus.destination' => destination,
36
- 'message_bus.response_queue' => @response_queue.name,
37
- 'message_bus.correlation_id' => correlation_id,
38
- 'freddy.timeout_in_seconds' => timeout_in_seconds
39
- })
29
+ span = Tracing.span_for_produce(
30
+ @exchange,
31
+ routing_key,
32
+ payload,
33
+ correlation_id: correlation_id, timeout_in_seconds: timeout_in_seconds
34
+ )
40
35
 
41
36
  container = SyncResponseContainer.new(
42
- on_timeout(correlation_id, destination, timeout_in_seconds, span)
37
+ on_timeout(correlation_id, routing_key, timeout_in_seconds, span)
43
38
  )
44
39
 
45
40
  @request_manager.store(correlation_id,
46
41
  callback: container,
47
42
  span: span,
48
- destination: destination)
43
+ destination: routing_key)
49
44
 
50
45
  properties[:expiration] = (timeout_in_seconds * 1000).to_i if delete_on_timeout
51
46
 
52
47
  properties = properties.merge(
53
- routing_key: destination, content_type: CONTENT_TYPE,
48
+ routing_key: routing_key, content_type: CONTENT_TYPE,
54
49
  correlation_id: correlation_id, reply_to: @response_queue.name,
55
50
  mandatory: true, type: 'request'
56
51
  )
57
- OpenTracing.global_tracer.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, TraceCarrier.new(properties))
58
- json_payload = Payload.dump(payload)
52
+ Tracing.inject_tracing_information_to_properties!(properties)
59
53
 
60
54
  # Connection adapters handle thread safety for #publish themselves. No
61
- # need to lock these.
62
- @topic_exchange.publish json_payload, properties.dup
63
- @exchange.publish json_payload, properties.dup
55
+ # need to lock this.
56
+ @exchange.publish Payload.dump(payload), properties.dup
64
57
 
65
58
  container.wait_for_response(timeout_in_seconds)
66
59
  end
@@ -82,28 +75,21 @@ class Freddy
82
75
  "with correlation_id #{delivery.correlation_id}"
83
76
  request[:callback].call(delivery.payload, delivery)
84
77
  rescue InvalidRequestError => e
85
- request[:span].set_tag('error', true)
86
- request[:span].log_kv(
87
- event: 'invalid request',
88
- message: e.message,
89
- 'error.object': e
90
- )
78
+ request[:span].record_exception(e)
79
+ request[:span].status = OpenTelemetry::Trace::Status.error
91
80
  raise e
92
81
  ensure
93
82
  request[:span].finish
94
83
  end
95
84
 
96
- def on_timeout(correlation_id, destination, timeout_in_seconds, span)
85
+ def on_timeout(correlation_id, routing_key, timeout_in_seconds, span)
97
86
  proc do
98
- @logger.warn "Request timed out waiting response from #{destination}"\
87
+ @logger.warn "Request timed out waiting response from #{routing_key}"\
99
88
  ", correlation id #{correlation_id}, timeout #{timeout_in_seconds}s"
100
89
 
101
90
  @request_manager.delete(correlation_id)
102
- span.set_tag('error', true)
103
- span.log_kv(
104
- event: 'timed out',
105
- message: "Timed out waiting response from #{destination}"
106
- )
91
+ span.add_event('timeout')
92
+ span.status = OpenTelemetry::Trace::Status.error("Timed out waiting response from #{routing_key}")
107
93
  span.finish
108
94
  end
109
95
  end
@@ -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.0.0'
5
+ 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
@@ -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|