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