forkandreturn 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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ # Copyright Erik Veenstra <fork_and_return@erikveen.dds.nl>
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU General Public License,
5
+ # version 2, as published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be
8
+ # useful, but WITHOUT ANY WARRANTY; without even the implied
9
+ # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
10
+ # PURPOSE. See the GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public
13
+ # License along with this program; if not, write to the Free
14
+ # Software Foundation, Inc., 59 Temple Place, Suite 330,
15
+ # Boston, MA 02111-1307 USA.
data/README ADDED
@@ -0,0 +1,11 @@
1
+ ForkAndReturn implements a couple of methods that simplifies
2
+ running a block of code in a subprocess. The result (Ruby
3
+ object or exception) of the block will be available in the
4
+ parent process.
5
+
6
+ ForkAndReturn uses Process.fork(), so it only runs on platforms
7
+ where Process.fork() is implemented.
8
+
9
+ ForkAndReturn implements the low level stuff. Enumerable is
10
+ enriched with some methods which should be used instead of
11
+ ForkAndReturn under normal circumstances.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,147 @@
1
+ module Enumerable
2
+ # For each object in the enumeration, call the block in a seperate process and pass the object to the block and collect the results of the blocks.
3
+ # It must be one of the easiest ways of parallel processing for Ruby.
4
+ #
5
+ # Example:
6
+ #
7
+ # [1, 2, 3, 4].concurrent_collect do |object|
8
+ # 2*object
9
+ # end # ===> [2, 4, 6, 8]
10
+ #
11
+ # This runs each "2*object" in a seperate process.
12
+ # Hopefully, the processes are spread over all available CPU's.
13
+ # That's a simple way of parallel processing!
14
+ #
15
+ # Note that the code in the block is run in a seperate process, so updating objects and variables in the block won't affect the parent process:
16
+ #
17
+ # count = 0
18
+ # [...].concurrent_collect do
19
+ # count += 1
20
+ # end
21
+ # count # ==> 0
22
+ #
23
+ # concurrent_collect() is suitable for handling a couple of very CPU intensive jobs, like parsing large XML files.
24
+
25
+ def concurrent_collect(max_concurrent_workers=-1, &block)
26
+ max_concurrent_workers = 0 unless ForkAndReturn::Util.multi_core? and ForkAndReturn::Util.forkable?
27
+
28
+ case
29
+ when max_concurrent_workers < 0 # No limit.
30
+ self.collect do |object|
31
+ ForkAndReturn.fork_and_return_core do
32
+ if block.arity > 1 and object.kind_of?(Enumerable)
33
+ yield(*object.to_a)
34
+ else
35
+ yield(object)
36
+ end
37
+ end
38
+ end.collect do |wait|
39
+ wait.call
40
+ end.collect do |load|
41
+ load.call
42
+ end.collect do |result|
43
+ result.call
44
+ end
45
+ when max_concurrent_workers == 0 # No fork.
46
+ self.collect(&block)
47
+ when max_concurrent_workers > 0
48
+ self.threaded_collect(max_concurrent_workers) do |object|
49
+ ForkAndReturn.fork_and_return_core do
50
+ if block.arity > 1 and object.kind_of?(Enumerable)
51
+ yield(*object.to_a)
52
+ else
53
+ yield(object)
54
+ end
55
+ end.call
56
+ end.collect do |load|
57
+ load.call
58
+ end.collect do |result|
59
+ result.call
60
+ end
61
+ end
62
+ end
63
+
64
+ # In clustered_concurrent_collect(), all objects in the enumeration are clustered.
65
+ # Each cluster is than handled in a seperate process. Compare this to concurrent_collect(), where each object is handled in a separate process.
66
+ #
67
+ # However, the caller won't will not be aware of the clusters: The interface is exactly the same as concurrent_collect() and Enumerable.collect().
68
+ #
69
+ # clustered_concurrent_collect() is suitable for handling a lot of not too CPU intensive jobs.
70
+
71
+ def clustered_concurrent_collect(number_of_clusters=ForkAndReturn::Util.cores, &block)
72
+ number_of_clusters = 0 unless ForkAndReturn::Util.multi_core? and ForkAndReturn::Util.forkable?
73
+
74
+ if number_of_clusters < 1
75
+ self.concurrent_collect(number_of_clusters, &block)
76
+ else
77
+ clusters = [] # One cluster per thread.
78
+ last_pos = nil
79
+ res = []
80
+
81
+ self.each_with_index do |object, pos|
82
+ (clusters[pos%number_of_clusters] ||= []) << object
83
+
84
+ last_pos = pos
85
+ end
86
+
87
+ clusters.concurrent_collect(-1) do |cluster|
88
+ cluster.collect do |object|
89
+ if block.arity > 1 and object.kind_of?(Enumerable)
90
+ yield(*object.to_a)
91
+ else
92
+ yield(object)
93
+ end
94
+ end + (cluster.length == clusters[0].length ? [] : [nil]) # Add padding nil, in order to be able to transpose
95
+ end.transpose.each do |array|
96
+ res.concat(array)
97
+ end
98
+
99
+ res[0..last_pos] # Remove padding nils.
100
+ end
101
+ end
102
+
103
+ # Like concurrent_collect, but it's "select" instead of "collect".
104
+
105
+ def concurrent_select(*args, &block)
106
+ self.zip(self.concurrent_collect(*args, &block)).inject([]){|r, (o, b)| r << o if b ; r}
107
+ end
108
+
109
+ # Like concurrent_collect, but it's "reject" instead of "collect".
110
+
111
+ def concurrent_reject(*args, &block)
112
+ self.zip(self.concurrent_collect(*args, &block)).inject([]){|r, (o, b)| r << o unless b ; r}
113
+ end
114
+
115
+ # Like concurrent_collect, but it's "each" instead of "collect".
116
+
117
+ def concurrent_each(*args, &block)
118
+ concurrent_collect(*args, &block)
119
+
120
+ self
121
+ end
122
+
123
+ # Like clustered_concurrent_select, but it's "select" instead of "collect".
124
+
125
+ def clustered_concurrent_select(*args, &block)
126
+ self.zip(self.clustered_concurrent_collect(*args, &block)).inject([]){|r, (o, b)| r << o if b ; r}
127
+ end
128
+
129
+ # Like clustered_concurrent_select, but it's "reject" instead of "collect".
130
+
131
+ def clustered_concurrent_reject(*args, &block)
132
+ self.zip(self.clustered_concurrent_collect(*args, &block)).inject([]){|r, (o, b)| r << o unless b ; r}
133
+ end
134
+
135
+ # Like clustered_concurrent_select, but it's "each" instead of "collect".
136
+
137
+ def clustered_concurrent_each(*args, &block)
138
+ clustered_concurrent_collect(*args, &block)
139
+
140
+ self
141
+ end
142
+
143
+ alias concurrent concurrent_collect
144
+ alias concurrent_map concurrent_collect
145
+ alias clustered_concurrent clustered_concurrent_collect
146
+ alias clustered_concurrent_map clustered_concurrent_collect
147
+ end
@@ -0,0 +1,4 @@
1
+ module ForkAndReturn
2
+ class WorkerError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,126 @@
1
+ # ForkAndReturn implements a couple of methods that simplifies running a block of code in a subprocess.
2
+ # The result (Ruby object or exception) of the block will be available in the parent process.
3
+ #
4
+ # The intermediate return value (or exception) will be Marshal'led to disk.
5
+ # This means that it is possible to (concurrently) run thousands of child process, with a relative low memory footprint.
6
+ # Just gather the results once all child process are done.
7
+ # ForkAndReturn will handle the writing, reading and deleting of the temporary file.
8
+ #
9
+ # The core of these methods is fork_and_return_core().
10
+ # It returns some nested lambdas, which are handled by the other methods and by Enumerable#concurrent_collect().
11
+ # These lambdas handle the WAITing, LOADing and RESULTing (explained in fork_and_return_core()).
12
+ #
13
+ # The child process exits with Process.exit!(), so at_exit() blocks are skipped in the child process.
14
+ # However, both $stdout and $stderr will be flushed.
15
+ #
16
+ # Only Marshal'lable Ruby objects can be returned.
17
+ #
18
+ # ForkAndReturn uses Process.fork(), so it only runs on platforms where Process.fork() is implemented.
19
+
20
+ module ForkAndReturn
21
+ # Fork a new process and run the block of code within that process.
22
+ #
23
+ # The WAITing, LOADing and RESULTing (explained in fork_and_return_core()) will be performed immediately and the return value of the block will be returned.
24
+ #
25
+ # Example:
26
+ #
27
+ # [1, 2, 3, 4].collect do |object|
28
+ # Thread.fork do
29
+ # ForkAndReturn.fork_and_return do
30
+ # 2*object
31
+ # end
32
+ # end
33
+ # end.collect do |thread|
34
+ # thread.value
35
+ # end # ===> [2, 4, 6, 8]
36
+ #
37
+ # This runs each "2*object" in a seperate process.
38
+ # Hopefully, the processes are spread over all available CPU's.
39
+ # That's a simple way of parallel processing!
40
+ # (Although Enumerable#concurrent_collect() is even simpler...)
41
+ #
42
+ # <i>*args</i> is passed to the block.
43
+
44
+ def self.fork_and_return(*args, &block)
45
+ wait = fork_and_return_core(*args, &block)
46
+
47
+ wait.call.call.call
48
+ end
49
+
50
+ # Fork a new process and run the block of code within that process.
51
+ #
52
+ # Returns a lambda.
53
+ # If you call it, the WAITing, LOADing and RESULTing (explained in fork_and_return_core()) will be performed in one go.
54
+ #
55
+ # <i>*args</i> is passed to the block.
56
+
57
+ def self.fork_and_return_later(*args, &block)
58
+ wait = fork_and_return_core(*args, &block)
59
+
60
+ lambda{wait.call.call.call}
61
+ end
62
+
63
+ # Fork a new process and run the block of code within that process.
64
+ #
65
+ # Returns some nested lambdas:
66
+ # The first lambda is the WAIT-lambda.
67
+ # If you call the WAIT-lambda, you're going to wait for the child process to finish.
68
+ # The WAIT-lambda returns the LOAD-lambda.
69
+ # If you call the LOAD-lambda, the result of the child process (the return value
70
+ # or the exception) will be loaded from the temporary file into memory and the temporary file will be deleted.
71
+ # The LOAD-lambda returns the RESULT-lambda.
72
+ # If you call RESULT-lambda, the result of the child process will be handled.
73
+ # This means either "return the return value of the block" or "raise the exception"
74
+ #
75
+ # <i>*args</i> is passed to the block.
76
+
77
+ def self.fork_and_return_core(*args, &block)
78
+ file = Util.tempfile
79
+
80
+ #begin
81
+ pid =
82
+ Process.fork do
83
+ begin
84
+ ok, res = true, yield(*args)
85
+ rescue
86
+ ok, res = false, $!
87
+ end
88
+
89
+ File.open(file, "wb"){|f| Marshal.dump([ok, res], f)}
90
+
91
+ $stdout.flush
92
+ $stderr.flush
93
+
94
+ Process.exit! # To avoid the execution of at_exit handlers.
95
+ end
96
+ #rescue Errno::EAGAIN # Resource temporarily unavailable - fork(2)
97
+ # Kernel.sleep 0.1
98
+
99
+ # retry # TODO: Reconsider.
100
+ #end
101
+
102
+ lambda do # Wait for the result.
103
+ Process.wait(pid) # To avoid zombies.
104
+
105
+ lambda do # Load the result and delete the temp file.
106
+ begin
107
+ ok, res = File.open(file, "rb"){|f| Marshal.load(f)}
108
+ rescue Errno::ENOENT # No such file or directory
109
+ ok, res = false, WorkerError.new("the worker hasn't returned a result")
110
+ rescue EOFError # end of file reached
111
+ ok, res = false, WorkerError.new("the worker hasn't returned a result")
112
+ rescue TypeError # can't be read
113
+ ok, res = false, WorkerError.new("the worker has returned corrupt data")
114
+ ensure
115
+ File.delete(file) if File.file?(file)
116
+ end
117
+
118
+ lambda do # Handle the result.
119
+ raise res unless ok
120
+
121
+ res
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,74 @@
1
+ module ForkAndReturn
2
+ module Util # :nodoc: Stuff copied from my personal library.
3
+ def self.multi_core?
4
+ @multi_core ||= cores > 1
5
+ end
6
+
7
+ def self.cores
8
+ @cores ||= (l = cpu_info.length) < 1 ? 1 : l
9
+ end
10
+
11
+ def self.cpu_info # TODO: Might not be correct, but it works for now.
12
+ @cpu_info ||=
13
+ begin
14
+ cpus = []
15
+
16
+ if File.file?("/proc/cpuinfo")
17
+ File.open("/proc/cpuinfo") do |f|
18
+ while line = f.gets
19
+ key, value = line.chomp.split(/\s*:\s*/, 2)
20
+
21
+ if key and value
22
+ cpus << {} if key == "processor"
23
+
24
+ cpus[-1][key] = value
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ cpus
31
+ end
32
+ end
33
+
34
+ def self.forkable?
35
+ @forkable ||=
36
+ begin
37
+ Process.wait(
38
+ Process.fork do
39
+ Process.exit!
40
+ end
41
+ )
42
+
43
+ true
44
+ rescue NotImplementedError
45
+ false
46
+ end
47
+ end
48
+
49
+ def self.generate_counter(count=0)
50
+ fun =
51
+ lambda do
52
+ Thread.exclusive do
53
+ count = count.succ
54
+ end
55
+ end
56
+
57
+ class << fun
58
+ alias next call
59
+ end
60
+
61
+ fun
62
+ end
63
+
64
+ @tempfile_counter = Util.generate_counter
65
+
66
+ def self.tempfile
67
+ File.join(tempdir, "%s.%d.%d.tmp" % ["fork_and_return", $$, @tempfile_counter.next])
68
+ end
69
+
70
+ def self.tempdir
71
+ [ENV["TMPDIR"], ENV["TMP"], ENV["TEMP"], "/tmp", "c:/temp"].compact.find{|dir| File.directory?(dir)}
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,6 @@
1
+ require "threadlimiter"
2
+
3
+ require "forkandreturn/forkandreturn"
4
+ require "forkandreturn/exceptions"
5
+ require "forkandreturn/util"
6
+ require "forkandreturn/enumerable"
data/test/test.rb ADDED
@@ -0,0 +1,253 @@
1
+ require "test/unit"
2
+ require "forkandreturn"
3
+
4
+ class ForkAndReturnTest < Test::Unit::TestCase
5
+ class ForkAndReturnTestException < StandardError
6
+ end
7
+
8
+ def test_fork_and_return
9
+ a = 1
10
+ b = 2
11
+ c = 3
12
+ r = ForkAndReturn.fork_and_return{[a, [b, c]]}
13
+
14
+ assert_equal([1, [2, 3]], r)
15
+ end
16
+
17
+ def test_fork_and_return_later
18
+ a = 1
19
+ b = 2
20
+ c = 3
21
+ r = ForkAndReturn.fork_and_return_later{[a, [b, c]]}
22
+
23
+ assert_equal([1, [2, 3]], r.call)
24
+ assert_equal(Proc, r.class)
25
+ end
26
+
27
+ def test_fork_and_return_lambda
28
+ a = 1
29
+ b = 2
30
+ c = 3
31
+ wait = ForkAndReturn.fork_and_return_core{[a, [b, c]]}
32
+ assert_kind_of(Proc, wait)
33
+ load = wait.call
34
+ assert_kind_of(Proc, load)
35
+ result = load.call
36
+ assert_kind_of(Proc, result)
37
+ r = result.call
38
+ assert_equal([1, [2, 3]], r)
39
+ end
40
+
41
+ def test_fork_and_return_exception
42
+ assert_raise(ForkAndReturnTestException) do
43
+ ForkAndReturn.fork_and_return do
44
+ raise ForkAndReturnTestException
45
+ end
46
+ end
47
+ end
48
+
49
+ def test_fork_and_return_later_exception
50
+ later =
51
+ ForkAndReturn.fork_and_return_later do
52
+ raise ForkAndReturnTestException
53
+ end
54
+
55
+ assert_equal(Proc, later.class)
56
+
57
+ assert_raise(ForkAndReturnTestException) do
58
+ later.call
59
+ end
60
+ end
61
+
62
+ def test_fork_and_return_lambda_exception
63
+ wait =
64
+ ForkAndReturn.fork_and_return_core do
65
+ raise ForkAndReturnTestException
66
+ end
67
+
68
+ load = wait.call
69
+ result = load.call
70
+
71
+ assert_raise(ForkAndReturnTestException) do
72
+ result.call
73
+ end
74
+ end
75
+
76
+ def test_fork_and_return_exit
77
+ wait =
78
+ ForkAndReturn.fork_and_return_core do
79
+ exit 1
80
+ end
81
+
82
+ load = wait.call
83
+ result = load.call
84
+
85
+ assert_raise(ForkAndReturn::WorkerError) do
86
+ result.call
87
+ end
88
+ end
89
+ end
90
+
91
+ class ForkAndReturnEnumerableTest < Test::Unit::TestCase
92
+ class ForkAndReturnEnumerableTestException < StandardError
93
+ end
94
+
95
+ def test_array
96
+ data = (1..10).to_a
97
+ block = lambda{|n| n**n}
98
+ result = data.collect(&block)
99
+
100
+ assert_equal(result, data.concurrent_collect(0, &block))
101
+ assert_equal(result, data.concurrent_collect(3, &block))
102
+ assert_equal(result, data.concurrent_collect(-1, &block))
103
+ end
104
+
105
+ def test_array_of_array
106
+ data = (1..10).zip(51..60)
107
+ block = lambda{|x, y| [x, y, x**y]}
108
+ result = data.collect(&block)
109
+
110
+ assert_equal(result, data.concurrent_collect(0, &block))
111
+ assert_equal(result, data.concurrent_collect(3, &block))
112
+ assert_equal(result, data.concurrent_collect(-1, &block))
113
+ end
114
+
115
+ def test_hash
116
+ data = Hash[*(1..10).zip(51..60).flatten]
117
+ block = lambda{|x, y| [x, y, x**y]}
118
+ result = data.collect(&block)
119
+
120
+ assert_equal(result, data.concurrent_collect(0, &block))
121
+ assert_equal(result, data.concurrent_collect(3, &block))
122
+ assert_equal(result, data.concurrent_collect(-1, &block))
123
+ end
124
+
125
+ def test_range
126
+ data = 1..10
127
+ block = lambda{|n| n**n}
128
+ result = data.collect(&block)
129
+
130
+ assert_equal(result, data.concurrent_collect(0, &block))
131
+ assert_equal(result, data.concurrent_collect(3, &block))
132
+ assert_equal(result, data.concurrent_collect(-1, &block))
133
+ end
134
+
135
+ def test_select
136
+ data = (1..10).zip(51..60)
137
+ block = lambda{|x, y| y%x==0}
138
+ result = data.select(&block)
139
+
140
+ assert_equal(result, data.concurrent_select(0, &block))
141
+ assert_equal(result, data.concurrent_select(3, &block))
142
+ assert_equal(result, data.concurrent_select(-1, &block))
143
+ end
144
+
145
+ def test_reject
146
+ data = (1..10).zip(51..60)
147
+ block = lambda{|x, y| y%x==0}
148
+ result = data.reject(&block)
149
+
150
+ assert_equal(result, data.concurrent_reject(0, &block))
151
+ assert_equal(result, data.concurrent_reject(3, &block))
152
+ assert_equal(result, data.concurrent_reject(-1, &block))
153
+ end
154
+
155
+ def test_each_as_well_as_scope
156
+ data = (1..10).zip(51..60)
157
+ count = nil
158
+ block = lambda{|x, y| count += x + y}
159
+
160
+ count = 0
161
+ result1 = data.each(&block)
162
+ result2 = count
163
+
164
+ count = 0
165
+ assert_equal(result1, data.concurrent_each(0, &block))
166
+ assert_equal(result2, count)
167
+
168
+ count = 0
169
+ assert_equal(result1, data.concurrent_each(3, &block))
170
+ assert_equal(0, count) # count isn't shared among processes!
171
+
172
+ count = 0
173
+ assert_equal(result1, data.concurrent_each(-1, &block))
174
+ assert_equal(0, count) # count isn't shared among processes!
175
+ end
176
+
177
+ def test_pids
178
+ data = 1..10
179
+ block = lambda{$$}
180
+ result = data.collect(&block)
181
+
182
+ assert_equal(result, data.concurrent_collect(0, &block))
183
+ assert_not_equal(result, data.concurrent_collect(3, &block))
184
+ assert_not_equal(result, data.concurrent_collect(-1, &block))
185
+ end
186
+
187
+ def test_exceptions
188
+ data = 1..10
189
+ block = lambda{|n| raise ForkAndReturnEnumerableTestException if n == 2}
190
+
191
+ assert_raise(ForkAndReturnEnumerableTestException){data.concurrent_collect(0, &block)}
192
+ assert_raise(ForkAndReturnEnumerableTestException){data.concurrent_collect(3, &block)}
193
+ assert_raise(ForkAndReturnEnumerableTestException){data.concurrent_collect(-1, &block)}
194
+ end
195
+
196
+ def test_at_exit_handler
197
+ data = 1..10
198
+ block = lambda{}
199
+ file = "/tmp/FORK_AND_RETURN_TEST"
200
+
201
+ File.delete(file) if File.file?(file)
202
+
203
+ at_exit do
204
+ File.open(file, "w"){|f| f.write "some data"}
205
+ end
206
+
207
+ data.concurrent_collect(&block)
208
+
209
+ assert(! File.file?(file))
210
+ end
211
+
212
+ def test_clustered_concurrent_collect
213
+ data = 1..10
214
+ block = lambda{$$}
215
+ result = data.collect(&block)
216
+
217
+ assert_equal(3, data.clustered_concurrent_collect(3, &block).sort.uniq.length)
218
+
219
+ assert_equal(result, data.clustered_concurrent_collect(0, &block))
220
+ assert_not_equal(result, data.clustered_concurrent_collect(3, &block))
221
+ assert_not_equal(result, data.clustered_concurrent_collect(-1, &block))
222
+ end
223
+
224
+ def test_clustered_select
225
+ data = (1..10).zip(51..60)
226
+ block = lambda{|x, y| y%x==0}
227
+ result = data.select(&block)
228
+
229
+ assert_equal(result, data.clustered_concurrent_select(0, &block))
230
+ assert_equal(result, data.clustered_concurrent_select(3, &block))
231
+ assert_equal(result, data.clustered_concurrent_select(-1, &block))
232
+ end
233
+
234
+ def test_clustered_reject
235
+ data = (1..10).zip(51..60)
236
+ block = lambda{|x, y| y%x==0}
237
+ result = data.reject(&block)
238
+
239
+ assert_equal(result, data.clustered_concurrent_reject(0, &block))
240
+ assert_equal(result, data.clustered_concurrent_reject(3, &block))
241
+ assert_equal(result, data.clustered_concurrent_reject(-1, &block))
242
+ end
243
+
244
+ def test_clustered_each
245
+ data = (1..10).zip(51..60)
246
+ block = lambda{|x, y| y%x==0}
247
+ result = data.each(&block)
248
+
249
+ assert_equal(result, data.clustered_concurrent_each(0, &block))
250
+ assert_equal(result, data.clustered_concurrent_each(3, &block))
251
+ assert_equal(result, data.clustered_concurrent_each(-1, &block))
252
+ end
253
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forkandreturn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Erik Veenstra
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-12 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: threadlimiter
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
24
+ description: Runs a block of code in a seperate process and collects the result later. Includes a lot of convenient methods on Enumerable.
25
+ email: forkandreturn@erikveen.dds.nl
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - lib/forkandreturn
34
+ - lib/forkandreturn/exceptions.rb
35
+ - lib/forkandreturn/enumerable.rb
36
+ - lib/forkandreturn/forkandreturn.rb
37
+ - lib/forkandreturn/util.rb
38
+ - lib/forkandreturn.rb
39
+ - README
40
+ - LICENSE
41
+ - VERSION
42
+ has_rdoc: true
43
+ homepage: http://www.erikveen.dds.nl/forkandreturn/index.html
44
+ post_install_message:
45
+ rdoc_options:
46
+ - README
47
+ - LICENSE
48
+ - VERSION
49
+ - --title
50
+ - forkandreturn (0.1.0)
51
+ - --main
52
+ - README
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project: forkandreturn
70
+ rubygems_version: 1.1.1
71
+ signing_key:
72
+ specification_version: 2
73
+ summary: Runs a block of code in a seperate process and collects the result later. Includes a lot of convenient methods on Enumerable.
74
+ test_files:
75
+ - test/test.rb