perfectqueue 0.7.32 → 0.8.0

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.
Files changed (52) hide show
  1. data/.gitignore +6 -0
  2. data/ChangeLog +0 -62
  3. data/Gemfile +3 -0
  4. data/README.md +239 -0
  5. data/Rakefile +19 -0
  6. data/lib/perfectqueue.rb +68 -4
  7. data/lib/perfectqueue/application.rb +30 -0
  8. data/lib/perfectqueue/application/base.rb +27 -0
  9. data/lib/perfectqueue/application/dispatch.rb +73 -0
  10. data/lib/perfectqueue/application/router.rb +69 -0
  11. data/lib/perfectqueue/backend.rb +44 -47
  12. data/lib/perfectqueue/backend/rdb_compat.rb +298 -0
  13. data/lib/perfectqueue/blocking_flag.rb +84 -0
  14. data/lib/perfectqueue/client.rb +117 -0
  15. data/lib/perfectqueue/command/perfectqueue.rb +108 -323
  16. data/lib/perfectqueue/daemons_logger.rb +80 -0
  17. data/lib/perfectqueue/engine.rb +85 -123
  18. data/lib/perfectqueue/error.rb +53 -0
  19. data/lib/perfectqueue/model.rb +37 -0
  20. data/lib/perfectqueue/multiprocess.rb +31 -0
  21. data/lib/perfectqueue/multiprocess/child_process.rb +108 -0
  22. data/lib/perfectqueue/multiprocess/child_process_monitor.rb +109 -0
  23. data/lib/perfectqueue/multiprocess/fork_processor.rb +164 -0
  24. data/lib/perfectqueue/multiprocess/thread_processor.rb +123 -0
  25. data/lib/perfectqueue/queue.rb +58 -0
  26. data/lib/perfectqueue/runner.rb +39 -0
  27. data/lib/perfectqueue/signal_queue.rb +112 -0
  28. data/lib/perfectqueue/task.rb +103 -0
  29. data/lib/perfectqueue/task_metadata.rb +98 -0
  30. data/lib/perfectqueue/task_monitor.rb +189 -0
  31. data/lib/perfectqueue/task_status.rb +27 -0
  32. data/lib/perfectqueue/version.rb +1 -3
  33. data/lib/perfectqueue/worker.rb +114 -196
  34. data/perfectqueue.gemspec +24 -0
  35. data/spec/queue_spec.rb +234 -0
  36. data/spec/spec_helper.rb +44 -0
  37. data/spec/worker_spec.rb +81 -0
  38. metadata +93 -40
  39. checksums.yaml +0 -7
  40. data/README.rdoc +0 -224
  41. data/lib/perfectqueue/backend/null.rb +0 -33
  42. data/lib/perfectqueue/backend/rdb.rb +0 -181
  43. data/lib/perfectqueue/backend/simpledb.rb +0 -139
  44. data/test/backend_test.rb +0 -259
  45. data/test/cat.sh +0 -2
  46. data/test/echo.sh +0 -4
  47. data/test/exec_test.rb +0 -61
  48. data/test/fail.sh +0 -2
  49. data/test/huge.sh +0 -2
  50. data/test/stress.rb +0 -99
  51. data/test/success.sh +0 -2
  52. data/test/test_helper.rb +0 -19
