qtrix 0.0.1
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 +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +317 -0
- data/Rakefile +1 -0
- data/bin/qtrix +15 -0
- data/lib/qtrix.rb +178 -0
- data/lib/qtrix/cli.rb +80 -0
- data/lib/qtrix/cli/config_sets.rb +87 -0
- data/lib/qtrix/cli/overrides.rb +121 -0
- data/lib/qtrix/cli/queues.rb +97 -0
- data/lib/qtrix/matrix.rb +83 -0
- data/lib/qtrix/matrix/analyzer.rb +46 -0
- data/lib/qtrix/matrix/common.rb +22 -0
- data/lib/qtrix/matrix/model.rb +16 -0
- data/lib/qtrix/matrix/queue_picker.rb +52 -0
- data/lib/qtrix/matrix/queue_prioritizer.rb +70 -0
- data/lib/qtrix/matrix/reader.rb +25 -0
- data/lib/qtrix/matrix/row_builder.rb +72 -0
- data/lib/qtrix/namespacing.rb +198 -0
- data/lib/qtrix/override.rb +77 -0
- data/lib/qtrix/queue.rb +77 -0
- data/lib/qtrix/version.rb +3 -0
- data/qtrix.gemspec +24 -0
- data/spec/qtrix/cli/config_sets_spec.rb +94 -0
- data/spec/qtrix/cli/overrides_spec.rb +101 -0
- data/spec/qtrix/cli/queues_spec.rb +70 -0
- data/spec/qtrix/cli/spec_helper.rb +18 -0
- data/spec/qtrix/matrix/analyzer_spec.rb +38 -0
- data/spec/qtrix/matrix/queue_picker_spec.rb +73 -0
- data/spec/qtrix/matrix_profile_spec.rb +72 -0
- data/spec/qtrix/matrix_spec.rb +71 -0
- data/spec/qtrix/namespacing_spec.rb +207 -0
- data/spec/qtrix/override_spec.rb +155 -0
- data/spec/qtrix/queue_spec.rb +183 -0
- data/spec/qtrix_spec.rb +204 -0
- data/spec/spec_helper.rb +48 -0
- metadata +178 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
require 'qtrix/cli'
|
4
|
+
|
5
|
+
shared_context "cli commands" do
|
6
|
+
let(:stdout_stream) {StringIO.new}
|
7
|
+
let(:stderr_stream) {StringIO.new}
|
8
|
+
|
9
|
+
def stdout
|
10
|
+
stdout_stream.rewind
|
11
|
+
stdout_stream.read
|
12
|
+
end
|
13
|
+
|
14
|
+
def stderr
|
15
|
+
stderr_stream.rewind
|
16
|
+
stderr_stream.read
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Qtrix::Matrix::Analyzer do
|
4
|
+
before(:each) do
|
5
|
+
Qtrix.map_queue_weights A: 40, B: 30, C: 20, D: 10
|
6
|
+
end
|
7
|
+
let(:matrix) {Qtrix::Matrix.queues_for!("host1", 4)}
|
8
|
+
|
9
|
+
describe "#breakdown" do
|
10
|
+
it "results in hash of queue names to arrays of counts in each column in the matrix" do
|
11
|
+
result = Qtrix::Matrix::Analyzer.breakdown(matrix)
|
12
|
+
result.should == {
|
13
|
+
A: [1,3,0,0],
|
14
|
+
B: [1,1,2,0],
|
15
|
+
C: [1,0,2,1],
|
16
|
+
D: [1,0,0,3]
|
17
|
+
}
|
18
|
+
result.dump
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#analyze!" do
|
23
|
+
it "should map queue weights, populate matrix then break it down." do
|
24
|
+
expected = {
|
25
|
+
A: [1,3,0,0],
|
26
|
+
B: [1,1,2,0],
|
27
|
+
C: [1,0,2,1],
|
28
|
+
D: [1,0,0,3]
|
29
|
+
}
|
30
|
+
result = Qtrix::Matrix::Analyzer.analyze! 4, \
|
31
|
+
A: 40,
|
32
|
+
B: 30,
|
33
|
+
C: 20,
|
34
|
+
D: 10
|
35
|
+
result.should == expected
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Qtrix::Matrix do
|
4
|
+
let(:matrix) {Qtrix::Matrix}
|
5
|
+
before(:each) do
|
6
|
+
Qtrix::Matrix.clear!
|
7
|
+
Qtrix.map_queue_weights \
|
8
|
+
A: 40,
|
9
|
+
B: 30,
|
10
|
+
C: 20,
|
11
|
+
D: 10
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#queues_for!" do
|
15
|
+
context "with no rows" do
|
16
|
+
it "should generate new rows" do
|
17
|
+
result = matrix.queues_for!('host1', 1)
|
18
|
+
result.should == [[:A, :B, :C, :D]]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should populate the persistant model" do
|
22
|
+
result = matrix.queues_for!('host1', 1)
|
23
|
+
result.should == matrix.to_table
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with existing rows" do
|
28
|
+
it "should maintain existing rows if no more needed" do
|
29
|
+
matrix.queues_for!('host1', 1)
|
30
|
+
matrix.queues_for!('host1', 1)
|
31
|
+
matrix.fetch.size.should == 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should add rows if more needed" do
|
35
|
+
matrix.queues_for!('host1', 1)
|
36
|
+
matrix.queues_for!('host1', 2)
|
37
|
+
matrix.fetch.size.should == 2
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should prune rows if less are needed" do
|
41
|
+
matrix.queues_for!('host1', 2)
|
42
|
+
matrix.queues_for!('host1', 1)
|
43
|
+
matrix.fetch.size.should == 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with multiple hosts" do
|
48
|
+
before do
|
49
|
+
matrix.queues_for!('host1', 2)
|
50
|
+
matrix.queues_for!('host2', 2)
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:host1_rows) {matrix.fetch.select{|row| row.hostname == 'host1'}}
|
54
|
+
let(:host2_rows) {matrix.fetch.select{|row| row.hostname == 'host2'}}
|
55
|
+
|
56
|
+
context "when rows are added" do
|
57
|
+
it "should associate them with the specific host" do
|
58
|
+
matrix.queues_for!('host1', 3)
|
59
|
+
host1_rows.size.should == 3
|
60
|
+
host2_rows.size.should == 2
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when rows are pruned" do
|
65
|
+
it "should prune them from the specific host" do
|
66
|
+
matrix.queues_for!('host2', 1)
|
67
|
+
host1_rows.size.should == 2
|
68
|
+
host2_rows.size.should == 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rspec-prof'
|
3
|
+
|
4
|
+
describe Qtrix::Matrix do
|
5
|
+
# For this test's purpose, a queues receives a score each time it appears
|
6
|
+
# in a row within the matrix -- 4 for being at the head of a row, 3 for
|
7
|
+
# being the next queue in the row, 2 for being the 3rd queue in the row
|
8
|
+
# and 1 for being the last queue in the row.
|
9
|
+
def cumulative_score_of(queue)
|
10
|
+
matrix = Qtrix::Matrix.to_table
|
11
|
+
scores = matrix.map{|row| 4 - row.index(queue)}
|
12
|
+
scores.inject(0) {|m, s| m += s}
|
13
|
+
end
|
14
|
+
|
15
|
+
def head_count_of(target_queue)
|
16
|
+
matrix = Qtrix::Matrix.to_table
|
17
|
+
heads = matrix.map{|row| row[0]}
|
18
|
+
heads.select{|queue| queue == target_queue}.size
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:a_score) {cumulative_score_of(:A)}
|
22
|
+
let(:b_score) {cumulative_score_of(:B)}
|
23
|
+
let(:c_score) {cumulative_score_of(:C)}
|
24
|
+
let(:d_score) {cumulative_score_of(:D)}
|
25
|
+
let(:a_heads) {head_count_of(:A)}
|
26
|
+
let(:b_heads) {head_count_of(:B)}
|
27
|
+
let(:c_heads) {head_count_of(:C)}
|
28
|
+
let(:d_heads) {head_count_of(:D)}
|
29
|
+
|
30
|
+
# Check to make sure that the following holds true for 4, 10 and 100
|
31
|
+
# worker setups:
|
32
|
+
#
|
33
|
+
# 1. every queue is at the head of a worker list at least once.
|
34
|
+
# 2. queues with a higher weight occur more frequently to the left of queues
|
35
|
+
# with a lower weight in the list of queues processed by all workers.
|
36
|
+
[2,5,50].each do |worker_count|
|
37
|
+
context "managing #{worker_count*2} workers across 2 hosts" do
|
38
|
+
# The following will generate profiling reports in the profiles dir.
|
39
|
+
profile(:all) do
|
40
|
+
before (:each) do
|
41
|
+
Qtrix::Matrix.clear!
|
42
|
+
Qtrix.map_queue_weights \
|
43
|
+
A: 40,
|
44
|
+
B: 30,
|
45
|
+
C: 20,
|
46
|
+
D: 10
|
47
|
+
Qtrix::Matrix.queues_for!('host1', worker_count)
|
48
|
+
Qtrix::Matrix.queues_for!('host2', worker_count)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should maintain the desired distribution of queues" do
|
52
|
+
a_score.should be >= b_score
|
53
|
+
b_score.should be >= c_score
|
54
|
+
c_score.should be >= d_score
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should have every queue at the head of at least one worker's queue list" do
|
58
|
+
a_heads.should_not == 0
|
59
|
+
b_heads.should_not == 0
|
60
|
+
c_heads.should_not == 0
|
61
|
+
d_heads.should_not == 0
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should maintain desired distribution of queues at the heead of queue lists" do
|
65
|
+
a_heads.should be >= b_heads
|
66
|
+
b_heads.should be >= c_heads
|
67
|
+
c_heads.should be >= d_heads
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
describe Qtrix::Matrix do
|
5
|
+
describe "namespaced operations" do
|
6
|
+
include_context "established default and night namespaces"
|
7
|
+
let(:matrix) {Qtrix::Matrix}
|
8
|
+
|
9
|
+
describe "#fetch" do
|
10
|
+
it "should default to the current namespace" do
|
11
|
+
result = matrix.fetch.map{|row| row.entries.map(&:queue)}.sort
|
12
|
+
result.should == [[:A, :B, :C]]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should allow fetching from a different namespace" do
|
16
|
+
result = matrix.fetch(:night).map{|row| row.entries.map(&:queue)}.sort
|
17
|
+
result.should == [[:X, :Y, :Z]]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#to_table" do
|
22
|
+
it "should default to the current namespace" do
|
23
|
+
matrix.to_table.should == [[:A, :B, :C]]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should allow fetching from a different namespace" do
|
27
|
+
matrix.to_table(:night).should == [[:X, :Y, :Z]]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#clear!" do
|
32
|
+
it "should default to the current namespace" do
|
33
|
+
matrix.clear!
|
34
|
+
raw_redis.keys("qtrix:default:matrix*").should == []
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should allow clearing of a different namespace" do
|
38
|
+
matrix.clear! :night
|
39
|
+
raw_redis.keys("qtrix:night:matrix*").should == []
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#queues_for!" do
|
44
|
+
context "with no namespace specified" do
|
45
|
+
it "should return queues from current namespace" do
|
46
|
+
matrix.queues_for!("host1", 1).should == [[:A, :B, :C]]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should auto-shuffle distribution of queues if they all have the same weight" do
|
50
|
+
Qtrix.map_queue_weights A: 1, B: 1, C: 1, D: 1, E: 1
|
51
|
+
matrix.queues_for!("host1", 100)
|
52
|
+
rows = matrix.to_table[5..-1]
|
53
|
+
dupes = Set.new
|
54
|
+
while(current_row = rows.shift) do
|
55
|
+
next if dupes.include? current_row
|
56
|
+
if rows.detect{|another_row| another_row == current_row}
|
57
|
+
dupes.add(current_row)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
dupes.size.should be < 10
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "with namespace specified" do
|
65
|
+
it "should return queues from the specified namespace" do
|
66
|
+
matrix.queues_for!(:night, "host1", 1).should == [[:X, :Y, :Z]]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Qtrix::Namespacing do
|
4
|
+
include Qtrix::Namespacing
|
5
|
+
let(:test_class) {
|
6
|
+
class Foo < Object
|
7
|
+
include Qtrix::Namespacing
|
8
|
+
end
|
9
|
+
}
|
10
|
+
let(:test_instance) {test_class.new}
|
11
|
+
|
12
|
+
shared_examples_for "@redis_namespace definers #redis" do
|
13
|
+
it "should return redis connection namespaced to :a" do
|
14
|
+
target.redis.namespace.should == :a
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should allow caller to specify namespacing" do
|
18
|
+
target.redis(:b, :c).set("d", 1)
|
19
|
+
result = raw_redis.keys.reject{|key| key[/namespace/]}
|
20
|
+
result.should == ["qtrix:default:a:b:c:d"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
[:test_class, :test_instance].each do |target_name|
|
25
|
+
context "for #{target_name}" do
|
26
|
+
let(:target) {send(target_name)}
|
27
|
+
|
28
|
+
describe "#redis" do
|
29
|
+
context "without @redis_namespace defined in self or self.class" do
|
30
|
+
it "should return redis connection namespaced to :default" do
|
31
|
+
target.redis.namespace.should == :default
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should allow caller to specify namespacing" do
|
35
|
+
target.redis(:a, :b).set("c", 1)
|
36
|
+
raw_redis.keys.include?("qtrix:a:b:c").should == true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should evaluate caller arg of :current to the current namespace" do
|
40
|
+
target.redis(:current).set("a", 1)
|
41
|
+
result = raw_redis.keys.select{|key| key[/default/]}
|
42
|
+
result.should == ["qtrix:default:a"]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with @redis_namespace defined in self" do
|
47
|
+
around(:each) do |example|
|
48
|
+
target.send(:instance_variable_set, :@redis_namespace, [:current, :a])
|
49
|
+
example.run
|
50
|
+
target.send(:instance_variable_set, :@redis_namespace, nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
it_should_behave_like "@redis_namespace definers #redis"
|
54
|
+
end
|
55
|
+
|
56
|
+
context "with @redis_namespace defined in class" do
|
57
|
+
around(:each) do |example|
|
58
|
+
target.class.send(:instance_variable_set, :@redis_namespace, [:current, :a])
|
59
|
+
example.run
|
60
|
+
target.class.send(:instance_variable_set, :@redis_namespace, nil)
|
61
|
+
end
|
62
|
+
|
63
|
+
it_should_behave_like "@redis_namespace definers #redis"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#redis_namespace" do
|
68
|
+
context "without @redis_namespace defined in self or self.class" do
|
69
|
+
it "should return nil" do
|
70
|
+
target.redis_namespace.should == []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "with @redis_namespace defined" do
|
75
|
+
around(:each) do |example|
|
76
|
+
target.send(:instance_variable_set, :@redis_namespace, [:current, :foo])
|
77
|
+
example.run
|
78
|
+
target.send(:instance_variable_set, :@redis_namespace, nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should return self@redis_namespace" do
|
82
|
+
target.redis_namespace.should == [:current, :foo]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "with @redis_namespace defined in superclass" do
|
87
|
+
around(:each) do |example|
|
88
|
+
target.class.send(:instance_variable_set, :@redis_namespace, [:current, :foo])
|
89
|
+
example.run
|
90
|
+
target.class.send(:instance_variable_set, :@redis_namespace, nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should return class@redis_namespace" do
|
94
|
+
target.redis_namespace.should == [:current, :foo]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe Qtrix::Namespacing::Manager do
|
102
|
+
let(:manager) {Qtrix::Namespacing::Manager.instance}
|
103
|
+
|
104
|
+
describe "#redis" do
|
105
|
+
context "with no args passed" do
|
106
|
+
it "should return a redis connection with no args" do
|
107
|
+
manager.redis.keys.should_not be_nil
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should return a redis connection namespaced to qtrix:default" do
|
111
|
+
manager.redis.namespace.should == :default
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "with args passed" do
|
116
|
+
it "each arg should be a namespace for keys defined in redis" do
|
117
|
+
manager.redis(:foo, :bar).set("a", 1)
|
118
|
+
raw_redis.keys.include?("qtrix:foo:bar:a").should == true
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should prune out any duplicate namespaces" do
|
122
|
+
manager.redis(:a, :a, :a).set("b", 1)
|
123
|
+
result = raw_redis.keys.grep(/qtrix:a/)
|
124
|
+
result.should == ["qtrix:a:b"]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "#add_namespace" do
|
130
|
+
it "should add a namespace to the system" do
|
131
|
+
manager.add_namespace(:night_distribution)
|
132
|
+
manager.namespaces.sort.should == [:default, :night_distribution]
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should error when nil passed" do
|
136
|
+
expect{manager.add_namespace(nil)}.to raise_error
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should error when valid pattern is not matched" do
|
140
|
+
expect{manager.add_namespace('#$*#($')}.to raise_error
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should error when attempting to add an existing namespace" do
|
144
|
+
manager.namespaces.should_not be_empty
|
145
|
+
expect{manager.add_namespace(:default)}.to raise_error
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#remove_namesapce" do
|
150
|
+
include_context "an established matrix"
|
151
|
+
before {manager.add_namespace(:transition_flood)}
|
152
|
+
|
153
|
+
it "should remove a namespace from the system" do
|
154
|
+
manager.remove_namespace!(:transition_flood)
|
155
|
+
manager.namespaces.should == [:default]
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should not allow you to delete the default namespace" do
|
159
|
+
expect{manager.remove_namespace!(:default)}.to raise_error
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should not allow you to remove the current namespace" do
|
163
|
+
Qtrix.map_queue_weights(:transition_flood, A: 1)
|
164
|
+
manager.change_current_namespace(:transition_flood)
|
165
|
+
expect{manager.remove_namespace!(:transition_flood)}.to raise_error
|
166
|
+
end
|
167
|
+
|
168
|
+
describe "cascading removal" do
|
169
|
+
before do
|
170
|
+
Qtrix.add_override(:transition_flood, ["A"], 1)
|
171
|
+
Qtrix.map_queue_weights :transition_flood, B: 10
|
172
|
+
Qtrix::Matrix.queues_for!(:transition_flood, "host1", 2)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should cascade deletes to data in the namespace" do
|
176
|
+
manager.remove_namespace!(:transition_flood)
|
177
|
+
raw_redis.keys("qtrix:transition_flood*").should == []
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#current_namespace" do
|
183
|
+
it "should default to :default" do
|
184
|
+
manager.current_namespace.should == :default
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#change_current_namespace" do
|
189
|
+
include_context "an established matrix"
|
190
|
+
before(:each) {manager.add_namespace(:night_distribution)}
|
191
|
+
|
192
|
+
it "should set the current namespace" do
|
193
|
+
Qtrix.map_queue_weights(:night_distribution, A: 1)
|
194
|
+
manager.change_current_namespace(:night_distribution)
|
195
|
+
manager.current_namespace.should == :night_distribution
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should error if trying to set the namespace to something unknown" do
|
199
|
+
expect{manager.change_current_namespace :foo}.to raise_error
|
200
|
+
end
|
201
|
+
|
202
|
+
it "should error if trying to change into an empty namespace" do
|
203
|
+
expect{manager.change_current_namespace(:night_distribution)}.to raise_error
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|