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,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
|