@@ -0,0 +1,73 @@
1
+ #
2
+ # PerfectQueue
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectQueue
20
+ module Application
21
+
22
+ class Dispatch < Runner
23
+ # Runner interface
24
+ def initialize(task)
25
+ base = self.class.router.route(task.type)
26
+ unless base
27
+ task.retry!
28
+ raise "Unknown task type #{task.type.inspect}" # TODO error class
29
+ end
30
+ @runner = base.new(task)
31
+ super
32
+ end
33
+
34
+ attr_reader :runner
35
+
36
+ def run
37
+ @runner.run
38
+ end
39
+
40
+ def kill(reason)
41
+ @runner.kill(reason)
42
+ end
43
+
44
+ # DSL interface
45
+ class << self
46
+ def route(options)
47
+ patterns = options.keys.select {|k| !k.is_a?(Symbol) }
48
+ klasses = patterns.map {|k| options.delete(k) }
49
+ patterns.zip(klasses).each {|pattern,sym|
50
+ add_route(pattern, sym, options)
51
+ }
52
+ nil
53
+ end
54
+
55
+ def add_route(pattern, klass, options)
56
+ router.add(pattern, klass, options)
57
+ end
58
+
59
+ def router=(router)
60
+ (class<<self;self;end).instance_eval do
61
+ self.__send__(:define_method, :router) { router }
62
+ end
63
+ router
64
+ end
65
+
66
+ def router
67
+ self.router = Router.new
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,69 @@
1
+ #
2
+ # PerfectQueue
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectQueue
20
+ module Application
21
+
22
+ class Router
23
+ def initialize
24
+ @patterns = []
25
+ @cache = {}
26
+ end
27
+
28
+ def add(pattern, sym, options={})
29
+ case pattern
30
+ when Regexp
31
+ # ok
32
+ when String, Symbol
33
+ pattern = /#{Regexp.escape(pattern)}/
34
+ else
35
+ raise ArguementError, "pattern should be String or Regexp but got #{pattern.class}: #{pattern.inspect}"
36
+ end
37
+
38
+ @patterns << [pattern, sym]
39
+ end
40
+
41
+ def route(type)
42
+ if @cache.has_key?(type)
43
+ return @cache[type]
44
+ end
45
+
46
+ @patterns.each {|(pattern,sym)|
47
+ if pattern.match(type)
48
+ base = resolve_application_base(sym)
49
+ return @cache[type] = base
50
+ end
51
+ }
52
+ return @cache[type] = nil
53
+ end
54
+ attr_reader :patterns
55
+
56
+ private
57
+ def resolve_application_base(sym)
58
+ case sym
59
+ when Symbol
60
+ self.class.const_get(sym)
61
+ else
62
+ sym
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+
@@ -1,52 +1,49 @@
1
+ #
2
+ # PerfectQueue
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
1
18
 
2
19
  module PerfectQueue
3
-
4
-
5
- class Task
6
- def initialize(id, created_at, data, resource=nil)
7
- @id = id
8
- @created_at = created_at
9
- @data = data
10
- @resource = resource
20
+ module Backend
21
+ def self.new_backend(client, config)
22
+ case config[:type]
23
+ when nil
24
+ raise ConfigError, "'type' option is not set"
25
+ when 'rdb_compat'
26
+ require_backend('rdb_compat')
27
+ RDBCompatBackend.new(client, config)
28
+ end
29
+ end
30
+
31
+ def self.require_backend(fname)
32
+ require File.expand_path("backend/#{fname}", File.dirname(__FILE__))
33
+ end
34
+ end
35
+
36
+ module BackendHelper
37
+ def initialize(client, config)
38
+ @client = client
39
+ @config = config
40
+ end
41
+
42
+ attr_reader :client
43
+
44
+ def close
45
+ # do nothing by default
46
+ end
11
47
  end
12
-
13
- attr_reader :id, :created_at, :data, :resource
14
- end
15
-
16
-
17
- class CanceledError < RuntimeError
18
- end
19
-
20
-
21
- class Backend
22
- # => list {|id,created_at,data,timeout| ... }
23
- def list(&block)
24
- end
25
-
26
- # => token, task
27
- def acquire(timeout, now=Time.now.to_i)
28
- end
29
-
30
- # => true (success) or false (canceled)
31
- def finish(token, delete_timeout=3600, now=Time.now.to_i)
32
- end
33
-
34
- # => nil
35
- def update(token, timeout)
36
- end
37
-
38
- # => true (success) or false (not found, canceled or finished)
39
- def cancel(id, delete_timeout=3600, now=Time.now.to_i)
40
- end
41
-
42
- # => true (success) or nil (already exists)
43
- def submit(id, data, time=Time.now.to_i, resource=nil)
44
- end
45
-
46
- def close
47
- end
48
- end
49
-
50
-
51
48
  end
52
49
 
