queue_to_the_future 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Devin Christensen
1
+ Copyright (c) 2009-2010 Devin Christensen
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ to keep overhead to a minimum.
17
17
 
18
18
  == Copyright
19
19
 
20
- Copyright (c) 2010 Devin Christensen. See LICENSE for details.
20
+ Copyright (c) 2009-2010 Devin Christensen. See LICENSE for details.
data/Rakefile CHANGED
@@ -37,7 +37,9 @@ task :default => :spec
37
37
 
38
38
  begin
39
39
  require 'yard'
40
- YARD::Rake::YardocTask.new
40
+ YARD::Rake::YardocTask.new do |t|
41
+ t.options = ["--files", "LICENSE"]
42
+ end
41
43
  rescue LoadError
42
44
  task :yardoc do
43
45
  abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.1
@@ -1,10 +1,16 @@
1
1
  require 'thread'
2
+ require 'mutex_m'
3
+ require 'singleton'
2
4
 
3
5
  require 'queue_to_the_future/coordinator'
4
- require 'queue_to_the_future/worker'
5
6
  require 'queue_to_the_future/job'
6
7
 
7
8
  module QueueToTheFuture
9
+ # Returns the current version of QueueToTheFuture
10
+ def self.VERSION
11
+ @@version ||= open(File.join(File.dirname(__FILE__), '..', 'VERSION')).read.strip
12
+ end
13
+
8
14
  # The maximum number of workers to create for processing jobs.
9
15
  #
10
16
  # @return [Fixnum] Default is 15
@@ -1,68 +1,78 @@
1
- require 'singleton'
2
-
3
1
  module QueueToTheFuture
2
+
4
3
  # The coordinator schedules jobs and maintains the workforce size to match. As jobs
5
4
  # get added the coordinator creates workers to complete them. The number of
6
5
  # workers created will never exceed {QueueToTheFuture::maximum_workers}.
7
6
  class Coordinator
8
7
  include Singleton
9
8
 
9
+ # Convenience class level methods that proxy to the singleton instance
10
+ %w/schedule queue_length workforce_size shutdown/.each do |meth|
11
+ instance_eval("def #{meth}(*args); instance.#{meth}(*args); end")
12
+ end
13
+
10
14
  # Creates the coordinator.
11
15
  #
12
16
  # Note: This is a singleton class. To access the instance use
13
17
  # {http://ruby-doc.org/stdlib/libdoc/singleton/rdoc/index.html Coordinator::instance}.
14
18
  def initialize
15
- @job_queue = []
16
- @workforce = []
17
- @lock = Mutex.new
18
- end
19
-
20
- # The next scheduled job.
21
- #
22
- # @return [QueueToTheFuture::Job] Next job
23
- def next_job
24
- synchronize { @job_queue.shift }
19
+ @job_queue = Queue.new
20
+ @workforce = []
21
+
22
+ @workforce.extend(Mutex_m)
25
23
  end
26
24
 
27
- # The number of jobs waiting to be completed.
25
+ # The current length of the job queue
28
26
  #
29
- # @return [Fixnum] Size of job queue
30
- def job_count
31
- synchronize { @job_queue.size }
27
+ # @return [Fixnum] Length of job queue
28
+ def queue_length
29
+ @job_queue.length
32
30
  end
33
31
 
34
32
  # The number of workers being utilized to complete jobs.
35
33
  #
36
34
  # @return [Fixnum] Number of workers
37
35
  def workforce_size
38
- synchronize { @workforce.size }
36
+ @workforce.size
39
37
  end
40
38
 
41
- # Removes a worker from the workforce.
39
+ # Append a QueueToTheFuture::Job to the queue.
42
40
  #
43
- # @param [QueueToTheFuture::Worker] worker
44
- def relieve(worker)
45
- synchronize { @workforce -= [worker] }
46
- end
47
-
48
- # Append a job to the job queue.
41
+ # If there are workers available, the first available worker will be
42
+ # woken up to perform the QueueToTheFuture::Job. If there are no
43
+ # available workers, one will be created as long as doing so will
44
+ # not cause the workforce to exceed QueueToTheFuture::maximum_workers.
49
45
  #
50
46
  # @param [QueueToTheFuture::Job] job
51
47
  def schedule(job)
