qmore 0.6.2 → 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/README.md +19 -7
- data/lib/qmore.rb +30 -3
- data/lib/qmore/attributes.rb +12 -85
- data/lib/qmore/configuration.rb +74 -0
- data/lib/qmore/job_reserver.rb +11 -6
- data/lib/qmore/persistence.rb +105 -0
- data/lib/qmore/server.rb +27 -7
- data/lib/qmore/version.rb +1 -1
- data/qmore.gemspec +1 -0
- data/spec/attributes_spec.rb +11 -74
- data/spec/configuration_spec.rb +112 -0
- data/spec/job_reserver_spec.rb +86 -9
- data/spec/persistance_spec.rb +57 -0
- data/spec/server_spec.rb +96 -85
- data/spec/spec_helper.rb +1 -1
- metadata +80 -45
- checksums.yaml +0 -7
data/CHANGELOG
CHANGED
data/README.md
CHANGED
@@ -15,11 +15,23 @@ Alternatively, if you have some other way of launching workers (e.g. qless-pool)
|
|
15
15
|
Qless::Pool.pool_factory.reserver_class = Qmore::JobReserver
|
16
16
|
Qmore.client == Qless::Pool.pool_factory.client
|
17
17
|
|
18
|
+
# Enabling Monitoring
|
19
|
+
# Redis persistence defaults to using the same connection
|
20
|
+
# used for reserving jobs, however it is not required that they
|
21
|
+
# be the same redis connection. I.e. you can store configuration
|
22
|
+
# on a completely separate instance of redis.
|
23
|
+
Qmore.persistence = Qless::Persistence::Redis.new(Qmore.client.redis)
|
24
|
+
# Configure the monitor thread with the persistence type, and the interval at which to update
|
25
|
+
# Monitor defaults to using Qmore.persistence and 2 minutes
|
26
|
+
Qmore.monitor = Qless::persistence::Monitor.new(Qmore.persistence, 120)
|
27
|
+
# Start up monitor thread
|
28
|
+
Qmore.monitor.start
|
29
|
+
|
18
30
|
To enable the web UI, use a config.ru similar to the following depending on your environment:
|
19
31
|
|
20
32
|
require 'qless/server'
|
21
33
|
require 'qmore-server'
|
22
|
-
|
34
|
+
|
23
35
|
Qless::Server.client = Qless::Client.new(:host => "some-host", :port => 7000)
|
24
36
|
Qmore.client = Qless::Server.client
|
25
37
|
run Qless::Server.new(Qmore.client)
|
@@ -94,18 +106,18 @@ And I run my worker with QUEUES=\*
|
|
94
106
|
|
95
107
|
If I set my patterns like:
|
96
108
|
|
97
|
-
high\_\* (fairly unchecked)
|
98
|
-
default (fairly unchecked)
|
99
|
-
low\_\* (fairly unchecked)
|
109
|
+
high\_\* (fairly unchecked)
|
110
|
+
default (fairly unchecked)
|
111
|
+
low\_\* (fairly unchecked)
|
100
112
|
|
101
113
|
Then, the worker will scan the queues for work in this order:
|
102
114
|
high_bar, high_baz, high_foo, myqueue, otherqueue, somequeue, low_bar, low_baz, low_foo
|
103
115
|
|
104
116
|
If I set my patterns like:
|
105
117
|
|
106
|
-
high\_\* (fairly checked)
|
107
|
-
default (fairly checked)
|
108
|
-
low\_\* (fairly checked)
|
118
|
+
high\_\* (fairly checked)
|
119
|
+
default (fairly checked)
|
120
|
+
low\_\* (fairly checked)
|
109
121
|
|
110
122
|
Then, the worker will scan the queues for work in this order:
|
111
123
|
|
data/lib/qmore.rb
CHANGED
@@ -1,16 +1,43 @@
|
|
1
1
|
require 'qless'
|
2
2
|
require 'qless/worker'
|
3
|
+
require 'gem_logger'
|
4
|
+
require 'qmore/configuration'
|
5
|
+
require 'qmore/persistence'
|
3
6
|
require 'qmore/attributes'
|
4
7
|
require 'qmore/job_reserver'
|
5
8
|
|
6
9
|
module Qmore
|
7
|
-
|
10
|
+
|
8
11
|
def self.client=(client)
|
9
12
|
@client = client
|
10
13
|
end
|
11
|
-
|
14
|
+
|
12
15
|
def self.client
|
13
|
-
@client ||= Qless::Client.new
|
16
|
+
@client ||= Qless::Client.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configuration
|
20
|
+
@configuration ||= Qmore::LegacyConfiguration.new(Qmore.persistence)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.configuration=(configuration)
|
24
|
+
@configuration = configuration
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.persistence
|
28
|
+
@persistence ||= Qmore::Persistence::Redis.new(self.client.redis)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.persistence=(manager)
|
32
|
+
@persistence = manager
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.monitor
|
36
|
+
@monitor ||= Qmore::Persistence::Monitor.new(self.persistence, 120)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.monitor=(monitor)
|
40
|
+
@monitor = monitor
|
14
41
|
end
|
15
42
|
end
|
16
43
|
|
data/lib/qmore/attributes.rb
CHANGED
@@ -1,82 +1,9 @@
|
|
1
1
|
require 'multi_json'
|
2
2
|
|
3
3
|
module Qmore
|
4
|
-
DYNAMIC_QUEUE_KEY = "qmore:dynamic"
|
5
|
-
PRIORITY_KEY = "qmore:priority"
|
6
|
-
DYNAMIC_FALLBACK_KEY = "default"
|
7
|
-
|
8
4
|
module Attributes
|
9
5
|
extend self
|
10
6
|
|
11
|
-
def redis
|
12
|
-
Qmore.client.redis
|
13
|
-
end
|
14
|
-
|
15
|
-
def decode(data)
|
16
|
-
MultiJson.load(data) if data
|
17
|
-
end
|
18
|
-
|
19
|
-
def encode(data)
|
20
|
-
MultiJson.dump(data)
|
21
|
-
end
|
22
|
-
|
23
|
-
def get_dynamic_queue(key, fallback=['*'])
|
24
|
-
data = redis.hget(DYNAMIC_QUEUE_KEY, key)
|
25
|
-
queue_names = decode(data)
|
26
|
-
|
27
|
-
if queue_names.nil? || queue_names.size == 0
|
28
|
-
data = redis.hget(DYNAMIC_QUEUE_KEY, DYNAMIC_FALLBACK_KEY)
|
29
|
-
queue_names = decode(data)
|
30
|
-
end
|
31
|
-
|
32
|
-
if queue_names.nil? || queue_names.size == 0
|
33
|
-
queue_names = fallback
|
34
|
-
end
|
35
|
-
|
36
|
-
return queue_names
|
37
|
-
end
|
38
|
-
|
39
|
-
def set_dynamic_queue(key, values)
|
40
|
-
if values.nil? or values.size == 0
|
41
|
-
redis.hdel(DYNAMIC_QUEUE_KEY, key)
|
42
|
-
else
|
43
|
-
redis.hset(DYNAMIC_QUEUE_KEY, key, encode(values))
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def set_dynamic_queues(dynamic_queues)
|
48
|
-
redis.multi do
|
49
|
-
redis.del(DYNAMIC_QUEUE_KEY)
|
50
|
-
dynamic_queues.each do |k, v|
|
51
|
-
set_dynamic_queue(k, v)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def get_dynamic_queues
|
57
|
-
result = {}
|
58
|
-
queues = redis.hgetall(DYNAMIC_QUEUE_KEY)
|
59
|
-
queues.each {|k, v| result[k] = decode(v) }
|
60
|
-
result[DYNAMIC_FALLBACK_KEY] ||= ['*']
|
61
|
-
return result
|
62
|
-
end
|
63
|
-
|
64
|
-
def get_priority_buckets
|
65
|
-
priorities = Array(redis.lrange(PRIORITY_KEY, 0, -1))
|
66
|
-
priorities = priorities.collect {|p| decode(p) }
|
67
|
-
priorities << {'pattern' => 'default'} unless priorities.find {|b| b['pattern'] == 'default' }
|
68
|
-
return priorities
|
69
|
-
end
|
70
|
-
|
71
|
-
def set_priority_buckets(data)
|
72
|
-
redis.multi do
|
73
|
-
redis.del(PRIORITY_KEY)
|
74
|
-
Array(data).each do |v|
|
75
|
-
redis.rpush(PRIORITY_KEY, encode(v))
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
7
|
# Returns a list of queues to use when searching for a job.
|
81
8
|
#
|
82
9
|
# A splat ("*") means you want every queue (in alpha order) - this
|
@@ -93,7 +20,7 @@ module Qmore
|
|
93
20
|
def expand_queues(queue_patterns, real_queues)
|
94
21
|
queue_patterns = queue_patterns.dup
|
95
22
|
real_queues = real_queues.dup
|
96
|
-
|
23
|
+
|
97
24
|
matched_queues = []
|
98
25
|
|
99
26
|
while q = queue_patterns.shift
|
@@ -103,7 +30,7 @@ module Qmore
|
|
103
30
|
key = $2.strip
|
104
31
|
key = Socket.gethostname if key.size == 0
|
105
32
|
|
106
|
-
add_queues =
|
33
|
+
add_queues = Qmore.configuration.dynamic_queues[key]
|
107
34
|
add_queues.map! { |q| q.gsub!(/^!/, '') || q.gsub!(/^/, '!') } if $1
|
108
35
|
|
109
36
|
queue_patterns.concat(add_queues)
|
@@ -149,34 +76,34 @@ module Qmore
|
|
149
76
|
end
|
150
77
|
|
151
78
|
bucket_queues, remaining = [], []
|
152
|
-
|
79
|
+
|
153
80
|
patterns = bucket_pattern.split(',')
|
154
81
|
patterns.each do |pattern|
|
155
82
|
pattern = pattern.strip
|
156
|
-
|
83
|
+
|
157
84
|
if pattern =~ /^!/
|
158
85
|
negated = true
|
159
86
|
pattern = pattern[1..-1]
|
160
87
|
end
|
161
|
-
|
88
|
+
|
162
89
|
patstr = pattern.gsub(/\*/, ".*")
|
163
90
|
pattern = /^#{patstr}$/
|
164
|
-
|
165
|
-
|
91
|
+
|
92
|
+
|
166
93
|
if negated
|
167
94
|
bucket_queues -= bucket_queues.grep(pattern)
|
168
95
|
else
|
169
96
|
bucket_queues.concat(real_queues.grep(pattern))
|
170
97
|
end
|
171
|
-
|
98
|
+
|
172
99
|
end
|
173
|
-
|
100
|
+
|
174
101
|
bucket_queues.uniq!
|
175
102
|
bucket_queues.shuffle! if fairly
|
176
103
|
real_queues = real_queues - bucket_queues
|
177
|
-
|
104
|
+
|
178
105
|
result << bucket_queues
|
179
|
-
|
106
|
+
|
180
107
|
end
|
181
108
|
|
182
109
|
# insert the remaining queues at the position the default item was at (or last)
|
@@ -186,6 +113,6 @@ module Qmore
|
|
186
113
|
|
187
114
|
return result
|
188
115
|
end
|
189
|
-
|
116
|
+
|
190
117
|
end
|
191
118
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Qmore
|
2
|
+
class Configuration
|
3
|
+
DYNAMIC_FALLBACK_KEY = "default".freeze
|
4
|
+
|
5
|
+
attr_accessor :dynamic_queues, :priority_buckets
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
# Initialize the dynamic queues
|
9
|
+
self.dynamic_queues = {}
|
10
|
+
self.priority_buckets = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def dynamic_queues=(hash)
|
14
|
+
queues = DynamicQueueHash.new
|
15
|
+
queues[DYNAMIC_FALLBACK_KEY] = ['*']
|
16
|
+
hash.each do |key, values|
|
17
|
+
queues[key] = values
|
18
|
+
end
|
19
|
+
@dynamic_queues = queues
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Array] priorities
|
23
|
+
def priority_buckets=(priorities)
|
24
|
+
priorities << {'pattern' => 'default'} unless priorities.find {|b| b['pattern'] == 'default' }
|
25
|
+
@priority_buckets = priorities
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
class DynamicQueueHash < Hash
|
31
|
+
# @param key [String]
|
32
|
+
# @param values [Array]
|
33
|
+
def []=(key,values)
|
34
|
+
# remove any keys that have been set to empty hash or nil.
|
35
|
+
if values.nil? || values.size == 0
|
36
|
+
self.delete(key)
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def [](key)
|
44
|
+
super(key) || super(DYNAMIC_FALLBACK_KEY)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Legacy style configuration which loads the configuration on each access.
|
50
|
+
class LegacyConfiguration
|
51
|
+
def initialize(persistence)
|
52
|
+
@configuration = Configuration.new
|
53
|
+
@persistence = persistence
|
54
|
+
end
|
55
|
+
|
56
|
+
def dynamic_queues=(hash)
|
57
|
+
@configuration.dynamic_queues = hash
|
58
|
+
end
|
59
|
+
|
60
|
+
def priority_buckets=(priorities)
|
61
|
+
@configuration.priority_buckets = priorities
|
62
|
+
end
|
63
|
+
|
64
|
+
def priority_buckets
|
65
|
+
@configuration.priority_buckets = @persistence.read_priority_buckets
|
66
|
+
@configuration.priority_buckets
|
67
|
+
end
|
68
|
+
|
69
|
+
def dynamic_queues
|
70
|
+
@configuration.dynamic_queues = @persistence.read_dynamic_queues
|
71
|
+
@configuration.dynamic_queues
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/qmore/job_reserver.rb
CHANGED
@@ -36,23 +36,28 @@ module Qmore
|
|
36
36
|
nil
|
37
37
|
end
|
38
38
|
|
39
|
-
|
40
|
-
|
39
|
+
# @param [Qless::Client] client - client to pull queues from.
|
40
|
+
# @param [Array] regexes - array of regular expressions to match
|
41
|
+
# queues against.
|
41
42
|
def extract_queues(client, regexes)
|
42
43
|
# Cache the queues so we don't make multiple calls.
|
43
|
-
|
44
|
+
# and remove any queues that don't have work.
|
45
|
+
actual_queues = client.queues.counts.reject do |queue|
|
46
|
+
total = %w(waiting recurring depends stalled scheduled).inject(0) { |sum, state| sum += queue[state].to_i }
|
47
|
+
total == 0
|
48
|
+
end
|
44
49
|
|
45
50
|
# Grab all the actual queue names from the client.
|
46
|
-
queue_names = actual_queues.
|
51
|
+
queue_names = actual_queues.collect {|h| h['name'] }
|
47
52
|
|
48
53
|
# Match the queue names against the regexes provided.
|
49
54
|
matched_names = expand_queues(regexes, queue_names)
|
50
55
|
|
51
56
|
# Prioritize the queues.
|
52
|
-
prioritized_names = prioritize_queues(
|
57
|
+
prioritized_names = prioritize_queues(Qmore.configuration.priority_buckets, matched_names)
|
53
58
|
|
54
59
|
# collect the matched queues names in prioritized order.
|
55
|
-
prioritized_names.collect {|name|
|
60
|
+
prioritized_names.collect {|name| client.queues[name] }
|
56
61
|
end
|
57
62
|
end
|
58
63
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Qmore::Persistence
|
2
|
+
class Monitor
|
3
|
+
include GemLogger::LoggerSupport
|
4
|
+
|
5
|
+
attr_reader :updating, :interval
|
6
|
+
# @param [Qmore::persistence] persistence - responsible for reading the configuration
|
7
|
+
# from some source (redis, file, db, etc)
|
8
|
+
# @param [Integer] interval - the period, in seconds, to wait between updates to the configuration.
|
9
|
+
# defaults to 1 minute
|
10
|
+
def initialize(persistence, interval)
|
11
|
+
@persistence = persistence
|
12
|
+
@interval = interval
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
return if @updating
|
17
|
+
@updating = true
|
18
|
+
|
19
|
+
# Ensure we load the configuration once from persistence before
|
20
|
+
# the background thread.
|
21
|
+
Qmore.configuration = @persistence.load
|
22
|
+
|
23
|
+
Thread.new do
|
24
|
+
while(@updating) do
|
25
|
+
sleep @interval
|
26
|
+
begin
|
27
|
+
Qmore.configuration = @persistence.load
|
28
|
+
rescue => e
|
29
|
+
logger.error "#{e.class.name} : #{e.message}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
@updating = false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Redis
|
41
|
+
DYNAMIC_QUEUE_KEY = "qmore:dynamic".freeze
|
42
|
+
PRIORITY_KEY = "qmore:priority".freeze
|
43
|
+
|
44
|
+
attr_reader :redis
|
45
|
+
|
46
|
+
def initialize(redis)
|
47
|
+
@redis = redis
|
48
|
+
end
|
49
|
+
|
50
|
+
def decode(data)
|
51
|
+
MultiJson.load(data) if data
|
52
|
+
end
|
53
|
+
|
54
|
+
def encode(data)
|
55
|
+
MultiJson.dump(data)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a Qmore::Configuration from the underlying data storage mechanism
|
59
|
+
# @return [Qmore::Configuration]
|
60
|
+
def load
|
61
|
+
configuration = Qmore::Configuration.new
|
62
|
+
configuration.dynamic_queues = self.read_dynamic_queues
|
63
|
+
configuration.priority_buckets = self.read_priority_buckets
|
64
|
+
configuration
|
65
|
+
end
|
66
|
+
|
67
|
+
# Writes out the configuration to the underlying data storage mechanism.
|
68
|
+
# @param[Qmore::Configuration] configuration to be persisted
|
69
|
+
def write(configuration)
|
70
|
+
write_dynamic_queues(configuration.dynamic_queues)
|
71
|
+
write_priority_buckets(configuration.priority_buckets)
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_dynamic_queues
|
75
|
+
result = {}
|
76
|
+
queues = redis.hgetall(DYNAMIC_QUEUE_KEY)
|
77
|
+
queues.each {|k, v| result[k] = decode(v) }
|
78
|
+
return result
|
79
|
+
end
|
80
|
+
|
81
|
+
def read_priority_buckets
|
82
|
+
priorities = Array(redis.lrange(PRIORITY_KEY, 0, -1))
|
83
|
+
priorities = priorities.collect {|p| decode(p) }
|
84
|
+
return priorities
|
85
|
+
end
|
86
|
+
|
87
|
+
def write_priority_buckets(data)
|
88
|
+
redis.multi do
|
89
|
+
redis.del(PRIORITY_KEY)
|
90
|
+
Array(data).each do |v|
|
91
|
+
redis.rpush(PRIORITY_KEY, encode(v))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def write_dynamic_queues(dynamic_queues)
|
97
|
+
redis.multi do
|
98
|
+
redis.del(DYNAMIC_QUEUE_KEY)
|
99
|
+
dynamic_queues.each do |k, v|
|
100
|
+
redis.hset(DYNAMIC_QUEUE_KEY, k, encode(v))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|