bunny-publisher 0.1.6 → 0.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 +4 -4
- data/CHANGELOG.md +7 -1
- data/lib/bunny_publisher.rb +3 -0
- data/lib/bunny_publisher/base.rb +76 -15
- data/lib/bunny_publisher/callbacks.rb +15 -26
- data/lib/bunny_publisher/errors.rb +11 -1
- data/lib/bunny_publisher/mandatory.rb +104 -77
- data/lib/bunny_publisher/version.rb +1 -1
- metadata +18 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 201692e546c2e4775b7312c3a2bef73caf0ded34abb7bde4f103c9e77500040c
|
|
4
|
+
data.tar.gz: c71d0c82c4ce663253430b8d24b41ec1818460b40d0d3da4d685ebf544bdde2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e7216b2416c32fb0cb5930faff903d49a287022fc8d60746b978f8bef944ee57ec7117a7a5ea5d3cc89f3cd510cb5e582966723cedc84f480d8c7f1c91368e2
|
|
7
|
+
data.tar.gz: fd85b2a03fe362ff0b1efe7e7229e552fdcab8c4adecf6186668e9fa7fda14accbbc0f355e3885f28f8a467797ea34458ddb194d4be0d8155d98723708aae7ce
|
data/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
6
|
|
|
7
|
-
## [Unreleased](https://github.com/veeqo/bunny-publisher/compare/v0.
|
|
7
|
+
## [Unreleased](https://github.com/veeqo/bunny-publisher/compare/v0.2.0...HEAD)
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
## [0.2.0](https://github.com/veeqo/bunny-publisher/compare/v0.1.6...v0.2.0) - 2020-11-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- [#9](https://github.com/veeqo/bunny-publisher/pull/9) Rework callbacks & Mandatory publisher
|
|
14
|
+
- [#10](https://github.com/veeqo/bunny-publisher/pull/10) Improve errors handling for cases when connection can be recovered
|
|
15
|
+
|
|
10
16
|
## [0.1.6](https://github.com/veeqo/bunny-publisher/compare/v0.1.5...v0.1.6) - 2020-11-24
|
|
11
17
|
|
|
12
18
|
### Fixed
|
data/lib/bunny_publisher.rb
CHANGED
data/lib/bunny_publisher/base.rb
CHANGED
|
@@ -6,7 +6,13 @@ module BunnyPublisher
|
|
|
6
6
|
class Base
|
|
7
7
|
include Callbacks
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# A list of errors that can be fixed by a connection recovery
|
|
10
|
+
RETRIABLE_ERRORS = [
|
|
11
|
+
Bunny::ConnectionClosedError,
|
|
12
|
+
Bunny::NetworkFailure,
|
|
13
|
+
Bunny::ConnectionLevelException,
|
|
14
|
+
Timeout::Error # can be raised by Bunny::Channel#with_continuation_timeout
|
|
15
|
+
].freeze
|
|
10
16
|
|
|
11
17
|
attr_reader :connection, :channel, :exchange
|
|
12
18
|
|
|
@@ -24,15 +30,19 @@ module BunnyPublisher
|
|
|
24
30
|
@connection = publish_connection || connection
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
def publish(message,
|
|
33
|
+
def publish(message, message_options = {})
|
|
28
34
|
@mutex.synchronize do
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
@message = message
|
|
36
|
+
@message_options = message_options
|
|
37
|
+
|
|
38
|
+
run_callbacks(:publish) do
|
|
39
|
+
with_errors_handling do
|
|
40
|
+
ensure_connection!
|
|
41
|
+
exchange.publish(message, message_options.dup) # Bunny modifies message options
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
ensure
|
|
45
|
+
@message = @message_options = nil
|
|
36
46
|
end
|
|
37
47
|
end
|
|
38
48
|
|
|
@@ -44,29 +54,68 @@ module BunnyPublisher
|
|
|
44
54
|
|
|
45
55
|
private
|
|
46
56
|
|
|
57
|
+
attr_reader :message, :message_options
|
|
58
|
+
|
|
59
|
+
delegate :logger, to: :connection
|
|
60
|
+
|
|
47
61
|
def ensure_connection!
|
|
48
62
|
@connection ||= build_connection
|
|
49
63
|
|
|
50
|
-
connection.start if
|
|
64
|
+
connection.start if should_start_connection?
|
|
51
65
|
|
|
52
|
-
wait_until_connection_ready
|
|
66
|
+
wait_until_connection_ready
|
|
53
67
|
|
|
54
68
|
@channel ||= connection.create_channel
|
|
55
69
|
@exchange ||= build_exchange
|
|
56
70
|
end
|
|
57
71
|
|
|
58
|
-
def
|
|
59
|
-
|
|
72
|
+
def reset_exchange!
|
|
73
|
+
ensure_connection!
|
|
74
|
+
@channel = connection.create_channel
|
|
75
|
+
@exchange = build_exchange
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def wait_until_connection_ready
|
|
79
|
+
Timeout.timeout(recovery_timeout * 2) do
|
|
60
80
|
loop do
|
|
61
|
-
return if
|
|
81
|
+
return if connection_open? || !connection.automatically_recover?
|
|
62
82
|
|
|
63
|
-
sleep 0.
|
|
83
|
+
sleep 0.01
|
|
64
84
|
end
|
|
65
85
|
end
|
|
66
86
|
rescue Timeout::Error
|
|
67
87
|
# Connection recovery takes too long, let the next interaction fail with error then.
|
|
68
88
|
end
|
|
69
89
|
|
|
90
|
+
def should_start_connection?
|
|
91
|
+
connection.status == :not_connected || # Lazy connection initialization
|
|
92
|
+
connection.closed?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def connection_can_recover?
|
|
96
|
+
connection.automatically_recover? && connection.should_retry_recovery?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def connection_open?
|
|
100
|
+
# Do not trust Bunny::Session#open? - it uses :connected & :connecting statuses as "open",
|
|
101
|
+
# while connection is not actually ready to work.
|
|
102
|
+
connection.instance_variable_get(:@status_mutex).synchronize do
|
|
103
|
+
connection.status == :open && connection.transport.open?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def recovery_timeout
|
|
108
|
+
# 60 seconds is a default heartbeat timeout https://www.rabbitmq.com/heartbeats.html#heartbeats-timeout
|
|
109
|
+
# Recommended timeout is 5-20 https://www.rabbitmq.com/heartbeats.html#false-positives
|
|
110
|
+
heartbeat_timeout = [
|
|
111
|
+
(connection.respond_to?(:heartbeat_timeout) ? connection.heartbeat_timeout : connection.heartbeat) || 60,
|
|
112
|
+
5
|
|
113
|
+
].max
|
|
114
|
+
|
|
115
|
+
# Using x2 of heartbeat timeout to get Bunny chance to detect connection failure & try to recover it
|
|
116
|
+
heartbeat_timeout * 2
|
|
117
|
+
end
|
|
118
|
+
|
|
70
119
|
def build_connection
|
|
71
120
|
Bunny.new(@options[:amqp] || ENV['RABBITMQ_URL'], @options)
|
|
72
121
|
end
|
|
@@ -76,5 +125,17 @@ module BunnyPublisher
|
|
|
76
125
|
|
|
77
126
|
channel.exchange(@exchange_name, @exchange_options)
|
|
78
127
|
end
|
|
128
|
+
|
|
129
|
+
def with_errors_handling
|
|
130
|
+
yield
|
|
131
|
+
rescue Bunny::ChannelAlreadyClosed
|
|
132
|
+
reset_exchange!
|
|
133
|
+
retry
|
|
134
|
+
rescue *RETRIABLE_ERRORS => e
|
|
135
|
+
raise unless connection_can_recover?
|
|
136
|
+
|
|
137
|
+
logger.warn { e.inspect }
|
|
138
|
+
retry
|
|
139
|
+
end
|
|
79
140
|
end
|
|
80
141
|
end
|
|
@@ -1,41 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/callbacks'
|
|
5
|
+
|
|
3
6
|
module BunnyPublisher
|
|
4
|
-
# Adds support for callbacks
|
|
7
|
+
# Adds support for callbacks
|
|
5
8
|
module Callbacks
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
include ActiveSupport::Callbacks
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
define_callbacks :publish
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
module ClassMethods
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
singleton_class.define_method(event) do |method_or_proc|
|
|
14
|
-
callbacks[event] = method_or_proc
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
+
def before_publish(*filters, &blk)
|
|
18
|
+
set_callback(:publish, :before, *filters, &blk)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
+
def around_publish(*filters, &blk)
|
|
22
|
+
set_callback(:publish, :around, *filters, &blk)
|
|
21
23
|
end
|
|
22
|
-
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def run_callback(event, *args, &block)
|
|
27
|
-
case (callback = callback_for_event(event))
|
|
28
|
-
when nil
|
|
29
|
-
yield if block_given?
|
|
30
|
-
when Symbol
|
|
31
|
-
send(callback, self, *args, &block)
|
|
32
|
-
when Proc
|
|
33
|
-
callback.call(self, *args, &block)
|
|
25
|
+
def after_publish(*filters, &blk)
|
|
26
|
+
set_callback(:publish, :after, *filters, &blk)
|
|
34
27
|
end
|
|
35
28
|
end
|
|
36
|
-
|
|
37
|
-
def callback_for_event(event)
|
|
38
|
-
self.class.callbacks[event]
|
|
39
|
-
end
|
|
40
29
|
end
|
|
41
30
|
end
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BunnyPublisher
|
|
4
|
-
class
|
|
4
|
+
class ReturnedMessageError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class CannotCreateQueue < ReturnedMessageError
|
|
7
|
+
def to_s
|
|
8
|
+
[
|
|
9
|
+
'Can not create queue for re-publishing. Set queue_name, routing_key, '\
|
|
10
|
+
'or override BunnyPublisher::Mandatory#declare_republish_queue',
|
|
11
|
+
super
|
|
12
|
+
].join(' ')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
5
15
|
end
|
|
@@ -6,23 +6,44 @@ module BunnyPublisher
|
|
|
6
6
|
# Creates queue/binding before re-publishing the same message again.
|
|
7
7
|
# This publisher DUPLICATES the connection for re-publishing messages!
|
|
8
8
|
module Mandatory
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
define_callbacks :republish
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
attr_reader :on_message_return_callback
|
|
17
|
+
|
|
18
|
+
def before_republish(*filters, &blk)
|
|
19
|
+
set_callback(:republish, :before, *filters, &blk)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def around_republish(*filters, &blk)
|
|
23
|
+
set_callback(:republish, :around, *filters, &blk)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def after_republish(*filters, &blk)
|
|
27
|
+
set_callback(:republish, :after, *filters, &blk)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_message_return(method_or_proc)
|
|
31
|
+
unless method_or_proc.is_a?(Proc) || method_or_proc.is_a?(Symbol)
|
|
32
|
+
raise ArgumentError, "Method or Proc expected, #{method_or_proc.class} given"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@on_message_return_callback = method_or_proc
|
|
36
|
+
end
|
|
14
37
|
end
|
|
15
38
|
|
|
16
39
|
attr_reader :queue_name, :queue_options
|
|
17
40
|
|
|
18
|
-
def initialize(
|
|
41
|
+
def initialize(queue: nil, queue_options: {}, timeout_at_exit: 5, **options)
|
|
19
42
|
super(**options)
|
|
20
43
|
|
|
21
44
|
@queue_name = queue
|
|
22
45
|
@queue_options = queue_options
|
|
23
|
-
|
|
24
|
-
@republish_mutex = Mutex.new
|
|
25
|
-
@republish_connection = republish_connection
|
|
46
|
+
@returned_messages = ::Queue.new # ruby queue, not Bunny's one
|
|
26
47
|
|
|
27
48
|
at_exit { wait_for_unrouted_messages_processing(timeout: timeout_at_exit) }
|
|
28
49
|
end
|
|
@@ -31,101 +52,100 @@ module BunnyPublisher
|
|
|
31
52
|
super(message, options.merge(mandatory: true))
|
|
32
53
|
end
|
|
33
54
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
super
|
|
38
|
-
end
|
|
55
|
+
def declare_republish_queue
|
|
56
|
+
name = queue_name || message_options[:routing_key]
|
|
39
57
|
|
|
40
|
-
|
|
58
|
+
ensure_can_create_queue!(name)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
republish_channel.queue(queue_name || return_info.routing_key, queue_options)
|
|
60
|
+
channel.queue(name, queue_options)
|
|
44
61
|
end
|
|
45
62
|
|
|
46
|
-
def declare_republish_queue_binding(queue
|
|
47
|
-
|
|
63
|
+
def declare_republish_queue_binding(queue)
|
|
64
|
+
routing_key = message_options[:routing_key] || queue_name
|
|
65
|
+
|
|
66
|
+
queue.bind(exchange, routing_key: routing_key)
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
private
|
|
51
70
|
|
|
52
|
-
attr_reader :
|
|
71
|
+
attr_reader :returned_messages
|
|
53
72
|
|
|
54
73
|
def ensure_connection!
|
|
55
74
|
super
|
|
56
75
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# `on_return` is called within a frameset of amqp connection.
|
|
60
|
-
# Any interaction within the same connection leads to error. This is why we need extra connection.
|
|
61
|
-
# https://github.com/ruby-amqp/bunny/blob/7fb05abf36637557f75a69790be78f9cc1cea807/lib/bunny/session.rb#L683
|
|
62
|
-
if callback_for_event(:on_message_return)
|
|
63
|
-
exchange.on_return { |*attrs| run_callback(:on_message_return, *attrs) }
|
|
64
|
-
else
|
|
65
|
-
exchange.on_return { |*attrs| on_message_return(*attrs) }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
@on_return_set = true
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def ensure_republish_connection!
|
|
72
|
-
@republish_connection ||= build_republish_connection
|
|
73
|
-
republish_connection.start if republish_connection.status == :not_connected # Lazy connection initialization.
|
|
74
|
-
|
|
75
|
-
wait_until_connection_ready(republish_connection)
|
|
76
|
-
|
|
77
|
-
@republish_channel ||= republish_connection.create_channel
|
|
78
|
-
@republish_exchange ||= clone_exchange_for_republish
|
|
76
|
+
configure_exchange_to_process_returns unless @on_return_set
|
|
79
77
|
end
|
|
80
78
|
|
|
81
|
-
def
|
|
82
|
-
|
|
79
|
+
def reset_exchange!
|
|
80
|
+
super
|
|
81
|
+
configure_exchange_to_process_returns
|
|
83
82
|
end
|
|
84
83
|
|
|
85
|
-
def
|
|
86
|
-
|
|
84
|
+
def configure_exchange_to_process_returns
|
|
85
|
+
case (callback = self.class.on_message_return_callback)
|
|
86
|
+
when nil
|
|
87
|
+
exchange.on_return { |*attrs| on_message_return(*attrs) }
|
|
88
|
+
when Proc
|
|
89
|
+
exchange.on_return { |*attrs| callback.call(*attrs) }
|
|
90
|
+
when Symbol
|
|
91
|
+
exchange.on_return { |*attrs| send(callback, *attrs) }
|
|
92
|
+
end
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
exchange.instance_variable_get(:'@options').merge(type: exchange.type)
|
|
94
|
+
@on_return_set = true
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
# `on_return` is called within a frameset of amqp connection.
|
|
98
|
+
# Any interaction within the same connection leads to error.
|
|
99
|
+
# This is why we process the returned message in a separate thread.
|
|
100
|
+
# https://github.com/ruby-amqp/bunny/blob/7fb05abf36637557f75a69790be78f9cc1cea807/lib/bunny/session.rb#L683
|
|
92
101
|
def on_message_return(return_info, properties, message)
|
|
93
|
-
|
|
102
|
+
message_options = properties.to_h.merge(routing_key: return_info.routing_key).compact
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
if return_info.reply_text == 'NO_ROUTE'
|
|
105
|
+
returned_messages << [message, message_options]
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
Thread.new { process_returned_message }.tap do |thread|
|
|
108
|
+
thread.abort_on_exception = false
|
|
109
|
+
thread.report_on_exception = true
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
# Do not raise error here!
|
|
113
|
+
# The best we can do here is to log to STDERR
|
|
114
|
+
warn 'BunnyPublisher::UnsupportedReplyText: '\
|
|
115
|
+
'Broker has returned the message with reply_text other than NO_ROUTE '\
|
|
116
|
+
"#{[return_info, properties, message]}"
|
|
102
117
|
end
|
|
103
|
-
run_callback(:after_republish, return_info, properties, message)
|
|
104
|
-
|
|
105
|
-
result
|
|
106
|
-
ensure
|
|
107
|
-
@unrouted_message_processing = false
|
|
108
118
|
end
|
|
109
119
|
|
|
110
|
-
def
|
|
111
|
-
@
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
def process_returned_message
|
|
121
|
+
@mutex.synchronize do
|
|
122
|
+
@unrouted_message_processing = true
|
|
123
|
+
@message, @message_options = returned_messages.pop
|
|
124
|
+
|
|
125
|
+
run_callbacks(:republish) do
|
|
126
|
+
with_errors_handling do
|
|
127
|
+
ensure_connection!
|
|
128
|
+
setup_queue_for_republish
|
|
129
|
+
exchange.publish(message, message_options.merge(mandatory: true))
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
ensure
|
|
133
|
+
@message = @message_options = nil
|
|
134
|
+
@unrouted_message_processing = false
|
|
120
135
|
end
|
|
121
136
|
end
|
|
122
137
|
|
|
123
|
-
def
|
|
124
|
-
|
|
138
|
+
def setup_queue_for_republish
|
|
139
|
+
queue = declare_republish_queue
|
|
140
|
+
|
|
141
|
+
# default exchange already has bindings with queues, but routing key is required
|
|
142
|
+
if exchange.name == ''
|
|
143
|
+
message_options[:routing_key] = queue.name
|
|
144
|
+
else
|
|
145
|
+
declare_republish_queue_binding(queue)
|
|
146
|
+
end
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
return_info: return_info,
|
|
128
|
-
properties: properties
|
|
148
|
+
channel.deregister_queue(queue) # we are not going to work with this queue in this channel
|
|
129
149
|
end
|
|
130
150
|
|
|
131
151
|
# TODO: introduce more reliable way to wait for handling of unrouted messages at exit
|
|
@@ -134,11 +154,18 @@ module BunnyPublisher
|
|
|
134
154
|
|
|
135
155
|
return unless @unrouted_message_processing
|
|
136
156
|
|
|
137
|
-
|
|
157
|
+
logger.warn { "Waiting up to #{timeout} seconds for unrouted messages handling" }
|
|
138
158
|
|
|
139
159
|
Timeout.timeout(timeout) { sleep 0.01 while @unrouted_message_processing }
|
|
140
160
|
rescue Timeout::Error
|
|
141
|
-
warn
|
|
161
|
+
logger.warn { 'Some unrouted messages are lost on process exit!' }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def ensure_can_create_queue!(name)
|
|
165
|
+
return if name.present?
|
|
166
|
+
|
|
167
|
+
raise BunnyPublisher::CannotCreateQueue, message: message,
|
|
168
|
+
message_options: message_options
|
|
142
169
|
end
|
|
143
170
|
end
|
|
144
171
|
end
|
metadata
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bunny-publisher
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rustam Sharshenov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-11-
|
|
11
|
+
date: 2020-11-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '4.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '4.2'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: bunny
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
16
30
|
requirements:
|
|
17
31
|
- - "~>"
|
|
18
32
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '2.
|
|
33
|
+
version: '2.17'
|
|
20
34
|
type: :runtime
|
|
21
35
|
prerelease: false
|
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
37
|
requirements:
|
|
24
38
|
- - "~>"
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '2.
|
|
40
|
+
version: '2.17'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: appraisal
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|