52
- synchronize do
53
- @job_queue.push(job)
54
-
55
- if @workforce.size < QueueToTheFuture.maximum_workers && @workforce.size < @job_queue.size
56
- @workforce.push Worker.new(@workforce.size)
57
- end
48
+
49
+ # If we can't get a lock on the @workforce then the Coordinator is most likely shutting down.
50
+ # We want to skip creating new workers in this case.
51
+ if @job_queue.num_waiting == 0 && @workforce.size < QueueToTheFuture.maximum_workers && @workforce.mu_try_lock
52
+ @workforce.push Thread.new() { while job = @job_queue.shift; job.__execute__; end }
53
+ @workforce.mu_unlock
58
54
  end
59
55
 
60
- job
56
+ @job_queue.push(job)
57
+
58
+ nil
61
59
  end
62
60
 
63
- private
64
- def synchronize(&block)
65
- @lock.synchronize(&block)
61
+ # Prevents more workers from being created and waits for all jobs
62
+ # to finish. Once the jobs have completed the workers are terminated.
63
+ #
64
+ # To start up again just QueueToTheFuture::schedule more jobs once
65
+ # this method returns.
66
+ #
67
+ # @param [true, false] force If set to true, shutdown immediately
68
+ # and clear the queue without waiting for any jobs to complete.
69
+ def shutdown(force = false)
70
+ @workforce.mu_synchronize do
71
+ Thread.pass until @job_queue.empty? unless force
72
+ while worker = @workforce.shift; worker.terminate; end
73
+ end
74
+
75
+ nil
66
76
  end
67
77
  end
68
- end
78
+ end
@@ -1,7 +1,7 @@
1
1
  module QueueToTheFuture
2
2
  # A proxy object for the future return value of a block.
3
3
  class Job
4
- (instance_methods - %w[__send__ __id__ object_id inspect]).each { |meth| undef_method(meth) }
4
+ instance_methods.each { |meth| undef_method(meth) unless %w(__send__ __id__ object_id inspect).include?(meth.to_s) }
5
5
 
6
6
  # Creates a job and schedules it by calling {Coordinator#schedule}.
7
7
  #
@@ -10,7 +10,8 @@ module QueueToTheFuture
10
10
  def initialize(*args, &block)
11
11
  @args = args
12
12
  @block = block
13
- Coordinator::instance.schedule(self)
13
+
14
+ Coordinator.schedule(self)
14
15
  end
15
16
 
16
17
  # Execute the job.
@@ -18,7 +19,12 @@ module QueueToTheFuture
18
19
  # This is called by the worker the job gets assigned to.
19
20
  # @return [nil]
20
21
  def __execute__
21
- @result = @block.call(*@args); nil
22
+ @result = @block[*@args]
23
+ rescue Exception => e
24
+ @result = e
25
+ ensure
26
+ # Prevent multiple executions
27
+ def self.__execute__; nil; end
22
28
  end
23
29
 
24
30
  # Allows the job to behave as the return value of the block.
@@ -26,8 +32,16 @@ module QueueToTheFuture
26
32
  # Accessing any method on the job will cause code to block
27
33
  # until the job is completed.
28
34
  def method_missing(*args, &block)
29
- Thread.pass while !defined?(@result)
30
- @result.send(*args, &block)
35
+ Thread.pass until defined?(@result)
36
+
37
+ case @result
38
+ when Exception
39
+ def self.method_missing(*args, &block); raise @result; end
40
+ else
41
+ def self.method_missing(*args, &block); @result.send(*args, &block); end
42
+ end
43
+
44
+ self.method_missing(*args, &block)
31
45
  end
32
46
  end
33
47
  end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module QueueToTheFuture
