freddy 1.5.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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