qmore 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,64 @@
1
+
2
+ <div class="page-header">
3
+ <h1>Dynamic Queues</h1>
4
+ </div>
5
+ <p class="intro">
6
+ The list below shows the dynamic queues currently defined. When you start a worker with a dynamic queue key (@key_name), that key is looked up from the list below to determine the actual queues the worker should pull from. Wildcards (*) and negation (leading !) can be used to select the queues the worker should process. There is always a fallback key - @default, which workers will use if the key for that worker is empty. If both the key and the fallback are empty, the worker defaults to processing '*'
7
+ </p>
8
+
9
+ <form action="/dynamicqueues" method="POST" style="float:none; margin-top:10px">
10
+
11
+ <table class='queues'>
12
+ <tr>
13
+ <th>Name</th>
14
+ <th>Value</th>
15
+ <th>Expanded</th>
16
+ <th></th>
17
+ </tr>
18
+ <% @queues.each_with_index do |data, i| %>
19
+ <tr class="line">
20
+ <td><input type="text" id="input-<%= i %>-name" name="queues[][name]" value="<%= data['name'] %>" /></td>
21
+ <td><input type="text" id="input-<%= i %>-value" name="queues[][value]" value="<%= data['value'] %>" /></td>
22
+ <td class="expanded"><%= data['expanded'] %></td>
23
+ <td>
24
+ <a href="#remove" class="remove">Remove</a>
25
+ </td>
26
+ </tr>
27
+ <% end %>
28
+ </table>
29
+
30
+ <a href="#add" class="add">Add</a>
31
+ <input type="submit" value="Save"/>
32
+
33
+ </form>
34
+
35
+ <script type="text/javascript" charset="utf-8">
36
+ function markDirty()
37
+ {
38
+ $("input[type=submit]").css({border:"3px orange solid"});
39
+ }
40
+
41
+ jQuery(function($) {
42
+
43
+ $("input").live("keypress", markDirty);
44
+
45
+ $("a.add").live("click", function(e) {
46
+ e.preventDefault();
47
+ var $table = $("table.queues");
48
+ var $newRow = $table.find("tr.line:first").clone();
49
+ $newRow.find("input[type=text]").attr("value", "");
50
+ $newRow.find("td.expanded").html("")
51
+ $newRow.appendTo($table);
52
+ markDirty();
53
+ });
54
+
55
+ $("a.remove").live("click", function(e) {
56
+ e.preventDefault();
57
+ var $link = $(this);
58
+ $link.parents("tr").remove();
59
+ markDirty();
60
+ });
61
+
62
+
63
+ });
64
+ </script>
@@ -0,0 +1,78 @@
1
+ <div class="page-header">
2
+ <h1>Queue Priority</h1>
3
+ </div>
4
+ <p class="intro">
5
+ The list below orders queue name patterns by the priority you wish them to be executed in. The "Fairly" option allows you to indicate you want the queues within that pattern space be selected in a fair (random) manner, i.e. like resque-fairly. The 'default' pattern must always exist, and matches against all queues that aren't in any of the other patterns.
6
+ </p>
7
+
8
+ <form action="/queuepriority" method="POST" style="float:none; margin-top:10px">
9
+
10
+ <table class="priorities">
11
+ <tr>
12
+ <th>Pattern</th>
13
+ <th>Fairly</th>
14
+ <th></th>
15
+ </tr>
16
+ <% @priorities.each_with_index do |priority, i| %>
17
+ <tr class="line">
18
+ <td><input type="text" id="input-<%= i %>-pattern" name="priorities[][pattern]" value="<%= priority["pattern"] %>" /></td>
19
+ <td><input type="checkbox" id="input-<%= i %>-fairly" name="priorities[][fairly]" value="true" <%= "checked" if priority["fairly"] %> /></td>
20
+ <td>
21
+ <a href="#up" class="up">Up</a> |
22
+ <a href="#down" class="down">Down</a> |
23
+ <a href="#remove" class="remove">Remove</a>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </table>
28
+
29
+ <a href="#add" class="add">Add</a>
30
+ <input type="submit" value="Save"/>
31
+
32
+ </form>
33
+
34
+ <script type="text/javascript" charset="utf-8">
35
+ function markDirty()
36
+ {
37
+ $("input[type=submit]").css({border:"3px orange solid"});
38
+ }
39
+
40
+ jQuery(function($) {
41
+ $("input").live("keypress", markDirty);
42
+ $("input[type=checkbox]").live("click", markDirty);
43
+
44
+ $("a.add").live("click", function(e) {
45
+ e.preventDefault();
46
+ var $table = $("table.priorities");
47
+ var $newRow = $table.find("tr.line:first").clone();
48
+ $newRow.find("input[type=text]").attr("value", "");
49
+ $newRow.find("input[type=checkbox]").attr("checked", false);
50
+ $newRow.appendTo($table);
51
+ markDirty();
52
+ });
53
+
54
+ $("a.remove").live("click", function(e) {
55
+ e.preventDefault();
56
+ var $link = $(this);
57
+ $link.parents("tr").remove();
58
+ markDirty();
59
+ });
60
+
61
+ $("a.up").live("click", function(e) {
62
+ e.preventDefault();
63
+ var $link = $(this);
64
+ var $row = $link.parents("tr");
65
+ $row.prev(".line").before($row);
66
+ markDirty();
67
+ });
68
+
69
+ $("a.down").live("click", function(e) {
70
+ e.preventDefault();
71
+ var $link = $(this);
72
+ var $row = $link.parents("tr");
73
+ $row.next(".line").after($row);
74
+ markDirty();
75
+ });
76
+
77
+ });
78
+ </script>
@@ -0,0 +1,3 @@
1
+ module Qmore
2
+ VERSION = "0.5.0"
3
+ end
data/qmore.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'qmore/version'
4
+
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "qmore"
8
+ s.version = Qmore::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Matt Conway"]
11
+ s.email = ["matt@conwaysplace.com"]
12
+ s.homepage = ""
13
+ s.summary = %q{A qless plugin that gives more control over how queues are processed}
14
+ s.description = %q{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.}
15
+
16
+ s.rubyforge_project = "qmore"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_dependency("qless", '~> 0.9.3')
24
+ s.add_dependency("multi_json", '~> 1.7.7')
25
+
26
+ s.add_development_dependency('rake')
27
+ s.add_development_dependency('rspec', '~> 2.5')
28
+ s.add_development_dependency('rack-test', '~> 0.5.4')
29
+ # Needed for correct ordering when passing hash params to rack-test
30
+ s.add_development_dependency('orderedhash')
31
+
32
+ end
33
+
@@ -0,0 +1,235 @@
1
+ require "spec_helper"
2
+
3
+ describe "Attributes" do
4
+ include Qmore::Attributes
5
+
6
+ before(:each) do
7
+ Qmore.client.redis.flushall
8
+ @real_queues = ["high_x", "foo", "high_y", "superhigh_z"]
9
+ end
10
+
11
+ context "dynamic attributes" do
12
+
13
+ it "should always have a fallback pattern" do
14
+ get_dynamic_queues.should == {'default' => ['*']}
15
+ end
16
+
17
+ it "should allow setting single patterns" do
18
+ get_dynamic_queue('foo').should == ['*']
19
+ set_dynamic_queue('foo', ['bar'])
20
+ get_dynamic_queue('foo').should == ['bar']
21
+ end
22
+
23
+ it "should allow setting multiple patterns" do
24
+ set_dynamic_queues({'foo' => ['bar'], 'baz' => ['boo']})
25
+ get_dynamic_queues.should == {'foo' => ['bar'], 'baz' => ['boo'], 'default' => ['*']}
26
+ end
27
+
28
+ it "should remove mapping when setting empty value" do
29
+ get_dynamic_queues
30
+ set_dynamic_queues({'foo' => ['bar'], 'baz' => ['boo']})
31
+ get_dynamic_queues.should == {'foo' => ['bar'], 'baz' => ['boo'], 'default' => ['*']}
32
+
33
+ set_dynamic_queues({'foo' => [], 'baz' => ['boo']})
34
+ get_dynamic_queues.should == {'baz' => ['boo'], 'default' => ['*']}
35
+ set_dynamic_queues({'baz' => nil})
36
+ get_dynamic_queues.should == {'default' => ['*']}
37
+
38
+ set_dynamic_queues({'foo' => ['bar'], 'baz' => ['boo']})
39
+ set_dynamic_queue('foo', [])
40
+ get_dynamic_queues.should == {'baz' => ['boo'], 'default' => ['*']}
41
+ set_dynamic_queue('baz', nil)
42
+ get_dynamic_queues.should == {'default' => ['*']}
43
+ end
44
+
45
+ end
46
+
47
+ context "priority attributes" do
48
+
49
+ it "can lookup a default priority" do
50
+ get_priority_buckets.should == [{'pattern' => 'default'}]
51
+ end
52
+
53
+ it "can set priorities" do
54
+ set_priority_buckets [{'pattern' => 'foo', 'fairly' => 'false'}]
55
+ get_priority_buckets.should == [{'pattern' => 'foo', 'fairly' => 'false'},
56
+ {'pattern' => 'default'}]
57
+ end
58
+
59
+ it "can set priorities including default" do
60
+ set_priority_buckets [{'pattern' => 'foo', 'fairly' => false},
61
+ {'pattern' => 'default', 'fairly' => false},
62
+ {'pattern' => 'bar', 'fairly' => true}]
63
+ get_priority_buckets.should == [{'pattern' => 'foo', 'fairly' => false},
64
+ {'pattern' => 'default', 'fairly' => false},
65
+ {'pattern' => 'bar', 'fairly' => true}]
66
+ end
67
+
68
+ end
69
+
70
+ context "basic queue patterns" do
71
+
72
+ it "can specify simple queues" do
73
+ expand_queues(["foo"], @real_queues).should == ["foo"]
74
+ expand_queues(["foo", "bar"], @real_queues).should == ["bar", "foo"]
75
+ end
76
+
77
+ it "can specify simple wildcard" do
78
+ worker = Qless::Worker.new("*")
79
+ expand_queues(["*"], @real_queues).should == ["foo", "high_x", "high_y", "superhigh_z"]
80
+ end
81
+
82
+ it "can include queues with pattern" do
83
+ expand_queues(["high*"], @real_queues).should == ["high_x", "high_y"]
84
+ expand_queues(["*high_z"], @real_queues).should == ["superhigh_z"]
85
+ expand_queues(["*high*"], @real_queues).should == ["high_x", "high_y", "superhigh_z"]
86
+ end
87
+
88
+ it "can blacklist queues" do
89
+ expand_queues(["*", "!foo"], @real_queues).should == ["high_x", "high_y", "superhigh_z"]
90
+ end
91
+
92
+ it "can blacklist queues with pattern" do
93
+ expand_queues(["*", "!*high*"], @real_queues).should == ["foo"]
94
+ end
95
+
96
+ end
97
+
98
+ context "redis backed queues" do
99
+
100
+ it "can dynamically lookup queues" do
101
+ set_dynamic_queue("mykey", ["foo", "bar"])
102
+ expand_queues(["@mykey"], @real_queues).should == ["bar", "foo"]
103
+ end
104
+
105
+ it "can blacklist dynamic queues" do
106
+ set_dynamic_queue("mykey", ["foo"])
107
+ expand_queues(["*", "!@mykey"], @real_queues).should == ["high_x", "high_y", "superhigh_z"]
108
+ end
109
+
110
+ it "can blacklist dynamic queues with negation" do
111
+ set_dynamic_queue("mykey", ["!foo", "high_x"])
112
+ expand_queues(["!@mykey"], @real_queues).should == ["foo"]
113
+ end
114
+
115
+ it "will not bloat the given real_queues" do
116
+ orig = @real_queues.dup
117
+ expand_queues(["@mykey"], @real_queues)
118
+ @real_queues.should == orig
119
+ end
120
+
121
+ it "uses hostname as default key in dynamic queues" do
122
+ host = `hostname`.chomp
123
+ set_dynamic_queue(host, ["foo", "bar"])
124
+ expand_queues(["@"], @real_queues).should == ["bar", "foo"]
125
+ end
126
+
127
+ it "can use wildcards in dynamic queues" do
128
+ set_dynamic_queue("mykey", ["*high*", "!high_y"])
129
+ expand_queues(["@mykey"], @real_queues).should == ["high_x", "superhigh_z"]
130
+ end
131
+
132
+ it "falls back to default queues when missing" do
133
+ set_dynamic_queue("default", ["foo", "bar"])
134
+ expand_queues(["@mykey"], @real_queues).should == ["bar", "foo"]
135
+ end
136
+
137
+ it "falls back to all queues when missing and no default" do
138
+ expand_queues(["@mykey"], @real_queues).should == ["foo", "high_x", "high_y", "superhigh_z"]
139
+ end
140
+
141
+ it "falls back to all queues when missing and no default and keep up to date" do
142
+ expand_queues(["@mykey"], @real_queues).should == ["foo", "high_x", "high_y", "superhigh_z"]
143
+ @real_queues << "bar"
144
+ expand_queues(["@mykey"], @real_queues).should == ["bar", "foo", "high_x", "high_y", "superhigh_z"]
145
+ end
146
+
147
+ end
148
+
149
+ context "queue priorities" do
150
+
151
+ it "should pick up all queues with default priority" do
152
+ priority_buckets = [{'pattern' => 'default', 'fairly' => false}]
153
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "foo", "high_y", "superhigh_z"]
154
+ end
155
+
156
+ it "should pick up all queues fairly" do
157
+ # do a bunch to reduce likelyhood of random match causing test failure
158
+ @real_queues = 50.times.collect { |i| "auto_#{i}" }
159
+ priority_buckets = [{'pattern' => 'default', 'fairly' => true}]
160
+ prioritize_queues(priority_buckets, @real_queues).should_not == @real_queues.sort
161
+ prioritize_queues(priority_buckets, @real_queues).sort.should == @real_queues.sort
162
+ end
163
+
164
+ it "should prioritize simple pattern" do
165
+ priority_buckets = [{'pattern' => 'superhigh_z', 'fairly' => false},
166
+ {'pattern' => 'default', 'fairly' => false}]
167
+ prioritize_queues(priority_buckets, @real_queues).should == ["superhigh_z", "high_x", "foo", "high_y"]
168
+ end
169
+
170
+ it "should prioritize multiple simple patterns" do
171
+ priority_buckets = [{'pattern' => 'superhigh_z', 'fairly' => false},
172
+ {'pattern' => 'default', 'fairly' => false},
173
+ {'pattern' => 'foo', 'fairly' => false}]
174
+ prioritize_queues(priority_buckets, @real_queues).should == ["superhigh_z", "high_x", "high_y", "foo"]
175
+ end
176
+
177
+ it "should prioritize simple wildcard pattern" do
178
+ priority_buckets = [{'pattern' => 'high*', 'fairly' => false},
179
+ {'pattern' => 'default', 'fairly' => false}]
180
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "high_y", "foo", "superhigh_z"]
181
+ end
182
+
183
+ it "should prioritize simple wildcard pattern with correct matching" do
184
+ priority_buckets = [{'pattern' => '*high*', 'fairly' => false},
185
+ {'pattern' => 'default', 'fairly' => false}]
186
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "high_y", "superhigh_z", "foo"]
187
+ end
188
+
189
+ it "should prioritize negation patterns" do
190
+ @real_queues.delete("high_x")
191
+ @real_queues << "high_x"
192
+ priority_buckets = [{'pattern' => 'high*,!high_x', 'fairly' => false},
193
+ {'pattern' => 'default', 'fairly' => false}]
194
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_y", "foo", "superhigh_z", "high_x"]
195
+ end
196
+
197
+ it "should not be affected by standalone negation patterns" do
198
+ priority_buckets = [{'pattern' => '!high_x', 'fairly' => false},
199
+ {'pattern' => 'default', 'fairly' => false}]
200
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "foo", "high_y", "superhigh_z"]
201
+ end
202
+
203
+ it "should allow multiple inclusive patterns" do
204
+ priority_buckets = [{'pattern' => 'high_x, superhigh*', 'fairly' => false},
205
+ {'pattern' => 'default', 'fairly' => false}]
206
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "superhigh_z", "foo", "high_y"]
207
+ end
208
+
209
+ it "should prioritize fully inclusive wildcard pattern" do
210
+ priority_buckets = [{'pattern' => '*high*', 'fairly' => false},
211
+ {'pattern' => 'default', 'fairly' => false}]
212
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "high_y", "superhigh_z", "foo"]
213
+ end
214
+
215
+ it "should handle empty default match" do
216
+ priority_buckets = [{'pattern' => '*', 'fairly' => false},
217
+ {'pattern' => 'default', 'fairly' => false}]
218
+ prioritize_queues(priority_buckets, @real_queues).should == ["high_x", "foo", "high_y", "superhigh_z"]
219
+ end
220
+
221
+ it "should pickup wildcard queues fairly" do
222
+ others = 5.times.collect { |i| "other#{i}" }
223
+ @real_queues = @real_queues + others
224
+
225
+ priority_buckets = [{'pattern' => 'other*', 'fairly' => true},
226
+ {'pattern' => 'default', 'fairly' => false}]
227
+ queues = prioritize_queues(priority_buckets, @real_queues)
228
+ queues[0..4].sort.should == others.sort
229
+ queues[5..-1].should == ["high_x", "foo", "high_y", "superhigh_z"]
230
+ queues.should_not == others.sort + ["high_x", "foo", "high_y", "superhigh_z"]
231
+ end
232
+
233
+ end
234
+
235
+ end
@@ -0,0 +1,87 @@
1
+ require "spec_helper"
2
+
3
+ describe "JobReserver" do
4
+ include Qmore::Attributes
5
+
6
+ before(:each) do
7
+ Qmore.client.redis.flushall
8
+ end
9
+
10
+ context "basic qless behavior still works" do
11
+
12
+ it "can reserve from multiple queues" do
13
+ high_queue = Qmore.client.queues['high']
14
+ critical_queue = Qmore.client.queues['critical']
15
+
16
+ high_queue.put(SomeJob, [])
17
+ critical_queue.put(SomeJob, [])
18
+
19
+ reserver = Qmore::JobReserver.new([critical_queue, high_queue])
20
+ reserver.reserve.queue.name.should == 'critical'
21
+ reserver.reserve.queue.name.should == 'high'
22
+ end
23
+
24
+ it "can work on multiple queues" do
25
+ high_queue = Qmore.client.queues['high']
26
+ critical_queue = Qmore.client.queues['critical']
27
+ high_queue.put(SomeJob, [])
28
+ critical_queue.put(SomeJob, [])
29
+
30
+ high_queue.length.should == 1
31
+ critical_queue.length.should == 1
32
+
33
+ reserver = Qmore::JobReserver.new([critical_queue, high_queue])
34
+
35
+ worker = Qless::Worker.new(reserver,
36
+ :run_as_single_process => true)
37
+ worker.work(0)
38
+
39
+ high_queue.length.should == 0
40
+ critical_queue.length.should == 0
41
+ end
42
+
43
+ it "can work on all queues" do
44
+ queues = []
45
+ ['high', 'critical', 'blahblah'].each do |q|
46
+ queue = Qmore.client.queues[q]
47
+ queue.put(SomeJob, [])
48
+ queue.length.should == 1
49
+ queues << queue
50
+ end
51
+
52
+ reserver = Qmore::JobReserver.new([Qmore.client.queues['*']])
53
+ worker = Qless::Worker.new(reserver,
54
+ :run_as_single_process => true)
55
+ worker.work(0)
56
+
57
+ queues.each do |q|
58
+ q.length.should == 0
59
+ end
60
+ end
61
+
62
+ it "handles priorities" do
63
+ set_priority_buckets [{'pattern' => 'foo*', 'fairly' => false},
64
+ {'pattern' => 'default', 'fairly' => false},
65
+ {'pattern' => 'bar', 'fairly' => true}]
66
+
67
+
68
+ queues = []
69
+ ['other', 'blah', 'foobie', 'bar', 'foo'].each do |q|
70
+ queue = Qmore.client.queues[q]
71
+ queue.put(SomeJob, [])
72
+ queue.length.should == 1
73
+ queues << queue
74
+ end
75
+
76
+ reserver = Qmore::JobReserver.new([Qmore.client.queues['*'], Qmore.client.queues['!blah']])
77
+
78
+ reserver.reserve.queue.name.should == 'foo'
79
+ reserver.reserve.queue.name.should == 'foobie'
80
+ reserver.reserve.queue.name.should == 'other'
81
+ reserver.reserve.queue.name.should == 'bar'
82
+ reserver.reserve.should be_nil
83
+ end
84
+
85
+ end
86
+
87
+ end