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.
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: []