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 +1 -1
- data/README.rdoc +1 -1
- data/Rakefile +3 -1
- data/VERSION +1 -1
- data/lib/queue_to_the_future.rb +7 -1
- data/lib/queue_to_the_future/coordinator.rb +45 -35
- data/lib/queue_to_the_future/job.rb +19 -5
- data/spec/queue_to_the_future/coordinator_spec.rb +43 -0
- data/spec/queue_to_the_future/job_spec.rb +30 -0
- data/spec/queue_to_the_future_spec.rb +15 -13
- data/spec/spec_helper.rb +1 -0
- metadata +6 -3
- data/lib/queue_to_the_future/worker.rb +0 -20
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
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
|
+
0.2.1
|
data/lib/queue_to_the_future.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
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
|
25
|
+
# The current length of the job queue
|
28
26
|
#
|
29
|
-
# @return [Fixnum]
|
30
|
-
def
|
31
|
-
|
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
|
-
|
36
|
+
@workforce.size
|
39
37
|
end
|
40
38
|
|
41
|
-
#
|
39
|
+
# Append a QueueToTheFuture::Job to the queue.
|
42
40
|
#
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
30
|
-
|
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
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
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.
|
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-
|
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
|
-
-
|
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
|