miner_mover 0.0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6bf5c39cc32d1d7f8b7bf4528223eb5be897f59a62851e8875f396a43a4197f
4
+ data.tar.gz: 297267fba1f86e49871742687d5df26a730804cb2dc67026647bdc2dc7f49855
5
+ SHA512:
6
+ metadata.gz: c7b446c0a36b0636cfd237b1b2adcbd0bbf9d2078a0eb0dc0246f678a9a46bfce0f7c1295e2c38a543dae192695dc23b87926bb3217f8c0ccaf878d57834702f
7
+ data.tar.gz: 9829ccf199d42f8d2a85a2aa392e0e89ef34a4f17d5de6bcd3d4adf5018216e7f80dc69fd7c4cd05384d20a0f25e961ce379ec320fadf536a2ae9452051e0a58
data/README.md ADDED
@@ -0,0 +1,321 @@
1
+ [![Test Status](https://github.com/rickhull/miner_mover/actions/workflows/test.yaml/badge.svg)](https://github.com/rickhull/miner_mover/actions/workflows/test.yaml)
2
+
3
+ # Miner Mover
4
+
5
+ This project provides a basic concurrency problem useful for exploring
6
+ different multitasking paradigms available in Ruby. Fundamentally, we have a
7
+ set of *miners* and a set of *movers.* A *miner* takes some amount of time to
8
+ mine ore, which is given to a *mover*. When a *move*r has enough ore for a full
9
+ batch, the delivery takes some amount of time before more ore can be
10
+ loaded.
11
+
12
+ ## Mining
13
+
14
+ A miner is given some depth (e.g. 1 to 100) to mine down to, which will
15
+ take an increasing amount of time with depth. More depth provides greater ore
16
+ results as well. Ore is gathered at each depth; either a fixed amount or
17
+ randomized, based on depth. The amount of time spent mining each level is
18
+ independent and may be randomized.
19
+
20
+ https://github.com/rickhull/miner_mover/blob/bd76ea400944aab8eab9e3ffcac85d1e28353eff/lib/miner_mover/worker.rb#L85-L99
21
+
22
+ In this case, miners are rewarded by calculating `fibonacci(depth)`, using
23
+ classic, inefficient fibonacci. 10M ore represents `fibonacci(35)`, which
24
+ takes around 0.75 seconds on my local VM.
25
+
26
+ ## Moving
27
+
28
+ A mover has a batch size, say 10. As the mover accumulates ore over time,
29
+ once the batch size is reached, the mover delivers the ore to the destination.
30
+ Larger batches take longer. The delivery time can be randomized.
31
+
32
+ https://github.com/rickhull/miner_mover/blob/bd76ea400944aab8eab9e3ffcac85d1e28353eff/lib/miner_mover/worker.rb#L135-L162
33
+
34
+ The time and work spent delivering ore can be simulated three ways,
35
+ configured via `:work_type`
36
+
37
+ * `:wait` - represents waiting on IO; calls `sleep(duration)`
38
+ * `:cpu` - busy work; calls `fibonacci(30)` until `duration` is reached
39
+ * `:instant` - useful for testing; returns immediately
40
+
41
+ # Usage
42
+
43
+ ## Install
44
+
45
+ You'll want to use Ruby 3.x to make the most of Fibers.
46
+
47
+ Clone the repo, then install dependencies:
48
+
49
+ * rake
50
+ * minitest
51
+ * compsci
52
+ * dotcfg
53
+ * fiber_scheduler
54
+
55
+ `gem install rake minitest compsci dotcfg fiber_scheduler`
56
+
57
+ ## Satisfy `LOAD_PATH`
58
+
59
+ Execute scripts and irb sessions from the project root, e.g. `~/miner_mover`.
60
+ Use `-I lib` as a flag to `ruby` or `irb` to add e.g. `~/miner_mover/lib`
61
+ to `LOAD_PATH` so that `require 'miner_mover'` will work.
62
+ This project does not use `require_relative`.
63
+
64
+ ## Exploration in `irb`
65
+
66
+ `$ irb -Ilib -rminer_mover/worker`
67
+
68
+ ```
69
+ irb(main):001:0> include MinerMover
70
+ => Object
71
+
72
+ irb(main):002:0> miner = Miner.new
73
+ =>
74
+ #<MinerMover::Miner:0x00007fdd649754e8
75
+ ...
76
+
77
+ irb(main):003:0> miner.mine_ore
78
+ => 0
79
+
80
+ irb(main):004:0> miner.mine_ore 5
81
+ => 3
82
+
83
+ irb(main):005:0> miner.mine_ore 20
84
+ => 6483
85
+
86
+ irb(main):006:0> mover = Mover.new
87
+ =>
88
+ #<MinerMover::Mover:0x00007fdd64979930
89
+ ...
90
+
91
+ irb(main):007:0> mover.load_ore 6483
92
+ => 6483
93
+
94
+ irb(main):008:0> mover.status
95
+ => "Batch 6483 / 10M 0% | Moved 0x (0M)"
96
+ ```
97
+
98
+ ## Included scripts
99
+
100
+ These scripts implement a full miner mover simulation using different
101
+ multitasking paradigms in Ruby.
102
+
103
+ * [`demo/serial.rb`](demo/serial.rb)
104
+ * [`demo/fiber.rb`](demo/fiber.rb)
105
+ * [`demo/fiber_scheduler.rb`](demo/fiber_scheduler.rb)
106
+ * [`demo/thread.rb`](demo/thread.rb)
107
+ * [`demo/ractor.rb`](demo/ractor.rb)
108
+
109
+ See [config/example.cfg](config/example.cfg) for configuration.
110
+ It will be loaded by default.
111
+ Note that serial.rb and fiber.rb have no concurrency and cannot use multiple
112
+ miners or movers.
113
+
114
+ Execute via e.g. `ruby -Ilib demo/ractor.rb`
115
+
116
+ # Multitasking
117
+
118
+ *Multitasking* here means "the most general sense of performing several tasks
119
+ or actions *at the same time*". *At the same time* can mean fast switching
120
+ between tasks, or left and right hands operating truly in parallel.
121
+
122
+ ## Concurrency
123
+
124
+ In the broadest sense, two tasks are *concurrent* if they happen *at the
125
+ same time*, as above. When I tell Siri to call home while I drive, I perform
126
+ these tasks concurrently.
127
+
128
+ ## Parallelism
129
+
130
+ In the strictest sense of parallelism, one executes several *identical* tasks
131
+ using multiple *facilities* that operate independently and in parallel.
132
+ Multiple lanes on a highway offer parallelism for the task of driving from
133
+ A to B.
134
+
135
+ If there is a bucket brigade to put out a fire, all members of the brigade are
136
+ operating in parallel. The last brigade member is dousing the fire instead of
137
+ handing the bucket to the next member. While this might not meet the most
138
+ strict definition of parallelism, it is broadly accepted as parallel. It is
139
+ certainly concurrent. Often though, *concurrent* means *merely concurrent*,
140
+ where there is only one *facility* switching between tasks rather than multiple
141
+ devices operating in parallel.
142
+
143
+ ## Multitasking from the perspective of the OS (Linux, Windows, MacOS)
144
+
145
+ * A modern OS executes _threads_ within a _process_
146
+ * Processes are mostly about accounting and containment
147
+ - Organization and safety from other processes and users
148
+ * By default, a process has a single thread of execution
149
+ * A single-threaded process cannot (easily) perform two tasks concurrently
150
+ - Maybe it implements green threads or coroutines?
151
+ * A process can (easily) create additional threads for multitasking
152
+ - Either within this process or via spawning a child process
153
+ * Process spawning implies more overhead than thread creation
154
+ - Threads can only share memory within a process
155
+ - fork() / CoW can provide thread-like efficiency
156
+ * Child processes are managed differently than threads
157
+ - Memory protection
158
+ - OS integration / init system
159
+
160
+ # Multitasking in Ruby
161
+
162
+ The default Ruby runtime is known as CRuby, named for its implementation in
163
+ the C language, also known as MRI (Matz Ruby Interpreter), named for its
164
+ creator Yukihiro Matsumoto. Some history:
165
+
166
+ ## Before YARV (up to Ruby 1.9):
167
+
168
+ * Execute-as-we-interpret
169
+ * Ruby code executes as the main thread of the main process
170
+ * Green threads implemented and scheduled by the runtime (not OS threads)
171
+ * GIL (Global Interpreter Lock) implies threads cannot execute in parallel
172
+ * Occasional concurrency, when a waiting thread is scheduled out in favor of a
173
+ running thread
174
+ - `schedule(waiting, running) YES`
175
+ - `schedule(waiting, waiting) NO`
176
+ - `schedule(running, running) NO`
177
+ - `schedule(running, waiting) OH DEAR`
178
+
179
+ ## YARV (Ruby 1.9 through 3.x):
180
+
181
+ * Interpret to bytecode, then execute
182
+ * YARV (Yet Another Ruby VM) is introduced, providing a runtime virtual
183
+ machine for executing bytecode
184
+ * Fiber is introduced for cooperative multitasking, lighter than threads
185
+ * Ruby code executes as the main fiber of the main thread of the main process
186
+ * Ruby threads are implemented as OS threads, scheduled by the OS
187
+ * YARV is single threaded (not threadsafe) thus requring a Global VM Lock (GVL)
188
+ - GVL is more fine grained than GIL
189
+ - Threads explicitly give up the execution lock when waiting (IO, sleep, etc)
190
+ * YARV typically achieves 2-4x concurrency with multiple threads
191
+ - Less concurrency when threads are CPU bound (thus waiting on GVL)
192
+ - More concurrency when threads are IO bound (thus yielding GVL)
193
+ - Less concurrency when not enough threads (GVL is underutilized)
194
+ - More waiting (BAD!) when too many threads (GVL is under contention)
195
+ - Thus, tuning is required, with many pathological cases
196
+ - Thread pools (where most threads are idle) make tuning easier but still
197
+ required
198
+ * As before, processes can be spawned for more more true parallelism
199
+ - typically via `fork` with Copy-on-write for efficiency
200
+ - management of child process lifecycles can be more difficult than
201
+ multithreading
202
+ - multiprocessing and multithreading can be combined, often with differing
203
+ task shapes
204
+ * Fibers offer even lighter weight concurrency primitives
205
+
206
+ ### Fibers
207
+
208
+ ```
209
+ Fiber.yield(arg) # call within a Fiber to suspend execution and yield a value
210
+ Fiber#resume # tell a Fiber to proceed and return the next yielded value
211
+ ```
212
+
213
+ ```ruby
214
+ fiber = Fiber.new do
215
+ Fiber.yield 1
216
+ 2
217
+ end
218
+
219
+ fiber.resume
220
+ #=> 1
221
+
222
+ fiber.resume
223
+ #=> 2
224
+
225
+ fiber.resume
226
+ # FiberError: attempt to resume a terminated fiber
227
+ ```
228
+
229
+ Any argument(s) passed to `Fiber#resume` on its first call (to start the Fiber)
230
+ will be passed to the `Fiber.new` block:
231
+
232
+ ```ruby
233
+ fiber = Fiber.new do |arg1, arg2|
234
+ Fiber.yield arg1
235
+ arg2
236
+ end
237
+
238
+ fiber.resume(:x, :y)
239
+ #=> :x
240
+
241
+ fiber.resume
242
+ #=> :y
243
+ ```
244
+
245
+ ## YARV with `Fiber::Scheduler` (Ruby 3.x)
246
+
247
+ * Non-blocking fibers are introduced
248
+ - any waits that would cause a fiber to block will cause the fiber to suspend
249
+ - `Fiber::Scheduler` is introduced to manage non-blocking fibers
250
+
251
+ ### Non-blocking Fibers
252
+
253
+ The concept of non-blocking fiber was introduced in Ruby 3.0. A non-blocking
254
+ fiber, when reaching a operation that would normally block the fiber (like
255
+ sleep, or wait for another process or I/O) will yield control to other fibers
256
+ and allow the scheduler to handle blocking and waking up (resuming) this fiber
257
+ when it can proceed.
258
+
259
+ For a Fiber to behave as non-blocking, it need to be created in `Fiber.new`
260
+ with `blocking: false` (which is the default), and `Fiber.scheduler` should be
261
+ set with `Fiber.set_scheduler`. If `Fiber.scheduler` is not set in the current
262
+ thread, blocking and non-blocking fibers’ behavior is identical.
263
+
264
+ Thus, any fiber without a scheduler is a blocking fiber. If a fiber is created
265
+ with `blocking: true`, it is a blocking fiber. Otherwise, if it has a
266
+ scheduler, it is non-blocking.
267
+
268
+ ### Fiber scheduling
269
+
270
+ ```
271
+ Fiber.scheduler # get the current scheduler
272
+ Fiber.set_scheduler # set the current scheduler
273
+ Fiber.schedule # perform a given block in a non-blocking manner
274
+ Fiber::Scheduler # scheduler interface
275
+ ```
276
+
277
+ ### `Fiber::Scheduler`
278
+
279
+ * `Fiber::Scheduler` is **not an implementation** but an **interface**
280
+ * The implementation is provided by a library / gem / user
281
+
282
+ ## YARV with Ractors (Ruby 3.x, experimental)
283
+
284
+ * YARV allows multiple threads but locks areas where multiple threads have
285
+ access to the same data
286
+ * Ractors are introduced, with no shared data, requiring messages to be passed
287
+ between Ractors
288
+ * Ruby code executes as the main Fiber of the main Thread of the main Ractor
289
+ of the main Process
290
+ * The default thread within each Ractor has its own OS thread, with as much
291
+ parallelism as the host OS provides
292
+ * Additional threads spawned by a Ractor are normal OS threads but they must
293
+ contend for the Ractor Lock (RL) to execute on YARV
294
+
295
+ ### Ractors
296
+
297
+ Ractors are an abstraction and a container for threads. Threads within a
298
+ Ractor can share memory. Threads must use message passaging to communicate
299
+ across Ractors. Also, Ractors hold the execution lock on YARV, so threads
300
+ in different Ractors have zero contention.
301
+
302
+ ```ruby
303
+ # get the current Ractor object
304
+ r = Ractor.current
305
+
306
+ # create a new Ractor (block will execute in parallel via thread creation)
307
+ Ractor.new(arg1, arg2, etc) { |arg1, arg2, etc|
308
+ # now use arg1 and arg2 from outside
309
+ }
310
+ ```
311
+
312
+ * Ractors communicate via messages
313
+ * send via outgoing port
314
+ * receive via incoming port (infinite storage, FIFO)
315
+
316
+ ```
317
+ Ractor#send - puts a message at the incoming port of a Ractor
318
+ Ractor.receive - returns a message from the current Ractor's incoming port
319
+ Ractor.yield - current Ractor sends a message on the outgoing port
320
+ Ractor#take - returns the next outgoing message from a Ractor
321
+ ```
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new :test do |t|
4
+ t.pattern = "test/*.rb"
5
+ t.warning = true
6
+ end
7
+
8
+ task default: :test
9
+
10
+ begin
11
+ require 'buildar'
12
+
13
+ Buildar.new do |b|
14
+ b.gemspec_file = 'miner_mover.gemspec'
15
+ b.version_file = 'VERSION'
16
+ b.use_git = true
17
+ end
18
+ rescue LoadError
19
+ warn "buildar tasks unavailable"
20
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0.3
data/demo/config.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'miner_mover/run'
2
+
3
+ MinerMover::Run.new.cfg_banner!
data/demo/fiber.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'miner_mover/run'
2
+
3
+ include MinerMover
4
+
5
+ run = Run.new.cfg_banner!(duration: 1)
6
+ run.timer.timestamp!
7
+ run.log "Starting"
8
+
9
+ stop_mining = false
10
+ Signal.trap("INT") {
11
+ run.timer.timestamp!
12
+ run.log " *** SIGINT *** Stop Mining"
13
+ stop_mining = true
14
+ }
15
+
16
+ # miner runs in its own Fiber
17
+ miner = Fiber.new(blocking: true) {
18
+ run.log "MINE Mining operation started [ctrl-c] to stop"
19
+ m = run.new_miner
20
+
21
+ ore_mined = 0
22
+
23
+ # miner waits for the SIGINT signal to quit
24
+ while !stop_mining
25
+ ore = m.mine_ore
26
+
27
+ # send any ore mined to the mover
28
+ Fiber.yield ore if ore > 0
29
+ ore_mined += ore
30
+
31
+ # stop mining after a while
32
+ if run.time_limit? or run.ore_limit?(ore_mined)
33
+ run.timer.timestamp!
34
+ m.log format("Mining limit reached: %s", Ore.display(ore_mined))
35
+ stop_mining = true
36
+ end
37
+ end
38
+
39
+ m.log format("MINE Miner finished after mining %s", Ore.display(ore_mined))
40
+ Fiber.yield :quit
41
+ ore_mined
42
+ }
43
+
44
+ mover = run.new_mover
45
+ run.log "MOVE Moving operation started"
46
+ run.log "WAIT Waiting for ore ..."
47
+
48
+ loop {
49
+ # pick up ore yielded by the miner
50
+ ore = miner.resume
51
+ break if ore == :quit
52
+
53
+ # load (and possibly move) the ore
54
+ mover.load_ore ore if ore > 0
55
+ }
56
+
57
+ # miner has quit; move any remaining ore and quit
58
+ mover.move_batch while mover.batch > 0
59
+ run.log "QUIT #{mover.status}"
60
+
61
+ ore_mined = miner.resume
62
+ ore_moved = mover.ore_moved
63
+ run.log format("MINE %s mined (%i)", Ore.display(ore_mined), ore_mined)
64
+ run.log format("MOVE %s moved (%i)", Ore.display(ore_moved), ore_moved)
65
+ run.timer.timestamp!
@@ -0,0 +1,117 @@
1
+ require 'miner_mover/run'
2
+ require 'fiber_scheduler'
3
+
4
+ include MinerMover
5
+
6
+ run = Run.new.cfg_banner!(duration: 1)
7
+ run.timer.timestamp!
8
+ run.log "Starting"
9
+
10
+ stop_mining = false
11
+ Signal.trap("INT") {
12
+ run.timer.timestamp!
13
+ run.log " *** SIGINT *** Stop Mining"
14
+ stop_mining = true
15
+ }
16
+
17
+ # for moving ore
18
+ queue = Thread::Queue.new
19
+
20
+ # for signalling between miners and supervisor
21
+ mutex = Mutex.new
22
+ miner_quit = ConditionVariable.new
23
+
24
+ # for getting results from scheduled fibers
25
+ mined = Thread::Queue.new
26
+ moved = Thread::Queue.new
27
+
28
+ # follow the rabbit
29
+ FiberScheduler do
30
+
31
+ # several miners, stored in an array
32
+ miners = Array.new(run.num_miners) { |i|
33
+
34
+ # each miner gets a fiber
35
+ Fiber.schedule do
36
+ m = run.new_miner
37
+ m.log "MINE Miner #{i} started"
38
+
39
+ ore_mined = 0
40
+
41
+ # miner waits for the SIGINT signal to quit
42
+ while !stop_mining
43
+ ore = m.mine_ore
44
+
45
+ # send any ore mined to the mover
46
+ queue.push(ore) if ore > 0
47
+ ore_mined += ore
48
+
49
+ # stop mining after a while
50
+ if run.time_limit? or run.ore_limit?(ore_mined)
51
+ run.timer.timestamp!
52
+ m.log format("Mining limit reached: %s", Ore.display(ore_mined))
53
+ stop_mining = true
54
+ end
55
+ end
56
+
57
+ m.log format("MINE Miner #{i} finished after mining %s",
58
+ Ore.display(ore_mined))
59
+
60
+ # register the ore mined (scheduled fiber can't return a value)
61
+ mined.push ore_mined
62
+
63
+ # signal to the supervisor that a miner is done
64
+ mutex.synchronize { miner_quit.signal }
65
+ end
66
+ }
67
+
68
+ # several movers, no need to store
69
+ run.num_movers.times { |i|
70
+
71
+ # each mover gets a fiber
72
+ Fiber.schedule do
73
+ m = run.new_mover
74
+ m.log "MOVE Mover #{i} started"
75
+
76
+ loop {
77
+ # pick up ore from the miner until we get a :quit message
78
+ ore = queue.pop
79
+ break if ore == :quit
80
+
81
+ # load (and possibly move) the ore
82
+ m.load_ore ore if ore > 0
83
+ }
84
+
85
+ # miners have quit; move any remaining ore and quit
86
+ m.move_batch while m.batch > 0
87
+ m.log "QUIT #{m.status}"
88
+
89
+ # register the ore moved (scheduled fiber can't return a value)
90
+ moved.push m.ore_moved
91
+ end
92
+ }
93
+
94
+ # supervisor waits for the miners to quit
95
+ # and signals the mover to quit by pushing :quit onto the queue
96
+ Fiber.schedule do
97
+ # every time a miner quits, check if any are left
98
+ mutex.synchronize { miner_quit.wait(mutex) while miners.any?(&:alive?) }
99
+
100
+ # tell every mover to quit
101
+ run.num_movers.times { queue.push(:quit) }
102
+
103
+ # queue closes once it is empty
104
+ # should helpfully cause errors if something is out of sync
105
+ queue.close
106
+ end
107
+ end
108
+
109
+ total_mined = 0
110
+ total_mined += mined.pop until mined.empty?
111
+
112
+ total_moved = 0
113
+ total_moved += moved.pop until moved.empty?
114
+
115
+ run.log format("MINE %s mined (%i)", Ore.display(total_mined), total_mined)
116
+ run.log format("MOVE %s moved (%i)", Ore.display(total_moved), total_moved)
117
+ run.timer.timestamp!
data/demo/process.rb ADDED
@@ -0,0 +1,109 @@
1
+ require 'miner_mover/run'
2
+ require 'thread'
3
+
4
+ include MinerMover
5
+
6
+ run = Run.new.cfg_banner!(duration: 1)
7
+ run.timer.timestamp!
8
+ run.log "Starting"
9
+
10
+ stop_mining = false
11
+ Signal.trap("INT") {
12
+ run.timer.timestamp!
13
+ run.log " *** SIGINT *** Stop Mining"
14
+ stop_mining = true
15
+ }
16
+
17
+ mover = Process.fork {
18
+ run.log "MOVE Moving operation started"
19
+
20
+ # mover queue
21
+ queue = Thread::Queue.new
22
+
23
+ # store the mover threads in an array
24
+ movers = Array.new(run.num_movers) { |i|
25
+ Thread.new {
26
+ m = run.new_mover
27
+ m.log "MOVE Mover #{i} started"
28
+
29
+ loop {
30
+ # a mover picks up ore from the queue
31
+ run.debug && m.log("POP ")
32
+ ore = queue.pop
33
+ run.debug && m.log("POPD #{ore}")
34
+
35
+ break if ore == :quit
36
+
37
+ # load (and possibly move) the ore
38
+ m.load_ore ore
39
+ }
40
+
41
+ # move any remaining ore and quit
42
+ m.move_batch while m.batch > 0
43
+ m.log "QUIT #{m.status}"
44
+ m
45
+ }
46
+ }
47
+
48
+ run.log "WAIT Waiting for ore ..."
49
+
50
+ # TODO: pipe or unix socket for ore
51
+
52
+ TODO = :notyet
53
+
54
+ loop {
55
+ # pull from the pipe / socket
56
+ ore = TODO
57
+ break if ore == :quit
58
+ queue.push ore
59
+ }
60
+ }
61
+
62
+ # our mining operation executes in the main Process, here
63
+ run.log "MINE Mining operation started [ctrl-c] to stop"
64
+
65
+ # store the miner threads in an array
66
+ miners = Array.new(run.num_miners) { |i|
67
+ Thread.new {
68
+ m = run.new_miner
69
+ m.log "MINE Miner #{i} started"
70
+ ore_mined = 0
71
+
72
+ # miners wait for the SIGINT signal to quit
73
+ while !stop_mining
74
+ ore = m.mine_ore
75
+
76
+ # send any ore mined to the mover Ractor
77
+ if ore > 0
78
+ run.debug && m.log("SEND #{ore}")
79
+ mover.send ore
80
+ run.debug && m.log("SENT #{ore}")
81
+ end
82
+
83
+ ore_mined += ore
84
+
85
+ # stop mining after a while
86
+ if run.time_limit? or run.ore_limit?(ore_mined)
87
+ run.timer.timestamp!
88
+ m.log format("Mining limit reached: %s", Ore.display(ore_mined))
89
+ stop_mining = true
90
+ end
91
+ end
92
+
93
+ m.log format("MINE Miner %i finished after mining %s",
94
+ i, Ore.display(ore_mined))
95
+ ore_mined
96
+ }
97
+ }
98
+
99
+ # wait on all mining threads to stop
100
+ ore_mined = miners.map { |thr| thr.value }.sum
101
+ run.log format("MINE %s mined (%i)", Ore.display(ore_mined), ore_mined)
102
+
103
+ # tell mover to quit
104
+ mover.send :quit
105
+
106
+ # wait for results
107
+ ore_moved = mover.take
108
+ run.log format("MOVE %s moved (%i)", Ore.display(ore_moved), ore_moved)
109
+ run.timer.timestamp!