qmore 0.6.2 → 0.6.3
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/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
|