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
@@ -7,11 +7,9 @@ class Freddy
7
7
  @logger = logger
8
8
  end
9
9
 
10
- def consume(_channel, queue)
10
+ def consume(_channel, queue, &block)
11
11
  @logger.debug "Consuming messages on #{queue.name}"
12
- queue.subscribe do |delivery|
13
- yield(delivery)
14
- end
12
+ queue.subscribe(&block)
15
13
  end
16
14
  end
17
15
  end
@@ -7,11 +7,13 @@ class Freddy
7
7
  new(*attrs).consume(&block)
8
8
  end
9
9
 
10
- def initialize(thread_pool:, pattern:, channel:, options:)
10
+ def initialize(thread_pool:, patterns:, channel:, options:)
11
11
  @consume_thread_pool = thread_pool
12
- @pattern = pattern
12
+ @patterns = patterns
13
13
  @channel = channel
14
14
  @options = options
15
+
16
+ raise 'Do not use durable queues without specifying a group' if durable? && !group
15
17
  end
16
18
 
17
19
  def consume(&block)
@@ -28,38 +30,52 @@ class Freddy
28
30
 
29
31
  def create_queue
30
32
  topic_exchange = @channel.topic(Freddy::FREDDY_TOPIC_EXCHANGE_NAME)
31
- group = @options.fetch(:group, nil)
32
-
33
- if group
34
- @channel
35
- .queue("groups.#{group}")
36
- .bind(topic_exchange, routing_key: @pattern)
37
- else
38
- @channel
39
- .queue('', exclusive: true)
40
- .bind(topic_exchange, routing_key: @pattern)
33
+
34
+ queue =
35
+ if group
36
+ @channel.queue("groups.#{group}", durable: durable?)
37
+ else
38
+ @channel.queue('', exclusive: true)
39
+ end
40
+
41
+ @patterns.each do |pattern|
42
+ queue.bind(topic_exchange, routing_key: pattern)
41
43
  end
44
+
45
+ queue
42
46
  end
43
47
 
44
48
  def process_message(_queue, delivery)
45
49
  @consume_thread_pool.process do
46
- begin
47
- scope = delivery.build_trace("freddy:observe:#{@pattern}",
48
- tags: {
49
- 'message_bus.destination' => @pattern,
50
- 'message_bus.correlation_id' => delivery.correlation_id,
51
- 'component' => 'freddy',
52
- 'span.kind' => 'consumer' # Message Bus
53
- },
54
- force_follows_from: true)
55
-
50
+ delivery.in_span(force_follows_from: true) do
56
51
  yield delivery.payload, delivery.routing_key
57
- ensure
58
- @channel.acknowledge(delivery.tag, false)
59
- scope.close
52
+ @channel.acknowledge(delivery.tag)
53
+ end
54
+ rescue StandardError
55
+ case on_exception
56
+ when :reject
57
+ @channel.reject(delivery.tag)
58
+ when :requeue
59
+ @channel.reject(delivery.tag, true)
60
+ else
61
+ @channel.acknowledge(delivery.tag)
60
62
  end
63
+
64
+ raise
61
65
  end
62
66
  end
67
+
68
+ def group
69
+ @options.fetch(:group, nil)
70
+ end
71
+
72
+ def durable?
73
+ @options.fetch(:durable, false)
74
+ end
75
+
76
+ def on_exception
77
+ @options.fetch(:on_exception, :ack)
78
+ end
63
79
  end
64
80
  end
65
81
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir[File.dirname(__FILE__) + '/consumers/*.rb'].each(&method(:require))
3
+ Dir["#{File.dirname(__FILE__)}/consumers/*.rb"].sort.each(&method(:require))
@@ -4,11 +4,12 @@ class Freddy
4
4
  class Delivery
5
5
  attr_reader :routing_key, :payload, :tag
6
6
 
7
- def initialize(payload, metadata, routing_key, tag)
7
+ def initialize(payload, metadata, routing_key, tag, exchange)
8
8
  @payload = payload
9
9
  @metadata = metadata
10
10
  @routing_key = routing_key
11
11
  @tag = tag
12
+ @exchange = exchange
12
13
  end
13
14
 
14
15
  def correlation_id
@@ -23,20 +24,52 @@ class Freddy
23
24
  @metadata.reply_to
24
25
  end
25
26
 
