contrails 0.1.0

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