event_people 1.0.7 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6177a8182d6ea892db42802e727cbfc8eb1de66341345d681ed124db88566cb5
4
- data.tar.gz: f368ab8690d1544a13e2782090cf4aadae00ede029c1f9557098cdb8cb85eba2
3
+ metadata.gz: ccaafb3d62e2c63881bfc08b080e4f129d79911a6f5d1a1e2aa3929a2b719bff
4
+ data.tar.gz: 889f25c2629daa9ee21a6c3621435ffc0eaae30c0ea9335bbf94da82eec126b8
5
5
  SHA512:
6
- metadata.gz: e7f0bec06ba76079f289270ec479a132ba1dc23b55cb0f14d5d822366c634e34a8f5033fc7ae86665d4e110eeb32dc623258791234a01dbd34e529e734766cbc
7
- data.tar.gz: b5e9916f23b0a6d8e3bf041342e0f8f3e3f106498a51c9f3d27651bf99d29a820baac0ea3801844eb42bd3bd24f32c34f1d7a522224af4e5d3ebb6bbbe84ea28
6
+ metadata.gz: b615ef14921b27781edc64e48bb637965e1fb91d99995ec231a0b731b5f21bcdb73c8001528646e051c52d1ab86c6f70a92c8250521b668d983bfb52a805adcc
7
+ data.tar.gz: 33628df7e0a91b4247724173e354117cb28f39e83bdd8bd0711df2265b632c183b8185c8fabdc290c24dcab4f55e085761a846720ab8b12fd89b4f90fd17da8f
data/.event_people.yml ADDED
@@ -0,0 +1,41 @@
1
+ spec_version: "1.1.0"
2
+ implementation_version: "1.2.0"
3
+ language: ruby
4
+ package: "event_people (RubyGems)"
5
+ status: stable
6
+
7
+ # Components implemented (with deviations/bugs noted separately)
8
+ implemented:
9
+ - Config
10
+ - Event
11
+ - Listener
12
+ - Emitter
13
+ - Daemon
14
+ - "Daemon.bindSignals"
15
+ - BaseListener
16
+ - ListenersManager
17
+ - BaseBroker
18
+ - RabbitBroker
19
+ - RabbitContext
20
+ - Topic
21
+ - Queue
22
+ - RetryManager
23
+ - "Event.retryCount"
24
+ - "Event.incrementRetryCount"
25
+ - "Config.maxAttempts"
26
+ - "Config.delayStrategy"
27
+ - "Config.dlqName"
28
+ - "Config.getRetryConfig"
29
+ - "Listener.on (retry params: maxAttempts, delayStrategy, dlqName)"
30
+ - "Context.maxRetries"
31
+ - "Context.isLastRetry"
32
+ - "RabbitContext.maxRetries"
33
+ - "RabbitContext.isLastRetry"
34
+ - "RabbitContext.dlqName"
35
+
36
+ # Spec components not yet implemented (pending in spec too)
37
+ pending: []
38
+
39
+ # Structural or naming departures from the spec
40
+ # (Ruby module-qualified class names are an accepted adaptation, not a deviation)
41
+ deviations: []
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- event_people (1.0.7)
4
+ event_people (1.2.0)
5
5
  bunny (~> 2.7)
6
6
 
7
7
  GEM
@@ -15,13 +15,13 @@ module EventPeople
15
15
  raise NotImplementedError.new('Must be implemented')
16
16
  end
17
17
 
18
- def self.consume(event_name, &block)
18
+ def self.consume(event_name, retry_config: {}, &block)
19
19
  consumers[event_name] ||= begin
20
- new.consume(event_name, &block)
20
+ new.consume(event_name, retry_config: retry_config, &block)
21
21
  end
22
22
  end
23
23
 
24
- def consume(event_name, &block)
24
+ def consume(event_name, retry_config: {}, &block)
25
25
  raise NotImplementedError.new('Must be implemented')
26
26
  end
27
27
 
@@ -1,17 +1,40 @@
1
1
  module EventPeople
2
2
  module Broker
3
3
  class Context
4
- def success!
4
+ def success
5
5
  raise NotImplementedError.new('Must be implemented')
6
6
  end
7
7
 
8
- def fail!
8
+ def fail
9
9
  raise NotImplementedError.new('Must be implemented')
10
10
  end
11
11
 
12
- def reject!
12
+ def reject
13
+ raise NotImplementedError.new('Must be implemented')
14
+ end
15
+
16
+ def max_retries
17
+ raise NotImplementedError.new('Must be implemented')
18
+ end
19
+
20
+ def is_last_retry
13
21
  raise NotImplementedError.new('Must be implemented')
14
22
  end
