forkner 1.1
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/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: []
|