resque_starter 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
+ SHA1:
3
+ metadata.gz: 0ab87ab83e696f9a2082b4cb7162dbbdd8a77efd
4
+ data.tar.gz: ab0e725521c9bf8e8d759224b69867bd7820feaa
5
+ SHA512:
6
+ metadata.gz: 4ea956678caef390ea95e715362b343052b0c61c95614960ccb6780b7ae6a59600131ddb1b4a7e2039671129ea2f594bbebfbaad3aeb3309306a79d5add38a99
7
+ data.tar.gz: 6b9894349095bc5774f9b1feab545f495aba696a668faa12d9f6be6387c67eb7c44db49b3e2caceae427160d361c26a161c78036c7dfeb598b63d170b25c4bbc
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /tmp/log/*
16
+ /tmp/pids/*
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+ gem 'pry'
5
+ gem 'pry-nav'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 sonots
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # ResqueStarter
2
+
3
+ [Resque](https://github.com/resque/resque) is widely used Redis-backed Ruby library for creating background jobs. [ResqueStarter](https://github.com/sonots/resque_starter) is a tool to start and manage multiple resque workers supporing graceful shutdown and restart, leveraging the [Copy-on-write (COW)](https://en.wikipedia.org/wiki/Copy-on-write).
4
+
5
+ ```
6
+ PID COMMAND
7
+ 14814 resque_starter # preload app
8
+ 14815 \_ resque:work[1]
9
+ 14816 \_ resque:work[2]
10
+ 14817 \_ resque:work[3]
11
+ ```
12
+
13
+ # Background
14
+
15
+ [Resque](https://github.com/resque/resque) provides [resque:workers](https://github.com/resque/resque#running-multiple-workers) task to run multiple resque workers, but it is only for development purpose as [code comments](https://github.com/resque/resque/blob/c295da9de0034b20ce79600e9f54fb279695f522/lib/resque/tasks.rb#L23-L38) says.
16
+ It also provides an example configuration of [god](http://godrb.com/) as [resque.god](https://github.com/resque/resque/blob/c295da9de0034b20ce79600e9f54fb279695f522/examples/god/resque.god), but it does not allow us to leverage Copy-on-write (CoW) to save memory of preloaded application.
17
+
18
+ # Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'resque_starter'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```
29
+ $ bundle
30
+ ```
31
+
32
+ Or install it yourself as:
33
+
34
+ ```
35
+ $ gem install resque_starter
36
+ ```
37
+
38
+ # Configuration
39
+
40
+ Please see [example/resque.conf.rb](./example/resque.conf.rb)
41
+
42
+ You can configure logger, pid file, number of concurrency, dequeue interval, queue lists.
43
+
44
+ # Usage
45
+
46
+ ```
47
+ bundle exec resque_starter -c /path/to/resque.conf.rb
48
+ ```
49
+
50
+ # Signals
51
+
52
+ Resque starter responds to a few different signals:
53
+
54
+ * TERM / INT - Quick shutdown, kills all workers immediately then exit
55
+ * QUIT - Graceful shutdown, waits for workers to finish processing then exit
56
+ * USR1 - Send USR1 to all workers, which immediately kill worker's child but don't exit
57
+ * USR2 - Send USR2 to all workers, which don't start to process any new jobs
58
+ * CONT - Send CONT to all workers, which start to process new jobs again after a USR2
59
+ * TTIN - Increment the number of worker processes by one
60
+ * TTOU - Decrement the number of worker processes by one with QUIT
61
+
62
+ # Graceful restart
63
+
64
+ Resque starter itself does not support graceful restart, yet. But, graceful restart can be done with [server-starter](https://github.com/sonots/ruby-server-starter).
65
+
66
+ Example configuration is available at [server-starter/example/resque](https://github.com/sonots/ruby-server-starter/blob/master/example/resque). See `start_server` and `config/resque.conf.rb` files.
67
+
68
+ **HOW IT WORKS**
69
+
70
+ On receiving HUP signal, server starter creates a new `resque_starter` (master) process.
71
+ The new `resque_starter` (master) process forks a new resque:work.
72
+ On `after_fork`, send `TTOU` to old `resque_starter` (master) process to gracefully shutdown one old resque:work.
73
+ By repeating this procedure, new `resque_starter` process can be gracefully restarted.
74
+ The number of working resque workers will be suppressed up to `concurrency + 1` in this way.
75
+
76
+ **ILLUSTRATION**
77
+
78
+ On bootup:
79
+
80
+ ```
81
+ PID COMMAND
82
+ 14813 server_starter
83
+ 14814 \_ resque_starter
84
+ 14815 \_ resque:work[1]
85
+ 14816 \_ resque:work[2]
86
+ 14817 \_ resque:work[3]
87
+ ```
88
+
89
+ Send HUP:
90
+
91
+ ```
92
+ PID COMMAND
93
+ 14813 server_starter
94
+ 14814 \_ resque_starter (old)
95
+ 14815 \_ resque:work[1] (dies)
96
+ 14816 \_ resque:work[2]
97
+ 14817 \_ resque:work[3]
98
+ 14818 \_ resque_starter (new)
99
+ 14819 \_ resque:work[1]
100
+ ```
101
+
102
+ Finally:
103
+
104
+ ```
105
+ PID COMMAND
106
+ 14813 server_starter
107
+ 14818 \_ resque_starter (new)
108
+ 14819 \_ resque:work[1]
109
+ 14820 \_ resque:work[2]
110
+ 14821 \_ resque:work[3]
111
+ ```
112
+
113
+ # Contributing
114
+
115
+ 1. Fork it ( https://github.com/sonots/resque_starter/fork )
116
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
117
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
118
+ 4. Push to the branch (`git push origin my-new-feature`)
119
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1 @@
1
+ resque_starter.rb
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require_relative '../lib/resque_starter'
5
+
6
+ opts = {
7
+ }
8
+ OptionParser.new do |opt|
9
+ opt.on(
10
+ '--config=file',
11
+ 'path to config file',
12
+ ) {|v| opts[:config_file] = v }
13
+ opt.on(
14
+ '--version',
15
+ 'print version',
16
+ ) {|v| puts ResqueStarter::VERSION; exit 0 }
17
+
18
+ opt.parse!(ARGV)
19
+
20
+ define_method(:usage) do |msg = nil|
21
+ puts opt.to_s
22
+ puts "error: #{msg}" if msg
23
+ exit 1
24
+ end
25
+ end
26
+
27
+ if opts[:config_file].nil?
28
+ usage 'Option --config is required'
29
+ end
30
+
31
+ starter = ResqueStarter.new(opts[:config_file])
32
+ starter.start_resque
data/bin/start_resque ADDED
@@ -0,0 +1 @@
1
+ resque_starter.rb
@@ -0,0 +1 @@
1
+ resque_starter.rb
@@ -0,0 +1,43 @@
1
+ # Number of concurrency (resque workers)
2
+ concurrency 2
3
+
4
+ # By default, resque_starter logs to stdout
5
+ # logger ::Logger.new(::File.expand_path('../../tmp/log/resque_starter.log', __FILE__))
6
+ # logger.level = ::Logger::INFO
7
+ # logger.formatter = ResqueStarter::Logger::Formatter.new
8
+
9
+ # Stores pid for resque starter (master) itself
10
+ pid_file ::File.expand_path('../../tmp/pids/resque_starter.pid', __FILE__)
11
+
12
+ # Status file stores pids of resque workers, etc
13
+ status_file ::File.expand_path('../../tmp/pids/resque_starter.stat', __FILE__)
14
+
15
+ # Watching queues of resque workers in priority orders
16
+ # Same with QUEUES environment variables for rake resque:work
17
+ # See https://github.com/resque/resque#priorities-and-queue-lists
18
+ #
19
+ # Default obtains from @queue class instance variables of worker class
20
+ # See https://github.com/resque/resque#overview
21
+ queues ['high', 'low']
22
+
23
+ # Polling frequency (default: 5)
24
+ # Same with INTERVAL environment variables for rake resque:work
25
+ # See https://github.com/resque/resque#polling-frequency
26
+ dequeue_interval 0.1
27
+
28
+ # Preload rails application codes before forking to save memory by CoW
29
+ # require ::File.expand_path('../../config/environment', __FILE__)
30
+ # Rails.application.eager_load!
31
+
32
+ before_fork do |starter, worker, worker_nr|
33
+ # the following is highly recomended for Rails + "preload_app true"
34
+ # as there's no need for the master process to hold a connection
35
+ defined?(ActiveRecord::Base) and
36
+ ActiveRecord::Base.connection.disconnect!
37
+ end
38
+
39
+ after_fork do |starter, worker, worker_nr|
40
+ # the following is *required* for Rails + "preload_app true",
41
+ defined?(ActiveRecord::Base) and
42
+ ActiveRecord::Base.establish_connection
43
+ end
@@ -0,0 +1,60 @@
1
+ require 'resque_starter/version'
2
+
3
+ class ResqueStarter::Config
4
+ def initialize(config_file)
5
+ @config_file = config_file
6
+ # TIPS: resque worker ENV configuration
7
+ # self.verbose = ENV['LOGGING'] || ENV['VERBOSE']
8
+ # self.very_verbose = ENV['VVERBOSE']
9
+ # self.term_timeout = ENV['RESQUE_TERM_TIMEOUT'] || 4.0
10
+ # self.term_child = ENV['TERM_CHILD']
11
+ # self.graceful_term = ENV['GRACEFUL_TERM']
12
+ # self.run_at_exit_hooks = ENV['RUN_AT_EXIT_HOOKS']
13
+ reload
14
+ end
15
+
16
+ def reload
17
+ @set = Hash.new
18
+ @set[:concurrency] = 1
19
+ @set[:dequeue_interval] = 5.0
20
+ @set[:logger] = ResqueStarter::Logger.new(STDOUT)
21
+ instance_eval(File.read(@config_file), @config_file)
22
+ end
23
+
24
+ %i[
25
+ concurrency
26
+ logger
27
+ pid_file
28
+ status_file
29
+ queues
30
+ dequeue_interval
31
+ ].each do |name|
32
+ define_method(name) do |val = nil|
33
+ if val
34
+ @set[name] = val
35
+ else
36
+ @set[name]
37
+ end
38
+ end
39
+ end
40
+
41
+ def [](key)
42
+ @set[key]
43
+ end
44
+
45
+ def before_fork(&block)
46
+ if block_given?
47
+ @set[:before_fork] = block
48
+ else
49
+ @set[:before_fork]
50
+ end
51
+ end
52
+
53
+ def after_fork(&block)
54
+ if block_given?
55
+ @set[:after_fork] = block
56
+ else
57
+ @set[:after_fork]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ require 'resque_starter/version'
2
+ require 'logger'
3
+
4
+ class ResqueStarter::Logger < ::Logger
5
+ class Formatter
6
+ def call(severity, time, progname, msg)
7
+ "#{time.iso8601} [#{severity}] #{format_message(msg)}\n"
8
+ end
9
+
10
+ def format_message(message)
11
+ case message
12
+ when ::Exception
13
+ e = message
14
+ "#{e.class} (#{e.message})\\n #{e.backtrace.join("\\n ")}"
15
+ else
16
+ message.to_s
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize(logdev, shift_age = 0, shift_size = 1048576)
22
+ logdev = STDOUT if logdev == 'STDOUT'
23
+ logdev = STDERR if logdev == 'STDERR'
24
+ super(logdev, shift_age, shift_size)
25
+ @formatter = Formatter.new
26
+ self.level = 'INFO'
27
+ end
28
+
29
+ def level=(level)
30
+ level = eval("::Logger::#{level.upcase}") if level.is_a?(String)
31
+ super(level)
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ class ResqueStarter
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,180 @@
1
+ require 'yaml'
2
+ require 'resque'
3
+ require 'resque_starter/version'
4
+ require 'resque_starter/config'
5
+ require 'resque_starter/logger'
6
+
7
+ class ResqueStarter
8
+ attr_reader :config, :logger, :old_workers, :num_workers
9
+
10
+ def initialize(config_file)
11
+ @config = ResqueStarter::Config.new(config_file)
12
+
13
+ @old_workers = {}
14
+ @num_workers = config[:concurrency]
15
+ @self_read, @self_write = IO.pipe
16
+
17
+ # open pid file
18
+ if config[:pid_file]
19
+ begin
20
+ File.open(config[:pid_file], "w") do |fh|
21
+ fh.puts $$
22
+ end
23
+ rescue => e
24
+ @logger.error "failed to open file:#{config[:pid_file]}"
25
+ exit(1)
26
+ end
27
+ at_exit { File.unlink config[:pid_file] rescue nil }
28
+ end
29
+
30
+ # logger
31
+ @logger = @config[:logger]
32
+
33
+ # create guard that removes the status file
34
+ if config[:status_file]
35
+ at_exit { File.unlink config[:status_file] rescue nil }
36
+ end
37
+ end
38
+
39
+ def start_resque
40
+ @logger.info "starting resque starter (master) #{Process.pid}"
41
+ maintain_worker_count
42
+
43
+ install_signal_handler
44
+ @handle_thr = start_handle_thread
45
+ while true
46
+ break unless @handle_thr.alive? # dies if @shutdown and @old_workers.empty?
47
+ begin
48
+ pid, status = Process.waitpid2(-1)
49
+ @logger.info "resque worker #{pid} died, status:#{status.exitstatus}"
50
+ @self_write.puts(pid)
51
+ rescue Errno::ECHILD
52
+ @handle_thr.kill if @shutdown # @old_workers should be empty
53
+ sleep 0.1 # avoid busy loop for no child by TTOU
54
+ end
55
+ end
56
+
57
+ @logger.info "shutting down resque starter (master) #{Process.pid}"
58
+ end
59
+
60
+ def install_signal_handler
61
+ %w(TERM INT QUIT USR1 USR2 CONT TTIN TTOU).each do |sig|
62
+ trap(sig) { @self_write.puts(sig) }
63
+ end
64
+ end
65
+
66
+ def start_handle_thread
67
+ Thread.new {
68
+ while readable_io = IO.select([@self_read])
69
+ s = readable_io.first[0].gets.strip
70
+ if pid = (Integer(s) rescue nil)
71
+ handle_waitpid2(pid)
72
+ else
73
+ handle_signal(s)
74
+ end
75
+ end
76
+ }
77
+ end
78
+
79
+ def update_status_file
80
+ return unless @config[:status_file]
81
+ # pid => worker_nr
82
+ status = {'old_workers' => @old_workers}.to_yaml
83
+ File.write(@config[:status_file], status)
84
+ end
85
+
86
+ def handle_waitpid2(pid)
87
+ @old_workers.delete(pid)
88
+ update_status_file
89
+ if @shutdown
90
+ @handle_thr.kill if @old_workers.empty?
91
+ else
92
+ maintain_worker_count
93
+ end
94
+ end
95
+
96
+ # Resque starter (master process) responds to a few different signals:
97
+ # * TERM / INT - Quick shutdown, kills all workers immediately then exit
98
+ # * QUIT - Graceful shutdown, waits for workers to finish processing then exit
99
+ # * USR1 - Send USR1 to all workers, which immediately kill worker's child but don't exit
100
+ # * USR2 - Send USR2 to all workers, which don't start to process any new jobs
101
+ # * CONT - Send CONT to all workers, which start to process new jobs again after a USR2
102
+ # * TTIN - Increment the number of worker processes by one
103
+ # * TTOU - Decrement the number of worker processes by one with QUIT
104
+ def handle_signal(sig)
105
+ pids = @old_workers.keys.sort
106
+ msg = "received #{sig}, "
107
+
108
+ # Resque workers respond to a few different signals:
109
+ # * TERM / INT - Immediately kill child then exit
110
+ # * QUIT - Wait for child to finish processing then exit
111
+ # * USR1 - Immediately kill child but don't exit
112
+ # * USR2 - Don't start to process any new jobs
113
+ # * CONT - Start to process new jobs again after a USR2
114
+ case sig
115
+ when 'TERM', 'INT'
116
+ @logger.info msg << "immediately kill all resque workers then exit:#{pids}"
117
+ @shutdown = true
118
+ Process.kill(sig, *pids)
119
+ when 'QUIT'
120
+ @logger.info msg << "wait for all resque workers to finish processing then exit:#{pids}"
121
+ @shutdown = true
122
+ Process.kill(sig, *pids)
123
+ when 'USR1'
124
+ @logger.info msg << "immediately kill the child of all resque workers:#{pids}"
125
+ Process.kill(sig, *pids)
126
+ when 'USR2'
127
+ @logger.info msg << "don't start to process any new jobs:#{pids}"
128
+ Process.kill(sig, *pids)
129
+ when 'CONT'
130
+ @logger.info msg << "start to process new jobs again after USR2:#{pids}"
131
+ Process.kill(sig, *pids)
132
+ when 'TTIN'
133
+ @num_workers += 1
134
+ @logger.info msg << "increment the number of resque workers:#{@num_workers}"
135
+ maintain_worker_count # ToDo: forking from a thread would be unsafe
136
+ when 'TTOU'
137
+ @num_workers -= 1 if @num_workers > 0
138
+ @logger.info msg << "decrement the number of resque workers:#{@num_workers}"
139
+ maintain_worker_count
140
+ end
141
+ rescue Errno::ESRCH
142
+ rescue => e
143
+ @logger.error(e)
144
+ exit(1)
145
+ end
146
+
147
+ def maintain_worker_count
148
+ return if (off = @old_workers.size - @num_workers) == 0
149
+ return spawn_missing_workers if off < 0
150
+ @old_workers.dup.each_pair {|pid, nr|
151
+ if nr >= @num_workers
152
+ Process.kill(:QUIT, pid) rescue nil
153
+ end
154
+ }
155
+ end
156
+
157
+ def spawn_missing_workers
158
+ worker_nr = -1
159
+ until (worker_nr += 1) == @num_workers
160
+ next if @old_workers.value?(worker_nr)
161
+ worker = Resque::Worker.new(*(config[:queues]))
162
+ config[:before_fork].call(self, worker, worker_nr)
163
+ GC.start
164
+ if pid = fork
165
+ @old_workers[pid] = worker_nr
166
+ update_status_file
167
+ @logger.info "starting new resque worker #{pid}"
168
+ else # child
169
+ @self_read.close rescue nil
170
+ @self_write.close rescue nil
171
+ config[:after_fork].call(self, worker, worker_nr)
172
+ worker.work(config[:dequeue_interval])
173
+ exit
174
+ end
175
+ end
176
+ rescue => e
177
+ @logger.error(e)
178
+ exit(1)
179
+ end
180
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque_starter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "resque_starter"
8
+ spec.version = ResqueStarter::VERSION
9
+ spec.authors = ["Naotoshi Seo"]
10
+ spec.email = ["sonots@gmail.com"]
11
+ spec.summary = %q{Start and manage multiple resque workers}
12
+ spec.description = %q{Start and manage multiple resque workers.}
13
+ spec.homepage = "https://github.com/sonots/resque_starter"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "resque"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ end
data/tmp/log/.gitkeep ADDED
File without changes
data/tmp/pids/.gitkeep ADDED
File without changes
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque_starter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Naotoshi Seo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-08-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: resque
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: Start and manage multiple resque workers.
56
+ email:
57
+ - sonots@gmail.com
58
+ executables:
59
+ - resque_starter
60
+ - resque_starter.rb
61
+ - start_resque
62
+ - start_resque.rb
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - ".gitignore"
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - bin/resque_starter
72
+ - bin/resque_starter.rb
73
+ - bin/start_resque
74
+ - bin/start_resque.rb
75
+ - example/resque.conf.rb
76
+ - lib/resque_starter.rb
77
+ - lib/resque_starter/config.rb
78
+ - lib/resque_starter/logger.rb
79
+ - lib/resque_starter/version.rb
80
+ - resque_starter.gemspec
81
+ - tmp/log/.gitkeep
82
+ - tmp/pids/.gitkeep
83
+ homepage: https://github.com/sonots/resque_starter
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.5.1
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Start and manage multiple resque workers
107
+ test_files: []