23
+
24
+ def success!
25
+ warn '[DEPRECATED] EventPeople: `success!` is deprecated, use `success` instead. Will be removed in a future version.'
26
+ success
27
+ end
28
+
29
+ def fail!
30
+ warn '[DEPRECATED] EventPeople: `fail!` is deprecated, use `fail` instead. Will be removed in a future version.'
31
+ self.fail
32
+ end
33
+
34
+ def reject!
35
+ warn '[DEPRECATED] EventPeople: `reject!` is deprecated, use `reject` instead. Will be removed in a future version.'
36
+ reject
37
+ end
15
38
  end
16
39
  end
17
40
  end
@@ -6,18 +6,21 @@ module EventPeople
6
6
  @channel.prefetch(1)
7
7
  end
8
8
 
9
- def self.subscribe(channel, routing_key, &block)
10
- new(channel).subscribe(routing_key, &block)
9
+ def self.subscribe(channel, routing_key, retry_config: {}, &block)
10
+ new(channel).subscribe(routing_key, retry_config: retry_config, &block)
11
11
  end
12
12
 
13
- def subscribe(routing_key, &block)
13
+ def subscribe(routing_key, retry_config: {}, &block)
14
14
  base_name = routing_key.split('.')[0..2].join('.')
15
15
  name = queue_name("#{base_name}.all")
16
16
 
17
- channel.queue(name, queue_options)
17
+ declare_dlx_and_dlq
18
+ declare_retry_queue(name)
19
+
20
+ channel.queue(name, main_queue_options)
18
21
  .bind(topic, routing_key: routing_key)
19
22
  .subscribe(manual_ack: true) do |delivery_info, properties, payload|
20
- callback(delivery_info, properties, payload, &block)
23
+ callback(delivery_info, properties, payload, name, retry_config, &block)
21
24
  end
22
25
  end
23
26
 
@@ -25,11 +28,37 @@ module EventPeople
25
28
 
26
29
  attr_reader :channel
27
30
 
28
- def callback(delivery_info, properties, payload, &block)
29
- event_name = delivery_info.routing_key
31
+ def declare_dlx_and_dlq
32
+ dlx_name = "#{EventPeople::Config::APP_NAME}_dlx"
33
+ dlq_name = "#{EventPeople::Config::APP_NAME}_dlq"
34
+
35
+ dlx = channel.fanout(dlx_name, durable: true)
36
+ channel.queue(dlq_name, durable: true).bind(dlx, routing_key: '')
37
+ end
38
+
39
+ def declare_retry_queue(main_queue_name)
40
+ channel.queue("#{main_queue_name}_retry", durable: true, arguments: {
41
+ 'x-dead-letter-exchange' => '',
42
+ 'x-dead-letter-routing-key' => main_queue_name
43
+ })
44
+ end
45
+
46
+ def callback(delivery_info, properties, payload, queue_name, retry_config, &block)
47
+ event_name = delivery_info.routing_key
48
+ retry_count = [(properties.headers&.dig('x-event-people-retries') || 0).to_i, 0].max
49
+
50
+ event = EventPeople::Event.new(event_name, payload, 1.0, retry_count: retry_count)
30
51
 
31
- event = EventPeople::Event.new(event_name, payload)
32
- context = EventPeople::Broker::Rabbit::RabbitContext.new(channel, delivery_info)
52
+ context = EventPeople::Broker::Rabbit::RabbitContext.new(
53
+ channel,
54
+ delivery_info,
55
+ retry_count: retry_count,
56
+ max_retries: retry_config[:max_attempts],
57
+ delay_strategy: retry_config[:delay_strategy],
58
+ dlq_name: retry_config[:dlq_name],
59
+ queue_name: queue_name,
60
+ original_payload: payload
61
+ )
33
62
 
34
63
  block.call(event, context)
35
64
  end
@@ -38,8 +67,11 @@ module EventPeople
38
67
  Rabbit::Topic.topic(channel)
39
68
  end
40
69
 
41
- def queue_options
42
- { durable: true }
70
+ def main_queue_options
71
+ {
72
+ durable: true,
73
+ arguments: { 'x-dead-letter-exchange' => "#{EventPeople::Config::APP_NAME}_dlx" }
74
+ }
43
75
  end
44
76
 
45
77
  def queue_name(routing_key)
@@ -1,22 +1,59 @@
1
1
  module EventPeople
2
2
  module Broker
3
3
  class Rabbit::RabbitContext < EventPeople::Broker::Context
