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.
@@ -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