simple-future 1.0.0.pre
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.
- checksums.yaml +7 -0
- data/lib/simple-future.rb +270 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 633585003f881b71370f40c29614937966544b01
|
4
|
+
data.tar.gz: 8e0826483b73039609f8fd8fba1c4a24fa85ff5c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b0bbd230530b9c0eed697bd881a2482449e111852ed91e108801e79473e4880768e0db7c69e49b979dc03adb77ec52b7ff210a4b5afcb68a36595731bd58f469
|
7
|
+
data.tar.gz: c102982aa6f2d765d9ef645f01b3e999fbc2ce865f3361f4224d269955aad0f377962206aef0a3be4742a051c401154a970791a2d2f970e7d4debe54cfef8d6a
|
@@ -0,0 +1,270 @@
|
|
1
|
+
# SimpleFuture: support for easy process-based concurrency.
|
2
|
+
#
|
3
|
+
# Copyright (C) 2018 Chris Reuter
|
4
|
+
# Released under the MIT license
|
5
|
+
# USE AT OWN RISK!
|
6
|
+
|
7
|
+
# Sanity check; fail if the platform doesn't support Process.fork.
|
8
|
+
raise "This Ruby does not implement Process.fork" unless
|
9
|
+
Process.respond_to? :fork
|
10
|
+
|
11
|
+
require 'etc'
|
12
|
+
require 'io/wait'
|
13
|
+
|
14
|
+
# A container holding the (eventual) result of a forked child process
|
15
|
+
# once that process finishes. The child process executes the code
|
16
|
+
# block that must be passed to the constructor:
|
17
|
+
#
|
18
|
+
# sf = SimpleFuture.new { do_slow_thing }
|
19
|
+
# ... do stuff ...
|
20
|
+
# use(sf.value)
|
21
|
+
#
|
22
|
+
# The code block **must** return a value that can be encoded by
|
23
|
+
# `Marshal` and **must not** exit prematurely.
|
24
|
+
#
|
25
|
+
# Exceptions thrown inside the block will trigger a
|
26
|
+
# `SimpleFuture::ChildError` in the parent process but that exception
|
27
|
+
# will contain the original in its `cause` field.
|
28
|
+
#
|
29
|
+
class SimpleFuture
|
30
|
+
|
31
|
+
# Exception class for errors related to SimpleFuture. All
|
32
|
+
# exceptions thrown by SimpleFuture are either `Error` or a
|
33
|
+
# subclass.
|
34
|
+
class Error < RuntimeError; end
|
35
|
+
|
36
|
+
# Exception class for the case(s) where the result is of a type that
|
37
|
+
# can't be returned (e.g. because it's one of the types
|
38
|
+
# `Marshal.dump()` fails on). This can also apply to exceptions; if
|
39
|
+
# an exception object holds an unmarshallable value, you'll get one
|
40
|
+
# of these instead of a `SimpleFuture::ChildError`.
|
41
|
+
class ResultTypeError < Error; end
|
42
|
+
|
43
|
+
# Exception class for the case where an uncaught exception is thrown
|
44
|
+
# in the child process.
|
45
|
+
class ChildError < Error
|
46
|
+
# If the child process threw an exception, this is it. Otherwise,
|
47
|
+
# it's nil.
|
48
|
+
attr_reader :cause
|
49
|
+
|
50
|
+
# @param msg [String] The exception text.
|
51
|
+
# @param cause [Exception] If valid, the exception raised in the child
|
52
|
+
def initialize(msg, cause = nil)
|
53
|
+
super(msg)
|
54
|
+
@cause = cause
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
result = super.to_s
|
59
|
+
result += " (cause: #{cause.class} '#{@cause.to_s}')" if @cause
|
60
|
+
return result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Container for holding a correct result. If an error occurred in
|
65
|
+
# the child, it will return the raw Exception instead. Wrapping a
|
66
|
+
# value (including an Exception) in a ResultContainer marks it as a
|
67
|
+
# correct result.
|
68
|
+
class ResultContainer
|
69
|
+
attr_reader :value
|
70
|
+
def initialize(v)
|
71
|
+
@value = v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private_constant :ResultContainer # Make this private
|
76
|
+
|
77
|
+
private # For some reason, YARD shows these if they're not private
|
78
|
+
|
79
|
+
@@max_tasks = Etc.nprocessors # Max. number of concurrent processes
|
80
|
+
@@in_progress = [] # List of active child processes
|
81
|
+
|
82
|
+
public
|
83
|
+
|
84
|
+
# In addition to creating a new `SimpleFuture`, the constructor
|
85
|
+
# creates a child process and evaluates `action` in it. If the
|
86
|
+
# maximum number of child processes would be exceeded, it will block
|
87
|
+
# until a process finishes.
|
88
|
+
def initialize(&action)
|
89
|
+
@readPipe = nil
|
90
|
+
@pid = nil
|
91
|
+
@complete = false
|
92
|
+
@result = nil
|
93
|
+
|
94
|
+
self.class.all_done? # Reclaim all completed children
|
95
|
+
block_until_clear()
|
96
|
+
launch(action)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Test if the child process has finished and its result is
|
100
|
+
# available.
|
101
|
+
#
|
102
|
+
# Note that this will only be true after a call to `wait` (i.e. the
|
103
|
+
# child process finished **and** its result has been retrieved.) If
|
104
|
+
# you want to see if the result is (probably) available, use
|
105
|
+
# `check_if_ready`.
|
106
|
+
def complete?() return @complete; end
|
107
|
+
|
108
|
+
# Return the result of the child process, blocking if it is not yet
|
109
|
+
# available. Blocking is done by calling `wait`, so the process
|
110
|
+
# will be cleaned up.
|
111
|
+
def value
|
112
|
+
wait
|
113
|
+
return @result
|
114
|
+
end
|
115
|
+
|
116
|
+
# Block until the child process finishes, recover its result and
|
117
|
+
# clean up the process. `wait` **must** be called for each
|
118
|
+
# `SimpleFuture` to prevent zombie processes. In practice, this is
|
119
|
+
# rarely a problem since `value` calls `wait` and you usually want
|
120
|
+
# to get all of the values. See `wait_for_all`.
|
121
|
+
#
|
122
|
+
# It is safe to call `wait` multiple times on a `SimpleFuture`.
|
123
|
+
#
|
124
|
+
# @raise [ChildError] The child process raised an uncaught exception.
|
125
|
+
# @raise [ResultTypeError] Marshal cannot encode the result
|
126
|
+
# @raise [Error] An error occurred in the IPC system or child process.
|
127
|
+
def wait
|
128
|
+
# Quit if the child has already exited
|
129
|
+
return if complete?
|
130
|
+
|
131
|
+
# Read the contents; this may block
|
132
|
+
data = @readPipe.read
|
133
|
+
|
134
|
+
# Reap the child process; this shouldn't block for long
|
135
|
+
Process.wait(@pid)
|
136
|
+
|
137
|
+
# And now we're complete, regardless of what happens next. (We
|
138
|
+
# set it early so that errors later on won't allow waiting again
|
139
|
+
# and associated mystery errors.)
|
140
|
+
@complete = true
|
141
|
+
|
142
|
+
# Close and discard the pipe; we're done with it
|
143
|
+
@readPipe.close
|
144
|
+
@readPipe = nil
|
145
|
+
|
146
|
+
# If the child process exited badly, this is an error
|
147
|
+
raise Error.new("Error in child process #{@pid}!") unless
|
148
|
+
$?.exitstatus == 0 && !data.empty?
|
149
|
+
|
150
|
+
# Decode the result. If it's an exception object, that's the
|
151
|
+
# error that was thrown in the child and that means an error here
|
152
|
+
# as well.
|
153
|
+
rbox = Marshal.load(data)
|
154
|
+
raise rbox if rbox.is_a? ResultTypeError
|
155
|
+
raise ChildError.new("Child process failed with an exception.", rbox) if
|
156
|
+
rbox.is_a? Exception
|
157
|
+
|
158
|
+
# Ensure rbox is a ResultContainer. This *probably* can't happen.
|
159
|
+
raise Error.new("Invalid result object type: #{rbox.class}") unless
|
160
|
+
rbox.is_a? ResultContainer
|
161
|
+
|
162
|
+
# Aaaaaand, retrieve the value.
|
163
|
+
@result = rbox.value
|
164
|
+
|
165
|
+
return # return nil
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
# Check if the child process has finished evaluating the block and
|
170
|
+
# has a result ready. If `check_if_ready` returns `true`, `wait`
|
171
|
+
# will not block when called.
|
172
|
+
#
|
173
|
+
# Note: `check_if_ready` tests if there's data on the pipe to the
|
174
|
+
# child process to see if it has finished. A sufficiently evil
|
175
|
+
# child block might be able to cause a true result while still
|
176
|
+
# blocking `wait`.
|
177
|
+
#
|
178
|
+
# Don't do that.
|
179
|
+
#
|
180
|
+
# @return [Boolean]
|
181
|
+
def check_if_ready
|
182
|
+
return true if complete?
|
183
|
+
return false unless @readPipe.ready?
|
184
|
+
wait
|
185
|
+
return true
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Return the maximum number of concurrent child processes allowed.
|
190
|
+
def self.max_tasks() return @@max_tasks; end
|
191
|
+
|
192
|
+
# Set the maximum number of concurrent child processes allowed. If
|
193
|
+
# set to less than 1, it is interpreted as meaning no limit.
|
194
|
+
#
|
195
|
+
# It is initially set to the number of available cores as provided
|
196
|
+
# by the `Etc` module.
|
197
|
+
def self.max_tasks=(value)
|
198
|
+
@@max_tasks = value
|
199
|
+
end
|
200
|
+
|
201
|
+
# Test if all instances created so far have run to completion. As a
|
202
|
+
# side effect, it will also call `wait` on instances whose child
|
203
|
+
# processes are running but have finished (i.e. their
|
204
|
+
# `check_if_ready` would return true.) This lets you use it as a
|
205
|
+
# non-blocking way to clean up the remaining children.
|
206
|
+
def self.all_done?
|
207
|
+
@@in_progress.select!{ |sp| !sp.check_if_ready }
|
208
|
+
return @@in_progress.size == 0
|
209
|
+
end
|
210
|
+
|
211
|
+
# Wait until all child processes have run to completion and recover
|
212
|
+
# their results. Programs should call this before exiting if there
|
213
|
+
# is a chance that an instance was created without having `wait`
|
214
|
+
# called on it.
|
215
|
+
def self.wait_for_all
|
216
|
+
@@in_progress.each{|sp| sp.wait}
|
217
|
+
@@in_progress = []
|
218
|
+
return
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
# Create a forked child process connected to this one with a pipe,
|
224
|
+
# eval `action` and return the marshalled result (or exception, in
|
225
|
+
# case of an error) via the pipe. Results are wrapped in a
|
226
|
+
# `ResultContainer` so that the parent can distinguish between
|
227
|
+
# exceptions and legitimately returned Exception objects.
|
228
|
+
def launch(action)
|
229
|
+
@readPipe, writePipe = IO.pipe
|
230
|
+
@pid = Process.fork do
|
231
|
+
@readPipe.close()
|
232
|
+
|
233
|
+
result = nil
|
234
|
+
begin
|
235
|
+
result = ResultContainer.new( action.call() )
|
236
|
+
rescue Exception => e
|
237
|
+
result = e
|
238
|
+
end
|
239
|
+
|
240
|
+
rs = nil
|
241
|
+
begin
|
242
|
+
rs = Marshal.dump(result)
|
243
|
+
rescue TypeError => e
|
244
|
+
rv = result
|
245
|
+
rv = rv.value if rv.class == ResultContainer
|
246
|
+
rs = Marshal.dump(ResultTypeError.new("Type #{rv.class} " +
|
247
|
+
"cannot be dumped."))
|
248
|
+
end
|
249
|
+
|
250
|
+
writePipe.write(rs)
|
251
|
+
writePipe.close
|
252
|
+
exit!(0)
|
253
|
+
end
|
254
|
+
|
255
|
+
writePipe.close
|
256
|
+
|
257
|
+
@@in_progress.push self
|
258
|
+
end
|
259
|
+
|
260
|
+
# If we're currently at maximum allowed processes, wait until the
|
261
|
+
# oldest of them finishes. (TO DO: if possible, make it wait until
|
262
|
+
# *any* process exits.)
|
263
|
+
def block_until_clear
|
264
|
+
return unless @@max_tasks > 0 && @@in_progress.size >= @@max_tasks
|
265
|
+
|
266
|
+
@@in_progress.shift.wait()
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple-future
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Reuter
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.7'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.7.0
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.7'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.7.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: yard
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.9.12
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.9.12
|
43
|
+
type: :development
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.9.12
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.9.12
|
53
|
+
description: |2
|
54
|
+
SimpleFuture is class that simplifies coarse-grained concurrency using
|
55
|
+
processes instead of threads.
|
56
|
+
|
57
|
+
Each instance represents the future result of a block that is passed
|
58
|
+
to it. The block is evaluated in a forked child process and its result
|
59
|
+
is returned to the SimpleFuture object. This only works on Ruby
|
60
|
+
implementations that provide Process.fork().
|
61
|
+
email: chris@blit.ca
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- lib/simple-future.rb
|
67
|
+
homepage: https://github.com/suetanvil/simple-future
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.2.0
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">"
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 1.3.1
|
85
|
+
requirements:
|
86
|
+
- A version of Ruby that implements Process.fork
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.6.14
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: A Future class for simple process-based concurrency.
|
92
|
+
test_files: []
|