futurevalue 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +2 -0
- data/Gemfile +4 -0
- data/README.markdown +93 -0
- data/Rakefile +1 -0
- data/TODO +1 -0
- data/futurevalue.gemspec +22 -0
- data/lib/futurevalue.rb +2 -0
- data/lib/futurevalue/threadpool.rb +47 -0
- data/lib/futurevalue/value.rb +61 -0
- data/lib/futurevalue/version.rb +3 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/threadpool_spec.rb +70 -0
- data/spec/value_spec.rb +42 -0
- metadata +73 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
Future
|
2
|
+
======
|
3
|
+
|
4
|
+
A little class to represent a promise of a value that will be provided at a later time. Useful to perform asynchronous operations without fouling the code up with event oriented cruft. A Future::Value can generally be used in place of the future value it represents.
|
5
|
+
|
6
|
+
Without Future:
|
7
|
+
|
8
|
+
# Get a number of feed items and concatenate them without Future::Value
|
9
|
+
Benchmark.measure do
|
10
|
+
['chewy', 'han solo', 'r2d2', 'yoda', 'c3po', 'at-at'].map do |name|
|
11
|
+
JSON.parse(Net::HTTP.get(URI.parse("http://api.twitter.com/search.json?q=rubygems")))['results']
|
12
|
+
end.flatten
|
13
|
+
end.real
|
14
|
+
=> 4.5 # seconds
|
15
|
+
|
16
|
+
|
17
|
+
The same code with the requests and processing wrapped in a Future::Value
|
18
|
+
|
19
|
+
# Get a number of feed items and concatenate them *with* Future::Value
|
20
|
+
Benchmark.measure do
|
21
|
+
['chewy', 'han solo', 'r2d2', 'yoda', 'c3po', 'at-at'].map do |name|
|
22
|
+
Future::Value.new { JSON.parse(Net::HTTP.get(URI.parse("http://api.twitter.com/search.json?q=rubygems")))['results'] }
|
23
|
+
end.flatten
|
24
|
+
end.real
|
25
|
+
=> 0.8 # seconds
|
26
|
+
|
27
|
+
Installation
|
28
|
+
============
|
29
|
+
|
30
|
+
Not yet published as a gem, so you'll have to
|
31
|
+
|
32
|
+
git clone git@github.com:simen/future.git
|
33
|
+
cd future
|
34
|
+
rake install
|
35
|
+
|
36
|
+
Or if you use the awesomeness that is bundler, you stick this in your Gemfile:
|
37
|
+
|
38
|
+
gem "future", :git => git@github.com:simen/future.git
|
39
|
+
|
40
|
+
Usage
|
41
|
+
=====
|
42
|
+
|
43
|
+
Basically a Future::Value wraps a thread in such a way that it looks like you get the result of a time consuming operation directly. Not until you try to access the result will the thread actually block. In this way you may write asynchronous code that looks like traditional straight down ruby. Especially useful for performing a number of http-requests and let them run in parallel while you perform other tasks.
|
44
|
+
|
45
|
+
# a, b and c are all "calculated" in parallel.
|
46
|
+
a = Future::Value.new { sleep(2); "Hello" }
|
47
|
+
b = Future::Value.new { sleep(3); "from the"}
|
48
|
+
c = Future::Value.new { sleep(1); "future!" }
|
49
|
+
# the moment the content of the values is needed, the main tread blocks until data is ready
|
50
|
+
"#{a} #{b} #{c}"
|
51
|
+
=> Hello from the future
|
52
|
+
# total time taken: 3 seconds as opposed to 6 with the traditional way
|
53
|
+
|
54
|
+
Future::Threadpool
|
55
|
+
==================
|
56
|
+
|
57
|
+
Future::Value use a nice and simple threadpool to process the values. By default a pool with seven workers is launched when you request your first Future::Value. If you need to adjust the number of workers you'll do this:
|
58
|
+
|
59
|
+
Future::Value.pool.workers = 12
|
60
|
+
|
61
|
+
Remember that if you use Future::Value with ActiveRecord you should visit your database.yml-file and make sure your database connection pool match the number of workers you employ.
|
62
|
+
|
63
|
+
If you need to wait for all jobs to finish and shut down the worker threads you do this:
|
64
|
+
|
65
|
+
Future::Value.pool.close
|
66
|
+
|
67
|
+
The Future::Threadpool is a useful little class in itself and can be put to good use for purposes besides computing future values. Here's how you use it:
|
68
|
+
|
69
|
+
# Creates a pool with 12 active workers
|
70
|
+
pool = Future::Threadpool.new(12)
|
71
|
+
|
72
|
+
# Resizes the pool by killing or adding worker threads
|
73
|
+
pool.workers = 6
|
74
|
+
|
75
|
+
# Adds a job to the pool
|
76
|
+
pool << proc do
|
77
|
+
# Work you'd like to have done in parallel
|
78
|
+
# Probably involving IO seeing as ruby doesn't really utilize more than one
|
79
|
+
# cpu core anyway.
|
80
|
+
end
|
81
|
+
|
82
|
+
# Waits for all pending jobs to finish then proceeds to kill the workers one by one
|
83
|
+
pool.close
|
84
|
+
|
85
|
+
A word of warning: Future::Threadpool will never be garbage collected if you let it out of your scope without closing it first, because the sleeping workers will keep a reference to it:
|
86
|
+
|
87
|
+
# How to leak memory and threads:
|
88
|
+
pool = Future::Threadpool.new(12)
|
89
|
+
pool = nil
|
90
|
+
# Congrats: you have just leaked an instance of Future::Threadpool + 12 sleeping threads
|
91
|
+
|
92
|
+
So kids, remember this: Close your pools if you plan to lose track of them.
|
93
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/futurevalue.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "futurevalue/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "futurevalue"
|
7
|
+
s.version = Future::VERSION
|
8
|
+
s.authors = ["Simen Svale Skogsrud"]
|
9
|
+
s.email = ["simen@bengler.no"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A promise of a value to be calculated in the future}
|
12
|
+
s.description = %q{A promise of a value to be calculated in the future}
|
13
|
+
|
14
|
+
s.rubyforge_project = "futurevalue"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
end
|
data/lib/futurevalue.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Future
|
4
|
+
class Threadpool
|
5
|
+
def initialize(count)
|
6
|
+
@queue = Queue.new
|
7
|
+
@workers = []
|
8
|
+
self.workers = count
|
9
|
+
end
|
10
|
+
|
11
|
+
def workers
|
12
|
+
@workers.size
|
13
|
+
end
|
14
|
+
|
15
|
+
def workers=(value)
|
16
|
+
if value > workers
|
17
|
+
(value-workers).times { @workers << new_worker }
|
18
|
+
else
|
19
|
+
(workers-value).times { @queue << proc { @workers.delete(Thread.current); Thread.current.exit } }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def << (job)
|
24
|
+
@queue << job
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
self.workers = 0
|
29
|
+
@workers.each(&:join)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def new_worker
|
35
|
+
Thread.new do
|
36
|
+
loop do
|
37
|
+
begin
|
38
|
+
@queue.pop.call
|
39
|
+
rescue Exception => e
|
40
|
+
puts e
|
41
|
+
puts e.backtrace
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Future
|
2
|
+
class Value
|
3
|
+
|
4
|
+
MINIMUM_WORKERS = 7
|
5
|
+
@pool = Future::Threadpool.new(0)
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :pool
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(&block)
|
12
|
+
self.class.pool.workers = MINIMUM_WORKERS if self.class.pool.workers < MINIMUM_WORKERS
|
13
|
+
|
14
|
+
@response_queue = Queue.new
|
15
|
+
self.class.pool << lambda do
|
16
|
+
begin
|
17
|
+
@response_queue << block.call
|
18
|
+
rescue Exception => @exception
|
19
|
+
@response_queue << nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def value
|
25
|
+
if @response_queue
|
26
|
+
@value = @response_queue.pop
|
27
|
+
@response_queue = nil
|
28
|
+
end
|
29
|
+
raise @exception if @exception
|
30
|
+
@value
|
31
|
+
end
|
32
|
+
|
33
|
+
def ready?
|
34
|
+
@response_queue.nil? || !@response_queue.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(method, *args, &block)
|
38
|
+
self.value.send(method, *args, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
self.value.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_str
|
46
|
+
self.value.to_str
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
if !ready?
|
51
|
+
"#<#{self.class.name} @value=[pending]>"
|
52
|
+
else
|
53
|
+
unless @exception
|
54
|
+
"#<#{self.class.name} @value=#{self.value.inspect}>"
|
55
|
+
else
|
56
|
+
"#<#{self.class.name} #{@exception.inspect}>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__)+'/../lib/futurevalue.rb'
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Future::Threadpool do
|
4
|
+
it "executes the jobs" do
|
5
|
+
p = Future::Threadpool.new(10)
|
6
|
+
sentinel = nil
|
7
|
+
p << -> { sentinel = "hello" }
|
8
|
+
sleep 0.1
|
9
|
+
sentinel.should eq "hello"
|
10
|
+
p.close
|
11
|
+
end
|
12
|
+
|
13
|
+
it "resizes gracefully" do
|
14
|
+
initial_threadcount = Thread.list.size
|
15
|
+
p = Future::Threadpool.new(10)
|
16
|
+
p.workers = 5
|
17
|
+
sleep 0.1
|
18
|
+
p.workers.should eq 5
|
19
|
+
Thread.list.size.should eq initial_threadcount+5
|
20
|
+
p.workers = 10
|
21
|
+
sleep 0.1
|
22
|
+
p.workers.should eq 10
|
23
|
+
Thread.list.size.should eq initial_threadcount+10
|
24
|
+
p.workers = 0
|
25
|
+
sleep 0.1
|
26
|
+
p.workers.should eq 0
|
27
|
+
Thread.list.size.should eq initial_threadcount
|
28
|
+
p.workers = -1
|
29
|
+
sleep 0.1
|
30
|
+
p.workers.should eq 0
|
31
|
+
Thread.list.size.should eq initial_threadcount
|
32
|
+
p.close
|
33
|
+
end
|
34
|
+
|
35
|
+
it "closes gracefully" do
|
36
|
+
former_threadcount = Thread.list.size
|
37
|
+
p = Future::Threadpool.new(10)
|
38
|
+
20.times { p << -> { 100.times { 1+1 } } }
|
39
|
+
p.close
|
40
|
+
Thread.list.size.should eq former_threadcount
|
41
|
+
end
|
42
|
+
|
43
|
+
it "runs a number of threads" do
|
44
|
+
p = Future::Threadpool.new(10)
|
45
|
+
p.workers.should eq 10
|
46
|
+
p.close
|
47
|
+
end
|
48
|
+
|
49
|
+
it "puts jobs on the queue" do
|
50
|
+
p = Future::Threadpool.new(0)
|
51
|
+
sentinel = nil
|
52
|
+
p << -> { sentinel = "hello" }
|
53
|
+
job = p.instance_variable_get('@queue').pop
|
54
|
+
job.call
|
55
|
+
sentinel.should eq "hello"
|
56
|
+
p.close
|
57
|
+
end
|
58
|
+
|
59
|
+
it "works a lot" do
|
60
|
+
p = Future::Threadpool.new(10)
|
61
|
+
sentinel = []
|
62
|
+
100.times{ p << -> { sentinel << "hepp" } }
|
63
|
+
sleep 0.5
|
64
|
+
sentinel.size.should == 100
|
65
|
+
p.close
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
end
|
data/spec/value_spec.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
describe Future::Value do
|
5
|
+
it "makes sure a minimum amount of workers are created" do
|
6
|
+
Future::Value.new { 1+1 }
|
7
|
+
sleep 0.1
|
8
|
+
Future::Value.pool.workers == Future::Value::MINIMUM_WORKERS
|
9
|
+
end
|
10
|
+
|
11
|
+
it "forwards missing methods" do
|
12
|
+
v = Future::Value.new { [1,2,3] }
|
13
|
+
v.value
|
14
|
+
v[1].should eq(2)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be pending until the data arrives" do
|
18
|
+
v = Future::Value.new { sleep(1); "hello" }
|
19
|
+
v.ready?.should eq(false)
|
20
|
+
v.value
|
21
|
+
v.ready?.should eq(true)
|
22
|
+
v.value.should eq("hello")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should block until the value is ready upon implicit conversion" do
|
26
|
+
v = Future::Value.new { sleep(0.5); "Hello" }
|
27
|
+
"#{v} World".should eq("Hello World")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "several futures should run together" do
|
31
|
+
# Calculating the sum of several futures that each should take one second to appear should take less than three seconds
|
32
|
+
Future::Value.pool.workers = 50
|
33
|
+
Benchmark.measure { (1..50).map { |i| Future::Value.new { sleep(1); i} }.inject(0){ |r, e| r+e } }.real.should be < 3.0
|
34
|
+
end
|
35
|
+
|
36
|
+
it "reports errors" do
|
37
|
+
f = Future::Value.new { sleep 1; raise Exception }
|
38
|
+
sleep 0.1
|
39
|
+
->{f.value}.should raise_error(Exception)
|
40
|
+
f.ready?.should eq true
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: futurevalue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Simen Svale Skogsrud
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-29 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70149445959820 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70149445959820
|
25
|
+
description: A promise of a value to be calculated in the future
|
26
|
+
email:
|
27
|
+
- simen@bengler.no
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- .rvmrc
|
34
|
+
- Gemfile
|
35
|
+
- README.markdown
|
36
|
+
- Rakefile
|
37
|
+
- TODO
|
38
|
+
- futurevalue.gemspec
|
39
|
+
- lib/futurevalue.rb
|
40
|
+
- lib/futurevalue/threadpool.rb
|
41
|
+
- lib/futurevalue/value.rb
|
42
|
+
- lib/futurevalue/version.rb
|
43
|
+
- spec/spec_helper.rb
|
44
|
+
- spec/threadpool_spec.rb
|
45
|
+
- spec/value_spec.rb
|
46
|
+
homepage: ''
|
47
|
+
licenses: []
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubyforge_project: futurevalue
|
66
|
+
rubygems_version: 1.8.10
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: A promise of a value to be calculated in the future
|
70
|
+
test_files:
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/threadpool_spec.rb
|
73
|
+
- spec/value_spec.rb
|