@@ -0,0 +1,298 @@
1
+ #
2
+ # PerfectQueue
3
+ #
4
+ # Copyright (C) 2012 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module PerfectQueue
20
+ module Backend
21
+ class RDBCompatBackend
22
+ include BackendHelper
23
+
24
+ class Token < Struct.new(:key)
25
+ end
26
+
27
+ def initialize(client, config)
28
+ super
29
+
30
+ require 'sequel'
31
+ url = config[:url]
32
+ @table = config[:table]
33
+ unless @table
34
+ raise ConfigError, ":table option is required"
35
+ end
36
+
37
+ #password = config[:password]
38
+ #user = config[:user]
39
+ @db = Sequel.connect(url, :max_connections=>1)
40
+ @mutex = Mutex.new
41
+
42
+ connect {
43
+ # connection test
44
+ }
45
+
46
+ @sql = <<SQL
47
+ SELECT id, timeout, data, created_at, resource
48
+ FROM `#{@table}`
49
+ LEFT JOIN (
50
+ SELECT resource AS res, COUNT(1) AS running
51
+ FROM `#{@table}` AS T
52
+ WHERE timeout > ? AND created_at IS NOT NULL AND resource IS NOT NULL
53
+ GROUP BY resource
54
+ ) AS R ON resource = res
55
+ WHERE timeout <= ? AND (running IS NULL OR running < #{MAX_RESOURCE})
56
+ ORDER BY timeout ASC LIMIT #{MAX_SELECT_ROW}
57
+ SQL
58
+
59
+ # sqlite doesn't support SELECT ... FOR UPDATE but
60
+ # sqlite doesn't need it because the db is not shared
61
+ unless url.split('//',2)[0].to_s.include?('sqlite')
62
+ @sql << 'FOR UPDATE'
63
+ end
64
+ end
65
+
66
+ attr_reader :db
67
+
68
+ MAX_SELECT_ROW = 8
69
+ MAX_RESOURCE = (ENV['PQ_MAX_RESOURCE'] || 4).to_i
70
+ #KEEPALIVE = 10
71
+ MAX_RETRY = 10
72
+
73
+ def init_database(options)
74
+ sql = %[
75
+ CREATE TABLE IF NOT EXISTS `#{@table}` (
76
+ id VARCHAR(256) NOT NULL,
77
+ timeout INT NOT NULL,
78
+ data BLOB NOT NULL,
79
+ created_at INT,
80
+ resource VARCHAR(256),
81
+ PRIMARY KEY (id)
82
+ );]
83
+ connect {
84
+ @db.run sql
85
+ }
86
+ end
87
+
88
+ # => TaskStatus
89
+ def get_task_metadata(key, options)
90
+ now = (options[:now] || Time.now).to_i
91
+
92
+ connect {
93
+ row = @db.fetch("SELECT timeout, data, created_at, resource FROM `#{@table}` LIMIT 1").first
94
+ unless row
95
+ raise NotFoundError, "task key=#{key} does no exist"
96
+ end
97
+ attributes = create_attributes(now, row)
98
+ return TaskMetadata.new(@client, key, attributes)
99
+ }
100
+ end
101
+
102
+ # => AcquiredTask
103
+ def preempt(key, alive_time, options)
104
+ raise NotSupportedError.new("preempt is not supported by rdb_compat backend")
105
+ end
106
+
107
+ # yield [TaskWithMetadata]
108
+ def list(options, &block)
109
+ now = (options[:now] || Time.now).to_i
110
+
111
+ connect {
112
+ #@db.fetch("SELECT id, timeout, data, created_at, resource FROM `#{@table}` WHERE !(created_at IS NULL AND timeout <= ?) ORDER BY timeout ASC;", now) {|row|
113
+ @db.fetch("SELECT id, timeout, data, created_at, resource FROM `#{@table}` ORDER BY timeout ASC;", now) {|row|
114
+ attributes = create_attributes(now, row)
115
+ task = TaskWithMetadata.new(@client, row[:id], attributes)
116
+ yield task
117
+ }
118
+ }
119
+ end
120
+
121
+ # => Task
122
+ def submit(key, type, data, options)
123
+ now = (options[:now] || Time.now).to_i
124
+ run_at = (options[:run_at] || now).to_i
125
+ user = options[:user]
126
+ priority = options[:priority] # not supported
127
+ data ||= {}
128
+ data['type'] = type
129
+
130
+ connect {
131
+ begin
132
+ n = @db["INSERT INTO `#{@table}` (id, timeout, data, created_at, resource) VALUES (?, ?, ?, ?, ?);", key, run_at, data.to_json, now, user].insert
133
+ return Task.new(@client, key)
134
+ rescue Sequel::DatabaseError
135
+ raise AlreadyExistsError, "task key=#{key} already exists"
136
+ end
137
+ }
138
+ end
139
+
140
+ # => [AcquiredTask]
141
+ def acquire(alive_time, max_acquire, options)
142
+ now = (options[:now] || Time.now).to_i
143
+ next_timeout = now + alive_time
144
+
145
+ connect {
146
+ while true
147
+ rows = 0
148
+ @db.transaction do
149
+ @db.fetch(@sql, now, now) {|row|
150
+ unless row[:created_at]
151
+ # finished task
152
+ @db["DELETE FROM `#{@table}` WHERE id=?;", row[:id]].delete
153
+
154
+ else
155
+ ## optimistic lock is not needed because the row is locked for update
156
+ #n = @db["UPDATE `#{@table}` SET timeout=? WHERE id=? AND timeout=?", timeout, row[:id], row[:timeout]].update
157
+ n = @db["UPDATE `#{@table}` SET timeout=? WHERE id=?", next_timeout, row[:id]].update
158
+ if n > 0
159
+ attributes = create_attributes(nil, row)
160
+ task_token = Token.new(row[:id])
161
+ task = AcquiredTask.new(@client, row[:id], attributes, task_token)
162
+ return [task]
163
+ end
164
+ end
165
+
166
+ rows += 1
167
+ }
168
+ end
169
+ break nil if rows < MAX_SELECT_ROW
170
+ end
171
+ }
172
+ end
173
+
174
+ # => nil
175
+ def cancel_request(key, options)
176
+ now = (options[:now] || Time.now).to_i
177
+
178
+ # created_at=-1 means cancel_requested
179
+ connect {
180
+ n = @db["UPDATE `#{@table}` SET created_at=-1 WHERE id=? AND created_at IS NOT NULL;", key].update
181
+ if n <= 0
182
+ raise AlreadyFinishedError, "task key=#{key} does not exist or already finished."
183
+ end
184
+ }
185
+ nil
186
+ end
187
+
188
+ def force_finish(key, retention_time, options)
189
+ finish(Token.new(key), retention_time, options)
190
+ end
191
+
192
+ # => nil
193
+ def finish(task_token, retention_time, options)
194
+ now = (options[:now] || Time.now).to_i
195
+ delete_timeout = now + retention_time
196
+ key = task_token.key
197
+
198
+ connect {
199
+ n = @db["UPDATE `#{@table}` SET timeout=?, created_at=NULL, resource=NULL WHERE id=? AND created_at IS NOT NULL;", delete_timeout, key].update
200
+ if n <= 0
201
+ raise AlreadyFinishedError, "task key=#{key} does not exist or already finished."
202
+ end
203
+ }
204
+ nil
205
+ end
206
+
207
+ # => nil
208
+ def heartbeat(task_token, alive_time, options)
209
+ now = (options[:now] || Time.now).to_i
210
+ next_timeout = now + alive_time
211
+ key = task_token.key
212
+
213
+ connect {
214
+ n = @db["UPDATE `#{@table}` SET timeout=? WHERE id=? AND created_at IS NOT NULL;", next_timeout, key].update
215
+ if n <= 0
216
+ row = @db.fetch("SELECT id, created_at FROM `#{@table}` WHERE id=? LIMIT 1", key).first
217
+ if row == nil
218
+ raise AlreadyFinishedError, "task key=#{key} already finished."
219
+ elsif row[:created_at] == -1
220
+ raise CancelRequestedError, "task key=#{key} is cancel requested."
221
+ else
222
+ raise AlreadyFinishedError, "task key=#{key} already finished."
223
+ end
224
+ end
225
+ }
226
+ nil
227
+ end
228
+
229
+ protected
230
+ def connect(&block)
231
+ #now = Time.now.to_i
232
+ @mutex.synchronize do
233
+ #if now - @last_time > KEEPALIVE
234
+ # @db.disconnect
235
+ #end
236
+ #@last_time = now
237
+ retry_count = 0
238
+ begin
239
+ block.call
240
+ rescue
241
+ # workaround for "Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction" error
242
+ if $!.to_s.include?('try restarting transaction')
243
+ err = ([$!] + $!.backtrace.map {|bt| " #{bt}" }).join("\n")
244
+ retry_count += 1
245
+ if retry_count < MAX_RETRY
246
+ STDERR.puts err + "\n retrying."
247
+ sleep 0.5
248
+ retry
249
+ else
250
+ STDERR.puts err + "\n abort."
251
+ end
252
+ end
253
+ raise
254
+ ensure
255
+ @db.disconnect
256
+ end
257
+ end
258
+ end
259
+
260
+ def create_attributes(now, row)
261
+ if row[:created_at] === nil
262
+ created_at = nil # unknown creation time
263
+ status = TaskStatus::FINISHED
264
+ elsif row[:created_at] == -1
265
+ created_at = 0
266
+ status = TaskStatus::CANCEL_REQUESTED
267
+ elsif now && row[:timeout] < now
268
+ created_at = row[:created_at]
269
+ status = TaskStatus::WAITING
270
+ else
271
+ created_at = row[:created_at]
272
+ status = TaskStatus::RUNNING
273
+ end
274
+
275
+ begin
276
+ data = JSON.parse(row[:data] || '{}')
277
+ rescue
278
+ data = {}
279
+ end
280
+
281
+ type = data['type'] || ''
282
+
283
+ attributes = {
284
+ :status => status,
285
+ :created_at => created_at,
286
+ :data => data,
287
+ :type => type,
288
+ :user => row[:resource],
289
+ :timeout => row[:timeout],
290
+ :message => nil, # not supported
291
+ :node => nil, # not supported
292
+ }
293
+ end
294
+
295
+ end
296
+ end
297
+ end
298
+