contrails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+
4
+ gem 'eventmachine'
5
+ gem 'rake'
6
+
7
+ group 'test' do
8
+ gem 'rspec'
9
+ gem 'rantly'
10
+ gem 'mocha'
11
+ end
12
+
13
+ group 'development' do
14
+ gem 'ruby-debug19'
15
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ archive-tar-minitar (0.5.2)
5
+ columnize (0.3.4)
6
+ diff-lcs (1.1.2)
7
+ eventmachine (0.12.10)
8
+ linecache19 (0.5.12)
9
+ ruby_core_source (>= 0.1.4)
10
+ mocha (0.9.12)
11
+ rake (0.8.7)
12
+ rantly (0.3.0)
13
+ rspec (2.6.0)
14
+ rspec-core (~> 2.6.0)
15
+ rspec-expectations (~> 2.6.0)
16
+ rspec-mocks (~> 2.6.0)
17
+ rspec-core (2.6.4)
18
+ rspec-expectations (2.6.0)
19
+ diff-lcs (~> 1.1.2)
20
+ rspec-mocks (2.6.0)
21
+ ruby-debug-base19 (0.11.25)
22
+ columnize (>= 0.3.1)
23
+ linecache19 (>= 0.5.11)
24
+ ruby_core_source (>= 0.1.4)
25
+ ruby-debug19 (0.11.6)
26
+ columnize (>= 0.3.1)
27
+ linecache19 (>= 0.5.11)
28
+ ruby-debug-base19 (>= 0.11.19)
29
+ ruby_core_source (0.1.5)
30
+ archive-tar-minitar (>= 0.5.2)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ eventmachine
37
+ mocha
38
+ rake
39
+ rantly
40
+ rspec
41
+ ruby-debug19
data/README.markdown ADDED
@@ -0,0 +1,29 @@
1
+ Contrails - declarative concurrency for EventMachine
2
+ ====================================================
3
+
4
+
5
+ ![Contrails](http://github.com/likely/contrails/raw/master/contrib/contrails.jpg)
6
+
7
+ Contrails is a lightweight DSL that allows concurrent processes to be specified using an intuitive, declarative syntax for execution by EventMachine. It consists of a process class that wraps a block, and can then be chained with other processes in series using the `>>` operator, and composed into a single process that executes its constituent processes in parallel using the `*` operator. A trivial example:
8
+
9
+ Contrails::Process.new { get_an_integer_from_somewhere } >> Contrails.Process.new {|x| x*2 } * {Contrails::Process.new {|x| x * 3} >> Contrails::Process.new {|x,y| x+y }
10
+
11
+ This takes a number from some input, computes its multiplication by two and three in parallel, then sums both values once both computations have finished.
12
+
13
+ There's still rather a lot of unsightly syntax there, right? Perhaps even more so than before. However, if you import the Contrails::Utils module, you'll get a few other useful methods:
14
+
15
+ * the 'trails' method is an alias for Contrails::Process.new
16
+
17
+ `trail { get_integer } >> trail { |x| x * 3} * trail { |x| x * 2 } >> trail {|x, y| x+y }`
18
+
19
+ * the 'seq' method sequences all its arguments
20
+
21
+ `seq(trail, trail2, trail3) == trail >> trail2 >> trail3`
22
+
23
+ * the 'par' method composes all its arguments in parallel
24
+
25
+ `par(trail, trail2, trail3) == trail * trail2 * trail3`
26
+
27
+ Enjoy! Questions, comments, feature requests and patches always welcome.
28
+
29
+ (Photo by [FrancoisRoche](http://www.flickr.com/photos/francoisroche/2563417399/) on flickr, Licence: CC BY-SA)
data/Rakefile ADDED
@@ -0,0 +1,97 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default => :test
6
+ task :test => :spec
7
+
8
+ if !defined?(RSpec)
9
+ puts "spec targets require RSpec"
10
+ else
11
+ desc "Run all examples"
12
+ RSpec::Core::RakeTask.new(:spec) do |t|
13
+ t.pattern = 'spec/**/*_spec.rb'
14
+ t.rspec_opts = ['-cfs']
15
+ end
16
+ end
17
+
18
+
19
+ require "rubygems"
20
+ require "rubygems/package_task"
21
+ require "rdoc/task"
22
+
23
+ require "rspec"
24
+ require "rspec/core/rake_task"
25
+ RSpec::Core::RakeTask.new do |t|
26
+ t.rspec_opts = %w(--format documentation --colour)
27
+ end
28
+
29
+
30
+ task :default => ["spec"]
31
+
32
+ # This builds the actual gem. For details of what all these options
33
+ # mean, and other ones you can add, check the documentation here:
34
+ #
35
+ # http://rubygems.org/read/chapter/20
36
+ #
37
+ spec = Gem::Specification.new do |s|
38
+
39
+ # Change these as appropriate
40
+ s.name = "contrails"
41
+ s.version = "0.1.0"
42
+ s.summary = "Declarative concurrency for EventMachine"
43
+ s.author = "Tim Cowlishaw"
44
+ s.email = "tim@timcowlishaw.co.uk"
45
+ s.homepage = "http://github.com/likely/contrails"
46
+
47
+ s.has_rdoc = true
48
+ s.extra_rdoc_files = %w(README.markdown)
49
+ s.rdoc_options = %w(--main README.markdown)
50
+
51
+ # Add any extra files to include in the gem
52
+ s.files = %w(Gemfile.lock Rakefile README.markdown Gemfile) + Dir.glob("{spec,lib}/**/*")
53
+ s.require_paths = ["lib"]
54
+
55
+ # If you want to depend on other gems, add them here, along with any
56
+ # relevant versions
57
+ s.add_dependency("eventmachine")
58
+
59
+ # If your tests use any gems, include them here
60
+ s.add_development_dependency("rspec")
61
+ end
62
+
63
+ # This task actually builds the gem. We also regenerate a static
64
+ # .gemspec file, which is useful if something (i.e. GitHub) will
65
+ # be automatically building a gem for this project. If you're not
66
+ # using GitHub, edit as appropriate.
67
+ #
68
+ # To publish your gem online, install the 'gemcutter' gem; Read more
69
+ # about that here: http://gemcutter.org/pages/gem_docs
70
+ Gem::PackageTask.new(spec) do |pkg|
71
+ pkg.gem_spec = spec
72
+ end
73
+
74
+ desc "Build the gemspec file #{spec.name}.gemspec"
75
+ task :gemspec do
76
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
77
+ File.open(file, "w") {|f| f << spec.to_ruby }
78
+ end
79
+
80
+ # If you don't want to generate the .gemspec file, just remove this line. Reasons
81
+ # why you might want to generate a gemspec:
82
+ # - using bundler with a git source
83
+ # - building the gem without rake (i.e. gem build blah.gemspec)
84
+ # - maybe others?
85
+ task :package => :gemspec
86
+
87
+ # Generate documentation
88
+ RDoc::Task.new do |rd|
89
+ rd.main = "README.markdown"
90
+ rd.rdoc_files.include("README.markdown", "lib/**/*.rb")
91
+ rd.rdoc_dir = "rdoc"
92
+ end
93
+
94
+ desc 'Clear out RDoc and generated packages'
95
+ task :clean => [:clobber_rdoc, :clobber_package] do
96
+ rm "#{spec.name}.gemspec"
97
+ end
@@ -0,0 +1,30 @@
1
+ module Contrails
2
+ module Chainable
3
+
4
+ def bind(other)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def distribute(other)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def call(*a)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def to_proc
17
+ lambda {|*a| self.call(*a) }
18
+ end
19
+
20
+ def >>(other)
21
+ self.bind(other)
22
+ end
23
+
24
+ def *(other)
25
+ self.distribute(other)
26
+ end
27
+
28
+ end
29
+ end
30
+
@@ -0,0 +1,9 @@
1
+ module Contrails
2
+ module Helpers
3
+ class << self
4
+ def unwrap_array(array)
5
+ array.inject([]) {|m,n| m + n}
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ require 'em/deferrable'
2
+ require 'contrails/chainable'
3
+ require 'contrails/semaphore'
4
+ module Contrails
5
+ class Parallel
6
+ include EventMachine::Deferrable
7
+ include Contrails::Chainable
8
+ def initialize(*procs)
9
+ results = []
10
+ @procs = procs
11
+ @semaphore = Semaphore.new(@procs.length, results)
12
+ ps = @procs.map.with_index do |p, i|
13
+ p = p.dup
14
+ p.callback {|*r| results[i] = r; @semaphore.signal}
15
+ p
16
+ end
17
+ internal_callback { |*a|
18
+ ps.each {|p| EM.next_tick(lambda { p.call(*a) }) }
19
+ }
20
+ end
21
+
22
+ alias_method :internal_callback, :callback
23
+ private :internal_callback
24
+
25
+ def callback(&b)
26
+ @semaphore.callback(&b)
27
+ end
28
+
29
+ def bind(other)
30
+ @semaphore.callback(&other)
31
+ self
32
+ end
33
+
34
+ def distribute(other)
35
+ Contrails::Parallel.new(*(@procs + [other]))
36
+ end
37
+
38
+ def call(*a)
39
+ self.succeed(*a)
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -0,0 +1,32 @@
1
+ require 'em/deferrable'
2
+ require 'contrails/parallel'
3
+ module Contrails
4
+ class Process
5
+ include EventMachine::Deferrable
6
+ include Contrails::Chainable
7
+
8
+ class << self
9
+ def return(&b)
10
+ self.new(&b)
11
+ end
12
+ end
13
+
14
+ def initialize(&l)
15
+ @lambda = l
16
+ end
17
+
18
+ def bind(other)
19
+ p = Contrails::Process.new(&self)
20
+ p.callback(&other)
21
+ return p
22
+ end
23
+
24
+ def distribute(other)
25
+ Contrails::Parallel.new(self, other)
26
+ end
27
+
28
+ def call(*a)
29
+ self.succeed(*@lambda.call(*a))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ require 'em/deferrable'
2
+ require 'contrails/helpers'
3
+ module Contrails
4
+ class Semaphore
5
+
6
+ include EventMachine::Deferrable
7
+
8
+ def initialize(n, results)
9
+ @value = n
10
+ @results = results
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def signal
15
+ @mutex.synchronize do
16
+ if(@value -= 1) == 0
17
+ self.succeed(*Contrails::Helpers.unwrap_array(@results))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,16 @@
1
+ require 'contrails/process'
2
+ module Contrails
3
+ module Utils
4
+ def trail(&block)
5
+ return Contrails::Process.new(&block)
6
+ end
7
+
8
+ def seq(*ps)
9
+ return ps.inject {|m,n| m.bind(n) }
10
+ end
11
+
12
+ def par(*ps)
13
+ return ps.inject {|m,n| m.distribute(n)}
14
+ end
15
+ end
16
+ end
data/lib/contrails.rb ADDED
@@ -0,0 +1,5 @@
1
+ $: << File.join(File.dirname(__FILE__))
2
+ module Contrails
3
+ end
4
+ require "contrails/process"
5
+ require "contrails/utils"
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ describe Contrails::Process do
4
+ describe "class methods:" do
5
+
6
+ describe "new" do
7
+ it "wraps the passed lambda in a process" do
8
+ for_all do
9
+ int = integer
10
+ async_assertion(Contrails::Process.new { |x| x**2 }, int) { |result|
11
+ result.should == int**2
12
+ }
13
+ end
14
+ end
15
+ end
16
+
17
+ describe "return" do
18
+ it "delegates to the constructor method" do
19
+ lamb = lambda {|x, y| x + y }
20
+ Contrails::Process.expects(:new) #sadly we can't test for block arguments with mocha
21
+ Contrails::Process.return(&lamb)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe "instance methods:" do
27
+ describe "bind" do
28
+ it "returns a process which sequences the second operation after the first" do
29
+ for_all do
30
+ context = mock
31
+ context.expects(:first).in_sequence
32
+ context.expects(:second).in_sequence
33
+ proc1 = Contrails::Process.new { context.first }
34
+ proc2 = Contrails::Process.new { context.second }
35
+ async_assertion(proc1.bind(proc2))
36
+ end
37
+ end
38
+ it "successively calls other processes bound onto the argument" do
39
+ for_all do
40
+ context = mock
41
+ context.expects(:first).in_sequence
42
+ context.expects(:second).in_sequence
43
+ context.expects(:third).in_sequence
44
+ proc1 = Contrails::Process.new { context.first }
45
+ proc2 = Contrails::Process.new { context.second }
46
+ proc3 = Contrails::Process.new { context.third }
47
+ async_assertion(proc1.bind(proc2).bind(proc3))
48
+ end
49
+ end
50
+
51
+ it "passes the return value of each process to its successors" do
52
+ for_all do
53
+ int= integer
54
+ proc1 = Contrails::Process.new { |x| x.should == int; x+1}
55
+ proc2 = Contrails::Process.new { |x| x.should == int+1; x+1}
56
+ proc3 = Contrails::Process.new { |x| x.should == int+2; x+1}
57
+ async_assertion(proc1.bind(proc2).bind(proc3), int) {|x| x.should == int+3}
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "distribute" do
63
+
64
+ it "returns a process that executes both processes, yielding the results of both as a pair" do
65
+ for_all {
66
+ int = integer
67
+ proc1 = Contrails::Process.new {|x| x*2}
68
+ proc2 = Contrails::Process.new {|x| x*3}
69
+ async_assertion(proc1.distribute(proc2), int) {|x1, x2|
70
+ x1.should == int*2
71
+ x2.should == int*3
72
+ }
73
+ }
74
+ end
75
+ it "yields the return value of both to subsequently bound processes" do
76
+ for_all {
77
+ int = integer
78
+ proc1 = Contrails::Process.new {|x| x*2 }
79
+ proc2 = Contrails::Process.new {|x| x*3 }
80
+ proc3 = Contrails::Process.new {|x,y| x + y }
81
+ async_assertion(proc1.distribute(proc2).bind(proc3), int) {|r|
82
+ r.should == int*2 + int*3
83
+ }
84
+ }
85
+ end
86
+
87
+ it "executes both processes in parallel" do
88
+ for_all {
89
+ time = float
90
+ guard time > 0.05
91
+ guard time < 0.25
92
+ context = mock
93
+ context.expects(:first).in_sequence
94
+ context.expects(:second).in_sequence
95
+ proc1 = Contrails::Process.new { sleep(time); context.second }
96
+ proc2 = Contrails::Process.new { context.first }
97
+ async_assertion(proc1.distribute(proc2))
98
+ }
99
+ end
100
+ it "executes any subsequently distributed processes in parallel" do
101
+ for_all {
102
+ time = float
103
+ guard time > 0.05
104
+ guard time < 0.25
105
+ context = mock
106
+ context.expects(:first).in_sequence
107
+ context.expects(:second).in_sequence
108
+ context.expects(:third).in_sequence
109
+ proc1 = Contrails::Process.new { sleep(time); context.third }
110
+ proc2 = Contrails::Process.new { sleep(time/2); context.second }
111
+ proc3 = Contrails::Process.new { context.first }
112
+ async_assertion(proc1.distribute(proc2).distribute(proc3))
113
+ }
114
+ end
115
+
116
+ it "collects the results of all processes into a single list of arguments to its callback" do
117
+ for_all {
118
+ int = integer
119
+ proc1 = Contrails::Process.new {|x| x+1 }
120
+ proc2 = Contrails::Process.new {|x| x+2 }
121
+ proc3 = Contrails::Process.new {|x| x+3 }
122
+ async_assertion(proc1.distribute(proc2).distribute(proc3), int) {|a|
123
+ a.should == [int+1, int+2, int+3]
124
+ }
125
+ }
126
+ end
127
+ end
128
+
129
+ describe "call" do
130
+ it "calls the lambda and sets itself as succeeded, passing the blocks return value" do
131
+ for_all do
132
+ int = integer
133
+ async_assertion(Contrails::Process.new { |x| x + 2}, int) { |result|
134
+ result.should == int + 2
135
+ }
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "to proc" do
141
+ it "returns a lambda which wraps the computation" do
142
+ p = Contrails::Process.new { 1+2 }
143
+ p.expects(:call)
144
+ p.to_proc.should be_a(Proc)
145
+ p.to_proc.call
146
+ end
147
+ end
148
+
149
+ describe ">>" do
150
+ it "delegates to the bind method" do
151
+ proc1 = Contrails::Process.new {|x| x**2}
152
+ proc2 = Contrails::Process.new {|x| x+1 }
153
+ proc1.expects(:bind).with(proc2)
154
+ proc1 >> proc2
155
+ end
156
+ end
157
+
158
+ describe "*" do
159
+ it "delegates to the distribute method" do
160
+ proc1 = Contrails::Process.new {|x| x**2}
161
+ proc2 = Contrails::Process.new {|x| x+1 }
162
+ proc1.expects(:distribute).with(proc2)
163
+ proc1 * proc2
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,38 @@
1
+ $: << File.join(File.dirname(__FILE__),"..")
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'rspec'
5
+ require 'rantly'
6
+ require 'mocha'
7
+ require 'eventmachine'
8
+ require 'em/deferrable'
9
+ require 'lib/contrails'
10
+ RSpec.configure do |config|
11
+ config.mock_framework = :mocha
12
+ end
13
+
14
+ Rantly.send(:include, Mocha::API)
15
+
16
+ def for_all(&block)
17
+ Rantly.each(ENV['RANTLY_LIMIT'] ? ENV['RANTLY_LIMIT'].to_i : 100, &block)
18
+ end
19
+
20
+ class DeferrableTest
21
+ include EventMachine::Deferrable
22
+ def initialize(*a, &b)
23
+ @args = a
24
+ @lambda = b
25
+ end
26
+
27
+ def to_proc
28
+ lambda { succeed(*@lambda.call(*@args)) }
29
+ end
30
+ end
31
+
32
+ def async_assertion(process, *args, &callback)
33
+ EM.run_block do
34
+ deferrable = DeferrableTest.new(*args, &process)
35
+ deferrable.callback(&callback) if callback
36
+ EM.defer(&deferrable)
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ class TestImplementation
3
+ include Contrails::Utils
4
+ end
5
+
6
+ describe Contrails::Utils do
7
+ before(:all) do
8
+ @ti = TestImplementation.new
9
+ end
10
+ describe "trail" do
11
+ it "creates a new process with the passed block" do
12
+ Contrails::Process.expects(:new)
13
+ @ti.trail {|x| x+1 }
14
+ end
15
+ end
16
+
17
+ describe "seq" do
18
+ it "binds together the supplied processes" do
19
+ p1 = mock
20
+ p2 = mock
21
+ p3 = mock
22
+ intermediate = mock
23
+ p1.expects(:bind).with(p2).returns(intermediate)
24
+ intermediate.expects(:bind).with(p3)
25
+ @ti.seq(p1,p2,p3)
26
+ end
27
+ end
28
+
29
+ describe "par" do
30
+ it "distributes between the supplied processes" do
31
+ p1 = mock
32
+ p2 = mock
33
+ p3 = mock
34
+ intermediate = mock
35
+ p1.expects(:distribute).with(p2).returns(intermediate)
36
+ intermediate.expects(:distribute).with(p3)
37
+ @ti.par(p1,p2,p3)
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: contrails
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Tim Cowlishaw
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-08-19 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: eventmachine
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: *id002
38
+ description:
39
+ email: tim@timcowlishaw.co.uk
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - README.markdown
46
+ files:
47
+ - Gemfile.lock
48
+ - Rakefile
49
+ - README.markdown
50
+ - Gemfile
51
+ - spec/utils_spec.rb
52
+ - spec/spec_helper.rb
53
+ - spec/process_spec.rb
54
+ - lib/contrails/process.rb
55
+ - lib/contrails/chainable.rb
56
+ - lib/contrails/parallel.rb
57
+ - lib/contrails/utils.rb
58
+ - lib/contrails/semaphore.rb
59
+ - lib/contrails/helpers.rb
60
+ - lib/contrails.rb
61
+ has_rdoc: true
62
+ homepage: http://github.com/likely/contrails
63
+ licenses: []
64
+
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --main
68
+ - README.markdown
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: -1260005474418307725
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: "0"
86
+ requirements: []
87
+
88
+ rubyforge_project:
89
+ rubygems_version: 1.5.2
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Declarative concurrency for EventMachine
93
+ test_files: []
94
+