miner_mover 0.0.0.3

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.
data/demo/ractor.rb ADDED
@@ -0,0 +1,117 @@
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
+ # the moving operation executes in its own Ractor
18
+ mover = Ractor.new(run) { |r|
19
+ r.log "MOVE Moving operation started"
20
+
21
+ # use queue to distribute incoming ore to mover threads
22
+ queue = Thread::Queue.new
23
+
24
+ # store the mover threads in an array
25
+ movers = Array.new(r.num_movers) { |i|
26
+ Thread.new {
27
+ m = r.new_mover
28
+ m.log "MOVE Mover #{i} started"
29
+
30
+ loop {
31
+ # a mover picks up ore from the queue
32
+ r.debug && m.log("POP ")
33
+ ore = queue.pop
34
+ r.debug && m.log("POPD #{ore}")
35
+
36
+ break if ore == :quit
37
+
38
+ # load (and possibly move) the ore
39
+ m.load_ore ore
40
+ }
41
+
42
+ # move any remaining ore and quit
43
+ m.move_batch while m.batch > 0
44
+ m.log "QUIT #{m.status}"
45
+ m
46
+ }
47
+ }
48
+
49
+ # Miners feed this Ractor with ore
50
+ # Pass the ore into a queue for the movers
51
+ # When the miners say to quit, tell the movers to quit
52
+ r.log "WAIT Waiting for ore ..."
53
+ loop {
54
+ # when the Ractor gets ore, push it into the queue
55
+ ore = Ractor.recv
56
+ r.debug && r.log("RECV #{ore}")
57
+
58
+ break if ore == :quit
59
+
60
+ r.debug && r.log("PUSH #{ore}")
61
+ queue.push ore
62
+ r.debug && r.log("PSHD #{ore}")
63
+ }
64
+
65
+ # tell all the movers to quit and gather their results
66
+ r.num_movers.times { queue.push :quit }
67
+ movers.map { |thr| thr.value.ore_moved }.sum
68
+ }
69
+
70
+ # our mining operation executes in the main Ractor, here
71
+ run.log "MINE Mining operation started [ctrl-c] to stop"
72
+
73
+ # store the miner threads in an array
74
+ miners = Array.new(run.num_miners) { |i|
75
+ Thread.new {
76
+ m = run.new_miner
77
+ m.log "MINE Miner #{i} started"
78
+ ore_mined = 0
79
+
80
+ # miners wait for the SIGINT signal to quit
81
+ while !stop_mining
82
+ ore = m.mine_ore
83
+
84
+ # send any ore mined to the mover Ractor
85
+ if ore > 0
86
+ run.debug && m.log("SEND #{ore}")
87
+ mover.send ore
88
+ run.debug && m.log("SENT #{ore}")
89
+ end
90
+
91
+ ore_mined += ore
92
+
93
+ # stop mining after a while
94
+ if run.time_limit? or run.ore_limit?(ore_mined)
95
+ run.timer.timestamp!
96
+ m.log format("Mining limit reached: %s", Ore.display(ore_mined))
97
+ stop_mining = true
98
+ end
99
+ end
100
+
101
+ m.log format("MINE Miner %i finished after mining %s",
102
+ i, Ore.display(ore_mined))
103
+ ore_mined
104
+ }
105
+ }
106
+
107
+ # wait on all mining threads to stop
108
+ ore_mined = miners.map { |thr| thr.value }.sum
109
+ run.log format("MINE %s mined (%i)", Ore.display(ore_mined), ore_mined)
110
+
111
+ # tell mover to quit
112
+ mover.send :quit
113
+
114
+ # wait for results
115
+ ore_moved = mover.take
116
+ run.log format("MOVE %s moved (%i)", Ore.display(ore_moved), ore_moved)
117
+ run.timer.timestamp!
data/demo/serial.rb ADDED
@@ -0,0 +1,50 @@
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
+ # system 'cpulimit', "--pid=#{Process.pid}", '--limit=1', '--background'
17
+
18
+ miner = run.new_miner
19
+ run.log "MINE Mining operation initialized [ctrl-c] to stop"
20
+
21
+ mover = run.new_mover
22
+ run.log "MOVE Moving operation initialized"
23
+
24
+ ore_mined = 0
25
+
26
+ # miner waits for the SIGINT signal to quit
27
+ while !stop_mining
28
+ # mine the ore
29
+ ore = miner.mine_ore
30
+ ore_mined += ore
31
+
32
+ # load (and possibly move) the ore
33
+ mover.load_ore ore if ore > 0
34
+
35
+ # stop mining after a while
36
+ if run.time_limit? or run.ore_limit?(ore_mined)
37
+ run.timer.timestamp!
38
+ miner.log format("Mining limit reached: %s", Ore.display(ore_mined))
39
+ stop_mining = true
40
+ end
41
+ end
42
+
43
+ # miner has quit; move any remaining ore and quit
44
+ mover.move_batch while mover.batch > 0
45
+ run.log "QUIT #{mover.status}"
46
+
47
+ ore_moved = mover.ore_moved
48
+ run.log format("MINE %s mined (%i)", Ore.display(ore_mined), ore_mined)
49
+ run.log format("MOVE %s moved (%i)", Ore.display(ore_moved), ore_moved)
50
+ run.timer.timestamp!
data/demo/thread.rb ADDED
@@ -0,0 +1,91 @@
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
+ run.log "MOVE Moving operation started"
18
+ queue = Thread::Queue.new
19
+ run.log "WAIT Waiting for ore ..."
20
+
21
+ # store mover threads in an array
22
+ movers = Array.new(run.num_movers) { |i|
23
+ Thread.new {
24
+ m = run.new_mover
25
+ run.log "MOVE Mover #{i} started"
26
+
27
+ loop {
28
+ # a mover picks up mined ore from the queue
29
+ run.debug && m.log("POP ")
30
+ ore = queue.pop
31
+ run.debug && m.log("POPD #{ore}")
32
+
33
+ break if ore == :quit
34
+
35
+ # load (and possibly move) the ore
36
+ m.load_ore ore
37
+ }
38
+
39
+ # move any remaining ore and quit
40
+ m.move_batch while m.batch > 0
41
+ m.log "QUIT #{m.status}"
42
+ m
43
+ }
44
+ }
45
+
46
+
47
+ run.log "MINE Mining operation started [ctrl-c] to stop"
48
+ # store the miner threads in an array
49
+ miners = Array.new(run.num_miners) { |i|
50
+ Thread.new {
51
+ m = run.new_miner
52
+ m.log "MINE Miner #{i} started"
53
+ ore_mined = 0
54
+
55
+ # miners wait for the SIGINT signal to quit
56
+ while !stop_mining
57
+ ore = m.mine_ore
58
+
59
+ # send any ore mined to the movers
60
+ if ore > 0
61
+ run.debug && m.log("PUSH #{ore}")
62
+ queue.push ore
63
+ run.debug && m.log("PSHD #{ore}")
64
+ end
65
+
66
+ ore_mined += ore
67
+
68
+ # stop mining after a while
69
+ if run.time_limit? or run.ore_limit?(ore_mined)
70
+ run.timer.timestamp!
71
+ m.log format("Mining limit reached: %s", Ore.display(ore_mined))
72
+ stop_mining = true
73
+ end
74
+ end
75
+
76
+ m.log format("MINE Miner %i finished after mining %s",
77
+ i, Ore.display(ore_mined))
78
+ ore_mined
79
+ }
80
+ }
81
+
82
+ # wait on all mining threads to stop
83
+ ore_mined = miners.map { |thr| thr.value }.sum
84
+ run.log format("MINE %s mined (%i)", Ore.display(ore_mined), ore_mined)
85
+
86
+ # tell all the movers to quit; gather their results
87
+ run.num_movers.times { queue.push :quit }
88
+
89
+ ore_moved = movers.map { |thr| thr.value.ore_moved }.sum
90
+ run.log format("MOVE %s moved (%i)", Ore.display(ore_moved), ore_moved)
91
+ run.timer.timestamp!
@@ -0,0 +1,78 @@
1
+ require 'dotcfg'
2
+
3
+ module MinerMover
4
+ module Config
5
+ class Error < RuntimeError; end
6
+
7
+ GLOB = '*/*.cfg'.freeze
8
+
9
+ # reasonable defaults for all known keys
10
+ DEFAULT = {
11
+ main: {
12
+ num_miners: 3,
13
+ num_movers: 3,
14
+ time_limit: 5,
15
+ ore_limit: 100,
16
+ logging: true,
17
+ }.freeze,
18
+ miner: {
19
+ depth: 30,
20
+ partial_reward: false,
21
+ variance: 0,
22
+ logging: true,
23
+ }.freeze,
24
+ mover: {
25
+ batch_size: 10,
26
+ rate: 2,
27
+ work_type: :wait,
28
+ variance: 0,
29
+ logging: true,
30
+ }.freeze,
31
+ }.freeze
32
+
33
+ # return an array of strings representing file paths
34
+ def self.gather(*globs)
35
+ (globs.unshift GLOB).inject([]) { |memo, glob| memo + Dir[glob] }
36
+ end
37
+
38
+ # return a file path as a string, or nil
39
+ def self.recent(*globs)
40
+ mtime, newest = Time.at(0), nil
41
+ self.gather(*globs).each { |file|
42
+ mt = File.mtime(file)
43
+ mtime, newest = mt, file if mt > mtime
44
+ }
45
+ newest
46
+ end
47
+
48
+ # return a hash with :miner, :mover, :main keys
49
+ def self.process(file = nil, cfg: nil)
50
+ cfg ||= DotCfg.new(file || self.recent)
51
+
52
+ if cfg['miner'] or cfg['mover'] or cfg['main']
53
+ # convert string keys to symbols
54
+ miner = (cfg['miner'] || {}).transform_keys { |k| k.to_sym }
55
+ mover = (cfg['mover'] || {}).transform_keys { |k| k.to_sym }
56
+ main = (cfg['main'] || {}).transform_keys { |k| k.to_sym }
57
+ elsif cfg[:miner] or cfg[:mover] or cfg[:main]
58
+ # assume all keys are symbols
59
+ miner = cfg[:miner] || {}
60
+ mover = cfg[:mover] || {}
61
+ main = cfg[:main] || {}
62
+ else
63
+ raise(Error, "couldn't find miner, mover, or main in #{file}")
64
+ end
65
+ { miner: DEFAULT[:miner].merge(miner),
66
+ mover: DEFAULT[:mover].merge(mover),
67
+ main: DEFAULT[:main].merge(main) }
68
+ end
69
+
70
+ # rewrites the dotcfg file, filling in any defaults, using symbols for keys
71
+ def self.rewrite(file)
72
+ cfg = DotCfg.new(file)
73
+ hsh = self.process(cfg: cfg)
74
+ hsh.each { |k, v| cfg[k] = v }
75
+ cfg.save
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,67 @@
1
+ require 'miner_mover/worker'
2
+ require 'miner_mover/config'
3
+
4
+ module MinerMover
5
+ class Run
6
+ def self.cfg_file(filename = nil)
7
+ f = filename || ARGV.shift || Config.recent
8
+ if f.nil?
9
+ raise(Config::Error, "no config file")
10
+ elsif !File.exist? f
11
+ raise(Config::Error, "can't find file #{f.inspect}")
12
+ elsif !File.readable? f
13
+ raise(Config::Error, "can't read file #{f.inspect}")
14
+ end
15
+ f
16
+ end
17
+
18
+ attr_accessor :debug, :logging
19
+ attr_accessor :num_miners, :num_movers
20
+ attr_accessor :cfg_file, :cfg, :miner, :mover, :timer
21
+ attr_accessor :time_limit, :ore_limit
22
+
23
+ def initialize(cfg_file: nil, timer: nil, debug: false)
24
+ @cfg_file = self.class.cfg_file(cfg_file)
25
+ @cfg = Config.process @cfg_file
26
+ main = @cfg.fetch :main
27
+ @miner = @cfg.fetch :miner
28
+ @mover = @cfg.fetch :mover
29
+
30
+ @time_limit = main.fetch :time_limit
31
+ @ore_limit = main.fetch :ore_limit
32
+ @logging = main.fetch :logging
33
+ @num_miners = main.fetch :num_miners
34
+ @num_movers = main.fetch :num_movers
35
+
36
+ @timer = timer || CompSci::Timer.new
37
+ @debug = debug
38
+ end
39
+
40
+ def cfg_banner!(duration: 0)
41
+ log "USING: #{@cfg_file}"
42
+ pp @cfg
43
+ sleep duration if duration > 0
44
+ self
45
+ end
46
+
47
+ def new_miner
48
+ Miner.new(**@miner)
49
+ end
50
+
51
+ def new_mover
52
+ Mover.new(**@mover)
53
+ end
54
+
55
+ def ore_limit?(ore_mined)
56
+ Ore.block(ore_mined) > @ore_limit
57
+ end
58
+
59
+ def time_limit?
60
+ @timer.elapsed > @time_limit
61
+ end
62
+
63
+ def log msg
64
+ @logging and puts(MinerMover.log_fmt(@timer, ' (main) ', msg))
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,164 @@
1
+ require 'miner_mover'
2
+ require 'compsci/timer'
3
+ require 'compsci/fibonacci'
4
+
5
+ module MinerMover
6
+ def self.work(duration, type = :wait, fib = 30)
7
+ case type
8
+ when :wait
9
+ sleep duration
10
+ duration
11
+ when :cpu
12
+ t = CompSci::Timer.new
13
+ CompSci::Fibonacci.classic(fib) while t.elapsed < duration
14
+ t.elapsed
15
+ when :instant
16
+ 0
17
+ else
18
+ raise "unknown work type: #{type.inspect}"
19
+ end
20
+ end
21
+
22
+ class Worker
23
+ attr_accessor :variance, :logging
24
+ attr_reader :timer
25
+
26
+ def initialize(variance: 0, logging: false, timer: nil)
27
+ @variance = variance
28
+ @logging = logging
29
+ @timer = timer || CompSci::Timer.new
30
+ end
31
+
32
+ def id
33
+ self.object_id.to_s.rjust(8, '0')
34
+ end
35
+
36
+ def state
37
+ { id: self.id,
38
+ logging: @logging,
39
+ timer: @timer.elapsed_ms.round,
40
+ variance: @variance }
41
+ end
42
+
43
+ def to_s
44
+ self.state.to_s
45
+ end
46
+
47
+ def log msg
48
+ @logging && puts(MinerMover.log_fmt(@timer, self.id, msg))
49
+ end
50
+
51
+ # 4 levels:
52
+ # 0 - no variance
53
+ # 1 - 12.5% variance (squeeze = 2)
54
+ # 2 - 25% variance (squeeze = 1)
55
+ # 3 - 50% variance (squeeze = 0)
56
+ def varied n
57
+ case @variance
58
+ when 0
59
+ n
60
+ when 1..3
61
+ MinerMover.randomize(n, 3 - @variance)
62
+ else
63
+ raise "unexpected variance: #{@variance.inspect}"
64
+ end
65
+ end
66
+ end
67
+
68
+ class Miner < Worker
69
+ attr_accessor :depth, :partial_reward
70
+
71
+ def initialize(depth: 10,
72
+ partial_reward: true,
73
+ variance: 0,
74
+ logging: false,
75
+ timer: nil)
76
+ @partial_reward = partial_reward
77
+ @depth = depth
78
+ super(variance: variance, logging: logging, timer: timer)
79
+ end
80
+
81
+ def state
82
+ super.merge(depth: @depth, partial_reward: @partial_reward)
83
+ end
84
+
85
+ def mine_ore(depth = @depth)
86
+ log format("MINE Depth %i", depth)
87
+ ores, elapsed = CompSci::Timer.elapsed {
88
+ # every new depth is a new mining operation
89
+ Array.new(depth) { |d|
90
+ # mine ore by calculating fibonacci for that depth
91
+ mined = CompSci::Fibonacci.classic(self.varied(d).round)
92
+ @partial_reward ? rand(1 + mined) : mined
93
+ }
94
+ }
95
+ total = ores.sum
96
+ log format("MIND %s %s (%.2f s)",
97
+ Ore.display(total), ores.inspect, elapsed)
98
+ total
99
+ end
100
+ end
101
+
102
+ class Mover < Worker
103
+ attr_reader :rate, :work_type, :batch, :batch_size, :batches, :ore_moved
104
+
105
+ def initialize(batch_size: 10,
106
+ rate: 2, # 2M ore per sec
107
+ work_type: :cpu,
108
+ variance: 0,
109
+ logging: false,
110
+ timer: nil)
111
+ @batch_size = batch_size * Ore::BLOCK
112
+ @rate = rate.to_f * Ore::BLOCK
113
+ @work_type = work_type
114
+ @batch, @batches, @ore_moved = 0, 0, 0
115
+ super(variance: variance, logging: logging, timer: timer)
116
+ end
117
+
118
+ def state
119
+ super.merge(work_type: @work_type,
120
+ batch_size: @batch_size,
121
+ batch: @batch,
122
+ batches: @batches,
123
+ ore_moved: @ore_moved)
124
+ end
125
+
126
+ def status
127
+ [format("Batch %s / %s %i%%",
128
+ Ore.units(@batch),
129
+ Ore.units(@batch_size),
130
+ @batch.to_f * 100 / @batch_size),
131
+ format("Moved %ix (%s)", @batches, Ore.units(@ore_moved)),
132
+ ].join(' | ')
133
+ end
134
+
135
+ def load_ore(amt)
136
+ @batch += amt
137
+ move_batch if @batch >= @batch_size
138
+ log format("LOAD %s", self.status)
139
+ @batch
140
+ end
141
+
142
+ def move(duration)
143
+ MinerMover.work(duration, @work_type)
144
+ end
145
+
146
+ def move_batch
147
+ raise "unexpected batch: #{@batch}" if @batch <= 0
148
+ amt = [@batch, @batch_size].min
149
+ duration = self.varied(amt / @rate)
150
+
151
+ log format("MOVE %s (%.2f s)", Ore.display(amt), duration)
152
+ _, elapsed = CompSci::Timer.elapsed { self.move(duration) }
153
+ log format("MOVD %s (%.2f s)", Ore.display(amt), elapsed)
154
+
155
+ # accounting
156
+ @ore_moved += amt
157
+ @batch -= amt
158
+ @batches += 1
159
+
160
+ # what moved
161
+ amt
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,46 @@
1
+ module MinerMover
2
+ # called by Worker instances, available for general use
3
+ def self.log_fmt(timer, id, msg)
4
+ format("%s %s %s", timer.elapsed_display, id, msg)
5
+ end
6
+
7
+ # i +- 50% at squeeze 0
8
+ # i +- 25% at squeeze 1, 12.5% at squeeze 2, etc.
9
+ def self.randomize(i, squeeze = 0)
10
+ r, base = rand, 0.5
11
+ # every squeeze, increase the base closer to 1 and cut the rand in half
12
+ squeeze.times { |s|
13
+ r *= 0.5
14
+ base += 0.5 ** (s+2)
15
+ }
16
+ i * (base + r)
17
+ end
18
+
19
+ # ore is handled in blocks of 1M
20
+ module Ore
21
+ BLOCK = 1_000_000
22
+
23
+ # raw ore in, blocks out
24
+ def self.block(ore, size = BLOCK)
25
+ ore.to_f / size
26
+ end
27
+
28
+ # mostly used for display purposes
29
+ def self.units(ore)
30
+ if ore % BLOCK == 0 or ore > BLOCK * 100
31
+ format("%iM", self.block(ore).round)
32
+ elsif ore > BLOCK
33
+ format("%.2fM", self.block(ore))
34
+ elsif ore > 10_000
35
+ format("%iK", self.block(ore, 1_000).round)
36
+ else
37
+ format("%i", ore)
38
+ end
39
+ end
40
+
41
+ # entirely used for display purposes
42
+ def self.display(ore)
43
+ format("%s ore", self.units(ore))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'miner_mover'
3
+ s.summary = "This project provides a basic concurrency problem useful for" <<
4
+ " exploring different multitasking paradigms available in Ruby"
5
+ s.description = <<EOF
6
+ Fundamentally, we have a set of miners and a set of movers. A miner takes some amount of time to mine ore, which is given to a mover. When a mover has enough ore for a full batch, the delivery takes some amount of time before more ore can be loaded.
7
+ EOF
8
+ s.authors = ["Rick Hull"]
9
+ s.homepage = "https://github.com/rickhull/miner_mover"
10
+ s.license = "LGPL-3.0"
11
+
12
+ s.required_ruby_version = "~> 2"
13
+
14
+ s.version = File.read(File.join(__dir__, 'VERSION')).chomp
15
+
16
+ s.files = %w[miner_mover.gemspec VERSION README.md Rakefile]
17
+ s.files += Dir['lib/**/*.rb']
18
+ s.files += Dir['test/**/*.rb']
19
+ s.files += Dir['demo/**/*.rb']
20
+
21
+ s.add_dependency "compsci", "~> 0.3"
22
+ s.add_dependency "dotcfg", "~> 1.0"
23
+ s.add_dependency "fiber_scehduler", "~> 0.13"
24
+
25
+ s.add_development_dependency "buildar", "~> 3.0"
26
+ s.add_development_dependency "minitest", "~> 5.0"
27
+ s.add_development_dependency "rake", "~> 13.0" # CVE-2020-8130
28
+ s.add_development_dependency "flog", "~> 0"
29
+ s.add_development_dependency "flay", "~> 0"
30
+ end
@@ -0,0 +1,33 @@
1
+ require 'minitest/autorun'
2
+ require 'miner_mover/worker'
3
+
4
+ describe MinerMover do
5
+ describe "MinerMover.work" do
6
+ it "rejects invalid work types" do
7
+ expect { MinerMover.work(2, :invalid) }.must_raise
8
+ end
9
+
10
+ it "sleeps for a duration to simulate waiting on an IO response" do
11
+ n = 0.1
12
+ expect(MinerMover.work(n, :wait)).must_equal n
13
+ end
14
+
15
+ it "performs fibonacci to simulate CPU work" do
16
+ expect(MinerMover.work(0.1, :cpu)).must_be(:<, 0.5)
17
+ end
18
+
19
+ it "returns instantly" do
20
+ expect(MinerMover.work(5, :instant)).must_equal 0
21
+ end
22
+ end
23
+
24
+ describe "MinerMover.mine_ore" do
25
+ before do
26
+ @miner = MinerMover::Miner.new(variance: 0, partial_reward: false)
27
+ end
28
+
29
+ it "mines to a depth, unsigned int" do
30
+ expect(@miner.mine_ore(1)).must_equal 0
31
+ end
32
+ end
33
+ end