4
- def initialize(channel, delivery_info)
5
- @channel = channel
6
- @delivery_info = delivery_info
4
+ attr_reader :max_retries, :dlq_name
5
+
6
+ def initialize(channel, delivery_info, retry_count: 0, max_retries: nil, delay_strategy: nil, dlq_name: nil, queue_name: nil, original_payload: nil)
7
+ @channel = channel
8
+ @delivery_info = delivery_info
9
+ @retry_count = retry_count.to_i
10
+ @max_retries = (max_retries || EventPeople::Config::MAX_ATTEMPTS).to_i
11
+ @delay_strategy = delay_strategy || EventPeople::Config::DELAY_STRATEGY
12
+ @dlq_name = dlq_name || EventPeople::Config::DLQ_NAME
13
+ @queue_name = queue_name
14
+ @original_payload = original_payload
15
+ end
16
+
17
+ def is_last_retry
18
+ @retry_count >= @max_retries - 1
7
19
  end
8
20
 
9
- def success!
21
+ def success
10
22
  @channel.ack(@delivery_info.delivery_tag, false)
11
23
  end
12
24
 
13
- def fail!
14
- @channel.nack(@delivery_info.delivery_tag, false, true)
25
+ def fail
26
+ retry_manager = Rabbit::RetryManager.new(@max_retries, @delay_strategy)
27
+
28
+ if retry_manager.should_retry?(@retry_count)
29
+ delay = retry_manager.get_next_delay(@retry_count)
30
+ begin
31
+ @channel.default_exchange.publish(
32
+ @original_payload,
33
+ routing_key: "#{@queue_name}_retry",
34
+ expiration: delay.to_s,
35
+ headers: { 'x-event-people-retries' => @retry_count + 1 }
36
+ )
37
+ @channel.ack(@delivery_info.delivery_tag, false)
38
+ rescue => e
39
+ # If publish+ack fails, nack so the message is redelivered from the main queue.
40
+ # This risks duplication if publish succeeded but ack failed, which is an inherent
41
+ # AMQP at-least-once limitation. We prefer redelivery over silent loss.
42
+ begin
43
+ @channel.nack(@delivery_info.delivery_tag, false, true)
44
+ rescue
45
+ # Channel may already be closed; nothing we can do.
46
+ end
47
+ raise e
48
+ end
49
+ else
50
+ @channel.nack(@delivery_info.delivery_tag, false, false)
51
+ end
15
52
  end
16
53
 
17
- def reject!
54
+ def reject
18
55
  @channel.reject(@delivery_info.delivery_tag, false)
19
56
  end
20
57
  end
21
58
  end
22
- end
59
+ end
@@ -0,0 +1,25 @@
1
+ module EventPeople
2
+ module Broker
3
+ class Rabbit::RetryManager
4
+ INITIAL_DELAY = (ENV['RABBIT_EVENT_PEOPLE_RETRY_TTL_MS'] || 1000).to_i
5
+ MAX_DELAY = 600_000
6
+
7
+ def initialize(max_attempts, delay_strategy = 'exponential')
8
+ @max_attempts = max_attempts
9
+ @delay_strategy = delay_strategy
10
+ end
11
+
12
+ def should_retry?(retry_count)
13
+ retry_count < @max_attempts
14
+ end
15
+
16
+ def get_next_delay(retry_count)
17
+ if @delay_strategy == 'fixed'
18
+ INITIAL_DELAY
19
+ else
20
+ [INITIAL_DELAY * (5**retry_count), MAX_DELAY].min
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -5,8 +5,8 @@ module EventPeople
5
5
  @@connection ||= session
6
6
  end
7
7
 
8
- def consume(event_name, &block)
9
- Queue.subscribe(channel, event_name, &block)
8
+ def consume(event_name, retry_config: {}, &block)
9
+ Queue.subscribe(channel, event_name, retry_config: retry_config, &block)
10
10
  end
11
11
 
12
12
  def produce(events)
@@ -6,8 +6,20 @@ module EventPeople
6
6
  URL = ENV['RABBIT_URL']
7
7
  FULL_URL = "#{ENV['RABBIT_URL']}/#{ENV['RABBIT_EVENT_PEOPLE_VHOST']}"
8
8
 
9
+ MAX_ATTEMPTS = (ENV['RABBIT_EVENT_PEOPLE_MAX_RETRIES'] || 3).to_i
10
+ DELAY_STRATEGY = 'exponential'
11
+ DLQ_NAME = "#{ENV['RABBIT_EVENT_PEOPLE_APP_NAME']}_dlq"
12
+
9
13
  def self.broker
10
14
  EventPeople::Broker::Rabbit
11
15
  end
16
+
17
+ def self.get_retry_config
18
+ {
19
+ max_attempts: MAX_ATTEMPTS,
20
+ delay_strategy: DELAY_STRATEGY,
21
+ dlq_name: DLQ_NAME
22
+ }
23
+ end
12
24
  end
13
25
  end
@@ -1,11 +1,12 @@
1
1
  module EventPeople
2
2
  class Event
3
- attr_reader :name, :headers, :body, :schema_version
3
+ attr_reader :name, :headers, :body, :schema_version, :retry_count
4
4
 
