process_pool 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,6 +1,7 @@
1
1
  disable_system_gems
2
2
 
3
3
  gem 'json'
4
+ gem 'jeweler'
4
5
 
5
6
  only :test do
6
7
  gem 'shoulda'
data/TODO ADDED
@@ -0,0 +1,12 @@
1
+ Extensions stuff:
2
+ - around extensions
3
+ - handling :stop
4
+ - helpers for anonymous extensions
5
+ - a couple of generic extensions:
6
+ - restarting active record connections on worker startup
7
+ - benchmarking
8
+ - restarting failed tasks
9
+
10
+ Queue:
11
+ - posix shared memory or message queue backend
12
+ - redis backend
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.1.3
@@ -0,0 +1,20 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'init')
2
+
3
+ class AnonymousExtension < BaseWorkerExtension
4
+
5
+ SUPPORTED_EXTENSION_METHODS = [:before, :after, :around, :startup, :shutdown]
6
+
7
+ def initialize(method, &block)
8
+ raise ArgumentError.new("Unknown method: #{method}") unless SUPPORTED_EXTENSION_METHODS.include?(method)
9
+
10
+ @method = method
11
+ @block = lambda(&block)
12
+ end
13
+
14
+ SUPPORTED_EXTENSION_METHODS.each do |method_name|
15
+ define_method(method_name) do |*args|
16
+ @block.call(*args) if method_name == @method
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,35 @@
1
+ # base class for process pool extensions
2
+ #
3
+ # it is not required that the extensions inherit from this class,
4
+ # it's here mostly for convenience and documentation sake
5
+ class BaseWorkerExtension
6
+
7
+ attr_accessor :logger, :process_pool
8
+
9
+ # will be called before task gets executed
10
+ # thowing :stop will halt the task execution process,
11
+ # no other filters will be executed for this task
12
+ def before(task)
13
+ end
14
+
15
+ # will be called after task gets executed
16
+ def after(task, result)
17
+ end
18
+
19
+ # will be called before task gets executed
20
+ # around is responsible for calling task.run,
21
+ # failing to do so might result in unpredictable behavior
22
+ # and failing to run a ProcessPool::EndTask will result in
23
+ # the process never ending
24
+ def around(task)
25
+ end
26
+
27
+ # will be called on worker startup, before executing any tasks
28
+ def startup
29
+ end
30
+
31
+ # will be called on worker shutdown
32
+ def shutdown
33
+ end
34
+
35
+ end
@@ -4,3 +4,5 @@ require 'json'
4
4
  require File.expand_path(File.join(File.dirname(__FILE__), 'simple_logger'))
5
5
  require File.expand_path(File.join(File.dirname(__FILE__), 'simple_queue'))
6
6
  require File.expand_path(File.join(File.dirname(__FILE__), 'process_pool'))
7
+ require File.expand_path(File.join(File.dirname(__FILE__), 'base_worker_extension'))
8
+ require File.expand_path(File.join(File.dirname(__FILE__), 'anonymous_extension'))
@@ -13,14 +13,25 @@ class ProcessPool
13
13
  self.workers_count = workers_count
14
14
  self.queue = queue
15
15
  self.worker_pids = []
16
+ self.extensions = []
16
17
  end
17
18
 
18
19
  def schedule(job_class, *args)
19
20
  raise InvalidStateError.new('Can not add more jobs after shut down was called') if is_shutdown?
20
- logger.debug("Scheduling task #{job_class}(#{args})")
21
+ logger.debug("Scheduling task #{job_class}(#{args.collect{ |a| a.inspect }.join(', ')})")
21
22
  push_task(job_class, args)
22
23
  end
23
24
 
25
+ def register_extension(extension)
26
+ raise InvalidStateError.new('Can not register extensions once the pool is started.') unless is_stopped?
27
+ raise ArgumentError.new('extension can not be nil') unless extension
28
+
29
+ extension.process_pool = self if extension.respond_to?(:process_pool=)
30
+ extension.logger = logger if extension.respond_to?(:logger=)
31
+
32
+ extensions << extension
33
+ end
34
+
24
35
  def start
