qtrix 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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