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