qmore 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ *.DS_Store
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ .idea
7
+ spec/dump.rdb
8
+ spec/redis-server.log
9
+ spec/redis.pid
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ script: bundle exec rspec spec
data/CHANGELOG ADDED
@@ -0,0 +1,4 @@
1
+ 0.5.0
2
+ -----
3
+
4
+ Initial version ported from resque-dynamic-queues 0.8.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in qmore.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Matt Conway (matt@conwaysplace.com)
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,118 @@
1
+ Qmore is a qless plugin that gives one more control over how queues are processed. Qmore allows one to specify the queues a worker processes by the use of wildcards, negations, or dynamic look up from redis. It also allows one to specify the relative priority between queues (rather than within a single queue). It plugs into the Qless webapp to make it easy to manage the queues.
2
+
3
+ Authored against Qless 0.9.3, so it at least works with that - try running the tests if you use a different version of qless
4
+
5
+ [![Build Status](https://secure.travis-ci.org/wr0ngway/qmore.png)](http://travis-ci.org/wr0ngway/qmore)
6
+
7
+ Usage
8
+ -----
9
+
10
+ To use the rake tasks built into qless, just adding qless to your Gemfile should cause it to get required before the task executes. If you aren't using a gemfile, then you'll need to require qmore directly so that it sets up ENV['JOB_RESERVER'] to use Qmore::JobReserver.
11
+
12
+ Alternatively, if you have some other way of launching workers (e.g. qless-pool), you can assign the reserver explicitly in the setup rake task or some other initializer:
13
+
14
+ Qless::Pool.pool_factory.reserver_class = Qmore::JobReserver
15
+ Qmore.client == Qless::Pool.pool_factory.client
16
+
17
+ To enable the web UI, use a config.ru similar to the following depending on your environment:
18
+
19
+ require 'qless/server'
20
+ require 'qmore-server'
21
+
22
+ Qless::Server.client = Qless::Client.new(:host => "some-host", :port => 7000)
23
+ Qmore.client = Qless::Server.client
24
+ run Qless::Server.new(Qmore.client)
25
+
26
+ Dynamic Queues
27
+ --------------
28
+
29
+ Start your workers with a QUEUES that can contain '\*' for zero-or more of any character, '!' to exclude the following pattern, or @key to look up the patterns from redis. Some examples help:
30
+
31
+ QUEUES='foo' rake qless:work
32
+
33
+ Pulls jobs from the queue 'foo'
34
+
35
+ QUEUES='*' rake qless:work
36
+
37
+ Pulls jobs from any queue
38
+
39
+ QUEUES='*foo' rake qless:work
40
+
41
+ Pulls jobs from queues that end in foo
42
+
43
+ QUEUES='*foo*' rake qless:work
44
+
45
+ Pulls jobs from queues whose names contain foo
46
+
47
+ QUEUES='*foo*,!foobar' rake qless:work
48
+
49
+ Pulls jobs from queues whose names contain foo except the foobar queue
50
+
51
+ QUEUES='*foo*,!*bar' rake qless:work
52
+
53
+ Pulls jobs from queues whose names contain foo except queues whose names end in bar
54
+
55
+ QUEUES='@key' rake qless:work
56
+
57
+ Pulls jobs from queue names stored in redis (use Qless.set\_dynamic\_queue("key", ["queuename1", "queuename2"]) to set them)
58
+
59
+ QUEUES='*,!@key' rake qless:work
60
+
61
+ Pulls jobs from any queue execept ones stored in redis
62
+
63
+ QUEUES='@' rake qless:work
64
+
65
+ Pulls jobs from queue names stored in redis using the hostname of the worker
66
+
67
+ Qless.set_dynamic_queue("key", ["*foo*", "!*bar"])
68
+ QUEUES='@key' rake qless:work
69
+
70
+ Pulls jobs from queue names stored in redis, with wildcards/negations
71
+
72
+ task :custom_worker do
73
+ ENV['QUEUES'] = "*foo*,!*bar"
74
+ Rake::Task['qless:work'].invoke
75
+ end
76
+
77
+ From a custom rake script
78
+
79
+ Queue Priority
80
+ --------------
81
+
82
+ Start your workers with a QUEUES that contains many queue names - the priority is most useful when using wildcards.
83
+
84
+ The qmore priority web ui is shown as a tab in the qless web UI, and allows you to define the queue priorities. To activate it, you need to require 'qmore-server' in whatever initializer you use to bring up qless-web.
85
+
86
+ Then you should set use the web ui to determine the order a worker will pick a queue for processing. The "Fairly" checkbox makes all queues that match that pattern get ordered in a random fashion.
87
+
88
+ For example, say my qless system has the queues:
89
+
90
+ low_foo, low_bar, low_baz, high_foo, high_bar, high_baz, otherqueue, somequeue, myqueue
91
+
92
+ And I run my worker with QUEUES=\*
93
+
94
+ If I set my patterns like:
95
+
96
+ high\_\* (fairly unchecked)
97
+ default (fairly unchecked)
98
+ low\_\* (fairly unchecked)
99
+
100
+ Then, the worker will scan the queues for work in this order:
101
+ high_bar, high_baz, high_foo, myqueue, otherqueue, somequeue, low_bar, low_baz, low_foo
102
+
103
+ If I set my patterns like:
104
+
105
+ high\_\* (fairly checked)
106
+ default (fairly checked)
107
+ low\_\* (fairly checked)
108
+
109
+ Then, the worker will scan the queues for work in this order:
110
+
111
+ \*[high_bar, high_baz, high_foo].shuffle, \*[myqueue, otherqueue, somequeue].shuffle, \*[low_bar, low_baz, low_foo].shuffle
112
+
113
+
114
+ Contributors
115
+ ------------
116
+
117
+ Matt Conway ( https://github.com/wr0ngway )
118
+ Bert Goethals ( https://github.com/Bertg )
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/config.ru ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'logger'
3
+
4
+ $LOAD_PATH.unshift ::File.expand_path(::File.dirname(__FILE__) + '/lib')
5
+ require 'qmore-server'
6
+
7
+ use Rack::ShowExceptions
8
+ run Qless::Server.new(Qmore.client)
@@ -0,0 +1,5 @@
1
+ require 'qmore'
2
+ require 'qless/server'
3
+ require 'qmore/server'
4
+
5
+ Qless::Server.register Qmore::Server
data/lib/qmore.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'qless'
2
+ require 'qless/worker'
3
+ require 'qmore/attributes'
4
+ require 'qmore/job_reserver'
5
+
6
+ module Qmore
7
+
8
+ def self.client=(client)
9
+ @client = client
10
+ end
11
+
12
+ def self.client
13
+ @client ||= Qless::Client.new
14
+ end
15
+ end
16
+
17
+ module Qless
18
+ module JobReservers
19
+ QmoreReserver = Qmore::JobReserver
20
+ end
21
+ end
22
+ ENV['JOB_RESERVER'] ||= 'QmoreReserver'
@@ -0,0 +1,191 @@
1
+ require 'multi_json'
2
+
3
+ module Qmore
4
+ DYNAMIC_QUEUE_KEY = "qmore:dynamic"
5
+ PRIORITY_KEY = "qmore:priority"
6
+ DYNAMIC_FALLBACK_KEY = "default"
7
+
8
+ module Attributes
9
+ extend self
10
+
11
+ def redis
12
+ Qmore.client.redis
13
+ end
14
+
15
+ def decode(data)
16
+ MultiJson.load(data) if data
17
+ end
18
+
19
+ def encode(data)
20
+ MultiJson.dump(data)
21
+ end
22
+
23
+ def get_dynamic_queue(key, fallback=['*'])
24
+ data = redis.hget(DYNAMIC_QUEUE_KEY, key)
25
+ queue_names = decode(data)
26
+
27
+ if queue_names.nil? || queue_names.size == 0
28
+ data = redis.hget(DYNAMIC_QUEUE_KEY, DYNAMIC_FALLBACK_KEY)
29
+ queue_names = decode(data)
30
+ end
31
+
32
+ if queue_names.nil? || queue_names.size == 0
33
+ queue_names = fallback
34
+ end
35
+
36
+ return queue_names
37
+ end
38
+
39
+ def set_dynamic_queue(key, values)
40
+ if values.nil? or values.size == 0
41
+ redis.hdel(DYNAMIC_QUEUE_KEY, key)
42
+ else
43
+ redis.hset(DYNAMIC_QUEUE_KEY, key, encode(values))
44
+ end
45
+ end
46
+
47
+ def set_dynamic_queues(dynamic_queues)
48
+ redis.multi do
49
+ redis.del(DYNAMIC_QUEUE_KEY)
50
+ dynamic_queues.each do |k, v|
51
+ set_dynamic_queue(k, v)
52
+ end
53
+ end
54
+ end
55
+
56
+ def get_dynamic_queues
57
+ result = {}
58
+ queues = redis.hgetall(DYNAMIC_QUEUE_KEY)
59
+ queues.each {|k, v| result[k] = decode(v) }
60
+ result[DYNAMIC_FALLBACK_KEY] ||= ['*']
61
+ return result
62
+ end
63
+
64
+ def get_priority_buckets
65
+ priorities = Array(redis.lrange(PRIORITY_KEY, 0, -1))
66
+ priorities = priorities.collect {|p| decode(p) }
67
+ priorities << {'pattern' => 'default'} unless priorities.find {|b| b['pattern'] == 'default' }
68
+ return priorities
69
+ end
70
+
71
+ def set_priority_buckets(data)
72
+ redis.multi do
73
+ redis.del(PRIORITY_KEY)
74
+ Array(data).each do |v|
75
+ redis.rpush(PRIORITY_KEY, encode(v))
76
+ end
77
+ end
78
+ end
79
+
80
+ # Returns a list of queues to use when searching for a job.
81
+ #
82
+ # A splat ("*") means you want every queue (in alpha order) - this
83
+ # can be useful for dynamically adding new queues.
84
+ #
85
+ # The splat can also be used as a wildcard within a queue name,
86
+ # e.g. "*high*", and negation can be indicated with a prefix of "!"
87
+ #
88
+ # An @key can be used to dynamically look up the queue list for key from redis.
89
+ # If no key is supplied, it defaults to the worker's hostname, and wildcards
90
+ # and negations can be used inside this dynamic queue list. Set the queue
91
+ # list for a key with set_dynamic_queue(key, ["q1", "q2"]
92
+ #
93
+ def expand_queues(queue_patterns, real_queues)
94
+ queue_patterns = queue_patterns.dup
95
+ real_queues = real_queues.dup
96
+
97
+ matched_queues = []
98
+
99
+ while q = queue_patterns.shift
100
+ q = q.to_s
101
+
102
+ if q =~ /^(!)?@(.*)/
103
+ key = $2.strip
104
+ key = Socket.gethostname if key.size == 0
105
+
106
+ add_queues = get_dynamic_queue(key)
107
+ add_queues.map! { |q| q.gsub!(/^!/, '') || q.gsub!(/^/, '!') } if $1
108
+
109
+ queue_patterns.concat(add_queues)
110
+ next
111
+ end
112
+
113
+ if q =~ /^!/
114
+ negated = true
115
+ q = q[1..-1]
116
+ end
117
+
118
+ patstr = q.gsub(/\*/, ".*")
119
+ pattern = /^#{patstr}$/
120
+ if negated
121
+ matched_queues -= matched_queues.grep(pattern)
122
+ else
123
+ matches = real_queues.grep(/^#{pattern}$/)
124
+ matches = [q] if matches.size == 0 && q == patstr
125
+ matched_queues.concat(matches)
126
+ end
127
+ end
128
+
129
+ return matched_queues.uniq.sort
130
+ end
131
+
132
+ def prioritize_queues(priority_buckets, real_queues)
133
+ real_queues = real_queues.dup
134
+ priority_buckets = priority_buckets.dup
135
+
136
+ result = []
137
+ default_idx = -1, default_fairly = false;
138
+
139
+ # Walk the priority patterns, extract each into its own bucket
140
+ priority_buckets.each do |bucket|
141
+ bucket_pattern = bucket['pattern']
142
+ fairly = bucket['fairly']
143
+
144
+ # note the position of the default bucket for inserting the remaining queues at that location
145
+ if bucket_pattern == 'default'
146
+ default_idx = result.size
147
+ default_fairly = fairly
148
+ next
149
+ end
150
+
151
+ bucket_queues, remaining = [], []
152
+
153
+ patterns = bucket_pattern.split(',')
154
+ patterns.each do |pattern|
155
+ pattern = pattern.strip
156
+
157
+ if pattern =~ /^!/
158
+ negated = true
159
+ pattern = pattern[1..-1]
160
+ end
161
+
162
+ patstr = pattern.gsub(/\*/, ".*")
163
+ pattern = /^#{patstr}$/
164
+
165
+
166
+ if negated
167
+ bucket_queues -= bucket_queues.grep(pattern)
168
+ else
169
+ bucket_queues.concat(real_queues.grep(pattern))
170
+ end
171
+
172
+ end
173
+
174
+ bucket_queues.uniq!
175
+ bucket_queues.shuffle! if fairly
176
+ real_queues = real_queues - bucket_queues
177
+
178
+ result << bucket_queues
179
+
180
+ end
181
+
182
+ # insert the remaining queues at the position the default item was at (or last)
183
+ real_queues.shuffle! if default_fairly
184
+ result.insert(default_idx, real_queues)
185
+ result.flatten!
186
+
187
+ return result
188
+ end
189
+
190
+ end
191
+ end
@@ -0,0 +1,37 @@
1
+ module Qmore
2
+ class JobReserver
3
+ include Qmore::Attributes
4
+ attr_reader :queues
5
+
6
+ def initialize(queues)
7
+ @queues = queues
8
+ end
9
+
10
+ def description
11
+ @description ||= @queues.map(&:name).join(', ') + " (qmore)"
12
+ end
13
+
14
+ def reserve
15
+ realize_queues.each do |q|
16
+ job = q.pop
17
+ return job if job
18
+ end
19
+
20
+ nil
21
+ end
22
+
23
+ private
24
+
25
+ def realize_queues
26
+ queue_names = @queues.collect(&:name)
27
+ real_queues = Qmore.client.queues.counts.collect {|h| h['name'] }
28
+
29
+ realized_queues = expand_queues(queue_names, real_queues)
30
+ realized_queues = prioritize_queues(get_priority_buckets, realized_queues)
31
+ realized_queues = realized_queues.collect {|q| Qmore.client.queues[q] }
32
+ realized_queues
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,93 @@
1
+ require 'qmore'
2
+
3
+ module Qmore
4
+ module Server
5
+
6
+ Attr = Qmore::Attributes
7
+
8
+ def self.registered(app)
9
+
10
+ app.helpers do
11
+
12
+ def qmore_view(filename, options = {}, locals = {})
13
+ options = {:layout => true, :locals => { :title => filename.to_s.capitalize }}.merge(options)
14
+ dir = File.expand_path("../server/views/", __FILE__)
15
+ erb(File.read(File.join(dir, "#{filename}.erb")), options, locals)
16
+ end
17
+
18
+ alias :original_tabs :tabs
19
+ def tabs
20
+ qmore_tabs = [
21
+ {:name => 'DynamicQueues', :path => '/dynamicqueues'},
22
+ {:name => 'QueuePriority', :path => '/queuepriority'}
23
+ ]
24
+ queue_tab_index = original_tabs.index {|t| t[:name] == 'Queues' }
25
+ original_tabs.insert(queue_tab_index + 1, *qmore_tabs)
26
+ end
27
+
28
+ end
29
+
30
+ #
31
+ # Dynamic queues
32
+ #
33
+
34
+ app.get "/dynamicqueues" do
35
+ @queues = []
36
+ real_queues = Qmore.client.queues.counts.collect {|q| q['name'] }
37
+ dqueues = Attr.get_dynamic_queues
38
+ dqueues.each do |k, v|
39
+ expanded = Attr.expand_queues(["@#{k}"], real_queues)
40
+ expanded = expanded.collect { |q| q.split(":").last }
41
+ view_data = {
42
+ 'name' => k,
43
+ 'value' => Array(v).join(", "),
44
+ 'expanded' => expanded.join(", ")
45
+ }
46
+ @queues << view_data
47
+ end
48
+
49
+ @queues.sort! do |a, b|
50
+ an = a['name']
51
+ bn = b['name']
52
+ if an == 'default'
53
+ 1
54
+ elsif bn == 'default'
55
+ -1
56
+ else
57
+ an <=> bn
58
+ end
59
+ end
60
+
61
+ qmore_view :dynamicqueues
62
+ end
63
+
64
+ app.post "/dynamicqueues" do
65
+ dynamic_queues = Array(params['queues'])
66
+ queues = {}
67
+ dynamic_queues.each do |queue|
68
+ key = queue['name']
69
+ values = queue['value'].to_s.split(',').collect { |q| q.gsub(/\s/, '') }
70
+ queues[key] = values
71
+ end
72
+ Attr.set_dynamic_queues(queues)
73
+ redirect "/dynamicqueues"
74
+ end
75
+
76
+ #
77
+ # Queue priorities
78
+ #
79
+
80
+ app.get "/queuepriority" do
81
+ @priorities = Attr.get_priority_buckets
82
+ qmore_view :priorities
83
+ end
84
+
85
+ app.post "/queuepriority" do
86
+ priorities = params['priorities']
87
+ Attr.set_priority_buckets priorities
88
+ redirect "/queuepriority"
89
+ end
90
+
91
+ end
92
+ end
93
+ end