25
36
  raise InvalidStateError.new('Can not start a pool more than once') unless is_stopped?
26
37
  logger.info("Starting process pool")
@@ -28,12 +39,16 @@ class ProcessPool
28
39
 
29
40
  workers_count.times do
30
41
  pid = fork do
42
+ run_extensions(:startup)
43
+ at_exit { run_extensions(:shutdown) }
31
44
  child_queue = get_child_queue()
32
45
  while true
33
46
  task_class, args = child_queue.pop
34
47
  begin
35
48
  task = get_task_class(task_class).new(*args)
36
- task.run
49
+ run_extensions(:before, task) unless task.is_a?(EndTask)
50
+ result = task.run
51
+ run_extensions(:after, task, result)
37
52
  rescue => e
38
53
  logger.warn("Exception occurred while executing task #{task_class}(#{args}): #{e}")
39
54
  end
@@ -73,7 +88,7 @@ class ProcessPool
73
88
 
74
89
  protected
75
90
 
76
- attr_accessor :state, :logger, :queue, :worker_pids
91
+ attr_accessor :state, :logger, :queue, :worker_pids, :extensions
77
92
  attr_writer :workers_count
78
93
 
79
94
  def push_task(job_class, args)
@@ -93,6 +108,14 @@ class ProcessPool
93
108
  Object.module_eval("::#{$1}", __FILE__, __LINE__)
94
109
  end
95
110
 
111
+ def run_extensions(method, *args)
112
+ extensions.each do |extension|
113
+ if extension.respond_to?(method)
114
+ extension.send(method, *args)
115
+ end
116
+ end
117
+ end
118
+
96
119
  class EndTask
97
120
  def run
98
121
  exit(0)
@@ -43,10 +43,14 @@ class SimpleQueue
43
43
 
44
44
  def self.create
45
45
  file = Tempfile.new('simple_queue')
46
- file.puts [].to_json
47
- uri = file.path
48
- file.close
49
- return new(uri)
46
+ path = File.expand_path(file.path)
47
+ file.close!
48
+
49
+ File.open(path, 'w+') do |f|
50
+ f.puts [].to_json
51
+ end
52
+
53
+ return new(path)
50
54
  end
51
55
 
52
56
  def self.get(uri)
@@ -5,15 +5,16 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{process_pool}
8
- s.version = "0.1.2"
8
+ s.version = "0.1.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Adam Pohorecki"]
12
- s.date = %q{2010-01-20}
12
+ s.date = %q{2010-02-18}
13
13
  s.email = %q{adam@pohorecki.pl}
14
14
  s.extra_rdoc_files = [
15
15
  "LICENSE",
16
- "README"
16
+ "README",
17
+ "TODO"
17
18
  ]
