resque-sliders 0.0.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.
- data/Gemfile +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +88 -0
- data/Rakefile +35 -0
- data/bin/kewatcher +68 -0
- data/config/config_example.yml +11 -0
- data/lib/resque-sliders/commander.rb +73 -0
- data/lib/resque-sliders/helpers.rb +72 -0
- data/lib/resque-sliders/kewatcher.rb +345 -0
- data/lib/resque-sliders/server/public/css/jqueryui-1.8.16/blitzer/jquery-ui.custom.css +568 -0
- data/lib/resque-sliders/server/public/css/sliders.css +30 -0
- data/lib/resque-sliders/server/public/images/ui-bg_diagonals-thick_75_f3d8d8_40x40.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_dots-small_65_a6a6a6_2x2.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_flat_0_333333_40x100.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_flat_65_ffffff_40x100.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_glass_55_fbf8ee_1x400.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_highlight-hard_100_eeeeee_1x100.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_highlight-hard_100_f6f6f6_1x100.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-bg_highlight-soft_15_cc0000_1x100.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-icons_004276_256x240.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-icons_cc0000_256x240.png +0 -0
- data/lib/resque-sliders/server/public/images/ui-icons_ffffff_256x240.png +0 -0
- data/lib/resque-sliders/server/public/js/jquery-ui-1.8.16.custom.min.js +791 -0
- data/lib/resque-sliders/server/public/js/sliders.js +64 -0
- data/lib/resque-sliders/server/views/index.erb +59 -0
- data/lib/resque-sliders/server.rb +71 -0
- data/lib/resque-sliders/version.rb +7 -0
- data/lib/resque-sliders.rb +9 -0
- data/resque-sliders.gemspec +29 -0
- data/test/redis-test.conf +115 -0
- data/test/resque-sliders_test.rb +12 -0
- data/test/test_helper.rb +74 -0
- metadata +117 -0
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) Kevin Mullin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
Resque Sliders
|
2
|
+
==============
|
3
|
+
|
4
|
+
[github.com/kmullin/resque-sliders](https://github.com/kmullin/resque-sliders)
|
5
|
+
|
6
|
+
Description
|
7
|
+
-----------
|
8
|
+
|
9
|
+
ResqueSliders is a [Resque](https://github.com/defunkt/resque) plugin which allows you
|
10
|
+
to control Resque workers from the Web-UI.
|
11
|
+
|
12
|
+
From the Resque-Web UI, you can:
|
13
|
+
|
14
|
+
* Start workers with any queue, or combination of queues on any host, and specify how many of each should be running
|
15
|
+
* Pause / Stop / Restart ALL running workers
|
16
|
+
|
17
|
+
|
18
|
+
ResqueSliders comes with two parts:
|
19
|
+
|
20
|
+
* `KEWatcher`: A daemon that runs on any machine that needs to run Resque workers, watches over the workers and controls which ones are running
|
21
|
+
* `Resque-Web Plugin`: A bunch of slider bars, with text-input box to specify what queues to run on the workers
|
22
|
+
|
23
|
+
Installation
|
24
|
+
------------
|
25
|
+
|
26
|
+
Install as a gem:
|
27
|
+
|
28
|
+
```
|
29
|
+
$ gem install resque-sliders
|
30
|
+
```
|
31
|
+
|
32
|
+
KEWatcher
|
33
|
+
---------
|
34
|
+
This is the daemon component that runs on any host that you want to run Resque workers on. The daemon's job is to manage how many Resque workers should be running, and what they should be running. It also provides an easy way to stop all workers during maintenance or deploys.
|
35
|
+
|
36
|
+
When the daemon first runs, it will register itself, by hostname with Redis:
|
37
|
+
|
38
|
+
* Adds a few persistent settings to the hash key `resque:plugins:resque-sliders:host_configs` (max_children, current_children)
|
39
|
+
* Gets any queues that need to be running on the host by looking at `resque:plugins:resque-sliders:` + `hostname`
|
40
|
+
|
41
|
+
```
|
42
|
+
Usage: kewatcher [options]
|
43
|
+
|
44
|
+
Options:
|
45
|
+
-c, --config CONFIG Resque Config (Yaml)
|
46
|
+
-r, --rakefile RAKEFILE Rakefile location
|
47
|
+
-p, --pidfile PIDFILE PID File location
|
48
|
+
-f, --force FORCE KILL ANY OTHER RUNNING KEWATCHERS
|
49
|
+
-v, --verbose Verbosity (Can be specified more than once, -vv)
|
50
|
+
-m, --max MAX Max Children
|
51
|
+
-h, --help This help
|
52
|
+
```
|
53
|
+
|
54
|
+
**Important Options**
|
55
|
+
|
56
|
+
* `Max Children (-m|--max MAX)`: Maximum number of workers to run on host (default: 10)
|
57
|
+
* `Rakefile (-r|--rakefile RAKEFILE)`: Pass along a rakefile to use when calling `rake ... resque:work` - shouldn't be needed if run from project directory
|
58
|
+
* `Force (-f|--force)`: Force any currently running KEWatcher processes to QUIT, waiting for it to do so, and starting in its place
|
59
|
+
|
60
|
+
**Controlling the Daemon**
|
61
|
+
|
62
|
+
`KEWatcher` supports all the same signals as `Resque`:
|
63
|
+
|
64
|
+
* `TERM`, `INT`, `QUIT`: Shutdown. Gracefully kill all child Resque workers, and wait for them to finish before exiting
|
65
|
+
* `HUP`: Restart all Resque workers by gracefully killing them, and starting new ones in their place
|
66
|
+
* `USR1`: Stop all Resque workers, and don't start any more
|
67
|
+
* `USR2`: Pause spawning of new queues, but leave current ones running
|
68
|
+
* `CONT`: Unpause. Continue spawning/managing child Resque workers
|
69
|
+
|
70
|
+
|
71
|
+
Resque-Web Integration
|
72
|
+
----------------------
|
73
|
+
**Main Screen:** showing 3 hosts (node01-03), and showing that nodes 1 and 3 aren't running their KEWatchers
|
74
|
+

|
75
|
+
|
76
|
+
**Host Screen:** showing 3 different `QUEUE` combinations (comma separated) and slider bars indicating how many of each of them should run on node02
|
77
|
+

|
78
|
+
|
79
|
+
To enable the Resque-Web Integration you'll need to load ResqueSliders to enable the Sliders tab. Just add:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
require 'resque-sliders'
|
83
|
+
```
|
84
|
+
to a file, like resque-web_init.rb, and run resque-web:
|
85
|
+
|
86
|
+
```
|
87
|
+
resque-web resque-web_init.rb
|
88
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'resque/tasks'
|
3
|
+
#
|
4
|
+
# Setup
|
5
|
+
#
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift 'lib'
|
8
|
+
|
9
|
+
def command?(command)
|
10
|
+
system("type #{command} > /dev/null 2>&1")
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
#
|
15
|
+
# Tests
|
16
|
+
#
|
17
|
+
|
18
|
+
task :default => :test
|
19
|
+
|
20
|
+
desc "Run the test suite"
|
21
|
+
task :test do
|
22
|
+
rg = command?(:rg)
|
23
|
+
Dir['test/**/*_test.rb'].each do |f|
|
24
|
+
rg ? sh("rg #{f}") : ruby(f)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Bump version"
|
29
|
+
task :git_tag_version do
|
30
|
+
require 'resque-sliders/version'
|
31
|
+
git_tags = `git tag -l`.split.map { |x| x.gsub(/v/, '') }
|
32
|
+
version = Resque::Plugins::ResqueSliders::Version
|
33
|
+
commit_sha = `git log -1 HEAD|head -n1|awk '{print $2}'`
|
34
|
+
(puts version, commit_sha; `git tag v#{version} #{commit_sha}`) unless git_tags.include?(version)
|
35
|
+
end
|
data/bin/kewatcher
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'yaml'
|
7
|
+
require 'optparse'
|
8
|
+
require 'resque-sliders/kewatcher'
|
9
|
+
|
10
|
+
options = {
|
11
|
+
:verbosity => 0,
|
12
|
+
:max_children => 10
|
13
|
+
}
|
14
|
+
|
15
|
+
OptionParser.new do |opt|
|
16
|
+
opt.banner = "Usage: #{File.basename($0)} [options]"
|
17
|
+
|
18
|
+
opt.separator ""
|
19
|
+
opt.separator "Options:"
|
20
|
+
|
21
|
+
opt.on("-c", "--config CONFIG", "Resque Config (Yaml)") do |config|
|
22
|
+
options[:config] = YAML.load_file(File.expand_path(config)) rescue nil
|
23
|
+
options[:config] ||= config
|
24
|
+
end
|
25
|
+
|
26
|
+
opt.on("-r", "--rakefile RAKEFILE", "Rakefile location") do |rakefile|
|
27
|
+
options[:rakefile] = rakefile
|
28
|
+
end
|
29
|
+
|
30
|
+
opt.on("-p", "--pidfile PIDFILE", "PID File location") do |pidfile|
|
31
|
+
options[:pidfile] = pidfile
|
32
|
+
end
|
33
|
+
|
34
|
+
opt.on("-f", "--force", "FORCE KILL ANY OTHER RUNNING KEWATCHERS") do
|
35
|
+
options[:force] = true
|
36
|
+
end
|
37
|
+
|
38
|
+
opt.on("-v", "--verbose", "Verbosity") do
|
39
|
+
options[:verbosity] += 1
|
40
|
+
end
|
41
|
+
|
42
|
+
opt.on("-m", "--max MAX", "Max Children") do |max|
|
43
|
+
options[:max_children] = max.to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
opt.on("-h", "--help", "This help") do
|
47
|
+
puts opt
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
end.parse!
|
52
|
+
|
53
|
+
begin
|
54
|
+
options[:config] = case options[:config]
|
55
|
+
when Hash
|
56
|
+
options[:config][ENV['RAILS_ENV'] || 'development']
|
57
|
+
when String
|
58
|
+
options[:config]
|
59
|
+
end
|
60
|
+
rescue Object => e
|
61
|
+
puts e
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
options[:config] ||= 'localhost:6379'
|
66
|
+
|
67
|
+
kewatcher = Resque::Plugins::ResqueSliders::KEWatcher.new(options)
|
68
|
+
kewatcher.run!
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module ResqueSliders
|
4
|
+
class Commander
|
5
|
+
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
# Hosts that have config data (queues and values), but the host is not running the daemon.
|
9
|
+
attr_reader :stale_hosts
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@host_status = redis_get_hash(host_config_key)
|
13
|
+
@stale_hosts = Resque.redis.keys("#{key_prefix}:*").map { |x| y = x.split(':').last; y unless x == host_config_key or hosts.include?(y) }.compact.sort
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return Array of currently online hosts
|
17
|
+
def hosts
|
18
|
+
@host_status.keys.select { |x| x unless x.split(':').last == 'reload' }.map { |x| x.split(':').first }.uniq.sort
|
19
|
+
end
|
20
|
+
|
21
|
+
# Array of all hosts (current + stale)
|
22
|
+
def all_hosts
|
23
|
+
(hosts + stale_hosts).sort
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return current children count or nil if Host hasn't registered itself.
|
27
|
+
def current_children(host)
|
28
|
+
@host_status["#{host}:current_children"].to_i if max_children(host)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return max children count or nil if Host hasn't registered itself.
|
32
|
+
def max_children(host)
|
33
|
+
max = @host_status["#{host}:max_children"].to_i
|
34
|
+
max == 0 ? nil : max # if Max isn't set its not running
|
35
|
+
end
|
36
|
+
|
37
|
+
# Override max_children on host (Dangerous!)
|
38
|
+
def max_children!(host, count)
|
39
|
+
@hostname = host
|
40
|
+
register_setting('max_children', count)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return Array of queues on host
|
44
|
+
def queues_on(host)
|
45
|
+
queue_values(host).keys if all_hosts.include?(host)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Changes queues to quantiy for host.
|
49
|
+
# Returns boolean.
|
50
|
+
def change(host, queue, quantity)
|
51
|
+
# queue is sanitized by:
|
52
|
+
# replacing punctuation with spaces, strip end spaces, split on remaining whitespace, and join again on comma.
|
53
|
+
# FIXME
|
54
|
+
queue2 = queue.downcase.gsub(/[^a-z 0-9,_\-\*]/, '').strip.split(/, */).reject {|x| (x.length == 1 and %w(- _).include?(x)) or x.empty? }.join(',')
|
55
|
+
raise 'Queue Different' unless queue == queue2
|
56
|
+
redis_set_hash("#{key_prefix}:#{host}", queue2, quantity) unless queue2.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deletes queue for host.
|
60
|
+
# Returns boolean.
|
61
|
+
def delete(host, queue)
|
62
|
+
redis_del_hash("#{key_prefix}:#{host}", queue)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sets reload flag on field of config_key to reload host's KEWatcher
|
66
|
+
def reload(host)
|
67
|
+
redis_set_hash(host_config_key, "#{host}:reload", 1)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module ResqueSliders
|
4
|
+
module Helpers
|
5
|
+
|
6
|
+
def key_prefix
|
7
|
+
"plugins:resque-sliders"
|
8
|
+
end
|
9
|
+
|
10
|
+
def host_config_key
|
11
|
+
"plugins:resque-sliders:host_configs"
|
12
|
+
end
|
13
|
+
|
14
|
+
def redis_get_hash(key)
|
15
|
+
Resque.redis.hgetall(key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def redis_get_hash_field(key, field)
|
19
|
+
Resque.redis.hget(key, field)
|
20
|
+
end
|
21
|
+
|
22
|
+
def redis_set_hash(key, field, fvalue)
|
23
|
+
Resque.redis.hset(key, field, fvalue) == 1 ? true : false
|
24
|
+
end
|
25
|
+
|
26
|
+
def redis_del_hash(key, field)
|
27
|
+
Resque.redis.hdel(key, field) == 1 ? true : false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return Hash: { queue => # }
|
31
|
+
def queue_values(host)
|
32
|
+
redis_get_hash("#{key_prefix}:#{host}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def register_setting(setting, value)
|
36
|
+
redis_set_hash(host_config_key, "#{@hostname}:#{setting}", value)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Signal Checking
|
41
|
+
|
42
|
+
|
43
|
+
# Gets signal field in redis config_key for this host. Don't call directly
|
44
|
+
def check_signal(host)
|
45
|
+
sig = caller[0][/`([^']*)'/, 1].gsub('?', '')
|
46
|
+
raise 'Dont call me that' unless %w(reload pause stop).include?(sig)
|
47
|
+
redis_get_hash_field(host_config_key, "#{host}:#{sig}").to_i == 1 ? true : false
|
48
|
+
end
|
49
|
+
|
50
|
+
def reload?(host)
|
51
|
+
check_signal(host)
|
52
|
+
end
|
53
|
+
|
54
|
+
def pause?(host)
|
55
|
+
check_signal(host)
|
56
|
+
end
|
57
|
+
|
58
|
+
def stop?(host)
|
59
|
+
check_signal(host)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return Hash of signals for host and their values
|
63
|
+
#def check_redis_for_signals(host)
|
64
|
+
# configs = redis_get_hash(host_config_key)
|
65
|
+
# signals = %w(reload pause stop).map { |x| [host,x].join(':') }
|
66
|
+
# Hash[configs.delete_if { |k,v| ! signals.include?(k) }.map { |k,v| [k.split(':').last.to_sym ,v] }]
|
67
|
+
#end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|