26
- def build_trace(operation_name, tags: {}, force_follows_from: false)
27
- carrier = TraceCarrier.new(@metadata)
28
- parent = OpenTracing.global_tracer.extract(OpenTracing::FORMAT_TEXT_MAP, carrier)
29
-
30
- references =
31
- if !parent
32
- []
33
- elsif force_follows_from
34
- [OpenTracing::Reference.follows_from(parent)]
35
- else
36
- [OpenTracing::Reference.child_of(parent)]
27
+ def in_span(force_follows_from: false, &block)
28
+ name = "#{@exchange}.#{@routing_key} receive"
29
+ kind = OpenTelemetry::Trace::SpanKind::CONSUMER
30
+ producer_context = OpenTelemetry.propagation.extract(@metadata[:headers] || {})
31
+
32
+ if force_follows_from
33
+ producer_span_context = OpenTelemetry::Trace.current_span(producer_context).context
34
+
35
+ links = []
36
+ links << OpenTelemetry::Trace::Link.new(producer_span_context) if producer_span_context.valid?
37
+
38
+ # In general we should start a new trace here and just link two traces
39
+ # together. But Zipkin (which we currently use) doesn't support links.
40
+ # So even though the root trace could finish before anything here
41
+ # starts executing, we'll continue with the root trace here as well.
42
+ OpenTelemetry::Context.with_current(producer_context) do
43
+ Freddy.tracer.in_span(name, attributes: span_attributes, links: links, kind: kind, &block)
37
44
  end
45
+ else
46
+ OpenTelemetry::Context.with_current(producer_context) do
47
+ Freddy.tracer.in_span(name, attributes: span_attributes, kind: kind, &block)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def span_attributes
55
+ destination_kind = @exchange == '' ? 'queue' : 'topic'
56
+
57
+ attributes = {
58
+ 'payload.type' => (@payload[:type] || 'unknown').to_s,
59
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_SYSTEM => 'rabbitmq',
60
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_DESTINATION => @exchange,
61
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_DESTINATION_KIND => destination_kind,
62
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_RABBITMQ_ROUTING_KEY => @routing_key,
63
+ OpenTelemetry::SemanticConventions::Trace::MESSAGING_OPERATION => 'receive'
64
+ }
65
+
66
+ # There's no correlation_id when a message was sent using
67
+ # `Freddy#deliver`.
68
+ if correlation_id
69
+ attributes[OpenTelemetry::SemanticConventions::Trace::MESSAGING_CONVERSATION_ID] = correlation_id
70
+ end
38
71
 
39
- OpenTracing.start_active_span(operation_name, references: references, tags: tags)
72
+ attributes
40
73
  end
41
74
  end
42
75
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ class Freddy
6
+ class Encoding
7
+ ZLIB_CONTENT_ENCODING = 'zlib'
8
+
9
+ def self.compress(data, encoding)
10
+ case encoding
11
+ when ZLIB_CONTENT_ENCODING
12
+ ::Zlib::Deflate.deflate(data)
13
+ else
14
+ data
15
+ end
16
+ end
17
+
18
+ def self.uncompress(data, encoding)
19
+ case encoding
20
+ when ZLIB_CONTENT_ENCODING
21
+ ::Zlib::Inflate.inflate(data)
22
+ else
23
+ data
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,17 +1,13 @@
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
12
- def self.parse(payload)
7
+ def self.parse(payload, encoding)
13
8
  return {} if payload == 'null'
14
9
 
10
+ payload = Freddy::Encoding.uncompress(payload, encoding)
15
11
  json_handler.parse(payload)
16
12
  end
17
13
 
@@ -20,7 +16,7 @@ class Freddy
20
16
  end
21
17
 
22
18
  def self.json_handler
23
- @json_handler ||= defined?(Oj) ? OjAdapter : JsonAdapter
19
+ @json_handler ||= OjAdapter
24
20
  end
25
21
 
26
22
  class OjAdapter
@@ -35,31 +31,5 @@ class Freddy
35
31
  Oj.dump(payload, DUMP_OPTIONS)
36
32
  end
37
33
  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
34
  end
65
35
  end
@@ -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,20 +11,19 @@ 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))
27
- json_payload = Payload.dump(payload)
21
+ Tracing.inject_tracing_information_to_properties!(properties)
22
+
23
+ json_payload = Freddy::Encoding.compress(
24
+ Payload.dump(payload),
25
+ properties[:content_encoding]
26
+ )
28
27
 
29
28
  # Connection adapters handle thread safety for #publish themselves. No
30
29
  # need to lock these.
@@ -33,8 +32,6 @@ class Freddy
33
32
  ensure
34
33
  # We don't know how many listeners there are and we do not know when
35
34
  # 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
35
  span.finish
39
36
  end
40
37
  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