seesaw 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,5 @@
1
+ == 0.1.0 / 2007-08-18
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
5
+
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/seesaw
6
+ lib/seesaw/init.rb
7
+ lib/seesaw/mongrel_cluster_patch.rb
8
+ test/test_seesaw.rb
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ seesaw
2
+ by Matt Allen and Max Muermann
3
+ (http://rubyforge.org/projects/rails-oceania)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ sudo gem install seesaw
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2007 FIX
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/seesaw/init.rb'
6
+
7
+ Hoe.new('seesaw', Seesaw::VERSION) do |p|
8
+ p.rubyforge_name = 'seesaw'
9
+ p.author = 'Matt Allen, Max Muermann'
10
+ p.email = 'max@muermann.org'
11
+ p.summary = 'Ripple-restart a mongrel cluster with no downtime'
12
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
13
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ p.extra_deps << ['gem_plugin', '>= 0.2.2'] << ['mongrel', '>= 1.0.1'] << ['mongrel_cluster', '>= 1.0.2']
16
+ p.spec_extras['autorequire'] = 'seesaw/init.rb'
17
+ end
18
+
19
+ # vim: syntax=Ruby
data/bin/seesaw ADDED
File without changes
@@ -0,0 +1,107 @@
1
+ require 'gem_plugin'
2
+ require 'mongrel'
3
+ require 'yaml'
4
+ require 'fileutils'
5
+ require 'seesaw/mongrel_cluster_patch'
6
+
7
+ module Seesaw
8
+ VERSION = '0.1.0'
9
+
10
+ module CommandBase
11
+ def symlink(cluster)
12
+ p "symlink to #{cluster}"
13
+ file = File.join(@config_path, @config_files[cluster])
14
+ p "target: #{file}"
15
+ FileUtils.ln_sf( file, @config_symlink)
16
+ end
17
+
18
+ def restart_mongrels(cluster=nil)
19
+ p "restart mongrels #{cluster}"
20
+ stop_mongrels(cluster)
21
+ start_mongrels(cluster)
22
+ end
23
+
24
+ def start_mongrels(cluster=nil)
25
+ p "start mongrels #{cluster}"
26
+ cmd = "mongrel_rails seesaw::start"
27
+ cmd << " --cluster #{cluster}" if cluster
28
+ system(cmd)
29
+ end
30
+
31
+ def stop_mongrels(cluster=nil)
32
+ p "stop mongrels #{cluster}"
33
+ cmd = "mongrel_rails seesaw::stop"
34
+ cmd << " --cluster #{cluster}" if cluster
35
+ system(cmd)
36
+ end
37
+
38
+ def restart_http(cluster=nil)
39
+ p "nginx restart"
40
+ system(@restart_cmd)
41
+ sleep 5
42
+ end
43
+
44
+ def shutdown_half_cluster(half)
45
+ p "shutdown half #{half}"
46
+ other_half = half == 1 ? 2 : 1
47
+ symlink(other_half)
48
+ restart_http
49
+ stop_mongrels(half)
50
+ end
51
+
52
+ def switch_to_half_cluster(half)
53
+ p "switch_to_half_cluster #{half}"
54
+ other_half = half == 1 ? 2 : 1
55
+ shutdown_half_cluster(other_half)
56
+ start_mongrels(other_half)
57
+ end
58
+
59
+ def start_cluster
60
+ p "start cluster"
61
+ symlink("all")
62
+ restart_http
63
+ end
64
+
65
+ def parse_options
66
+ @options = YAML.load(File.read(@config_file))
67
+ @config_path = @options["config_path"] || "/opt/local/etc/apache"
68
+ @config_files = @options["config_files"] || {}
69
+ @config_files[1] ||= "cluster_1.conf"
70
+ @config_files[2] ||= "cluster_2.conf"
71
+ @config_files["all"] ||= "cluster_all.conf"
72
+ @config_symlink = @options["config_symlink"] || "cluster.conf"
73
+ @config_symlink = File.join(@config_path, @config_symlink)
74
+ @restart_cmd = @options["restart_cmd"] || "apachectl graceful"
75
+ end
76
+
77
+ def validate
78
+ valid_exists?(@config_file, "Configuration file does not exist. Run mongrel_rails seesaw::configure.")
79
+ return @valid
80
+ end
81
+
82
+ def configure
83
+ options [
84
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/seesaw.yml"],
85
+ ]
86
+ end
87
+
88
+ end
89
+
90
+ class Migrate < GemPlugin::Plugin "/commands"
91
+ include Mongrel::Command::Base
92
+ include CommandBase
93
+
94
+ def run
95
+ parse_options
96
+ p "c1"
97
+ switch_to_half_cluster(1)
98
+ p "c2"
99
+ switch_to_half_cluster(2)
100
+ p "nighty night"
101
+ sleep 20
102
+ p "all"
103
+ start_cluster
104
+ p "done"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,368 @@
1
+ require 'gem_plugin'
2
+ require 'mongrel'
3
+ require 'yaml'
4
+ require 'enumerator'
5
+
6
+ module Seesaw
7
+ module ExecBase
8
+ include Mongrel::Command::Base
9
+
10
+ STATUS_OK = 0
11
+ STATUS_ERROR = 2
12
+
13
+ def validate
14
+ valid_exists?(@config_file, "Configuration file does not exist. Run mongrel_rails cluster::configure.")
15
+ return @valid
16
+ end
17
+
18
+ def read_options
19
+ @options = {
20
+ "environment" => ENV['RAILS_ENV'] || "development",
21
+ "port" => 3000,
22
+ "pid_file" => "tmp/pids/mongrel.pid",
23
+ "log_file" => "log/mongrel.log",
24
+ "servers" => 2
25
+ }
26
+ conf = YAML.load_file(@config_file)
27
+ @options.merge! conf if conf
28
+
29
+ process_pid_file @options["pid_file"]
30
+ process_log_file @options["log_file"]
31
+
32
+ start_port = end_port = @only
33
+ start_port ||= @options["port"].to_i
34
+ end_port ||= start_port + @options["servers"] - 1
35
+ @ports = (start_port..end_port).to_a
36
+ end
37
+
38
+ def process_pid_file(pid_file)
39
+ @pid_file_ext = File.extname(pid_file)
40
+ @pid_file_base = File.basename(pid_file, @pid_file_ext)
41
+ @pid_file_dir = File.dirname(pid_file)
42
+ end
43
+
44
+ def process_log_file(log_file)
45
+ @log_file_ext = File.extname(log_file)
46
+ @log_file_base = File.basename(log_file, @log_file_ext)
47
+ @log_file_dir = File.dirname(log_file)
48
+ end
49
+
50
+ def port_pid_file(port)
51
+ pid_file = [@pid_file_base, port].join(".") + @pid_file_ext
52
+ File.join(@pid_file_dir, pid_file)
53
+ end
54
+
55
+ def port_log_file(port)
56
+ log_file = [@log_file_base, port].join(".") + @log_file_ext
57
+ File.join(@log_file_dir, log_file)
58
+ end
59
+
60
+ def start
61
+ argv = [ "mongrel_rails" ]
62
+ argv << "start"
63
+ argv << "-d"
64
+ argv << "-e #{@options["environment"]}" if @options["environment"]
65
+ argv << "-a #{@options["address"]}" if @options["address"]
66
+ argv << "-c #{@options["cwd"]}" if @options["cwd"]
67
+ argv << "-t #{@options["timeout"]}" if @options["timeout"]
68
+ argv << "-m #{@options["mime_map"]}" if @options["mime_map"]
69
+ argv << "-r #{@options["docroot"]}" if @options["docroot"]
70
+ argv << "-n #{@options["num_procs"]}" if @options["num_procs"]
71
+ argv << "-B" if @options["debug"]
72
+ argv << "-S #{@options["config_script"]}" if @options["config_script"]
73
+ argv << "--user #{@options["user"]}" if @options["user"]
74
+ argv << "--group #{@options["group"]}" if @options["group"]
75
+ argv << "--prefix #{@options["prefix"]}" if @options["prefix"]
76
+ cmd = argv.join " "
77
+
78
+ @ports.each do |port|
79
+ if @clean && pid_file_exists?(port) && !check_process(port)
80
+ pid_file = port_pid_file(port)
81
+ log "missing process: removing #{pid_file}"
82
+ File.unlink(pid_file)
83
+ end
84
+
85
+ if pid_file_exists?(port) && check_process(port)
86
+ log "already started port #{port}"
87
+ next
88
+ end
89
+
90
+ exec_cmd = cmd + " -p #{port} -P #{port_pid_file(port)}"
91
+ exec_cmd += " -l #{port_log_file(port)}"
92
+ log "starting port #{port}"
93
+ log_verbose exec_cmd
94
+ output = `#{exec_cmd}`
95
+ log_error output unless $?.success?
96
+ end
97
+ end
98
+
99
+ def stop
100
+ argv = [ "mongrel_rails" ]
101
+ argv << "stop"
102
+ argv << "-c #{@options["cwd"]}" if @options["cwd"]
103
+ argv << "-f" if @force
104
+ cmd = argv.join " "
105
+
106
+ @ports.each do |port|
107
+ pid = check_process(port)
108
+ if @clean && pid && !pid_file_exists?(port)
109
+ log "missing pid_file: killing mongrel_rails port #{port}, pid #{pid}"
110
+ Process.kill("KILL", pid.to_i)
111
+ end
112
+
113
+ if !check_process(port)
114
+ log "already stopped port #{port}"
115
+ next
116
+ end
117
+
118
+ exec_cmd = cmd + " -P #{port_pid_file(port)}"
119
+ log "stopping port #{port}"
120
+ log_verbose exec_cmd
121
+ output = `#{exec_cmd}`
122
+ log_error output unless $?.success?
123
+
124
+ end
125
+ end
126
+
127
+ def status
128
+ status = STATUS_OK
129
+
130
+ @ports.each do |port|
131
+ pid = check_process(port)
132
+ unless pid_file_exists?(port)
133
+ log "missing pid_file: #{port_pid_file(port)}"
134
+ status = STATUS_ERROR
135
+ else
136
+ log "found pid_file: #{port_pid_file(port)}"
137
+ end
138
+ if pid
139
+ log "found mongrel_rails: port #{port}, pid #{pid}"
140
+ else
141
+ log "missing mongrel_rails: port #{port}"
142
+ status = STATUS_ERROR
143
+ end
144
+ puts ""
145
+ end
146
+
147
+ return status
148
+ end
149
+
150
+ def pid_file_exists?(port)
151
+ pid_file = port_pid_file(port)
152
+ exists = false
153
+ chdir_cwd do
154
+ exists = File.exists?(pid_file)
155
+ end
156
+ exists
157
+ end
158
+
159
+ def check_process(port)
160
+ if pid_file_exists?(port)
161
+ pid = read_pid(port)
162
+ ps_output = `ps -o #{cmd_name}= -p #{pid}`
163
+ pid = ps_output =~ /mongrel_rails/ ? pid : nil
164
+ else
165
+ pid = find_pid(port)
166
+ end
167
+ return pid
168
+ end
169
+
170
+ def cmd_name
171
+ RUBY_PLATFORM =~ /solaris/i ? "args" : "command"
172
+ end
173
+
174
+ def chdir_cwd
175
+ pwd = Dir.pwd
176
+ Dir.chdir(@options["cwd"]) if @options["cwd"]
177
+ yield
178
+ Dir.chdir(pwd) if @options["cwd"]
179
+ end
180
+
181
+ def read_pid(port)
182
+ pid_file = port_pid_file(port)
183
+ pid = 0
184
+ chdir_cwd do
185
+ pid = File.read(pid_file)
186
+ end
187
+ return pid
188
+ end
189
+
190
+ def find_pid(port)
191
+ ps_cmd = "ps -ewwo pid,#{cmd_name}"
192
+ ps_output = `#{ps_cmd}`
193
+ ps_output.each do |line|
194
+ if line =~ /-P #{Regexp.escape(port_pid_file(port))} /
195
+ pid = line.split[0]
196
+ return pid
197
+ end
198
+ end
199
+ return nil
200
+ end
201
+
202
+ def log_error(message)
203
+ log(message)
204
+ end
205
+
206
+ def log_verbose(message)
207
+ log(message) if @verbose
208
+ end
209
+
210
+ def log(message)
211
+ puts message
212
+ end
213
+
214
+ def clustered &block
215
+ if @cluster
216
+ @only = nil
217
+ read_options
218
+ @ports.enum_slice((@ports.size/2.0).ceil).map{|x|x}[@cluster.to_i-1].each do |port|
219
+ @only = port
220
+ read_options
221
+ yield
222
+ end
223
+ else
224
+ read_options
225
+ yield
226
+ end
227
+ end
228
+
229
+ end
230
+ class Start < GemPlugin::Plugin "/commands"
231
+ include ExecBase
232
+
233
+ def configure
234
+ options [
235
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
236
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
237
+ ['', '--clean', "Remove pid_file if needed before starting", :@clean, false],
238
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
239
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
240
+ ]
241
+ end
242
+
243
+ def run
244
+ clustered {start}
245
+ end
246
+ end
247
+
248
+ class Stop < GemPlugin::Plugin "/commands"
249
+ include ExecBase
250
+
251
+ def configure
252
+ options [
253
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
254
+ ['-f', '--force', "Force the shutdown.", :@force, false],
255
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
256
+ ['', '--clean', "Remove orphaned process if needed before stopping", :@clean, false],
257
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
258
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
259
+ ]
260
+ end
261
+
262
+ def run
263
+ clustered {stop}
264
+ end
265
+ end
266
+
267
+ class Restart < GemPlugin::Plugin "/commands"
268
+ include ExecBase
269
+
270
+ def configure
271
+ options [
272
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
273
+ ['-f', '--force', "Force the shutdown.", :@force, false],
274
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
275
+ ['', '--clean', "Call stop and start with --clean", :@clean, false],
276
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
277
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
278
+ ]
279
+ end
280
+
281
+ def run
282
+ clustered do
283
+ stop
284
+ start
285
+ end
286
+ end
287
+
288
+ end
289
+
290
+ class Configure < GemPlugin::Plugin "/commands"
291
+ include ExecBase
292
+
293
+ def configure
294
+ options [
295
+ ["-e", "--environment ENV", "Rails environment to run as", :@environment, nil],
296
+ ['-p', '--port PORT', "Starting port to bind to", :@port, 3000],
297
+ ['-a', '--address ADDR', "Address to bind to", :@address, nil],
298
+ ['-l', '--log FILE', "Where to write log messages", :@log_file, "log/mongrel.log"],
299
+ ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "tmp/pids/mongrel.pid"],
300
+ ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, nil],
301
+ ['-t', '--timeout SECONDS', "Timeout all requests after SECONDS time", :@timeout, nil],
302
+ ['-m', '--mime PATH', "A YAML file that lists additional MIME types", :@mime_map, nil],
303
+ ['-r', '--root PATH', "Set the document root (default 'public')", :@docroot, nil],
304
+ ['-n', '--num-procs INT', "Number of processor threads to use", :@num_procs, nil],
305
+ ['-B', '--debug', "Enable debugging mode", :@debug, nil],
306
+ ['-S', '--script PATH', "Load the given file as an extra config script.", :@config_script, nil],
307
+ ['-N', '--num-servers INT', "Number of Mongrel servers", :@servers, 2],
308
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
309
+ ['', '--user USER', "User to run as", :@user, nil],
310
+ ['', '--group GROUP', "Group to run as", :@group, nil],
311
+ ['', '--prefix PREFIX', "Rails prefix to use", :@prefix, nil]
312
+ ]
313
+ end
314
+
315
+ def validate
316
+ @servers = @servers.to_i
317
+
318
+ valid?(@servers > 0, "Must give a valid number of servers")
319
+ valid_dir? File.dirname(@config_file), "Path to config file not valid: #{@config_file}"
320
+
321
+ return @valid
322
+ end
323
+
324
+ def run
325
+ @options = {
326
+ "port" => @port,
327
+ "servers" => @servers,
328
+ "pid_file" => @pid_file
329
+ }
330
+
331
+ @options["log_file"] = @log_file if @log_file
332
+ @options["debug"] = @debug if @debug
333
+ @options["num_procs"] = @num_procs if @num_procs
334
+ @options["docroot"] = @docroot if @docroots
335
+ @options["address"] = @address if @address
336
+ @options["timeout"] = @timeout if @timeout
337
+ @options["environment"] = @environment if @environment
338
+ @options["mime_map"] = @mime_map if @mime_map
339
+ @options["config_script"] = @config_script if @config_script
340
+ @options["cwd"] = @cwd if @cwd
341
+ @options["user"] = @user if @user
342
+ @options["group"] = @group if @group
343
+ @options["prefix"] = @prefix if @prefix
344
+
345
+ log "Writing configuration file to #{@config_file}."
346
+ File.open(@config_file,"w") {|f| f.write(@options.to_yaml)}
347
+ end
348
+ end
349
+
350
+ class Status < GemPlugin::Plugin "/commands"
351
+ include ExecBase
352
+
353
+ def configure
354
+ options [
355
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
356
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
357
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
358
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
359
+ ]
360
+ end
361
+
362
+ def run
363
+ clustered {status}
364
+ end
365
+
366
+ end
367
+ end
368
+
File without changes
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: seesaw
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-08-18 00:00:00 +10:00
8
+ summary: Ripple-restart a mongrel cluster with no downtime
9
+ require_paths:
10
+ - lib
11
+ email: max@muermann.org
12
+ homepage: " by Matt Allen and Max Muermann"
13
+ rubyforge_project: seesaw
14
+ description: "== FEATURES/PROBLEMS: * FIX (list of features or problems) == SYNOPSIS: FIX (code sample of usage) == REQUIREMENTS:"
15
+ autorequire: seesaw/init.rb
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Matt Allen, Max Muermann
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - bin/seesaw
37
+ - lib/seesaw/init.rb
38
+ - lib/seesaw/mongrel_cluster_patch.rb
39
+ - test/test_seesaw.rb
40
+ test_files:
41
+ - test/test_seesaw.rb
42
+ rdoc_options:
43
+ - --main
44
+ - README.txt
45
+ extra_rdoc_files:
46
+ - History.txt
47
+ - Manifest.txt
48
+ - README.txt
49
+ executables:
50
+ - seesaw
51
+ extensions: []
52
+
53
+ requirements: []
54
+
55
+ dependencies:
56
+ - !ruby/object:Gem::Dependency
57
+ name: gem_plugin
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Version::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.2.2
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: mongrel
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 1.0.1
73
+ version:
74
+ - !ruby/object:Gem::Dependency
75
+ name: mongrel_cluster
76
+ version_requirement:
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.0.2
82
+ version:
83
+ - !ruby/object:Gem::Dependency
84
+ name: hoe
85
+ version_requirement:
86
+ version_requirements: !ruby/object:Gem::Version::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 1.2.2
91
+ version: