forkner 1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +78 -0
- data/lib/forkner.rb +231 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b52b5779f3a60718d88c8211ffef94cd28c6f0c4f2c6b5912e15548b271673b5
|
4
|
+
data.tar.gz: 78ad9cbbe65fc736daef6ab882193f7a6b2ff2bd9ab1e4d9d7d45dce9ed49cf2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 050ae01b17dd141a6164f0748df4a9f0087b8199cc24291275e69b92664c47b8c53de227a877bdacfce1a915ba2b3c7d287bbc80f6f5d1ddea4790d5f12e8a85
|
7
|
+
data.tar.gz: bd8a8bf7a164c6d1219b20c3b3ef6ba5c9b2e0e16a4ebbfe1779f87e1ad1253a02ac8f23feb7e8c32699cced346959dcc36a0b97babd3dd5357a9c0b6672f54c
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Forkner
|
2
|
+
|
3
|
+
Forkner helps you manage multiple child processes and getting data back from
|
4
|
+
them when they finish.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
Consider a situation in which you need to run a bunch of parallel processes, but
|
9
|
+
no more than five at a time. You might do that like this:
|
10
|
+
|
11
|
+
@@code basic basic
|
12
|
+
|
13
|
+
First we load the `forkner` gem. Then we call Forkner's container `method`. All
|
14
|
+
child processes within `container` will completed before the method is done.
|
15
|
+
`container` yields a Forkner object.
|
16
|
+
Inside the `container` block we run whatever commands are necessary to set up
|
17
|
+
for running the child processes. In this simple example we simply loop 100
|
18
|
+
times.
|
19
|
+
Inside the loop, we use the forkner object to fork off into a child process.
|
20
|
+
Everything in `child` is run within the child process. At the end of that block
|
21
|
+
the child processes exits.
|
22
|
+
|
23
|
+
When the loop gets around and calls `child` again, another child process is only
|
24
|
+
run when there are fewer than five (the number we passed into `container`) child
|
25
|
+
processes. `child` pauses until there is a slot available.
|
26
|
+
At the bottom of the `container` block, Forkner waits until any remaining
|
27
|
+
child processes have finished.
|
28
|
+
|
29
|
+
### Getting information from the child process
|
30
|
+
|
31
|
+
Forkner allows you to communicate information form the child process back to the
|
32
|
+
parent process. Doing so requires two steps. First, set up a block that
|
33
|
+
processes information returned from child processes. Second, in the child
|
34
|
+
processes, finish the block with a value that can be stored as JSON.
|
35
|
+
|
36
|
+
Consider this example:
|
37
|
+
|
38
|
+
@@code retrieve retrieve
|
39
|
+
|
40
|
+
In this example, inside the `container` block we call the Forkner object's
|
41
|
+
`reaper` method with a block. In that block we get a single parameter which
|
42
|
+
contains information from the child process. In this case we know it's a hash
|
43
|
+
and we output two of its values.
|
44
|
+
|
45
|
+
Inside the `child` block, the child generates a random value and a timestamp. The
|
46
|
+
last line in the block is a hash of those values.
|
47
|
+
|
48
|
+
Behind the scenes, that hash is converted to JSON and stored in a temporary
|
49
|
+
file. When the `reaper` method is called, the hash is reconstituted from the
|
50
|
+
JSON file and returned to the `reaper` block. That's why it's important that the
|
51
|
+
child block end with a value that can be stored as JSON.
|
52
|
+
|
53
|
+
### Using Forkner without the container block
|
54
|
+
|
55
|
+
If you prefer to get a little closer to the metal, you can directly create a
|
56
|
+
Forkner object and call its `child` method. Just be sure to call the `wait_all`
|
57
|
+
method after all the child processes have been called. The following code does
|
58
|
+
exactly the same thing as the previous example.
|
59
|
+
|
60
|
+
@@code no-container no-container
|
61
|
+
|
62
|
+
## Install
|
63
|
+
|
64
|
+
```
|
65
|
+
gem install forkner
|
66
|
+
```
|
67
|
+
|
68
|
+
## Author
|
69
|
+
|
70
|
+
Mike O'Sullivan
|
71
|
+
mike@idocs.com
|
72
|
+
|
73
|
+
## History
|
74
|
+
|
75
|
+
| version | date | notes |
|
76
|
+
|---------|-------------|-----------------------------------------------|
|
77
|
+
| 1.0 | Jan 7, 2020 | Initial upload. |
|
78
|
+
| 1.1 | Jan 7, 2020 | Fixed some typos. No change to functionality. |
|
data/lib/forkner.rb
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
|
5
|
+
#===============================================================================
|
6
|
+
# Forkner
|
7
|
+
#
|
8
|
+
class Forkner
|
9
|
+
|
10
|
+
# The maximum number of child processes to run at once.
|
11
|
+
attr_accessor :max
|
12
|
+
|
13
|
+
# Temporary directory for storing information from child processes. Defaults
|
14
|
+
# to /tmp.
|
15
|
+
attr_accessor :tmp_dir
|
16
|
+
|
17
|
+
##
|
18
|
+
# Version 1.1
|
19
|
+
VERSION = '1.1'
|
20
|
+
|
21
|
+
|
22
|
+
#---------------------------------------------------------------------------
|
23
|
+
# self.container
|
24
|
+
#
|
25
|
+
|
26
|
+
# container is probably the easiest way to use Forkner. Run all the code
|
27
|
+
# that will have child processes inside a container block. The single
|
28
|
+
# parameter for container is the maximum number of child processes to allow.
|
29
|
+
# After the container block, Forkner waits for all remaining child processes
|
30
|
+
# to exit.
|
31
|
+
#
|
32
|
+
# So, for example, this code loops 100 times, but will not fork more than
|
33
|
+
# five children at a time:
|
34
|
+
#
|
35
|
+
# Forkner.container(5) do |forkner|
|
36
|
+
# 100.times do
|
37
|
+
# forkner.child do
|
38
|
+
# # do stuff in the child process
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
|
43
|
+
def self.container(max)
|
44
|
+
forkner = Forkner.new(max)
|
45
|
+
yield forkner
|
46
|
+
forkner.wait_all
|
47
|
+
end
|
48
|
+
#
|
49
|
+
# self.container
|
50
|
+
#---------------------------------------------------------------------------
|
51
|
+
|
52
|
+
|
53
|
+
#---------------------------------------------------------------------------
|
54
|
+
# initialize
|
55
|
+
#
|
56
|
+
|
57
|
+
# Creates a new Forkner object. The single parameter is the maximum number
|
58
|
+
# of child processes to run at once. So, for example, the following code
|
59
|
+
# creates a Forkner object that will allow up to five child processes at
|
60
|
+
# once.
|
61
|
+
#
|
62
|
+
# forkner = Forkner.new(5)
|
63
|
+
|
64
|
+
def initialize(max)
|
65
|
+
@max = max
|
66
|
+
@children = {}
|
67
|
+
@tmp_dir = '/tmp'
|
68
|
+
@reaper = nil
|
69
|
+
end
|
70
|
+
#
|
71
|
+
# initialize
|
72
|
+
#---------------------------------------------------------------------------
|
73
|
+
|
74
|
+
|
75
|
+
#---------------------------------------------------------------------------
|
76
|
+
# child
|
77
|
+
#
|
78
|
+
|
79
|
+
# Runs a child process. If there are already the maximum number of children
|
80
|
+
# running then this method waits until one of them exits. The last line of
|
81
|
+
# the block is information that can be stored in JSON, and if you have
|
82
|
+
# defined a reaper block, then that information will be conveyed back to the
|
83
|
+
# parent process.
|
84
|
+
#
|
85
|
+
# For example, this child process generates a random number and a timestamp.
|
86
|
+
# The last line of the block is a hash that will be sent back to the parent
|
87
|
+
# process.
|
88
|
+
#
|
89
|
+
# forkner.child do
|
90
|
+
# myrand = rand()
|
91
|
+
# timestamp = Time.now
|
92
|
+
# {'myrand'=>myrand, 'timestamp'=>timestamp}
|
93
|
+
# end
|
94
|
+
|
95
|
+
def child
|
96
|
+
# init
|
97
|
+
json_path = nil
|
98
|
+
|
99
|
+
# transfer file
|
100
|
+
if @reaper
|
101
|
+
json_path = Random.rand().to_s
|
102
|
+
json_path = json_path.sub(/\A.*\./mu, '')
|
103
|
+
json_path = @tmp_dir + '/' + json_path
|
104
|
+
end
|
105
|
+
|
106
|
+
# wait until we have a space for a process
|
107
|
+
waiter @max - 1
|
108
|
+
|
109
|
+
# parent process
|
110
|
+
if new_child_pid = Process.fork()
|
111
|
+
# set child record
|
112
|
+
child = @children[new_child_pid] = {}
|
113
|
+
|
114
|
+
# file handle
|
115
|
+
if @reaper
|
116
|
+
child['json_path'] = json_path
|
117
|
+
end
|
118
|
+
|
119
|
+
# return true
|
120
|
+
return true
|
121
|
+
|
122
|
+
# child process
|
123
|
+
else
|
124
|
+
# yield if necessary
|
125
|
+
if block_given?
|
126
|
+
rv = yield()
|
127
|
+
|
128
|
+
# save to json file if necessary
|
129
|
+
if json_path
|
130
|
+
File.write json_path, JSON.generate(rv)
|
131
|
+
end
|
132
|
+
|
133
|
+
# exit child process
|
134
|
+
exit
|
135
|
+
end
|
136
|
+
|
137
|
+
# always return false
|
138
|
+
return false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
#
|
142
|
+
# child
|
143
|
+
#---------------------------------------------------------------------------
|
144
|
+
|
145
|
+
|
146
|
+
#---------------------------------------------------------------------------
|
147
|
+
# reaper
|
148
|
+
#
|
149
|
+
|
150
|
+
# Defines the block to run when a child process finishes. The single param
|
151
|
+
# passed to the block is a hash or array of information from the child
|
152
|
+
# process. For example, if the child process returns a hash of information,
|
153
|
+
# you might display it like this:
|
154
|
+
#
|
155
|
+
# forkner.reaper() do |rv|
|
156
|
+
# puts '-----'
|
157
|
+
# puts rv['myrand']
|
158
|
+
# puts rv['timestamp']
|
159
|
+
# end
|
160
|
+
|
161
|
+
def reaper(&block)
|
162
|
+
@reaper = block
|
163
|
+
end
|
164
|
+
#
|
165
|
+
# reaper
|
166
|
+
#---------------------------------------------------------------------------
|
167
|
+
|
168
|
+
|
169
|
+
#---------------------------------------------------------------------------
|
170
|
+
# wait_all
|
171
|
+
#
|
172
|
+
|
173
|
+
##
|
174
|
+
# Waits for all child processes to finish. You don't need to call this
|
175
|
+
# method if you're using Forkner.container.
|
176
|
+
def wait_all
|
177
|
+
waiter 0
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# waitall is just an alias for wait_all because I can never remember
|
182
|
+
# whether or not there's an underscore in the method.
|
183
|
+
alias waitall wait_all
|
184
|
+
|
185
|
+
#
|
186
|
+
# wait_all
|
187
|
+
#---------------------------------------------------------------------------
|
188
|
+
|
189
|
+
|
190
|
+
# private
|
191
|
+
private
|
192
|
+
|
193
|
+
|
194
|
+
#---------------------------------------------------------------------------
|
195
|
+
# waiter
|
196
|
+
#
|
197
|
+
def waiter(wait_max)
|
198
|
+
# loop until we have fewer than @max children
|
199
|
+
while @children.length > wait_max
|
200
|
+
begin
|
201
|
+
# wait
|
202
|
+
old_child_pid = Process.wait(-1, Process::WNOHANG)
|
203
|
+
old_child = @children.delete(old_child_pid)
|
204
|
+
|
205
|
+
# if child
|
206
|
+
if old_child and old_child['json_path']
|
207
|
+
# if reaper
|
208
|
+
if @reaper
|
209
|
+
# slurp in JSON
|
210
|
+
rv = JSON.parse(File.read(old_child['json_path']))
|
211
|
+
|
212
|
+
# delete transfer file
|
213
|
+
File.unlink old_child['json_path']
|
214
|
+
|
215
|
+
# call reaper
|
216
|
+
@reaper.call rv
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# TODO: Handle system errors
|
221
|
+
rescue SystemCallError
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
#
|
226
|
+
# waiter
|
227
|
+
#---------------------------------------------------------------------------
|
228
|
+
end
|
229
|
+
#
|
230
|
+
# Forkner
|
231
|
+
#===============================================================================
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: forkner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike O'Sullivan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-01-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Manages forking multiple processes and returns results to the parent
|
14
|
+
process
|
15
|
+
email: mike@idocs.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/forkner.rb
|
22
|
+
homepage: https://rubygems.org/gems/forkner
|
23
|
+
licenses:
|
24
|
+
- MIT
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.7.6
|
43
|
+
signing_key:
|
44
|
+
specification_version: 4
|
45
|
+
summary: Fork manager with return values
|
46
|
+
test_files: []
|