sneakers-queue-migrator 0.1.3 → 0.1.4
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/Gemfile.lock +1 -1
- data/README.md +7 -1
- data/lib/sneakers/migrator/version.rb +1 -1
- data/lib/sneakers/migrator.rb +216 -27
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa500b657d410fd1686b993cdeea8c99b415b8cf502c66b4f29cca6aa9104554
|
4
|
+
data.tar.gz: b46319b9ff9a6b1821aaf63b5895e98df85e1be4b980f8f25a320ceacdb9a003
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 600d85156bacd65cd41005f58ee3d41f0343ca3aef7fd8c66556da258d0b8112b9fb6dc41c132b5abc41b3bf2369c1b3ac4db6185660c670e7463c2ca68768fd
|
7
|
+
data.tar.gz: 4a3644e771e0f88f3ee15167e2db7c10ea486b31c84e30a963adf6d122754499fb61f11ffe5db4d29fa3b41fa330aca9b1d57f354959ebd378ee21c465bf4e39
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -35,10 +35,16 @@ require 'sneakers/migrator'
|
|
35
35
|
Sneakers::Migrator.migrate!(
|
36
36
|
amqp_url: 'amqp://user:password@rabbitmq:5672/',
|
37
37
|
amqp_api_url: 'http://user:password@rabbitmq:15672',
|
38
|
-
subscriber_paths: ['app/workers']
|
38
|
+
subscriber_paths: ['app/workers'],
|
39
|
+
parse_only: false
|
39
40
|
)
|
40
41
|
```
|
41
42
|
|
43
|
+
`amqp_url` - regular bunny connection string
|
44
|
+
`amqp_api_url` - url WITH PORT for the management interface
|
45
|
+
`subscriber_paths` - array of folders to scan for subscribers
|
46
|
+
`parse_only` - `false` requires the files and reflects the arguments, `true` parses the from_queue block with regex
|
47
|
+
|
42
48
|
## Development
|
43
49
|
|
44
50
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
data/lib/sneakers/migrator.rb
CHANGED
@@ -13,19 +13,38 @@ module Sneakers
|
|
13
13
|
|
14
14
|
class << self
|
15
15
|
# Main migration entrypoint
|
16
|
-
def migrate!(amqp_url:, amqp_api_url:, subscriber_paths: [], logger: nil)
|
16
|
+
def migrate!(amqp_url:, amqp_api_url:, subscriber_paths: [], logger: nil, parse_only: false)
|
17
17
|
logger ||= $stdout
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
18
|
+
# Optionally parse subscribers with regex/static analysis
|
19
|
+
queues = if parse_only
|
20
|
+
parse_queues_from_files(subscriber_paths, logger)
|
21
|
+
else
|
22
|
+
load_files_to_find_queues(subscriber_paths, logger)
|
23
|
+
end
|
24
|
+
|
25
|
+
conn = Bunny.new(amqp_url)
|
26
|
+
conn.start
|
27
|
+
ch = conn.create_channel
|
28
|
+
|
29
|
+
# Remove unused api_uri and fix variable usage
|
30
|
+
rabbitmq_api_user = URI.parse(amqp_api_url).user
|
31
|
+
rabbitmq_api_pass = URI.parse(amqp_api_url).password
|
32
|
+
all_queues = fetch_all_queues(amqp_api_url, rabbitmq_api_user, rabbitmq_api_pass)
|
33
|
+
|
34
|
+
queues.each do |q|
|
35
|
+
existing_queue_info = all_queues.find { |info| info["name"] == q[:name] }
|
36
|
+
if existing_queue_info.nil?
|
37
|
+
logger.puts "Migrator: Queue #{q[:name]} does not exist, creating..."
|
38
|
+
ch.queue(q[:name], durable: q[:durable], arguments: q[:arguments])
|
39
|
+
next
|
26
40
|
end
|
41
|
+
migrate_queue!(q, ch, amqp_api_url, rabbitmq_api_user, rabbitmq_api_pass, logger, existing_queue_info)
|
27
42
|
end
|
43
|
+
end
|
28
44
|
|
45
|
+
private
|
46
|
+
|
47
|
+
def load_files_to_find_queues(subscriber_paths, logger)
|
29
48
|
# Load all subscribers (recursively)
|
30
49
|
subscriber_paths.each do |path|
|
31
50
|
Dir[File.join(path, "**", "*.rb")].sort.each do |f|
|
@@ -51,10 +70,10 @@ module Sneakers
|
|
51
70
|
logger.puts "Migrator: Found #{subscribers.size} subscribers:"
|
52
71
|
subscribers.each { |s| logger.puts " - #{s.name}" }
|
53
72
|
|
54
|
-
|
73
|
+
subscribers.map do |klass|
|
55
74
|
opts = klass.instance_variable_get(:@queue_opts) || {}
|
56
|
-
name = klass.instance_variable_get(:@queue_name) ||
|
57
|
-
(klass.const_defined?(:QUEUE_NAME) ? klass.const_get(:QUEUE_NAME) : nil) ||
|
75
|
+
name = klass.instance_variable_get(:@queue_name) || \
|
76
|
+
(klass.const_defined?(:QUEUE_NAME) ? klass.const_get(:QUEUE_NAME) : nil) || \
|
58
77
|
klass.name.gsub("::", "_").gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
59
78
|
{
|
60
79
|
name: name,
|
@@ -64,28 +83,198 @@ module Sneakers
|
|
64
83
|
durable: opts.fetch(:durable, true)
|
65
84
|
}.tap { |q| q[:arguments]["x-queue-type"] ||= "classic" }
|
66
85
|
end.uniq { |q| q[:name] }
|
86
|
+
end
|
67
87
|
|
68
|
-
|
69
|
-
|
70
|
-
|
88
|
+
# Robustly parse all from_queue calls in .rb files under subscriber_paths
|
89
|
+
def parse_queues_from_files(subscriber_paths, logger)
|
90
|
+
require "ripper"
|
91
|
+
require "yaml"
|
92
|
+
queues = []
|
93
|
+
seen = {}
|
94
|
+
subscriber_paths.each do |path|
|
95
|
+
Dir[File.join(path, "**", "*.rb")].sort.each do |file|
|
96
|
+
logger.puts "Migrator: Parsing subscriber file: #{file}"
|
97
|
+
src = File.read(file)
|
98
|
+
# Use a robust regex to find from_queue calls (multi-line, with hash)
|
99
|
+
# Improved: allow multi-line, nested, and indented argument hashes
|
100
|
+
# Custom scan to robustly extract the full options hash or argument list (handles nested braces)
|
101
|
+
# Use a more robust approach: for each from_queue, extract the full argument list (multi-line, nested)
|
102
|
+
# Multi-line robust extraction for from_queue calls
|
103
|
+
src_lines = src.lines
|
104
|
+
src_lines.each_with_index do |line, idx|
|
105
|
+
next unless line =~ /from_queue\s+("|')([\w\-:]+)\1\s*,?\s*$/
|
71
106
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
107
|
+
queue_name = ::Regexp.last_match(2)
|
108
|
+
# Multi-line argument collection: collect all subsequent lines that are more indented than the from_queue line
|
109
|
+
from_indent = line[/^\s*/].size
|
110
|
+
opts_lines = []
|
111
|
+
j = idx + 1
|
112
|
+
while j < src_lines.size
|
113
|
+
l = src_lines[j]
|
114
|
+
# Stop if line is less indented or blank or looks like a new method/class/def
|
115
|
+
break if l.strip.empty? || l[/^\s*/].size <= from_indent || l =~ /^\s*(def |class |module )/
|
76
116
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
117
|
+
opts_lines << l
|
118
|
+
j += 1
|
119
|
+
end
|
120
|
+
opts_str = opts_lines.join(" ").strip
|
121
|
+
puts "Migrator: Parsing queue: #{queue_name} with options: #{opts_str}"
|
122
|
+
opts_hash = parse_options_hash_for_file(opts_str)
|
123
|
+
puts "Migrator: Parsed options for #{queue_name}: #{opts_hash.inspect}"
|
124
|
+
routing_keys = opts_hash[:routing_key]
|
125
|
+
routing_keys = [routing_keys] if routing_keys && !routing_keys.is_a?(Array)
|
126
|
+
arguments = opts_hash[:arguments] || {}
|
127
|
+
# Always stringify keys deeply for arguments
|
128
|
+
arguments = stringify_keys_deep(arguments)
|
129
|
+
# Add "x-queue-type" if not present as string
|
130
|
+
arguments["x-queue-type"] ||= "classic"
|
131
|
+
# Remove any symbol keys that duplicate string keys
|
132
|
+
arguments.delete_if { |k, _| k.is_a?(Symbol) && arguments.key?(k.to_s) }
|
133
|
+
|
134
|
+
exchange_options = opts_hash[:exchange_options] || {}
|
135
|
+
exchange_options = stringify_keys_deep(exchange_options)
|
136
|
+
exchange_options.delete_if { |k, _| k.is_a?(Symbol) && exchange_options.key?(k.to_s) }
|
137
|
+
|
138
|
+
q = {
|
139
|
+
name: queue_name,
|
140
|
+
exchange: opts_hash[:exchange],
|
141
|
+
exchange_options: exchange_options,
|
142
|
+
routing_keys: routing_keys || [],
|
143
|
+
arguments: arguments,
|
144
|
+
durable: opts_hash.key?(:durable) ? opts_hash[:durable] : true
|
145
|
+
}
|
146
|
+
unless seen[q[:name]]
|
147
|
+
queues << q
|
148
|
+
seen[q[:name]] = true
|
149
|
+
end
|
150
|
+
end
|
83
151
|
end
|
84
|
-
migrate_queue!(q, ch, amqp_api_url, rabbitmq_api_user, rabbitmq_api_pass, logger, existing_queue_info)
|
85
152
|
end
|
153
|
+
puts "Migrator: Found #{queues.size} queues to migrate:"
|
154
|
+
queues.each { |q| puts " - #{q[:name]} (exchange: #{q[:exchange]}, routing_keys: #{Array(q[:routing_keys]).join(", ")}, arguments: #{q[:arguments].inspect})" }
|
155
|
+
queues
|
86
156
|
end
|
87
157
|
|
88
|
-
|
158
|
+
# Recursively convert all hash keys to strings
|
159
|
+
def stringify_keys_deep(obj)
|
160
|
+
case obj
|
161
|
+
when Hash
|
162
|
+
obj.each_with_object({}) { |(k, v), h| h[k.to_s] = stringify_keys_deep(v) }
|
163
|
+
when Array
|
164
|
+
obj.map { |v| stringify_keys_deep(v) }
|
165
|
+
else
|
166
|
+
obj
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Parse a Ruby hash from a string for file-based queue declarations (robust for multi-line, symbol, string keys, nested hashes)
|
171
|
+
def parse_options_hash_for_file(str)
|
172
|
+
return {} unless str
|
173
|
+
|
174
|
+
# Remove comments and excessive whitespace
|
175
|
+
code = str.gsub(/#.*$/, "").gsub(/\n/, " ").gsub(/\s+/, " ")
|
176
|
+
# Convert Ruby hashrockets and symbol keys to YAML style, and handle symbol values and numbers with underscores
|
177
|
+
yaml_str = code
|
178
|
+
.gsub(/:(\w+)\s*=>/, '"\\1":') # :foo =>
|
179
|
+
.gsub(/(\w+):/, '"\\1":') # foo:
|
180
|
+
.gsub(/=>/, ":") # fallback for any =>
|
181
|
+
.gsub(/:([\w\d_]+)/, '"\\1"') # symbol values :foo => "foo"
|
182
|
+
.gsub(/([\d_]+)(?=,|\s|\})/) { |m| m.delete("_") } # numbers with underscores
|
183
|
+
# Ensure the string is wrapped in braces for YAML parsing
|
184
|
+
yaml_str = "{#{yaml_str}}" unless yaml_str.strip.start_with?("{") && yaml_str.strip.end_with?("}")
|
185
|
+
begin
|
186
|
+
# Try to parse as YAML (single argument for compatibility)
|
187
|
+
hash = YAML.safe_load(yaml_str) || {}
|
188
|
+
# Symbolize keys recursively and dedupe symbol/string keys
|
189
|
+
dedupe_symbol_and_string_keys_deep(symbolize_keys_deep(hash))
|
190
|
+
rescue StandardError => e
|
191
|
+
warn "Migrator: parse_options_hash_for_file YAML parse error: #{e.class}: #{e.message}"
|
192
|
+
# Fallback: try to extract common keys manually
|
193
|
+
out = {}
|
194
|
+
out[:exchange] = ::Regexp.last_match(1) if str =~ /(?:["']?exchange["']?\s*(?:=>|:)\s*["']?([^"'\s,}]+)["']?)/
|
195
|
+
out[:routing_key] = ::Regexp.last_match(1) if str =~ /(?:["']?routing_key["']?\s*(?:=>|:)\s*["']?([\w\-:]+)["']?)/
|
196
|
+
if str =~ /arguments:\s*\{([^}]*)\}/m
|
197
|
+
begin
|
198
|
+
args = YAML.safe_load("{#{::Regexp.last_match(1)}}") || {}
|
199
|
+
out[:arguments] = dedupe_symbol_and_string_keys_deep(symbolize_keys_deep(args))
|
200
|
+
rescue StandardError => e2
|
201
|
+
warn "Migrator: parse_options_hash_for_file arguments parse error: #{e2.class}: #{e2.message}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
if str =~ /exchange_options:\s*\{([^}]*)\}/m
|
205
|
+
begin
|
206
|
+
ex_opts = YAML.safe_load("{#{::Regexp.last_match(1)}}") || {}
|
207
|
+
out[:exchange_options] = dedupe_symbol_and_string_keys_deep(symbolize_keys_deep(ex_opts))
|
208
|
+
rescue StandardError => e3
|
209
|
+
warn "Migrator: parse_options_hash_for_file exchange_options parse error: #{e3.class}: #{e3.message}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
out
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Remove string keys if the same key exists as a symbol, recursively
|
217
|
+
def dedupe_symbol_and_string_keys_deep(obj)
|
218
|
+
case obj
|
219
|
+
when Hash
|
220
|
+
# Remove string keys if symbol key exists
|
221
|
+
symbol_keys = obj.keys.select { |k| k.is_a?(Symbol) }
|
222
|
+
obj.reject { |k, _| k.is_a?(String) && symbol_keys.include?(k.to_sym) }
|
223
|
+
.transform_values { |v| dedupe_symbol_and_string_keys_deep(v) }
|
224
|
+
when Array
|
225
|
+
obj.map { |v| dedupe_symbol_and_string_keys_deep(v) }
|
226
|
+
else
|
227
|
+
obj
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Recursively symbolize keys in a hash
|
232
|
+
def symbolize_keys_deep(obj)
|
233
|
+
case obj
|
234
|
+
when Hash
|
235
|
+
obj.each_with_object({}) { |(k, v), h| h[k.to_sym] = symbolize_keys_deep(v) }
|
236
|
+
when Array
|
237
|
+
obj.map { |v| symbolize_keys_deep(v) }
|
238
|
+
else
|
239
|
+
obj
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Parse a Ruby hash from a string (robust for multi-line, symbol, and string keys)
|
244
|
+
# This is used for parsing options from rabbitmq management API strings
|
245
|
+
def parse_options_hash(str)
|
246
|
+
return {} unless str
|
247
|
+
|
248
|
+
# Remove newlines and excessive whitespace for easier parsing
|
249
|
+
compact = str.gsub(/\n/, " ").gsub(/\s+/, " ")
|
250
|
+
# Convert Ruby symbols and hashrockets to YAML style
|
251
|
+
yaml_str = compact
|
252
|
+
.gsub(/:(\w+)\s*=>/, '"\\1":') # :foo =>
|
253
|
+
.gsub(/(\w+):/, '"\\1":') # foo:
|
254
|
+
.gsub(/=>/, ":") # fallback for any =>
|
255
|
+
begin
|
256
|
+
# Try to parse as YAML (single argument for compatibility)
|
257
|
+
YAML.safe_load("{#{yaml_str}}") || {}
|
258
|
+
rescue StandardError => e
|
259
|
+
warn "Migrator: parse_options_hash YAML parse error: #{e.class}: #{e.message}"
|
260
|
+
# Fallback: try to extract exchange and routing_key robustly (symbol or string keys, colon or hashrocket, any quotes)
|
261
|
+
out = {}
|
262
|
+
# Match exchange: "exchange" => "test", :exchange => 'test', exchange: "test", etc.
|
263
|
+
out[:exchange] = ::Regexp.last_match(1) if str =~ /(?:["']?exchange["']?\s*(?:=>|:)\s*["']?([^"'\s,}]+)["']?)/
|
264
|
+
# Match routing_key: "routing_key" => "foo", :routing_key => 'foo', routing_key: "foo", etc.
|
265
|
+
out[:routing_key] = ::Regexp.last_match(1) if str =~ /(?:["']?routing_key["']?\s*(?:=>|:)\s*["']?([\w\-:]+)["']?)/
|
266
|
+
if str =~ /arguments:\s*\{([^}]*)\}/m
|
267
|
+
# Try to parse arguments as a hash
|
268
|
+
begin
|
269
|
+
args = YAML.safe_load("{#{::Regexp.last_match(1)}}") || {}
|
270
|
+
out[:arguments] = args
|
271
|
+
rescue StandardError => e2
|
272
|
+
warn "Migrator: parse_options_hash arguments parse error: #{e2.class}: #{e2.message}"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
out
|
276
|
+
end
|
277
|
+
end
|
89
278
|
|
90
279
|
def migrate_queue!(q, ch, amqp_api_url, rabbitmq_api_user, rabbitmq_api_pass, logger, existing_queue_info)
|
91
280
|
tmp_queue_name = "#{q[:name]}_tmp"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sneakers-queue-migrator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Gane
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-07-
|
10
|
+
date: 2025-07-04 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: bunny
|