sneakers-queue-migrator 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fc0b131523f71a4bd19d4d5d0275fbe7e47814e55ad51b0d927b2672ed7578cb
4
+ data.tar.gz: 3ee9adb466b69696575011fd7a4a192cac88f9370226b33566cb07a084a637f8
5
+ SHA512:
6
+ metadata.gz: 663174974230df88b4c71ed747c682c5329347d5c815087fd0c59cfa93d05c2e05d8dd196d1345a05b2c4e787850513fc40e727e5a0a7ba61dd63ed0d63677d1
7
+ data.tar.gz: b4023d485167c76211c06ba6765c550589fceaae77e1c6ed47d91a22d6d228a31b673168d06429a8fbc2b207e94f8845b06243b13386a71e3dc607403058f724
data/.rubocop.yml ADDED
@@ -0,0 +1,39 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ SuggestExtensions: false
4
+
5
+ Style/StringLiterals:
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ EnforcedStyle: double_quotes
10
+
11
+ Style/Documentation:
12
+ Enabled: false
13
+
14
+ Style/MultilineBlockChain:
15
+ Enabled: false
16
+
17
+ Metrics/BlockLength:
18
+ Enabled: false
19
+
20
+ Metrics/ClassLength:
21
+ Enabled: false
22
+
23
+ Metrics/CyclomaticComplexity:
24
+ Enabled: false
25
+
26
+ Metrics/ModuleLength:
27
+ Enabled: false
28
+
29
+ Metrics/AbcSize:
30
+ Enabled: false
31
+
32
+ Metrics/MethodLength:
33
+ Enabled: false
34
+
35
+ Metrics/PerceivedComplexity:
36
+ Enabled: false
37
+
38
+ Layout/LineLength:
39
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in sneakers-queue-migrator.gemspec
6
+ gemspec
7
+
8
+ gem "rake", ">= 12.0"
9
+
10
+ gem "minitest", "~> 5.16"
11
+
12
+ gem "rubocop", "~> 1.21"
13
+
14
+ gem "sneakers", "~> 2.12"
data/Gemfile.lock ADDED
@@ -0,0 +1,76 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sneakers-queue-migrator (0.1.0)
5
+ bunny (~> 2.0)
6
+ json (~> 2.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ amq-protocol (2.3.4)
12
+ ast (2.4.3)
13
+ bunny (2.24.0)
14
+ amq-protocol (~> 2.3)
15
+ sorted_set (~> 1, >= 1.0.2)
16
+ concurrent-ruby (1.3.5)
17
+ json (2.10.2)
18
+ language_server-protocol (3.17.0.4)
19
+ lint_roller (1.1.0)
20
+ minitest (5.16.3)
21
+ parallel (1.26.3)
22
+ parser (3.3.7.4)
23
+ ast (~> 2.4.1)
24
+ racc
25
+ prism (1.4.0)
26
+ racc (1.8.1)
27
+ rainbow (3.1.1)
28
+ rake (12.3.3)
29
+ rbtree (0.4.6)
30
+ regexp_parser (2.10.0)
31
+ rubocop (1.75.2)
32
+ json (~> 2.3)
33
+ language_server-protocol (~> 3.17.0.2)
34
+ lint_roller (~> 1.1.0)
35
+ parallel (~> 1.10)
36
+ parser (>= 3.3.0.2)
37
+ rainbow (>= 2.2.2, < 4.0)
38
+ regexp_parser (>= 2.9.3, < 3.0)
39
+ rubocop-ast (>= 1.44.0, < 2.0)
40
+ ruby-progressbar (~> 1.7)
41
+ unicode-display_width (>= 2.4.0, < 4.0)
42
+ rubocop-ast (1.44.0)
43
+ parser (>= 3.3.7.2)
44
+ prism (~> 1.4)
45
+ ruby-progressbar (1.13.0)
46
+ serverengine (2.1.1)
47
+ sigdump (~> 0.2.2)
48
+ set (1.1.2)
49
+ sigdump (0.2.5)
50
+ sneakers (2.12.0)
51
+ bunny (~> 2.14)
52
+ concurrent-ruby (~> 1.0)
53
+ rake (~> 12.3)
54
+ serverengine (~> 2.1.0)
55
+ thor
56
+ sorted_set (1.0.3)
57
+ rbtree
58
+ set (~> 1.0)
59
+ thor (1.3.2)
60
+ unicode-display_width (3.1.4)
61
+ unicode-emoji (~> 4.0, >= 4.0.4)
62
+ unicode-emoji (4.0.4)
63
+
64
+ PLATFORMS
65
+ ruby
66
+ x86_64-linux
67
+
68
+ DEPENDENCIES
69
+ minitest (~> 5.16)
70
+ rake (>= 12.0)
71
+ rubocop (~> 1.21)
72
+ sneakers (~> 2.12)
73
+ sneakers-queue-migrator!
74
+
75
+ BUNDLED WITH
76
+ 2.6.3
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Sneakers Queue Migrator
2
+
3
+ A Ruby gem to safely migrate RabbitMQ queues when queue arguments change, with dynamic Sneakers subscriber discovery, queue argument comparison, and safe message shoveling.
4
+
5
+ ## Features
6
+ - Discovers all Sneakers subscribers dynamically
7
+ - Compares current and desired queue arguments using the RabbitMQ Management API
8
+ - Safely migrates messages between queues using Bunny
9
+ - Exposes a class-based API for integration
10
+ - Optionally provides a Rake task for Rails apps
11
+
12
+ ## Installation
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'sneakers-queue-migrator'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install sneakers-queue-migrator
26
+
27
+ ## Usage
28
+
29
+ You need the sneakers or kicks gem for this to work
30
+
31
+ ```ruby
32
+ require 'sneakers/migrator'
33
+
34
+ # Example usage (API subject to change)
35
+ Sneakers::Migrator.migrate!(
36
+ amqp_url: 'amqp://user:password@rabbitmq:5672/',
37
+ amqp_api_url: 'http://user:password@rabbitmq:15672',
38
+ subscriber_paths: ['app/workers/**/*.rb']
39
+ )
40
+ ```
41
+
42
+ ## Development
43
+
44
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
45
+
46
+ ## Contributing
47
+ Bug reports and pull requests are welcome!
48
+
49
+ ## License
50
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,16 @@
1
+ version: '3.8'
2
+ services:
3
+ rabbitmq:
4
+ image: rabbitmq:4.1-management
5
+ ports:
6
+ - "5672:5672"
7
+ - "15672:15672"
8
+ environment:
9
+ RABBITMQ_DEFAULT_USER: guest
10
+ RABBITMQ_DEFAULT_PASS: guest
11
+ RABBITMQ_DEFAULT_VHOST: test_vhost
12
+ healthcheck:
13
+ test: ["CMD", "rabbitmq-diagnostics", "ping"]
14
+ interval: 5s
15
+ timeout: 5s
16
+ retries: 10
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sneakers
4
+ module Migrator
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "migrator/version"
4
+ require "bunny"
5
+ require "json"
6
+ require "net/http"
7
+ require "uri"
8
+
9
+ module Sneakers
10
+ # Do not define a module named Queue here to avoid conflict with Sneakers::Queue (which is a class, not a module)
11
+ module Migrator
12
+ class Error < StandardError; end
13
+
14
+ class << self
15
+ # Main migration entrypoint
16
+ def migrate!(amqp_url:, amqp_api_url:, subscriber_paths: [], logger: nil)
17
+ logger ||= $stdout
18
+ # Try to require sneakers or kicks
19
+ begin
20
+ require "sneakers"
21
+ rescue LoadError
22
+ begin
23
+ require "kicks"
24
+ rescue LoadError
25
+ raise Error, "You must have either the 'sneakers' or 'kicks' gem installed to use subscriber discovery."
26
+ end
27
+ end
28
+ # Load all subscribers
29
+ subscriber_paths.each do |path|
30
+ Dir[File.join(path, "*.rb")].each { |f| require f }
31
+ end
32
+ # Find all worker classes for the loaded framework
33
+ subscribers = ObjectSpace.each_object(Class).select do |klass|
34
+ klass.included_modules.include?(::Sneakers::Worker)
35
+ rescue StandardError
36
+ false
37
+ end
38
+ queues = subscribers.map do |klass|
39
+ opts = klass.instance_variable_get(:@queue_opts) || {}
40
+ name = klass.instance_variable_get(:@queue_name) ||
41
+ (klass.const_defined?(:QUEUE_NAME) ? klass.const_get(:QUEUE_NAME) : nil) ||
42
+ klass.name.gsub("::", "_").gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
43
+ {
44
+ name: name,
45
+ exchange: opts[:exchange] || "domain_events",
46
+ routing_keys: opts[:routing_key] || [],
47
+ arguments: opts[:arguments] || {},
48
+ durable: opts.fetch(:durable, true)
49
+ }.tap { |q| q[:arguments]["x-queue-type"] ||= "classic" }
50
+ end.uniq { |q| q[:name] }
51
+
52
+ conn = Bunny.new(amqp_url)
53
+ conn.start
54
+ ch = conn.create_channel
55
+
56
+ # Remove unused api_uri and fix variable usage
57
+ rabbitmq_api_user = URI.parse(amqp_api_url).user
58
+ rabbitmq_api_pass = URI.parse(amqp_api_url).password
59
+ all_queues = fetch_all_queues(amqp_api_url, rabbitmq_api_user, rabbitmq_api_pass)
60
+
61
+ queues.each do |q|
62
+ queue_info = all_queues.find { |info| info["name"] == q[:name] }
63
+ tmp_queue_name = "#{q[:name]}_tmp"
64
+ if queue_info.nil?
65
+ logger.puts "Migrator: Queue #{q[:name]} does not exist, creating..."
66
+ ch.queue(q[:name], durable: q[:durable], arguments: q[:arguments])
67
+ next
68
+ end
69
+ current_args = (queue_info["arguments"] || {}).transform_keys(&:to_s)
70
+ desired_args = (q[:arguments] || {}).transform_keys(&:to_s)
71
+ logger.puts "Migrator: Checking queue: #{q[:name]}"
72
+ logger.puts "Migrator: Current args: #{current_args.inspect}"
73
+ logger.puts "Migrator: Desired args: #{desired_args.inspect}"
74
+ if queue_args_match?({ arguments: current_args }, desired_args)
75
+ logger.puts "Migrator: Queue #{q[:name]} arguments match, nothing to do."
76
+ next
77
+ end
78
+ logger.puts "Migrator: Queue #{q[:name]} arguments differ, migrating..."
79
+
80
+ # --- Fetch and store bindings (exchange/routing_key pairs) ---
81
+ bindings = fetch_queue_bindings(amqp_api_url, rabbitmq_api_user, rabbitmq_api_pass, queue_info)
82
+ logger.puts "Migrator: Found bindings: #{bindings.inspect}"
83
+
84
+ ch.queue(tmp_queue_name, durable: queue_info["durable"], arguments: desired_args)
85
+ Array(q[:routing_keys]).each do |rk|
86
+ next if rk.nil? || rk.empty?
87
+
88
+ # bind the tmp queue first so new messages go somewhere
89
+ ch.queue(tmp_queue_name).bind(q[:exchange], routing_key: rk)
90
+ # unbind the original queue from the exchange
91
+ logger.puts "Migrator: Unbinding queue #{q[:name]} from exchange #{q[:exchange]} with routing key #{rk}"
92
+ ch.queue(q[:name], passive: true).unbind(q[:exchange], routing_key: rk)
93
+ end
94
+ count = 0
95
+ queue = ch.queue(q[:name], passive: true)
96
+ loop do
97
+ delivery_info, headers, payload = queue.pop
98
+ break unless payload
99
+
100
+ logger.puts "Migrator: Moving message to #{tmp_queue_name}: payload=#{payload.inspect}, headers=#{headers.inspect}, delivery_info=#{delivery_info.inspect}"
101
+ safe_basic_publish(ch, payload, tmp_queue_name, headers)
102
+ count += 1
103
+ end
104
+ logger.puts "Migrator: Moved #{count} messages to #{tmp_queue_name}"
105
+ ch.queue(q[:name], passive: true).delete
106
+
107
+ # create new queue with desired arguments
108
+ logger.puts "Migrator: Recreating queue #{q[:name]} with new arguments: #{q[:arguments].inspect}"
109
+ ch.queue(q[:name], durable: q[:durable], arguments: q[:arguments])
110
+
111
+ # --- Restore bindings ---
112
+ bindings.each do |binding|
113
+ # Only restore if it's a binding from an exchange (not from a queue)
114
+ next unless binding["source"] && !binding["source"].empty?
115
+
116
+ ch.queue(q[:name]).bind(binding["source"], routing_key: binding["routing_key"])
117
+ logger.puts "Migrator: Restored binding: exchange=#{binding["source"]}, routing_key=#{binding["routing_key"]}"
118
+ # now drop the tmp queue binding
119
+ ch.queue(tmp_queue_name).unbind(binding["source"], routing_key: binding["routing_key"])
120
+ rescue StandardError => e
121
+ logger.puts "Migrator: Failed to restore binding: #{e.class}: #{e.message}"
122
+ end
123
+
124
+ count = 0
125
+ tmp_queue = ch.queue(tmp_queue_name, passive: true)
126
+ loop do
127
+ delivery_info, headers, payload = tmp_queue.pop
128
+ break unless payload
129
+
130
+ logger.puts "Migrator: Moving message back to #{q[:name]}: payload=#{payload.inspect}, headers=#{headers.inspect}, delivery_info=#{delivery_info.inspect}"
131
+ safe_basic_publish(ch, payload, q[:name], headers)
132
+ count += 1
133
+ end
134
+ logger.puts "Migrator: Moved #{count} messages to #{q[:name]}"
135
+ ch.queue(tmp_queue_name, passive: true).delete
136
+ logger.puts "Migrator: Migration for #{q[:name]} complete."
137
+ end
138
+ ch.close
139
+ conn.close
140
+ end
141
+
142
+ # Fetches all bindings for a queue using the RabbitMQ Management API
143
+ def fetch_queue_bindings(api_url, user, pass, queue_info)
144
+ vhost = queue_info["vhost"] || "/"
145
+ queue_name = queue_info["name"]
146
+ uri = URI.parse(api_url)
147
+ scheme = uri.scheme || "http"
148
+ host = uri.host
149
+ port = uri.port || 15_672
150
+ vhost_enc = URI.encode_www_form_component(vhost)
151
+ queue_enc = URI.encode_www_form_component(queue_name)
152
+ url = URI.parse("#{scheme}://#{host}:#{port}/api/queues/#{vhost_enc}/#{queue_enc}/bindings")
153
+ http = Net::HTTP.new(url.hostname, url.port)
154
+ http.use_ssl = (scheme == "https")
155
+ req = Net::HTTP::Get.new(url)
156
+ req.basic_auth(user, pass)
157
+ res = http.request(req)
158
+ return [] unless res.is_a?(Net::HTTPSuccess)
159
+
160
+ JSON.parse(res.body)
161
+ end
162
+
163
+ private
164
+
165
+ def fetch_all_queues(api_url, user, pass)
166
+ uri = URI.parse(api_url)
167
+ scheme = uri.scheme || "http"
168
+ host = uri.host
169
+ port = uri.port || 15_672
170
+ url = URI.parse("#{scheme}://#{host}:#{port}/api/queues")
171
+ http = Net::HTTP.new(url.hostname, url.port)
172
+ http.use_ssl = (scheme == "https")
173
+ req = Net::HTTP::Get.new(url)
174
+ req.basic_auth(user, pass)
175
+ res = http.request(req)
176
+ return [] unless res.is_a?(Net::HTTPSuccess)
177
+
178
+ JSON.parse(res.body)
179
+ end
180
+
181
+ def queue_args_match?(queue_info, desired_args)
182
+ (desired_args.to_a - queue_info[:arguments].to_a).empty? &&
183
+ (queue_info[:arguments].to_a - desired_args.to_a).empty?
184
+ end
185
+
186
+ def safe_basic_publish(channel, payload, queue_name, headers)
187
+ # Bunny's queue.pop returns [payload, properties, delivery_info]
188
+ # The payload is always the message body as a string or nil
189
+ # The properties hash contains all AMQP properties
190
+ amqp_property_keys = %i[routing_key persistent mandatory timestamp expiration type reply_to content_type content_encoding correlation_id priority message_id user_id app_id]
191
+ # Normalize headers to a Hash if it's an Array (Bunny can return either)
192
+ props = case headers
193
+ when Hash then headers
194
+ when Array then headers.to_h
195
+ else {}
196
+ end
197
+ sanitized_properties = props.select { |k, _| amqp_property_keys.include?(k.to_sym) }
198
+ # Always pass the payload as-is (should be a string or nil)
199
+ queue = channel.queue(queue_name, passive: true)
200
+ if sanitized_properties.any?
201
+ queue.publish(payload, sanitized_properties)
202
+ else
203
+ queue.publish(payload)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sneakers/migrator/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sneakers-queue-migrator"
7
+ spec.version = Sneakers::Migrator::VERSION
8
+ spec.authors = ["Mike Gane"]
9
+ spec.email = ["mike.gane@nexusmods.com"]
10
+
11
+ spec.summary = "Ruby sneakers queue migrator for RabbitMQ when you need to change queue arguments."
12
+ spec.description = "A migrator that handles changing RabbitMQ queue arguments for Sneakers subscribers, ensuring smooth transitions without data loss."
13
+ spec.homepage = "https://gitlab.nexdev.uk/nexus-mods/public/sneakers-queue-migrator"
14
+ spec.required_ruby_version = ">= 3.1.0"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(__dir__) do
19
+ `git ls-files -z`.split("\x0").reject do |f|
20
+ (File.expand_path(f) == __FILE__) || f.start_with?(
21
+ "bin/",
22
+ "test/",
23
+ "spec/",
24
+ "features/",
25
+ ".git",
26
+ ".circleci",
27
+ "appveyor"
28
+ )
29
+ end
30
+ end
31
+
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_dependency "bunny", "~> 2.0"
37
+ spec.add_dependency "json", "~> 2.0"
38
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sneakers-queue-migrator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mike Gane
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-07-03 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bunny
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ description: A migrator that handles changing RabbitMQ queue arguments for Sneakers
41
+ subscribers, ensuring smooth transitions without data loss.
42
+ email:
43
+ - mike.gane@nexusmods.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rubocop.yml"
49
+ - Gemfile
50
+ - Gemfile.lock
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - docker-compose.yml
55
+ - lib/sneakers/migrator.rb
56
+ - lib/sneakers/migrator/version.rb
57
+ - sneakers-queue-migrator.gemspec
58
+ homepage: https://gitlab.nexdev.uk/nexus-mods/public/sneakers-queue-migrator
59
+ licenses: []
60
+ metadata: {}
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.1.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.3
76
+ specification_version: 4
77
+ summary: Ruby sneakers queue migrator for RabbitMQ when you need to change queue arguments.
78
+ test_files: []