5
- def initialize(name, body, schema_version = 1.0)
5
+ def initialize(name, body, schema_version = 1.0, retry_count: 0)
6
6
  @name = name
7
7
  @body = body.is_a?(String) ? JSON.parse(body) : body
8
8
  @schema_version = @body&.dig('headers', 'schemaVersion') || schema_version
9
+ @retry_count = retry_count.to_i
9
10
 
10
11
  if name?
11
12
  generate_headers
@@ -15,6 +16,10 @@ module EventPeople
15
16
  build_payload if @body&.key?('headers')
16
17
  end
17
18
 
19
+ def increment_retry_count
20
+ @retry_count += 1
21
+ end
22
+
18
23
  def payload
19
24
  { headers: headers, body: body }.to_json
20
25
  end
@@ -2,16 +2,21 @@ require 'bunny'
2
2
 
3
3
  module EventPeople
4
4
  class Listener
5
- def self.on(event_name, &block)
6
- new.on(event_name, &block)
5
+ def self.on(event_name, max_attempts: nil, delay_strategy: nil, dlq_name: nil, &block)
6
+ new.on(event_name, max_attempts: max_attempts, delay_strategy: delay_strategy, dlq_name: dlq_name, &block)
7
7
  end
8
8
 
9
- def on(event_name, &block)
9
+ def on(event_name, max_attempts: nil, delay_strategy: nil, dlq_name: nil, &block)
10
10
  raise(MissingAttributeError, 'Event name must be present') unless event_name&.size&.positive?
11
11
 
12
12
  event_name = consumed_event_name(event_name)
13
13
 
14
- EventPeople::Config.broker.consume(event_name, &block)
14
+ retry_config = EventPeople::Config.get_retry_config
15
+ retry_config[:max_attempts] = max_attempts unless max_attempts.nil?
16
+ retry_config[:delay_strategy] = delay_strategy unless delay_strategy.nil?
17
+ retry_config[:dlq_name] = dlq_name unless dlq_name.nil?
18
+
19
+ EventPeople::Config.broker.consume(event_name, retry_config: retry_config, &block)
15
20
  end
16
21
 
17
22
  private
@@ -9,16 +9,31 @@ module EventPeople
9
9
  send method_name, event
10
10
  end
11
11
 
12
+ def success
13
+ @context.success
14
+ end
15
+
16
+ def fail
17
+ @context.fail
18
+ end
19
+
20
+ def reject
21
+ @context.reject
22
+ end
23
+
12
24
  def success!
13
- @context.success!
25
+ warn '[DEPRECATED] EventPeople: `success!` is deprecated, use `success` instead. Will be removed in a future version.'
26
+ success
14
27
  end
15
28
 
16
29
  def fail!
17
- @context.fail!
30
+ warn '[DEPRECATED] EventPeople: `fail!` is deprecated, use `fail` instead. Will be removed in a future version.'
31
+ self.fail
18
32
  end
19
33
 
20
34
  def reject!
21
- @context.reject!
35
+ warn '[DEPRECATED] EventPeople: `reject!` is deprecated, use `reject` instead. Will be removed in a future version.'
36
+ reject
22
37
  end
23
38
 
24
39
  def self.bind(method, event_name)
@@ -1,3 +1,3 @@
1
1
  module EventPeople
2
- VERSION = '1.0.7'
2
+ VERSION = '1.2.0'
3
3
  end
data/lib/event_people.rb CHANGED
@@ -11,6 +11,7 @@ require 'event_people/broker/context'
11
11
  require 'event_people/broker/rabbit'
12
12
  require 'event_people/broker/rabbit/queue'
13
13
  require 'event_people/broker/rabbit/rabbit_context'
14
+ require 'event_people/broker/rabbit/retry_manager'
14
15
  require 'event_people/broker/rabbit/topic'
15
16
 
16
17
  module EventPeople
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_people
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pin People
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-13 00:00:00.000000000 Z
11
+ date: 2026-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -89,6 +89,7 @@ extra_rdoc_files: []
89
89
  files:
90
90
  - ".circleci/config.yml"
91
91
  - ".env.example"
92
+ - ".event_people.yml"
92
93
  - ".gitignore"
93
94
  - ".rspec"
94
95
  - ".ruby-version"
@@ -108,6 +109,7 @@ files:
108
109
  - lib/event_people/broker/rabbit.rb
109
110
  - lib/event_people/broker/rabbit/queue.rb
110
111
  - lib/event_people/broker/rabbit/rabbit_context.rb
112
+ - lib/event_people/broker/rabbit/retry_manager.rb
111
113
  - lib/event_people/broker/rabbit/topic.rb
112
114
  - lib/event_people/config.rb
113
115
  - lib/event_people/daemon.rb