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 CHANGED
@@ -1,3 +1,10 @@
1
+ 0.6.3 (06/13/2014)
2
+ ------------------
3
+
4
+ Prevents updating configuration every time we want to retrieve queues.
5
+ (see monitoring in README)
6
+ Optimizations to job reserving - doesn't look at empty queues.
7
+
1
8
  0.6.2 (03/10/2014)
2
9
  ------------------
3
10
 
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
 
@@ -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
 
@@ -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 = get_dynamic_queue(key)
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
@@ -36,23 +36,28 @@ module Qmore
36
36
  nil
37
37
  end
38
38
 
39
- protected
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
- actual_queues = client.queues
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.counts.collect {|h| h['name'] }
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(get_priority_buckets, matched_names)
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| actual_queues[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