qtrix 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,46 @@
|
|
1
|
+
module Qtrix
|
2
|
+
module Matrix
|
3
|
+
##
|
4
|
+
# Utility class to examining the distribution of queues
|
5
|
+
# within a matrix. Its operations result in a hash of
|
6
|
+
# queue names mapped to arrays containing counts that
|
7
|
+
# the queue appeared in that column index of the matrix.
|
8
|
+
module Analyzer
|
9
|
+
# Breaks down any old matrix
|
10
|
+
def self.breakdown(matrix)
|
11
|
+
result_hash_for(matrix).tap do |result|
|
12
|
+
matrix.each do |row|
|
13
|
+
row.each_with_index do |queue, column|
|
14
|
+
result[queue][column] += 1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def result.to_s
|
19
|
+
self.map{|queue, pos| "#{queue}: #{pos.join(',')}"}.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def result.dump
|
23
|
+
puts self
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Maps the specified queue weights, generates a matrix
|
30
|
+
# with the specified number of rows, then breaks it down
|
31
|
+
# as above.
|
32
|
+
def self.analyze!(rows, queue_weights={})
|
33
|
+
Qtrix::Matrix.clear!
|
34
|
+
Qtrix::Queue.clear!
|
35
|
+
Qtrix::Queue.map_queue_weights(queue_weights)
|
36
|
+
Qtrix::Matrix.queues_for!(`hostname`, rows)
|
37
|
+
breakdown(Qtrix::Matrix.to_table)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def self.result_hash_for(matrix)
|
42
|
+
Hash.new{|h, k| h[k] = [0] * matrix.first.size}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Qtrix
|
4
|
+
module Matrix
|
5
|
+
module Common
|
6
|
+
REDIS_KEY = :matrix
|
7
|
+
def self.included(base)
|
8
|
+
base.send(:extend, self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def pack(item)
|
12
|
+
# Marshal is fast but not human readable, might want to
|
13
|
+
# go for json or yaml. This is fast at least.
|
14
|
+
Marshal.dump(item)
|
15
|
+
end
|
16
|
+
|
17
|
+
def unpack(item)
|
18
|
+
Marshal.restore(item)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'qtrix/matrix/common'
|
3
|
+
|
4
|
+
module Qtrix
|
5
|
+
module Matrix
|
6
|
+
##
|
7
|
+
# An entry (or cell) in the matrix, contains a single queue and its value
|
8
|
+
# relative to the other entries to the left in the same row.
|
9
|
+
Entry = Struct.new(:queue, :resource_percentage, :value)
|
10
|
+
|
11
|
+
##
|
12
|
+
# A row in the matrix, contains the hostname the row is for and the entries
|
13
|
+
# of queues within the row.
|
14
|
+
Row = Struct.new(:hostname, :entries)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Qtrix
|
4
|
+
module Matrix
|
5
|
+
##
|
6
|
+
# Responsible for picking a number of queue lists from the matrix
|
7
|
+
# for a specific host. Will return already picked lists if they
|
8
|
+
# exist. Will generate new queue lists if they are needed and
|
9
|
+
# prune old lists as they are no longer needed, maintaining a row
|
10
|
+
# in the matrix for the number of workers for the host.
|
11
|
+
class QueuePicker
|
12
|
+
include Namespacing
|
13
|
+
include Common
|
14
|
+
attr_reader :namespace, :reader, :hostname, :workers
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
@namespace, @reader, @hostname, @workers = extract_args(3, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def pick!
|
21
|
+
delta = workers - rows_for_host.size
|
22
|
+
new_queues = []
|
23
|
+
if delta > 0
|
24
|
+
generate(delta)
|
25
|
+
elsif delta < 0
|
26
|
+
prune(delta)
|
27
|
+
end
|
28
|
+
rows_for_host.map(&to_queues)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def to_queues
|
33
|
+
lambda {|row| row.entries.map(&:queue)}
|
34
|
+
end
|
35
|
+
|
36
|
+
def rows_for_host
|
37
|
+
# TODO fix me.
|
38
|
+
reader.rows_for_host(hostname, namespace)
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate(count)
|
42
|
+
RowBuilder.new(namespace, hostname, count).build
|
43
|
+
end
|
44
|
+
|
45
|
+
def prune(count)
|
46
|
+
count.abs.times.each do
|
47
|
+
redis(namespace).lrem(REDIS_KEY, -2, pack(rows_for_host.pop))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'qtrix/matrix/common'
|
3
|
+
|
4
|
+
module Qtrix
|
5
|
+
module Matrix
|
6
|
+
##
|
7
|
+
# Maintains current prioritization of queues based on the
|
8
|
+
# state of all rows in the matrix.
|
9
|
+
|
10
|
+
class QueuePrioritizer
|
11
|
+
attr_reader :desired_distribution, :heads, :all_entries
|
12
|
+
|
13
|
+
def initialize(desired_distribution, heads, all_entries)
|
14
|
+
@desired_distribution = desired_distribution
|
15
|
+
@heads = heads
|
16
|
+
@all_entries = all_entries
|
17
|
+
@new_head_picked = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_priority_queue
|
21
|
+
# Not a true priority queue, since we are sorting after all elements
|
22
|
+
# are inserted
|
23
|
+
queue = queue_priority_tuples_from desired_distribution
|
24
|
+
prioritized_queue = queue.sort &by_priority_of_tuple
|
25
|
+
to_simple_queues_from prioritized_queue
|
26
|
+
end
|
27
|
+
|
28
|
+
def queue_priority_tuples_from(dist)
|
29
|
+
dist.map{|queue| [queue, current_priority_of(queue)]}
|
30
|
+
end
|
31
|
+
|
32
|
+
def by_priority_of_tuple(context={})
|
33
|
+
lambda {|i, j| j[1] <=> i[1]}
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_simple_queues_from(prioritized_queue)
|
37
|
+
prioritized_queue.map {|tuple| tuple[0]}
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_priority_of(queue)
|
41
|
+
if has_appeared_at_head_of_row?(queue.name) || @new_head_picked
|
42
|
+
normal_priority_for queue
|
43
|
+
else
|
44
|
+
@new_head_picked = true
|
45
|
+
starting_priority_for queue
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_appeared_at_head_of_row?(queue_name)
|
50
|
+
heads.include?(queue_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def normal_priority_for(queue)
|
54
|
+
queue.resource_percentage / (1 + sum_of(entries_for(queue)))
|
55
|
+
end
|
56
|
+
|
57
|
+
def starting_priority_for(queue)
|
58
|
+
queue.resource_percentage * 1000
|
59
|
+
end
|
60
|
+
|
61
|
+
def sum_of(entries)
|
62
|
+
entries.inject(0) {|memo, e| memo += e.value}
|
63
|
+
end
|
64
|
+
|
65
|
+
def entries_for(queue)
|
66
|
+
all_entries.select{|entry| entry.queue == queue.name}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Qtrix
|
4
|
+
module Matrix
|
5
|
+
##
|
6
|
+
# Class responsible for reading & returning the persistent state of
|
7
|
+
# the matrix.
|
8
|
+
class Reader
|
9
|
+
include Namespacing
|
10
|
+
include Common
|
11
|
+
|
12
|
+
def self.fetch(namespace=:current)
|
13
|
+
redis(namespace).lrange(REDIS_KEY, 0, -1).map{|dump| unpack(dump)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.to_table(namespace=:current)
|
17
|
+
fetch(namespace).map{|row| row.entries.map(&:queue)}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.rows_for_host(hostname, namespace=:current)
|
21
|
+
fetch(namespace).select{|row| row.hostname == hostname}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'qtrix/matrix/common'
|
3
|
+
|
4
|
+
module Qtrix
|
5
|
+
module Matrix
|
6
|
+
##
|
7
|
+
# Carries out the construction of rows within the matrix for a number of
|
8
|
+
# workers for a specific hostname.
|
9
|
+
class RowBuilder
|
10
|
+
include Qtrix::Namespacing
|
11
|
+
include Matrix::Common
|
12
|
+
attr_reader :namespace, :hostname, :workers, :matrix,
|
13
|
+
:desired_distribution, :heads, :all_entries
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
@namespace, @hostname, @workers = extract_args(2, *args)
|
17
|
+
@matrix = Qtrix::Matrix.fetch(namespace)
|
18
|
+
@desired_distribution = Qtrix.desired_distribution(namespace)
|
19
|
+
@heads = matrix.map{|row| row.entries.first.queue}
|
20
|
+
@all_entries = matrix.map(&:entries).flatten
|
21
|
+
end
|
22
|
+
|
23
|
+
def build
|
24
|
+
[].tap do |result|
|
25
|
+
(1..workers).each do
|
26
|
+
queues_for_row = queue_prioritizer.current_priority_queue
|
27
|
+
build_row_for! hostname, queues_for_row
|
28
|
+
result << queues_for_row.map(&:name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def queue_prioritizer
|
35
|
+
QueuePrioritizer.new(desired_distribution, heads, all_entries)
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_row_for!(hostname, queues)
|
39
|
+
row = Row.new(hostname, [])
|
40
|
+
queues.each do |queue|
|
41
|
+
build_entry(row, queue, next_val_for(row))
|
42
|
+
end
|
43
|
+
heads << row.entries[0].queue
|
44
|
+
store(row)
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_entry(row, queue, entry_val)
|
49
|
+
entry = Entry.new(
|
50
|
+
queue.name,
|
51
|
+
queue.resource_percentage,
|
52
|
+
entry_val
|
53
|
+
)
|
54
|
+
all_entries << entry
|
55
|
+
row.entries << entry
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_val_for(row)
|
59
|
+
raw_result = 1.0 - sum_of_resource_percentages_for(row.entries)
|
60
|
+
BigDecimal.new(raw_result, 4)
|
61
|
+
end
|
62
|
+
|
63
|
+
def sum_of_resource_percentages_for(entries)
|
64
|
+
entries.inject(0) {|memo, entry| memo += entry.resource_percentage}
|
65
|
+
end
|
66
|
+
|
67
|
+
def store(row)
|
68
|
+
redis(namespace).rpush(REDIS_KEY, pack(row))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'redis-namespace'
|
3
|
+
|
4
|
+
module Qtrix
|
5
|
+
##
|
6
|
+
# Provides support for namespacing the various redis keys so we can have multiple
|
7
|
+
# configuration sets and a pointer to the current namespace or configuration set.
|
8
|
+
#
|
9
|
+
# This will allow for us to set up different configurations for different scenarios
|
10
|
+
# and to switch between them easily.
|
11
|
+
#
|
12
|
+
# Scenarios might be things like:
|
13
|
+
#
|
14
|
+
# - day vs night configuration
|
15
|
+
# - weekday vs weekend configuration
|
16
|
+
# - common flood handling distributions.
|
17
|
+
#
|
18
|
+
# Most interaction should be through the mixin and not the manager singelton here.
|
19
|
+
# Example:
|
20
|
+
#
|
21
|
+
# class Foo
|
22
|
+
# include Qtrix::Namespacing
|
23
|
+
# @redis_namespace = :foo # or [:current, :foo]
|
24
|
+
#
|
25
|
+
# def some_method
|
26
|
+
# redis.keys # constrained to :qtrix:foo:*
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
module Namespacing
|
30
|
+
def self.included(base)
|
31
|
+
# make redis_key and current_namespaced available to both class and
|
32
|
+
# instance methods
|
33
|
+
base.instance_exec do
|
34
|
+
extend Namespacing
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Returns a redis client namespaced to the #redis_namespace defined in the
|
40
|
+
# object/class, or to the id within the parent option. An id of :current
|
41
|
+
# will be evaluated to the current namespace. By default, this
|
42
|
+
# a root (qtrix:) namespaced client. Examples:
|
43
|
+
def redis(*namespaces)
|
44
|
+
all_namespaces = redis_namespace + namespaces
|
45
|
+
Manager.instance.redis(*all_namespaces)
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns the redis namespace as defined in the instance or class
|
50
|
+
# @redis_namespace variable.
|
51
|
+
def redis_namespace
|
52
|
+
namespaces = Array(
|
53
|
+
self.instance_variable_get(:@redis_namespace) ||
|
54
|
+
self.class.instance_variable_get(:@redis_namespace)
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Extracts the namespace, if any, from the arg list.
|
60
|
+
def extract_args(arg_count, *args)
|
61
|
+
if arg_count == args.size
|
62
|
+
[:current] + args
|
63
|
+
else
|
64
|
+
args
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Manages namespaces. Uses Redis::Namespace to impose the namespacing
|
70
|
+
# on calls to redis, and maintains the known config namespaces within
|
71
|
+
# redis. Should not be working directly with this too much, it should
|
72
|
+
# be transparen when mixing in the Qtrix::Namespacing module.
|
73
|
+
class Manager
|
74
|
+
include Singleton
|
75
|
+
NAMESPACING_KEY = :namespacing
|
76
|
+
DEFAULT_NAMESPACE = :default
|
77
|
+
attr_reader :connection_config
|
78
|
+
|
79
|
+
def connection_config(opts={})
|
80
|
+
@connection_config ||= opts
|
81
|
+
end
|
82
|
+
|
83
|
+
def redis(*namespaces)
|
84
|
+
namespaced_client = Redis::Namespace.new(:qtrix, redis: client)
|
85
|
+
namespaced_redis({redis: namespaced_client}, *evaluate(namespaces.uniq))
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_namespace(namespace)
|
89
|
+
validate namespace
|
90
|
+
namespacing_redis.sadd(:namespaces, namespace)
|
91
|
+
end
|
92
|
+
|
93
|
+
def remove_namespace!(namespace)
|
94
|
+
raise "Cannot remove default namespace" if namespace == :default
|
95
|
+
raise "Cannot remove current namespace" if namespace == current_namespace
|
96
|
+
namespacing_redis.srem(:namespaces, namespace)
|
97
|
+
Qtrix::Override.clear!(namespace)
|
98
|
+
Qtrix::Queue.clear!(namespace)
|
99
|
+
Qtrix::Matrix.clear!(namespace)
|
100
|
+
end
|
101
|
+
|
102
|
+
def namespaces
|
103
|
+
if default_namespace_does_not_exist?
|
104
|
+
namespacing_redis.sadd(:namespaces, DEFAULT_NAMESPACE)
|
105
|
+
end
|
106
|
+
namespacing_redis.smembers(:namespaces).map{|ns| unpack(ns)}
|
107
|
+
end
|
108
|
+
|
109
|
+
def change_current_namespace(namespace)
|
110
|
+
unless namespaces.include? namespace
|
111
|
+
raise "Unknown namespace: #{namespace}"
|
112
|
+
end
|
113
|
+
if not_ready?(namespace)
|
114
|
+
raise "#{namespace} is empty"
|
115
|
+
end
|
116
|
+
namespacing_redis.set(:current_namespace, namespace)
|
117
|
+
end
|
118
|
+
|
119
|
+
def current_namespace
|
120
|
+
if no_current_namespace?
|
121
|
+
self.change_current_namespace DEFAULT_NAMESPACE
|
122
|
+
end
|
123
|
+
unpack(namespacing_redis.get(:current_namespace))
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def not_ready?(namespace)
|
128
|
+
namespace != :default &&
|
129
|
+
Qtrix.desired_distribution(namespace).empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
def evaluate(namespaces)
|
133
|
+
ensure_a_namespace_in(namespaces)
|
134
|
+
namespaces.map do |namespace|
|
135
|
+
namespace == :current ? current_namespace : namespace
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def ensure_a_namespace_in(namespaces)
|
140
|
+
if namespaces.compact.empty?
|
141
|
+
namespaces << :current
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def client
|
146
|
+
@client ||= Redis.connect(connection_config)
|
147
|
+
end
|
148
|
+
|
149
|
+
def namespacing_redis
|
150
|
+
@namespacing_redis ||= redis(:namespacing)
|
151
|
+
end
|
152
|
+
|
153
|
+
def namespaced_redis(ctx, *namespaces)
|
154
|
+
current, *others = namespaces
|
155
|
+
if others.empty?
|
156
|
+
if current
|
157
|
+
Redis::Namespace.new(current, redis: ctx[:redis])
|
158
|
+
else
|
159
|
+
ctx[:redis]
|
160
|
+
end
|
161
|
+
else
|
162
|
+
next_ctx = {redis: Redis::Namespace.new(current, redis: ctx[:redis])}
|
163
|
+
namespaced_redis(next_ctx, *others)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def validate(namespace)
|
168
|
+
raise "cannot be nil" if namespace.nil?
|
169
|
+
unless only_letters_numbers_and_underscores? namespace
|
170
|
+
raise "must contain alphanumerics and underscores"
|
171
|
+
end
|
172
|
+
raise "#{namespace} already exists" if namespacing_redis.sismember(:namespaces, namespace)
|
173
|
+
end
|
174
|
+
|
175
|
+
def only_letters_numbers_and_underscores?(namespace)
|
176
|
+
namespace =~ /^[\w\d_]+$/
|
177
|
+
end
|
178
|
+
|
179
|
+
def default_namespace_does_not_exist?
|
180
|
+
!namespacing_redis.smembers(:namespaces).include? DEFAULT_NAMESPACE
|
181
|
+
end
|
182
|
+
|
183
|
+
def no_current_namespace?
|
184
|
+
namespacing_redis.get(:current_namespace).nil?
|
185
|
+
end
|
186
|
+
|
187
|
+
def unpack(value)
|
188
|
+
value.nil? ? nil : value.to_sym
|
189
|
+
end
|
190
|
+
|
191
|
+
def ensure_default_exists
|
192
|
+
unless namespaces.include?(DEFAULT_NAMESPACE)
|
193
|
+
self.add_namespace(DEFAULT_NAMESPACE)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|