forkner 1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +78 -0
  3. data/lib/forkner.rb +231 -0
  4. 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: []