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.
- checksums.yaml +7 -0
- data/README.md +321 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/demo/config.rb +3 -0
- data/demo/fiber.rb +65 -0
- data/demo/fiber_scheduler.rb +117 -0
- data/demo/process.rb +109 -0
- data/demo/ractor.rb +117 -0
- data/demo/serial.rb +50 -0
- data/demo/thread.rb +91 -0
- data/lib/miner_mover/config.rb +78 -0
- data/lib/miner_mover/run.rb +67 -0
- data/lib/miner_mover/worker.rb +164 -0
- data/lib/miner_mover.rb +46 -0
- data/miner_mover.gemspec +30 -0
- data/test/miner_mover.rb +33 -0
- metadata +176 -0
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
|
data/lib/miner_mover.rb
ADDED
@@ -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
|
data/miner_mover.gemspec
ADDED
@@ -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
|
data/test/miner_mover.rb
ADDED
@@ -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
|