queue_to_the_future 0.1.1 → 0.2.1

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