bug_bunny 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +8 -0
- data/README.md +31 -0
- data/Rakefile +4 -0
- data/bug_bunny.gemspec +41 -0
- data/lib/bug_bunny/adapter.rb +384 -0
- data/lib/bug_bunny/controller.rb +16 -0
- data/lib/bug_bunny/exception.rb +61 -0
- data/lib/bug_bunny/helpers.rb +36 -0
- data/lib/bug_bunny/message.rb +161 -0
- data/lib/bug_bunny/queue.rb +23 -0
- data/lib/bug_bunny/rabbit.rb +110 -0
- data/lib/bug_bunny/response.rb +15 -0
- data/lib/bug_bunny/security.rb +19 -0
- data/lib/bug_bunny/version.rb +5 -0
- data/lib/bug_bunny.rb +16 -0
- data/sig/bug_bunny.rbs +4 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d3bbeb691836f8f7b516454ed095e36fe7e719dc99f8a435fb58e2ed995e2d80
|
4
|
+
data.tar.gz: 8c1b3ed5758f0a19d0ee73233a4b92e328aaa114a18c851433f2bedbb2edeabe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e7cbbe33d45b497526a1a35b6d841bfd141edda37baf91975d64d218617f2cb8d8a9ec858efd4d75097d0b564cb0f96ae7326d7cb9be51c9ec926e621882b6c
|
7
|
+
data.tar.gz: c59537749ecd8c3dab9cff99c4f41ad5a62af569ddbbad13223ec3b3ad2ed9f2c3acaf96d6a736718afbfe5bd40e28dcfe147c61e5e5d3861fc71007db7580b4
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# BugBunny
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/bug_bunny`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Development
|
24
|
+
|
25
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
+
|
27
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/bug_bunny.
|
data/Rakefile
ADDED
data/bug_bunny.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/bug_bunny/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "bug_bunny"
|
7
|
+
spec.version = BugBunny::VERSION
|
8
|
+
spec.authors = ["gabix"]
|
9
|
+
spec.email = ["gab.edera@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Gem for sync and async comunication via rabbit bunny."
|
12
|
+
spec.description = "Gem for sync and async comunication via rabbit bunny."
|
13
|
+
spec.homepage = "https://github.com/gedera/bug_bunny"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/gedera/bug_bunny"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md"
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
# Uncomment to register a new dependency of your gem
|
34
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
35
|
+
|
36
|
+
spec.add_dependency "bunny", "~> 2.20"
|
37
|
+
spec.add_development_dependency "rubocop"
|
38
|
+
|
39
|
+
# For more information and examples about making a new gem, check out our
|
40
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
41
|
+
end
|
@@ -0,0 +1,384 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Adapter
|
3
|
+
PERSIST_MESSAGE = true
|
4
|
+
|
5
|
+
SERVICE_HEALTH_CHECK = :health_check
|
6
|
+
TIMEOUT = 3
|
7
|
+
BOMBA = :bomba
|
8
|
+
PUBLISH_TIMEOUT = :publish_timeout
|
9
|
+
CONSUMER_TIMEOUT = :consumer_timeout
|
10
|
+
COMUNICATION_ERROR = :comunication_error
|
11
|
+
CONSUMER_COUNT_ZERO = :consumer_count_zero
|
12
|
+
|
13
|
+
PG_EXCEPTIONS_TO_EXIT = %w[PG::ConnectionBad PG::UnableToSend].freeze
|
14
|
+
|
15
|
+
attr_accessor :consumer,
|
16
|
+
:rabbit,
|
17
|
+
:logger,
|
18
|
+
:time_to_wait,
|
19
|
+
:communication_response,
|
20
|
+
:service_message,
|
21
|
+
:consume_response
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@logger = Logger.new('./log/bug_bunny.log', 'monthly')
|
25
|
+
@communication_response = ::BugBunny::Response.new status: false
|
26
|
+
@time_to_wait = 2
|
27
|
+
create_adapter_with_rabbit
|
28
|
+
end
|
29
|
+
|
30
|
+
def publish!(message, publish_queue, opts = {})
|
31
|
+
Timeout::timeout(TIMEOUT) do
|
32
|
+
if opts[:check_consumers_count] && publish_queue.check_consumers.zero?
|
33
|
+
self.communication_response = ::BugBunny::Response.new(status: false, response: CONSUMER_COUNT_ZERO)
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
publish_opts = { routing_key: publish_queue.name,
|
38
|
+
persistent: opts[:persistent],
|
39
|
+
correlation_id: message.correlation_id }
|
40
|
+
|
41
|
+
publish_opts[:reply_to] = opts[:reply_to] if opts[:reply_to]
|
42
|
+
|
43
|
+
# Esta es la idea en el caso que nos pongamos mas mañosos y queramos cambiar las exchange a la hora de publicar.
|
44
|
+
# _exchange = if opts.has_key?(:exchange_type)
|
45
|
+
# channel.exchange(opts[:exchange_type].to_s, { type: opts[:exchange_type] })
|
46
|
+
# else
|
47
|
+
# exchange
|
48
|
+
# end
|
49
|
+
# _exchange.publish(message.to_json, publish_opts)
|
50
|
+
logger.debug("#{publish_queue.name}-Send Request: (#{message})")
|
51
|
+
|
52
|
+
rabbit.exchange.publish(message.to_json, publish_opts)
|
53
|
+
rabbit.channel.wait_for_confirms if rabbit.confirm_select
|
54
|
+
|
55
|
+
self.communication_response = ::BugBunny::Response.new(status: true)
|
56
|
+
end
|
57
|
+
rescue Timeout::Error => e
|
58
|
+
logger.error(e)
|
59
|
+
close_connection!
|
60
|
+
self.communication_response = ::BugBunny::Response.new(status: false, response: PUBLISH_TIMEOUT, exception: e)
|
61
|
+
rescue StandardError => e
|
62
|
+
logger.error(e)
|
63
|
+
close_connection!
|
64
|
+
self.communication_response = ::BugBunny::Response.new(status: false, response: BOMBA, exception: e)
|
65
|
+
end
|
66
|
+
|
67
|
+
def consume!(queue, thread: false, manual_ack: true, exclusive: false, block: true, opts: {})
|
68
|
+
Signal.trap('INT') { exit }
|
69
|
+
|
70
|
+
logger.debug("Suscribe consumer to: #{queue.name}")
|
71
|
+
logger.debug("ENTRO AL CONSUMER #{rabbit.try(:identifier)}")
|
72
|
+
|
73
|
+
self.consumer = queue.rabbit_queue.subscribe(manual_ack: manual_ack, exclusive: exclusive, block: block) do |delivery_info, metadata, json_payload|
|
74
|
+
# Session depends on thread info, subscribe block cleans thread info
|
75
|
+
# ::Session.init unless Session.tags_context
|
76
|
+
|
77
|
+
begin
|
78
|
+
payload = ActiveSupport::JSON.decode(json_payload).deep_symbolize_keys # Timezones pulenteado
|
79
|
+
rescue StandardError
|
80
|
+
payload = JSON.parse(json_payload).deep_symbolize_keys
|
81
|
+
end
|
82
|
+
|
83
|
+
# Session for Sentry logger
|
84
|
+
# locale, version, service_name
|
85
|
+
# payload.except(:body, :service_name).each do |k, v|
|
86
|
+
# Session.assign(k, v)
|
87
|
+
# end
|
88
|
+
# Session.from_service = payload[:service_name]
|
89
|
+
# Session.correlation_id = metadata.correlation_id
|
90
|
+
# Session.queue_name = queue.name
|
91
|
+
|
92
|
+
unless defined?(ActiveRecord) && ActiveRecord::Base.connection_pool.with_connection(&:active?)
|
93
|
+
logger.error('[PG] PG connection down')
|
94
|
+
exit 7
|
95
|
+
end
|
96
|
+
|
97
|
+
begin
|
98
|
+
message = ::BugBunny::Message.new(correlation_id: metadata.correlation_id, reply_to: metadata.reply_to, **payload)
|
99
|
+
|
100
|
+
# Default sentry info
|
101
|
+
# ::Session.request_id = message.correlation_id rescue nil
|
102
|
+
# ::Session.tags_context.merge!(
|
103
|
+
# server_version: message.version,
|
104
|
+
# service_action: message.service_action,
|
105
|
+
# service_name: message.service_name,
|
106
|
+
# isp_id: (message.body&.fetch(:isp_id, nil) rescue nil)
|
107
|
+
# )
|
108
|
+
# ::Session.extra_context[:message] = message.body
|
109
|
+
|
110
|
+
logger.info("#{queue.name}-Received Request: (#{message.service_action})")
|
111
|
+
logger.debug("#{queue.name}-Received Request: (#{message})")
|
112
|
+
logger.debug("Message will be yield")
|
113
|
+
logger.debug("Block given? #{block_given?}")
|
114
|
+
yield(message) if block_given?
|
115
|
+
logger.debug('Message processed')
|
116
|
+
|
117
|
+
begin
|
118
|
+
Timeout.timeout(5) do
|
119
|
+
rabbit.channel.ack delivery_info.delivery_tag if delivery_info[:consumer].manual_acknowledgement?
|
120
|
+
end
|
121
|
+
rescue Timeout::Error => e
|
122
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)} can not check manual_ack #{e.to_s}")
|
123
|
+
rescue ::StandardError => e
|
124
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)} can not check manual_ack #{e.to_s}")
|
125
|
+
end
|
126
|
+
|
127
|
+
self.service_message = message
|
128
|
+
self.communication_response = ::BugBunny::Response.new(status: true)
|
129
|
+
rescue ::SystemExit => e # Ensure exit code
|
130
|
+
raise e
|
131
|
+
rescue => e
|
132
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)}")
|
133
|
+
logger.error(e)
|
134
|
+
|
135
|
+
close_connection!
|
136
|
+
|
137
|
+
# Session.clean!
|
138
|
+
self.communication_response = ::BugBunny::Response.new(status: false, response: BOMBA, exception: e)
|
139
|
+
end
|
140
|
+
|
141
|
+
if thread # sync consumer flag :D
|
142
|
+
begin
|
143
|
+
Timeout::timeout(1) do
|
144
|
+
delivery_info[:consumer].cancel
|
145
|
+
end
|
146
|
+
rescue Timeout::Error => e
|
147
|
+
close_connection!
|
148
|
+
thread.exit
|
149
|
+
end
|
150
|
+
close_connection!
|
151
|
+
thread.exit
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
if thread
|
156
|
+
close_connection!
|
157
|
+
thread.exit
|
158
|
+
else
|
159
|
+
while true
|
160
|
+
begin
|
161
|
+
logger.debug("SALIO DEL CONSUMER #{rabbit.try(:identifier)}")
|
162
|
+
logger.debug(rabbit.status)
|
163
|
+
exit # consumer.cancel
|
164
|
+
rescue Bunny::NotAllowedError => e
|
165
|
+
logger.debug("NOT ALLOWED #{e.to_s}")
|
166
|
+
break
|
167
|
+
rescue Timeout::Error => e
|
168
|
+
if queue.rabbit_queue.channel.status == :closed || queue.rabbit_queue.channel.connection.status == :closed
|
169
|
+
logger.debug("Channel or connection closed")
|
170
|
+
break
|
171
|
+
end
|
172
|
+
|
173
|
+
sleep time_to_wait
|
174
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)}")
|
175
|
+
logger.error(e)
|
176
|
+
retry
|
177
|
+
rescue StandardError => e
|
178
|
+
if queue.rabbit_queue.channel.status == :closed || queue.rabbit_queue.channel.connection.status == :closed
|
179
|
+
logger.debug("Channel or connection closed")
|
180
|
+
break
|
181
|
+
end
|
182
|
+
|
183
|
+
sleep time_to_wait
|
184
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)}")
|
185
|
+
logger.error(e)
|
186
|
+
retry
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
rescue Timeout::Error => e
|
191
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)}")
|
192
|
+
logger.error(e)
|
193
|
+
close_connection!
|
194
|
+
::BugBunny::Response.new(status: false, response: CONSUMER_TIMEOUT, exception: e)
|
195
|
+
rescue StandardError => e
|
196
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)}")
|
197
|
+
logger.error(e)
|
198
|
+
close_connection!
|
199
|
+
::BugBunny::Response.new(status: false, response: BOMBA, exception: e)
|
200
|
+
end
|
201
|
+
|
202
|
+
def publish_and_consume!(publish_message, sync_queue, opts={})
|
203
|
+
reply_queue = build_queue('', initialize: true, exclusive: true, durable: false, auto_delete: true)
|
204
|
+
|
205
|
+
retries = 0
|
206
|
+
begin
|
207
|
+
publish!(publish_message, sync_queue, opts.merge(reply_to: reply_queue.name))
|
208
|
+
rescue
|
209
|
+
if (retries += 1) <= 3
|
210
|
+
sleep 0.5
|
211
|
+
retry
|
212
|
+
end
|
213
|
+
|
214
|
+
close_connection!
|
215
|
+
raise
|
216
|
+
end
|
217
|
+
|
218
|
+
return communication_response unless communication_response.success?
|
219
|
+
|
220
|
+
t = Thread.new do
|
221
|
+
retries = 0
|
222
|
+
begin
|
223
|
+
consume!(reply_queue, thread: Thread.current, exclusive: true) do |msg|
|
224
|
+
yield(msg) if block_given?
|
225
|
+
end
|
226
|
+
rescue
|
227
|
+
if (retries += 1) <= 3
|
228
|
+
sleep 0.5
|
229
|
+
retry
|
230
|
+
end
|
231
|
+
raise
|
232
|
+
end
|
233
|
+
end
|
234
|
+
t.join
|
235
|
+
communication_response
|
236
|
+
end
|
237
|
+
|
238
|
+
def build_queue(name, opts = {})
|
239
|
+
init = opts.key?(:initialize) ? opts[:initialize] : true
|
240
|
+
new_queue = ::BugBunny::Queue.new(opts.merge(name: name))
|
241
|
+
|
242
|
+
if init
|
243
|
+
logger.debug("Building rabbit new_queue: #{name} status: #{rabbit.status} queue_options: #{new_queue.options}")
|
244
|
+
|
245
|
+
retries = 0
|
246
|
+
begin
|
247
|
+
built_queue = rabbit.channel.queue(new_queue.name.to_s, new_queue.options)
|
248
|
+
rescue StandardError
|
249
|
+
if (retries += 1) <= 3
|
250
|
+
sleep 0.5
|
251
|
+
retry
|
252
|
+
end
|
253
|
+
raise
|
254
|
+
end
|
255
|
+
|
256
|
+
new_queue.rabbit_queue = built_queue
|
257
|
+
new_queue.name = new_queue.rabbit_queue.name
|
258
|
+
end
|
259
|
+
|
260
|
+
new_queue
|
261
|
+
rescue Timeout::Error, StandardError => e
|
262
|
+
logger.debug("Rabbit Identifier: #{rabbit.try(:identifier)}")
|
263
|
+
logger.debug("Status adapter created: #{rabbit.status}")
|
264
|
+
logger.error(e)
|
265
|
+
|
266
|
+
close_connection!
|
267
|
+
raise Exception::ComunicationRabbitError.new(COMUNICATION_ERROR, e.backtrace)
|
268
|
+
end
|
269
|
+
|
270
|
+
def self.make_response(comunication_result, consume_result = nil)
|
271
|
+
if comunication_result.success?
|
272
|
+
consume_result || comunication_result
|
273
|
+
else
|
274
|
+
comunication_result.response = comunication_result.response.to_s
|
275
|
+
comunication_result
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def make_response
|
280
|
+
if communication_response.success?
|
281
|
+
consume_response || communication_response
|
282
|
+
else
|
283
|
+
communication_response.response = communication_response.response.to_s
|
284
|
+
communication_response
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def status
|
289
|
+
rabbit.try(:status)
|
290
|
+
end
|
291
|
+
|
292
|
+
def close_connection!
|
293
|
+
rabbit.try(:close)
|
294
|
+
end
|
295
|
+
|
296
|
+
def check_pg_exception!(exception)
|
297
|
+
# el consumidor no reconecta (rails tasks) asi que salimos a la goma
|
298
|
+
if PG_EXCEPTIONS_TO_EXIT.any? { |msg| exception.try(:message)&.starts_with?(msg) }
|
299
|
+
exit 7 # salimos con un int especial para identificarlo
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
304
|
+
|
305
|
+
# AMQ::Protocol::EmptyResponseError: Este error lo note cuando el rabbit
|
306
|
+
# acepta la connection pero aun no ha terminado de inicializar el servicio,
|
307
|
+
# por lo que salta esta exception.
|
308
|
+
# Errno::ECONNRESET: Este error se presenta cuando justo esta arrancando
|
309
|
+
# el rabbit y se quiere conectar al mismo. El rabbit resetea la connection,
|
310
|
+
# haciendo saltar esta exception.
|
311
|
+
def create_adapter_with_rabbit
|
312
|
+
self.rabbit = ::BugBunny::Rabbit.new(confirm_select: true, logger: logger)
|
313
|
+
rescue Bunny::NetworkFailure, Bunny::TCPConnectionFailed,
|
314
|
+
Bunny::ConnectionForced, AMQ::Protocol::EmptyResponseError,
|
315
|
+
Errno::ECONNRESET => e
|
316
|
+
|
317
|
+
logger.debug(e)
|
318
|
+
close_connection!
|
319
|
+
raise Exception::ComunicationRabbitError.new(COMUNICATION_ERROR, e.backtrace)
|
320
|
+
rescue OpenSSL::SSL::SSLError, OpenSSL::X509::CertificateError => e
|
321
|
+
# el `e.to_s` devuelve alguno de los sgtes errores. Por ej:
|
322
|
+
# SSL_connect returned=1 errno=0 state=unknown state: sslv3
|
323
|
+
# alert bad certificate
|
324
|
+
# SSL_CTX_use_PrivateKey: key values mismatch
|
325
|
+
# OpenSSL::X509::CertificateError: not enough data // headers too short
|
326
|
+
if respond_to?(:handle_ssl_issues)
|
327
|
+
handle_ssl_issues # esto pide los certificados de nuevo
|
328
|
+
@retries ||= 0
|
329
|
+
@retries += 1
|
330
|
+
sleep 1
|
331
|
+
retry if @retries < 4
|
332
|
+
@retries = 0 # reset the counter
|
333
|
+
end
|
334
|
+
|
335
|
+
# Si sigue fallando desp de 3 veces o no tiene definido el handle
|
336
|
+
# bombita rodriguez
|
337
|
+
logger.error(e)
|
338
|
+
|
339
|
+
close_connection!
|
340
|
+
raise Exception::ComunicationRabbitError.new(COMUNICATION_ERROR, e.backtrace)
|
341
|
+
rescue Timeout::Error, StandardError => e
|
342
|
+
logger.error(e)
|
343
|
+
|
344
|
+
close_connection!
|
345
|
+
raise Exception::ComunicationRabbitError.new(COMUNICATION_ERROR, e.backtrace)
|
346
|
+
end
|
347
|
+
|
348
|
+
def self.health_check(request: nil)
|
349
|
+
message_to_publish = ::BugBunny::Message.new(service_action: self::SERVICE_HEALTH_CHECK, body: {})
|
350
|
+
|
351
|
+
request ||= self::ROUTING_KEY_SYNC_REQUEST
|
352
|
+
|
353
|
+
begin
|
354
|
+
service_adapter = new
|
355
|
+
sync_queue = service_adapter.build_queue(
|
356
|
+
request,
|
357
|
+
self::QUEUES_PROPS[self::ROUTING_KEY_SYNC_REQUEST]
|
358
|
+
)
|
359
|
+
rescue ::BugBunny::Exception::ComunicationRabbitError => e
|
360
|
+
(service_adapter ||= nil).try(:close_connection!)
|
361
|
+
return ::BugBunny::Response.new(status: false, response: e.message)
|
362
|
+
end
|
363
|
+
|
364
|
+
service_adapter.publish_and_consume!(message_to_publish, sync_queue, check_consumers_count: true)
|
365
|
+
|
366
|
+
service_adapter.close_connection! # ensure the adapter is close
|
367
|
+
|
368
|
+
if service_adapter.communication_response.success?
|
369
|
+
msg = service_adapter.service_message
|
370
|
+
|
371
|
+
result = case msg.status
|
372
|
+
when 'success'
|
373
|
+
{ status: true }
|
374
|
+
when 'error', 'critical'
|
375
|
+
{ status: false }
|
376
|
+
end
|
377
|
+
|
378
|
+
service_adapter.consume_response = ::BugBunny::Response.new(result)
|
379
|
+
end
|
380
|
+
|
381
|
+
service_adapter.make_response
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Controller
|
3
|
+
def self.health_check(_message)
|
4
|
+
{ status: :success, body: {} }
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.exec_action(message)
|
8
|
+
send(message.service_action, message)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.method_missing(name, message, *args, &block)
|
12
|
+
Session.message = message
|
13
|
+
message.build_message(reply_to: message.reply_to).server_no_action!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Exception
|
3
|
+
|
4
|
+
class ServiceError < StandardError
|
5
|
+
def to_s
|
6
|
+
:service_error
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class NeedSignature < StandardError
|
11
|
+
def to_s
|
12
|
+
:need_signature
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class InvalidSignature < StandardError
|
17
|
+
def to_s
|
18
|
+
:invalid_signature
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class GatewayError < StandardError
|
23
|
+
def to_s
|
24
|
+
:gateway_error
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class UltraCriticError < StandardError
|
29
|
+
end
|
30
|
+
|
31
|
+
class ComunicationRabbitError < StandardError
|
32
|
+
attr_accessor :backtrace
|
33
|
+
|
34
|
+
def initialize(msg, backtrace)
|
35
|
+
@backtrace = backtrace
|
36
|
+
super(msg)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class RetryWithoutError < StandardError
|
41
|
+
def to_s
|
42
|
+
"retry_sidekiq_without_error"
|
43
|
+
end
|
44
|
+
|
45
|
+
def backtrace
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
ServiceClasses = [
|
51
|
+
Exception::NeedSignature,
|
52
|
+
Exception::InvalidSignature,
|
53
|
+
Exception::ServiceError,
|
54
|
+
Exception::GatewayError,
|
55
|
+
Exception::RetryWithoutError
|
56
|
+
]
|
57
|
+
|
58
|
+
# Exceptions from ActiveRecord::StatementInvalid
|
59
|
+
PG_EXCEPTIONS_TO_EXIT = %w[PG::ConnectionBad PG::UnableToSend].freeze
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BugBunny
|
2
|
+
module Helpers
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def datetime_values_to_utc(data)
|
6
|
+
case data
|
7
|
+
when Hash
|
8
|
+
data.inject({}) {|memo, (k, v)| memo.merge!({k => datetime_values_to_utc(v)}) }
|
9
|
+
when Array
|
10
|
+
data.map {|e| datetime_values_to_utc(e) }
|
11
|
+
when DateTime, Time
|
12
|
+
data.to_time.utc.iso8601
|
13
|
+
else
|
14
|
+
data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def utc_values_to_local(data)
|
19
|
+
case data
|
20
|
+
when Hash
|
21
|
+
data.inject({}) {|memo, (k, v)| memo.merge!({k => utc_values_to_local(v)}) }
|
22
|
+
when Array
|
23
|
+
data.map {|e| utc_values_to_local(e) }
|
24
|
+
when DateTime, Time
|
25
|
+
data.to_time.localtime # ensure we always use Time instances
|
26
|
+
when /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:.*Z$/
|
27
|
+
t = Time.respond_to?(:zone) ? Time.zone.parse(data) : Time.parse(data)
|
28
|
+
t.to_time.localtime
|
29
|
+
when /^\d{4}-\d{2}-\d{2}$/
|
30
|
+
Date.parse(data)
|
31
|
+
else
|
32
|
+
data
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Message
|
3
|
+
# API ERROR RESPONSE KEY
|
4
|
+
FIELD_ERROR = :field
|
5
|
+
SERVER_ERROR = :server
|
6
|
+
|
7
|
+
# API ERROR RESPONSE CODES
|
8
|
+
MISSING_FIELD = :missing
|
9
|
+
UNKNOWN_FIELD = :unknown
|
10
|
+
NOT_FOUND = :not_found
|
11
|
+
NO_ACTION = :no_action
|
12
|
+
TIMEOUT = :timeout
|
13
|
+
BOMBA = :bomba
|
14
|
+
|
15
|
+
attr_accessor :correlation_id,
|
16
|
+
:body,
|
17
|
+
:signature,
|
18
|
+
:errors,
|
19
|
+
:status,
|
20
|
+
:service_name,
|
21
|
+
:service_action,
|
22
|
+
:version,
|
23
|
+
:reply_to,
|
24
|
+
:exception
|
25
|
+
|
26
|
+
def initialize(opts = {})
|
27
|
+
@correlation_id = opts[:correlation_id] || SecureRandom.uuid
|
28
|
+
@body = deserialize_body(opts[:body] || opts[:response] || {})
|
29
|
+
@errors = opts[:errors]
|
30
|
+
@status = opts[:status] || :success
|
31
|
+
@service_name = opts[:service_name]
|
32
|
+
@service_action = opts[:service_action] # Deberiamos raisear si esto no viene...
|
33
|
+
@version = opts[:version]
|
34
|
+
@signature = opts[:signature]
|
35
|
+
@reply_to = opts[:reply_to]
|
36
|
+
@exception = opts[:exception]
|
37
|
+
end
|
38
|
+
|
39
|
+
def server_not_found!
|
40
|
+
server_error! [NOT_FOUND]
|
41
|
+
end
|
42
|
+
|
43
|
+
def server_timeout!
|
44
|
+
server_error! [TIMEOUT]
|
45
|
+
end
|
46
|
+
|
47
|
+
def server_no_action!
|
48
|
+
server_error! [NO_ACTION]
|
49
|
+
end
|
50
|
+
|
51
|
+
def server_error!(errors=nil)
|
52
|
+
self.status = :error
|
53
|
+
self.body = {}
|
54
|
+
if errors
|
55
|
+
self.errors ||= {}
|
56
|
+
self.errors[SERVER_ERROR] ||= []
|
57
|
+
self.errors[SERVER_ERROR] += [errors].flatten # just in case
|
58
|
+
else
|
59
|
+
self.exception = Exception::ServiceError.new
|
60
|
+
end
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def signed?
|
65
|
+
signature.present?
|
66
|
+
end
|
67
|
+
|
68
|
+
def sign!(key)
|
69
|
+
self.signature = ::BugBunny::Security.sign_message(key, body.to_json)
|
70
|
+
end
|
71
|
+
|
72
|
+
def invalid_signature?(key)
|
73
|
+
!valid_signature?(key)
|
74
|
+
end
|
75
|
+
|
76
|
+
def valid_signature?(key)
|
77
|
+
return if signature.blank?
|
78
|
+
|
79
|
+
::BugBunny::Security.check_sign(key, signature, body.to_json)
|
80
|
+
end
|
81
|
+
|
82
|
+
def formatted
|
83
|
+
resp = {
|
84
|
+
correlation_id: correlation_id,
|
85
|
+
version: version,
|
86
|
+
status: status,
|
87
|
+
service_name: service_name,
|
88
|
+
service_action: service_action,
|
89
|
+
signature: signature,
|
90
|
+
errors: errors,
|
91
|
+
body: serialize_body
|
92
|
+
}
|
93
|
+
|
94
|
+
if exception
|
95
|
+
# resp[:exception] = exception.backtrace.join("\n") rescue nil
|
96
|
+
resp[:exception] = [
|
97
|
+
exception.to_s,
|
98
|
+
exception.try(:backtrace) || []
|
99
|
+
].flatten.join("\n") rescue exception.to_s
|
100
|
+
|
101
|
+
unless ::BugBunny::Exception::ServiceClasses.include?(exception.class)
|
102
|
+
self.exception = Exception::ServiceError.new
|
103
|
+
end
|
104
|
+
resp[:errors] ||= {}
|
105
|
+
unless resp[:errors][SERVER_ERROR]&.any?
|
106
|
+
resp[:errors][SERVER_ERROR] ||= []
|
107
|
+
resp[:errors][SERVER_ERROR] << exception.to_s
|
108
|
+
end
|
109
|
+
resp[:status] = self.status = :critical
|
110
|
+
end
|
111
|
+
|
112
|
+
resp
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_json
|
116
|
+
formatted.to_json
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
to_json # Asegurarse de que siempre se llame al "formatted"
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_h
|
124
|
+
formatted
|
125
|
+
rescue StandardError
|
126
|
+
original_to_h
|
127
|
+
end
|
128
|
+
|
129
|
+
alias :original_to_h :to_h
|
130
|
+
|
131
|
+
def success?
|
132
|
+
status.to_sym == :success
|
133
|
+
end
|
134
|
+
|
135
|
+
def error?
|
136
|
+
status.to_sym == :error
|
137
|
+
end
|
138
|
+
|
139
|
+
def critical?
|
140
|
+
status.to_sym == :critical
|
141
|
+
end
|
142
|
+
|
143
|
+
def build_message(params = {})
|
144
|
+
Message.new({ version: version, correlation_id: correlation_id, service_action: service_action }.merge(params))
|
145
|
+
end
|
146
|
+
|
147
|
+
def serialize_body
|
148
|
+
Helpers.datetime_values_to_utc(body)
|
149
|
+
end
|
150
|
+
|
151
|
+
def deserialize_body(body)
|
152
|
+
Helpers.utc_values_to_local(body)
|
153
|
+
end
|
154
|
+
|
155
|
+
# def critical_response
|
156
|
+
# ::BugBunny::ParserMessage.humanize_error(errors, :adapter)
|
157
|
+
# rescue StandarError
|
158
|
+
# [:general_error]
|
159
|
+
# end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Queue
|
3
|
+
attr_accessor :name, :auto_delete, :durable, :exclusive, :rabbit_queue
|
4
|
+
|
5
|
+
def initialize(attrs={})
|
6
|
+
# "Real" queue opts
|
7
|
+
@name = attrs.fetch(:name, 'undefined')
|
8
|
+
@auto_delete = attrs.fetch(:auto_delete, true)
|
9
|
+
@durable = attrs.fetch(:durable, false)
|
10
|
+
@exclusive = attrs.fetch(:exclusive, false)
|
11
|
+
end
|
12
|
+
|
13
|
+
def options
|
14
|
+
{ durable: durable, exclusive: exclusive, auto_delete: auto_delete }
|
15
|
+
end
|
16
|
+
|
17
|
+
def check_consumers
|
18
|
+
rabbit_queue.consumer_count
|
19
|
+
rescue StandardError
|
20
|
+
0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Rabbit
|
3
|
+
require 'bunny'
|
4
|
+
|
5
|
+
attr_accessor :exchange,
|
6
|
+
:rabbit_channel,
|
7
|
+
:confirm_select,
|
8
|
+
:no_ack,
|
9
|
+
:persistent,
|
10
|
+
:block,
|
11
|
+
:logger,
|
12
|
+
:identifier,
|
13
|
+
:connection
|
14
|
+
|
15
|
+
def initialize(attrs = {})
|
16
|
+
@block = attrs[:block] || true
|
17
|
+
@no_ack = attrs[:no_ack] || true
|
18
|
+
@persistent = attrs[:persistent] || true
|
19
|
+
@confirm_select = attrs[:confirm_select] || true
|
20
|
+
@logger = attrs[:logger] || Logger.new('./log/bug_rabbit.log', 'monthly')
|
21
|
+
@identifier = SecureRandom.uuid
|
22
|
+
|
23
|
+
create_connection
|
24
|
+
set_channel
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_channel
|
28
|
+
logger.debug("Set Channel: #{connection.status}") if logger
|
29
|
+
try(:close_channel)
|
30
|
+
@rabbit_channel = connection.create_channel
|
31
|
+
@exchange = channel.default_exchange
|
32
|
+
channel.confirm_select if confirm_select
|
33
|
+
@rabbit_channel
|
34
|
+
end
|
35
|
+
|
36
|
+
def channel
|
37
|
+
open? ? @rabbit_channel : set_channel
|
38
|
+
end
|
39
|
+
|
40
|
+
def close
|
41
|
+
@rabbit_channel.close if defined?(@rabbit_channel)
|
42
|
+
connection.close if connection.present?
|
43
|
+
rescue Bunny::ChannelAlreadyClosed
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def close_channel
|
48
|
+
@rabbit_channel.close if defined?(@rabbit_channel)
|
49
|
+
end
|
50
|
+
|
51
|
+
def status
|
52
|
+
{
|
53
|
+
connection: connection.status,
|
54
|
+
channel: @rabbit_channel.status,
|
55
|
+
identifier: identifier
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def open?
|
60
|
+
(connection.status == :open) &&
|
61
|
+
(@rabbit_channel.status == :open)
|
62
|
+
end
|
63
|
+
|
64
|
+
def connection_openned?
|
65
|
+
[:open, :connecting, :connected].include?(connection.status)
|
66
|
+
end
|
67
|
+
|
68
|
+
# status = :open, :connected, :connecting,
|
69
|
+
# :closing, :disconnected, :not_connected, :closed
|
70
|
+
def create_connection
|
71
|
+
options = {}
|
72
|
+
|
73
|
+
# if WisproUtils::Config.defaults.use_tls
|
74
|
+
# path = (Rails.root.join('private', 'certs') rescue './private/certs')
|
75
|
+
# options.merge!(tls: true,
|
76
|
+
# port: ENV['RABBIT_SSL_PORT'] || 5671,
|
77
|
+
# log_level: ENV['LOG_LEVEL'] || :debug,
|
78
|
+
# verify_peer: true,
|
79
|
+
# tls_cert: "#{path}/cert.pem",
|
80
|
+
# tls_key: "#{path}/key.pem",
|
81
|
+
# tls_ca_certificates: ["#{path}/ca.pem"])
|
82
|
+
# end
|
83
|
+
|
84
|
+
logger&.debug('Stablish new connection to rabbit')
|
85
|
+
logger&.debug("amqp://#{ENV['RABBIT_USER']}:" \
|
86
|
+
"#{ENV['RABBIT_PASS']}@#{ENV['RABBIT_HOST']}" \
|
87
|
+
"/#{ENV['RABBIT_VIRTUAL_HOST']}")
|
88
|
+
|
89
|
+
|
90
|
+
bunny_logger = ::Logger.new('./log/bunny.log', 7, 10485760)
|
91
|
+
bunny_logger.level = ::Logger::DEBUG
|
92
|
+
options.merge!(
|
93
|
+
heartbeat_interval: 20, # 20.seconds per connection
|
94
|
+
logger: bunny_logger,
|
95
|
+
# Override bunny client_propierties
|
96
|
+
client_properties: { product: identifier, platform: ''}
|
97
|
+
)
|
98
|
+
|
99
|
+
rabbit_conn = Bunny.new("amqp://#{ENV['RABBIT_USER']}" \
|
100
|
+
":#{ENV['RABBIT_PASS']}@"\
|
101
|
+
"#{ENV['RABBIT_HOST']}/"\
|
102
|
+
"#{ENV['RABBIT_VIRTUAL_HOST']}",
|
103
|
+
options)
|
104
|
+
rabbit_conn.start
|
105
|
+
logger&.debug("New status connection: #{rabbit_conn.status}")
|
106
|
+
|
107
|
+
self.connection = rabbit_conn
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module BugBunny
|
2
|
+
class Response
|
3
|
+
attr_accessor :status, :response, :exception
|
4
|
+
|
5
|
+
def initialize(attrs={})
|
6
|
+
@status = attrs[:status]
|
7
|
+
@response = attrs[:response]
|
8
|
+
@exception = attrs[:exception]
|
9
|
+
end
|
10
|
+
|
11
|
+
def success?
|
12
|
+
@status
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module BugBunny
|
2
|
+
module Security
|
3
|
+
def self.sign_message(secret, message)
|
4
|
+
digest = OpenSSL::Digest.new('SHA512')
|
5
|
+
private_key = OpenSSL::PKey::RSA.new(secret)
|
6
|
+
Base64.encode64(private_key.sign(digest, message))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.check_sign(key, signature, message)
|
10
|
+
pub_key = OpenSSL::PKey::RSA.new(key)
|
11
|
+
digest = OpenSSL::Digest.new('SHA512')
|
12
|
+
if pub_key.verify(digest, Base64.decode64(signature), message)
|
13
|
+
true
|
14
|
+
else
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/bug_bunny.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bunny'
|
4
|
+
require_relative "bug_bunny/version"
|
5
|
+
require_relative "bug_bunny/adapter"
|
6
|
+
require_relative "bug_bunny/controller"
|
7
|
+
require_relative "bug_bunny/exception"
|
8
|
+
require_relative "bug_bunny/message"
|
9
|
+
require_relative "bug_bunny/queue"
|
10
|
+
require_relative "bug_bunny/rabbit"
|
11
|
+
require_relative "bug_bunny/response"
|
12
|
+
require_relative "bug_bunny/security"
|
13
|
+
require_relative "bug_bunny/helpers"
|
14
|
+
|
15
|
+
module BugBunny
|
16
|
+
end
|
data/sig/bug_bunny.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bug_bunny
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- gabix
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bunny
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.20'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.20'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Gem for sync and async comunication via rabbit bunny.
|
42
|
+
email:
|
43
|
+
- gab.edera@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- CHANGELOG.md
|
49
|
+
- Gemfile
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- bug_bunny.gemspec
|
53
|
+
- lib/bug_bunny.rb
|
54
|
+
- lib/bug_bunny/adapter.rb
|
55
|
+
- lib/bug_bunny/controller.rb
|
56
|
+
- lib/bug_bunny/exception.rb
|
57
|
+
- lib/bug_bunny/helpers.rb
|
58
|
+
- lib/bug_bunny/message.rb
|
59
|
+
- lib/bug_bunny/queue.rb
|
60
|
+
- lib/bug_bunny/rabbit.rb
|
61
|
+
- lib/bug_bunny/response.rb
|
62
|
+
- lib/bug_bunny/security.rb
|
63
|
+
- lib/bug_bunny/version.rb
|
64
|
+
- sig/bug_bunny.rbs
|
65
|
+
homepage: https://github.com/gedera/bug_bunny
|
66
|
+
licenses: []
|
67
|
+
metadata:
|
68
|
+
homepage_uri: https://github.com/gedera/bug_bunny
|
69
|
+
source_code_uri: https://github.com/gedera/bug_bunny
|
70
|
+
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.6.0
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.4.6
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Gem for sync and async comunication via rabbit bunny.
|
90
|
+
test_files: []
|