futurevalue 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm 1.9.2
2
+ export rvm_pretty_print_flag=1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in future.gemspec
4
+ gemspec
@@ -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
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/TODO ADDED
@@ -0,0 +1 @@
1
+ Improve logging and exception handling for workers
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'future/threadpool'
2
+ require 'future/value'
@@ -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
@@ -0,0 +1,3 @@
1
+ module Future
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -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