event_people 1.1.0 → 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: f6d904d51b4d70c11a857e96579c5faada37cd4a41c386fb7f640ec623b62cc6
4
- data.tar.gz: f75a42c90be3495a3e05cc51db564773380014ab09cf9bf1ae85e5b4c0cdf3ee
3
+ metadata.gz: ccaafb3d62e2c63881bfc08b080e4f129d79911a6f5d1a1e2aa3929a2b719bff
4
+ data.tar.gz: 889f25c2629daa9ee21a6c3621435ffc0eaae30c0ea9335bbf94da82eec126b8
5
5
  SHA512:
6
- metadata.gz: ade6969611ece666b6082dcbfb5e9cbf517628139c6dc9a189e7f25c22e42060dff26d19969793e43fb47a785cd16cc88b097b027a0f3cdeab89118c8ed28740
7
- data.tar.gz: ee304195e724aa495f8fe58b8fbd9f32cc4e12000feb539f275862a74b3b976d733bb12671ff35558fba1598b3ba5b8cf229620022e27384d98ba3d74c62d76b
6
+ metadata.gz: b615ef14921b27781edc64e48bb637965e1fb91d99995ec231a0b731b5f21bcdb73c8001528646e051c52d1ab86c6f70a92c8250521b668d983bfb52a805adcc
7
+ data.tar.gz: 33628df7e0a91b4247724173e354117cb28f39e83bdd8bd0711df2265b632c183b8185c8fabdc290c24dcab4f55e085761a846720ab8b12fd89b4f90fd17da8f
data/.event_people.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  spec_version: "1.1.0"
2
- implementation_version: "1.1.0"
2
+ implementation_version: "1.2.0"
3
3
  language: ruby
4
4
  package: "event_people (RubyGems)"
5
5
  status: stable
@@ -19,9 +19,6 @@ implemented:
19
19
  - RabbitContext
20
20
  - Topic
21
21
  - Queue
22
-
23
- # Spec components not yet implemented (pending in spec too)
24
- pending:
25
22
  - RetryManager
26
23
  - "Event.retryCount"
27
24
  - "Event.incrementRetryCount"
@@ -36,6 +33,9 @@ pending:
36
33
  - "RabbitContext.isLastRetry"
37
34
  - "RabbitContext.dlqName"
38
35
 
36
+ # Spec components not yet implemented (pending in spec too)
37
+ pending: []
38
+
39
39
  # Structural or naming departures from the spec
40
40
  # (Ruby module-qualified class names are an accepted adaptation, not a deviation)
41
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
 
@@ -13,6 +13,14 @@ module EventPeople
13
13
  raise NotImplementedError.new('Must be implemented')
14
14
  end
15
15
 
16
+ def max_retries
17
+ raise NotImplementedError.new('Must be implemented')
18
+ end
19
+
20
+ def is_last_retry
21
+ raise NotImplementedError.new('Must be implemented')
22
+ end
23
+
16
24
  def success!
17
25
  warn '[DEPRECATED] EventPeople: `success!` is deprecated, use `success` instead. Will be removed in a future version.'
18
26
  success
@@ -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,9 +1,21 @@
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
21
  def success
@@ -11,7 +23,32 @@ module EventPeople
11
23
  end
12
24
 
13
25
  def fail
14
- @channel.nack(@delivery_info.delivery_tag, false, true)
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
54
  def reject
@@ -19,4 +56,4 @@ module EventPeople
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
@@ -1,3 +1,3 @@
1
1
  module EventPeople
2
- VERSION = '1.1.0'
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.1.0
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: 2026-06-11 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
@@ -109,6 +109,7 @@ files:
109
109
  - lib/event_people/broker/rabbit.rb
110
110
  - lib/event_people/broker/rabbit/queue.rb
111
111
  - lib/event_people/broker/rabbit/rabbit_context.rb
112
+ - lib/event_people/broker/rabbit/retry_manager.rb
112
113
  - lib/event_people/broker/rabbit/topic.rb
113
114
  - lib/event_people/config.rb
114
115
  - lib/event_people/daemon.rb