qmore 0.5.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.
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