4
+ describe "Coordinator" do
5
+ before(:each) do
6
+ QueueToTheFuture.maximum_workers = 15
7
+ Coordinator.shutdown
8
+ end
9
+
10
+ it "should return the length of the job queue" do
11
+ QueueToTheFuture.maximum_workers = 1
12
+ Job.new() { sleep(0.1) }
13
+
14
+ 5.times do |i|
15
+ Coordinator.queue_length.should == i
16
+ Job.new() { sleep(0.1) }
17
+ Coordinator.queue_length.should == i + 1
18
+ end
19
+ end
20
+
21
+ it "should create up to the maximum allowed workers" do
22
+ (QueueToTheFuture.maximum_workers + 10).times do
23
+ Job.new { sleep(0.1) }
24
+ end
25
+
26
+ Coordinator.queue_length.should > 0
27
+ Coordinator.workforce_size.should == QueueToTheFuture.maximum_workers
28
+ end
29
+
30
+ it "should shutdown gracefully" do
31
+ 5.times { Job.new { sleep(1) } }
32
+
33
+ Coordinator.workforce_size.should == 5
34
+ Coordinator.shutdown
35
+ Coordinator.workforce_size.should == 0
36
+ end
37
+
38
+ it "should allow shutdown to be forced" do
39
+ 5.times { Job.new { Thread.stop } }
40
+ Coordinator.shutdown(true)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module QueueToTheFuture
4
+ describe "Job" do
5
+ it "should return immediately" do
6
+ Benchmark.realtime() do
7
+ j = Job.new { sleep(0.1); "value" }
8
+ end.should be_close(0, 0.001)
9
+ end
10
+
11
+ it "should block until completed on access" do
12
+ Benchmark.realtime() do
13
+ j = Job.new { sleep(0.1); "blargh" }
14
+ j.should == "blargh"
15
+ end.should be_close(0.1, 0.001)
16
+ end
17
+
18
+ it "should pass all arguments to the block" do
19
+ j = Job.new(1,2,3) { |*args| args.join(",") }
20
+ j.should == "1,2,3"
21
+ end
22
+
23
+ it "should handle exceptions" do
24
+ j = nil
25
+
26
+ lambda { j = Job.new { raise StandardError.new("Mock Exception") } }.should_not raise_exception()
27
+ lambda { j.to_s }.should raise_exception(StandardError, "Mock Exception")
28
+ end
29
+ end
30
+ end
@@ -1,18 +1,20 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  describe "QueueToTheFuture" do
4
- it "should work" do
5
- start = Time.now.to_f
6
-
7
- f = Future(1, 2, 3) do |*args|
8
- sleep(0.1); args
9
- end
10
-
11
- QueueToTheFuture::Coordinator.instance.workforce_size.should be(1)
12
- f.inspect.should match(/^#<QueueToTheFuture::Job/)
13
- f.should eql([1,2,3])
14
- (Time.now.to_f - start).should be_close(0.1, 0.001)
15
- Thread.pass
16
- QueueToTheFuture::Coordinator.instance.workforce_size.should be(0)
4
+ it "should allow the workforce size to be modified" do
5
+ lambda { QueueToTheFuture.maximum_workers = 20 }.should_not raise_exception()
6
+ end
7
+
8
+ it "should not accept a workforce size less than 1" do
9
+ lambda { QueueToTheFuture.maximum_workers = 0 }.should raise_exception(StandardError, /Bad workforce size/)
10
+ end
11
+
12
+ it "should provide the version being used" do
13
+ QueueToTheFuture.VERSION.should match(/\d+\.\d+\.\d+/)
14
+ end
15
+
16
+ it "should provide a Kernel level API for job creation" do
17
+ f = Future() { sleep(0.1); "blargh" }
18
+ f.inspect.should match(/QueueToTheFuture::Job/)
17
19
  end
18
20
  end
@@ -3,6 +3,7 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
3
  require 'queue_to_the_future'
4
4
  require 'spec'
5
5
  require 'spec/autorun'
6
+ require 'benchmark'
6
7
 
7
8
  Spec::Runner.configure do |config|
8
9
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: queue_to_the_future
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Devin Christensen
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-18 00:00:00 -07:00
12
+ date: 2010-03-07 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -51,7 +51,8 @@ files:
51
51
  - lib/queue_to_the_future.rb
52
52
  - lib/queue_to_the_future/coordinator.rb
53
53
  - lib/queue_to_the_future/job.rb
54
- - lib/queue_to_the_future/worker.rb
54
+ - spec/queue_to_the_future/coordinator_spec.rb
55
+ - spec/queue_to_the_future/job_spec.rb
55
56
  - spec/queue_to_the_future_spec.rb
56
57
  - spec/spec.opts
57
58
  - spec/spec_helper.rb
@@ -84,5 +85,7 @@ signing_key:
84
85
  specification_version: 3
85
86
  summary: Futures for Ruby.
86
87
  test_files:
88
+ - spec/queue_to_the_future/coordinator_spec.rb
89
+ - spec/queue_to_the_future/job_spec.rb
87
90
  - spec/queue_to_the_future_spec.rb
88
91
  - spec/spec_helper.rb
@@ -1,20 +0,0 @@
1
- module QueueToTheFuture
2
- class Worker
3
- def initialize(index)
4
- @index = index
5
- @coordinator = Coordinator.instance
6
- dispatch
7
- end
8
-
9
- def dispatch
10
- Thread.new(@index) do |index|
11
- while index < QueueToTheFuture.maximum_workers && (work = @coordinator.next_job)
12
- work.__execute__
13
- Thread.pass()
14
- end
15
-
16
- @coordinator.relieve(self)
17
- end
18
- end
19
- end
20
- end