18
19
  s.files = [
19
20
  ".gitignore",
@@ -21,12 +22,16 @@ Gem::Specification.new do |s|
21
22
  "LICENSE",
22
23
  "README",
23
24
  "Rakefile",
25
+ "TODO",
24
26
  "VERSION",
27
+ "lib/anonymous_extension.rb",
28
+ "lib/base_worker_extension.rb",
25
29
  "lib/init.rb",
26
30
  "lib/process_pool.rb",
27
31
  "lib/simple_logger.rb",
28
32
  "lib/simple_queue.rb",
29
33
  "process_pool.gemspec",
34
+ "test/anonymous_extension_test.rb",
30
35
  "test/process_pool_test.rb",
31
36
  "test/simple_queue_test.rb",
32
37
  "test/test_helper.rb"
@@ -37,7 +42,8 @@ Gem::Specification.new do |s|
37
42
  s.rubygems_version = %q{1.3.5}
38
43
  s.summary = %q{ProcessPool with interchangeable job queue backends for Ruby}
39
44
  s.test_files = [
40
- "test/process_pool_test.rb",
45
+ "test/anonymous_extension_test.rb",
46
+ "test/process_pool_test.rb",
41
47
  "test/simple_queue_test.rb",
42
48
  "test/test_helper.rb"
43
49
  ]
@@ -0,0 +1,41 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+
3
+ class AnonymousExtensionTest < Test::Unit::TestCase
4
+
5
+ should "raise ArgumentError when called with an unsupported method" do
6
+ assert_raises ArgumentError do
7
+ AnonymousExtension.new(:unsupported_method) {}
8
+ end
9
+ end
10
+
11
+ AnonymousExtension::SUPPORTED_EXTENSION_METHODS.each do |method_name|
12
+ should "not raise anything when called with #{method_name}" do
13
+ assert_nothing_raised do
14
+ AnonymousExtension.new(method_name) {}
15
+ end
16
+ end
17
+
18
+ context "with method #{method_name}" do
19
+ setup do
20
+ @value = 0
21
+ @ext = AnonymousExtension.new(method_name) { |x| @value = x }
22
+ end
23
+
24
+ should "execute the block when right method is called" do
25
+ @ext.send(method_name, 1)
26
+ assert_equal 1, @value
27
+ end
28
+
29
+ AnonymousExtension::SUPPORTED_EXTENSION_METHODS.each do |other_method|
30
+ next if method_name == other_method
31
+
32
+ should "not execute the block when #{other_method} is called" do
33
+ @ext.send(other_method, 1)
34
+ assert_equal 0, @value
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -65,6 +65,21 @@ class WrongArgumentsTask
65
65
  end
66
66
  end
67
67
 
68
+ class WriteToFileTask
69
+
70
+ def initialize(path, n)
71
+ @path = path
72
+ @n = n
73
+ end
74
+
75
+ def run
76
+ File.open(@path, 'a+') do |file|
77
+ file.puts "task #{@n}"
78
+ end
79
+ end
80
+
81
+ end
82
+
68
83
  class ProcessPoolTest < Test::Unit::TestCase
69
84
 
70
85
  context "state" do
@@ -102,6 +117,21 @@ class ProcessPoolTest < Test::Unit::TestCase
102
117
  end
103
118
  end
104
119
 
120
+ should "raise InvalidStateError when calling register_extension on a started pool" do
121
+ @pool.start
122
+ assert_raises ProcessPool::InvalidStateError do
123
+ @pool.register_extension(BaseWorkerExtension.new)
124
+ end
125
+ end
126
+
127
+ should "raise InvalidStateError when calling register_extension on a shutdown pool" do
128
+ @pool.start
129
+ @pool.shutdown
130
+ assert_raises ProcessPool::InvalidStateError do
131
+ @pool.register_extension(BaseWorkerExtension.new)
132
+ end
133
+ end
134
+
105
135
  should "raise InvalidStateError when calling start twice" do
106
136
  @pool.start
107
137
  assert_raises ProcessPool::InvalidStateError do
@@ -158,7 +188,7 @@ class ProcessPoolTest < Test::Unit::TestCase
158
188
  context :shutdown do
159
189
  setup do
160
190
  @queue = SampleQueue.new
161
- @pool = ProcessPool.new(3, @queue, SimpleLogger.new(:debug)) # no workers so that nothing processes the queue
191
+ @pool = ProcessPool.new(3, @queue, SimpleLogger.new(:debug))
162
192
  @pool.schedule(SampleTask)
163
193
  @pool.stubs(:fork => 1)
164
194
  @pool.start
@@ -230,4 +260,185 @@ class ProcessPoolTest < Test::Unit::TestCase
230
260
 
231
261
  end
232
262
 
263
+ def self.should_write_lines(*lines)
264
+ should "write lines #{lines.collect{|line| line.inspect}.join(', ')} to file" do
265
+ text = open(@path).read
266
+ file_lines = text.split("\n").collect { |line| line.strip }
267
+ assert_equal lines, file_lines
268
+ end
269
+ end
270
+
271
+ def write_to_shared_file(str)
272
+ File.open(@path, 'a+') do |file|
273
+ file.puts str
274
+ end
275
+ end
276
+
277
+ context "extensions" do
278
+ setup do
279
+ @logger = SimpleLogger.new(:debug)
280
+ @pool = ProcessPool.new(1, SimpleQueue.create, @logger)
281
+ @shared_file = Tempfile.new('test')
282
+ @path = @shared_file.path
283
+ end
284
+
285
+ teardown do
286
+ @shared_file.close
287
+ end
288
+
289
+ should "raise ArgumentError if called with nil" do
290
+ assert_raises ArgumentError do
291
+ @pool.register_extension nil
292
+ end
293
+ end
294
+
295
+ should "inject process_pool to the extension" do
296
+ extension = BaseWorkerExtension.new
297
+ assert_nil extension.process_pool
298
+ @pool.register_extension(extension)
299
+ assert_equal @pool, extension.process_pool
300
+ end
301
+
302
+ should "inject logger to the extension" do
303
+ extension = BaseWorkerExtension.new
304
+ assert_nil extension.logger
305
+ @pool.register_extension(extension)
306
+ assert_equal @logger, extension.logger
307
+ end
308
+
309
+ context "with no extensions" do
310
+ setup do
311
+ @pool.schedule(WriteToFileTask, @path, 1)
312
+ @pool.schedule(WriteToFileTask, @path, 2)
313
+
314
+ @pool.start
315
+ @pool.shutdown
316
+ end
317
+
318
+ should_write_lines "task 1", "task 2"
319
+ end
320
+
321
+ context "with before extension" do
322
+ setup do
323
+ @pool.schedule(WriteToFileTask, @path, 1)
324
+ @pool.schedule(WriteToFileTask, @path, 2)
325
+
326
+ @pool.register_extension AnonymousExtension.new(:before) { |t| write_to_shared_file("before") }
327
+
328
+ @pool.start
329
+ @pool.shutdown
330
+ end
331
+
332
+ should_write_lines "before", "task 1", "before", "task 2"
333
+ end
334
+
335
+ context "with after extension" do
336
+ setup do
337
+ @pool.schedule(WriteToFileTask, @path, 1)
338
+ @pool.schedule(WriteToFileTask, @path, 2)
339
+
340
+ @pool.register_extension AnonymousExtension.new(:after) { |t, r| write_to_shared_file("after") }
341
+
342
+ @pool.start
343
+ @pool.shutdown
344
+ end
345
+
346
+ should_write_lines "task 1", "after", "task 2", "after"
347
+ end
348
+
349
+ context "with around extension" do
350
+ setup do
351
+ @pool.schedule(WriteToFileTask, @path, 1)
352
+ @pool.schedule(WriteToFileTask, @path, 2)
353
+
354
+ @pool.register_extension AnonymousExtension.new(:around) { |t|
355
+ write_to_shared_file("around 1")
356
+ t.run
357
+ write_to_shared_file("around 2")
358
+ }
359
+
360
+ @pool.start
361
+ @pool.shutdown
362
+ end
363
+
364
+ should_write_lines "around 1", "task 1", "around 2", "around 1", "task 2", "around 2"
365
+ end
366
+
367
+ context "with multiple around extensions" do
368
+ setup do
369
+ @pool.schedule(WriteToFileTask, @path, 1)
370
+ @pool.schedule(WriteToFileTask, @path, 2)
371
+
372
+ @pool.register_extension AnonymousExtension.new(:around) { |t|
373
+ write_to_shared_file("around a")
374
+ t.run
375
+ write_to_shared_file("around b")
376
+ }
377
+
378
+ @pool.register_extension AnonymousExtension.new(:around) { |t|
379
+ write_to_shared_file("around 1")
380
+ t.run
381
+ write_to_shared_file("around 2")
382
+ }
383
+
384
+ @pool.start
385
+ @pool.shutdown
386
+ end
387
+
388
+ should_write_lines "around a", "around 1", "task 1", "around 2", "around b", "around a", "around 1", "task 2", "around 2", "around b"
389
+ end
390
+
391
+ context "with startup extension" do
392
+ setup do
393
+ @pool.schedule(WriteToFileTask, @path, 1)
394
+ @pool.schedule(WriteToFileTask, @path, 2)
395
+
396
+ @pool.register_extension AnonymousExtension.new(:startup) { write_to_shared_file("startup") }
397
+
398
+ @pool.start
399
+ @pool.shutdown
400
+ end
401
+
402
+ should_write_lines "startup", "task 1", "task 2"
403
+ end
404
+
405
+ context "with shutdown extension" do
406
+ setup do
407
+ @pool.schedule(WriteToFileTask, @path, 1)
408
+ @pool.schedule(WriteToFileTask, @path, 2)
409
+
410
+ @pool.register_extension AnonymousExtension.new(:shutdown) { write_to_shared_file("shutdown") }
411
+
412
+ @pool.start
413
+ @pool.shutdown
414
+ end
415
+
416
+ should_write_lines "task 1", "task 2", "shutdown"
417
+ end
418
+
419
+ context "with all kinds of extensions" do
420
+ setup do
421
+ @pool.schedule(WriteToFileTask, @path, 1)
422
+ @pool.schedule(WriteToFileTask, @path, 2)
423
+
424
+ @pool.register_extension AnonymousExtension.new(:around) { |t|
425
+ write_to_shared_file("around 1")
426
+ t.run
427
+ write_to_shared_file("around 2")
428
+ }
429
+
430
+ @pool.register_extension AnonymousExtension.new(:before) { |t| write_to_shared_file("before") }
431
+ @pool.register_extension AnonymousExtension.new(:after) { |t, r| write_to_shared_file("after") }
432
+
433
+ @pool.register_extension AnonymousExtension.new(:startup) { write_to_shared_file("startup") }
434
+ @pool.register_extension AnonymousExtension.new(:shutdown) { write_to_shared_file("shutdown") }
435
+
436
+ @pool.start
437
+ @pool.shutdown
438
+ end
439
+
440
+ should_write_lines "startup", "before", "around 1", "task 1", "around 2", "after", "before", "around 1", "task 2", "around 2", "after", "shutdown"
441
+ end
442
+ end
443
+
233
444
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Pohorecki
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-20 00:00:00 +01:00
12
+ date: 2010-02-18 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -31,18 +31,23 @@ extensions: []
31
31
  extra_rdoc_files:
32
32
  - LICENSE
33
33
  - README
34
+ - TODO
34
35
  files:
35
36
  - .gitignore
36
37
  - Gemfile
37
38
  - LICENSE
38
39
  - README
39
40
  - Rakefile
41
+ - TODO
40
42
  - VERSION
43
+ - lib/anonymous_extension.rb
44
+ - lib/base_worker_extension.rb
41
45
  - lib/init.rb
42
46
  - lib/process_pool.rb
43
47
  - lib/simple_logger.rb
44
48
  - lib/simple_queue.rb
45
49
  - process_pool.gemspec
50
+ - test/anonymous_extension_test.rb
46
51
  - test/process_pool_test.rb
47
52
  - test/simple_queue_test.rb
48
53
  - test/test_helper.rb
@@ -75,6 +80,7 @@ signing_key:
75
80
  specification_version: 3
76
81
  summary: ProcessPool with interchangeable job queue backends for Ruby
77
82
  test_files:
83
+ - test/anonymous_extension_test.rb
78
84
  - test/process_pool_test.rb
79
85
  - test/simple_queue_test.rb
80
86
  - test/test_helper.rb