zold-stress 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'time'
24
+ require 'rainbow'
25
+ require 'backtrace'
26
+ require 'zold/log'
27
+ require 'zold/age'
28
+
29
+ # Pool of wallets.
30
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
31
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
32
+ # License:: MIT
33
+ module Zold::Stress
34
+ # Stats
35
+ class Stats
36
+ def initialize
37
+ @start = Time.now
38
+ @history = {}
39
+ @mutex = Mutex.new
40
+ end
41
+
42
+ def to_console
43
+ [
44
+ "#{(total('arrived') / (Time.now - @start)).round(2)} tps",
45
+ %w[update push pull paid].map do |m|
46
+ if @history[m]
47
+ t = "#{m}: #{total(m)}/#{Zold::Age.new(Time.now - avg(m), limit: 1)}"
48
+ errors = total(m + '_error')
49
+ t += errors.zero? ? '' : '/' + Rainbow(errors.to_s).red
50
+ t
51
+ else
52
+ "#{m}: none"
53
+ end
54
+ end
55
+ ].join('; ')
56
+ end
57
+
58
+ def total(metric)
59
+ (@history[metric] || []).count
60
+ end
61
+
62
+ def avg(metric)
63
+ array = @history[metric].map { |a| a[:value] } || []
64
+ sum = array.inject(&:+) || 0
65
+ count = [array.count, 1].max
66
+ sum.to_f / count
67
+ end
68
+
69
+ def to_json
70
+ @history.map do |m, h|
71
+ data = h.map { |a| a[:value] }
72
+ sum = data.inject(&:+) || 0
73
+ [
74
+ m,
75
+ {
76
+ 'total': data.count,
77
+ 'sum': sum,
78
+ 'avg': (data.empty? ? 0 : (sum / data.count)),
79
+ 'max': data.max || 0,
80
+ 'min': data.min || 0,
81
+ 'age': (h.map { |a| a[:time] }.max || 0) - (h.map { |a| a[:time] }.min || 0)
82
+ }
83
+ ]
84
+ end.to_h
85
+ end
86
+
87
+ def exec(metric, swallow: true)
88
+ start = Time.now
89
+ yield
90
+ put(metric + '_ok', Time.now - start)
91
+ rescue StandardError => ex
92
+ put(metric + '_error', Time.now - start)
93
+ raise ex unless swallow
94
+ ensure
95
+ put(metric, Time.now - start)
96
+ end
97
+
98
+ def put(metric, value)
99
+ raise "Invalid type of \"#{value}\" (#{value.class.name})" unless value.is_a?(Integer) || value.is_a?(Float)
100
+ @mutex.synchronize do
101
+ @history[metric] = [] unless @history[metric]
102
+ @history[metric] << { time: Time.now, value: value }
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'zold/key'
24
+ require 'zold/id'
25
+ require 'zold/log'
26
+ require 'zold/http'
27
+ require 'zold/score'
28
+ require 'zold/wallets'
29
+ require 'zold/remotes'
30
+ require 'zold/verbose_thread'
31
+ require 'zold/commands/node'
32
+ require 'tmpdir'
33
+ require 'random-port'
34
+ require_relative '../test__helper'
35
+
36
+ module Zold::Stress
37
+ class FakeNode
38
+ def initialize(log)
39
+ @log = log
40
+ end
41
+
42
+ def exec
43
+ RandomPort::Pool::SINGLETON.acquire do |port|
44
+ Dir.mktmpdir do |home|
45
+ thread = Thread.start do
46
+ Zold::VerboseThread.new(@log).run do
47
+ node = Zold::Node.new(
48
+ wallets: Zold::Wallets.new(home),
49
+ remotes: Zold::Remotes.new(file: File.join(home, 'remotes')),
50
+ copies: File.join(home, 'copies'),
51
+ log: @log
52
+ )
53
+ node.run(
54
+ [
55
+ '--home', home,
56
+ '--network=test',
57
+ '--port', port.to_s,
58
+ '--host=localhost',
59
+ '--bind-port', port.to_s,
60
+ '--threads=0',
61
+ '--standalone',
62
+ '--no-metronome',
63
+ '--dump-errors',
64
+ '--halt-code=test',
65
+ '--strength=2',
66
+ '--routine-immediately',
67
+ '--invoice=NOPREFIX@ffffffffffffffff'
68
+ ]
69
+ )
70
+ end
71
+ end
72
+ attempt = 0
73
+ loop do
74
+ code = Zold::Http.new(uri: "http://localhost:#{port}/").get.code
75
+ break if code == '200'
76
+ @log.debug("Waiting for the node at localhost:#{port} (attempt no.#{attempt}): #{code}...")
77
+ attempt += 1
78
+ sleep 1
79
+ raise 'Can\'t start a node' if attempt > 100
80
+ end
81
+ begin
82
+ yield port
83
+ ensure
84
+ Zold::Http.new(uri: "http://localhost:#{port}/?halt=test").get
85
+ thread.join
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'minitest/autorun'
24
+ require 'zold/id'
25
+ require 'parallelize'
26
+ require_relative '../test__helper'
27
+ require_relative '../../../lib/zold/stress/air'
28
+
29
+ class AirTest < Minitest::Test
30
+ def test_adds_and_removes
31
+ air = Zold::Stress::Air.new
32
+ pmt = { start: Time.now, source: Zold::Id::ROOT, target: Zold::Id::ROOT, details: 'Hi!' }
33
+ air.add(pmt)
34
+ assert_equal(1, air.fetch.count)
35
+ air.fetch.each do |p|
36
+ assert_equal(pmt, p)
37
+ end
38
+ air.delete(pmt)
39
+ assert_equal(0, air.fetch.count)
40
+ end
41
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'minitest/autorun'
24
+ require 'zold/key'
25
+ require 'zold/id'
26
+ require 'zold/log'
27
+ require 'zold/wallets'
28
+ require 'zold/sync_wallets'
29
+ require 'zold/remotes'
30
+ require 'zold/commands/create'
31
+ require 'tmpdir'
32
+ require 'slop'
33
+ require_relative '../test__helper'
34
+ require_relative 'fake_node'
35
+ require_relative '../../../lib/zold/stress/pmnts'
36
+ require_relative '../../../lib/zold/stress/stats'
37
+
38
+ class PmntsTest < Minitest::Test
39
+ def test_pays_one_on_one
40
+ Dir.mktmpdir do |home|
41
+ wallets = Zold::SyncWallets.new(Zold::Wallets.new(home))
42
+ remotes = Zold::Remotes.new(file: File.join(home, 'remotes'), network: 'test')
43
+ Zold::Create.new(wallets: wallets, log: test_log).run(
44
+ ['create', '--public-key=fixtures/id_rsa.pub', Zold::Id::ROOT.to_s, '--network=test']
45
+ )
46
+ id = Zold::Create.new(wallets: wallets, log: test_log).run(
47
+ ['create', '--public-key=fixtures/id_rsa.pub', '--network=test']
48
+ )
49
+ Zold::Pay.new(wallets: wallets, remotes: remotes, log: test_log).run(
50
+ ['pay', Zold::Id::ROOT.to_s, id.to_s, '7.00', 'start', '--private-key=fixtures/id_rsa']
51
+ )
52
+ sent = Zold::Stress::Pmnts.new(
53
+ pvt: Zold::Key.new(file: 'fixtures/id_rsa'),
54
+ wallets: wallets,
55
+ remotes: remotes,
56
+ stats: Zold::Stress::Stats.new,
57
+ opts: test_opts('--batch=1'),
58
+ log: test_log, vlog: test_log
59
+ ).send
60
+ assert_equal(1, sent.count)
61
+ assert_equal(id, sent[0][:source])
62
+ assert_equal(Zold::Id::ROOT, sent[0][:target])
63
+ assert_equal(Zold::Amount.new(zld: 3.5), wallets.find(id, &:balance))
64
+ assert_equal(Zold::Amount.new(zld: -3.5), wallets.find(Zold::Id::ROOT, &:balance))
65
+ end
66
+ end
67
+
68
+ def test_pays
69
+ Dir.mktmpdir do |home|
70
+ wallets = Zold::Wallets.new(home)
71
+ remotes = Zold::Remotes.new(file: File.join(home, 'remotes'), network: 'test')
72
+ Zold::Create.new(wallets: wallets, log: test_log).run(
73
+ ['create', '--public-key=fixtures/id_rsa.pub', Zold::Id::ROOT.to_s, '--network=test']
74
+ )
75
+ ids = []
76
+ 5.times do
77
+ id = Zold::Create.new(wallets: wallets, log: test_log).run(
78
+ ['create', '--public-key=fixtures/id_rsa.pub', '--network=test']
79
+ )
80
+ Zold::Pay.new(wallets: wallets, remotes: remotes, log: test_log).run(
81
+ ['pay', Zold::Id::ROOT.to_s, id.to_s, '7.00', 'start', '--private-key=fixtures/id_rsa']
82
+ )
83
+ ids << id
84
+ end
85
+ sent = Zold::Stress::Pmnts.new(
86
+ pvt: Zold::Key.new(file: 'fixtures/id_rsa'),
87
+ wallets: wallets,
88
+ remotes: remotes,
89
+ stats: Zold::Stress::Stats.new,
90
+ opts: test_opts('--batch=20'),
91
+ log: test_log, vlog: test_log
92
+ ).send
93
+ assert_equal(20, sent.count)
94
+ assert_equal(ids.sort, sent.map { |s| s[:source] }.sort.uniq)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'minitest/autorun'
24
+ require 'zold/key'
25
+ require 'zold/id'
26
+ require 'zold/log'
27
+ require 'zold/wallets'
28
+ require 'zold/sync_wallets'
29
+ require 'zold/remotes'
30
+ require 'tmpdir'
31
+ require 'slop'
32
+ require_relative '../test__helper'
33
+ require_relative 'fake_node'
34
+ require_relative '../../../lib/zold/stress/pool'
35
+ require_relative '../../../lib/zold/stress/stats'
36
+
37
+ class PoolTest < Minitest::Test
38
+ def test_reloads_wallets
39
+ Zold::Stress::FakeNode.new(test_log).exec do |port|
40
+ Dir.mktmpdir do |home|
41
+ wallets = Zold::SyncWallets.new(Zold::Wallets.new(home))
42
+ Zold::Create.new(wallets: wallets, log: test_log).run(
43
+ ['create', '--public-key=fixtures/id_rsa.pub', Zold::Id::ROOT.to_s, '--network=test']
44
+ )
45
+ wallets.find(Zold::Id::ROOT) do |w|
46
+ w.add(Zold::Txn.new(1, Time.now, Zold::Amount.new(zld: 1.0), 'NOPREFIX', Zold::Id.new, '-'))
47
+ end
48
+ remotes = Zold::Remotes.new(file: File.join(home, 'remotes'), network: 'test')
49
+ remotes.clean
50
+ remotes.add('localhost', port)
51
+ size = 3
52
+ Zold::Stress::Pool.new(
53
+ wallets: wallets,
54
+ remotes: remotes,
55
+ copies: File.join(home, 'copies'),
56
+ stats: Zold::Stress::Stats.new,
57
+ opts: test_opts("--pool=#{size}"),
58
+ log: test_log, vlog: test_log
59
+ ).rebuild
60
+ assert_equal(size, wallets.all.count)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'minitest/autorun'
24
+ require 'slop'
25
+ require 'zold/key'
26
+ require 'zold/id'
27
+ require 'zold/log'
28
+ require 'zold/http'
29
+ require 'zold/score'
30
+ require 'zold/wallets'
31
+ require 'zold/sync_wallets'
32
+ require 'zold/cached_wallets'
33
+ require 'zold/remotes'
34
+ require 'zold/commands/create'
35
+ require 'zold/commands/pay'
36
+ require 'zold/commands/push'
37
+ require 'zold/commands/node'
38
+ require 'tmpdir'
39
+ require_relative '../test__helper'
40
+ require_relative 'fake_node'
41
+ require_relative '../../../lib/zold/stress/round'
42
+
43
+ class StressTest < Minitest::Test
44
+ def test_runs_a_few_full_cycles
45
+ Zold::Stress::FakeNode.new(Zold::Log::Quiet.new).exec do |port|
46
+ Dir.mktmpdir do |home|
47
+ remotes = Zold::Remotes.new(file: File.join(home, 'remotes'), network: 'test')
48
+ remotes.clean
49
+ remotes.add('localhost', port)
50
+ wallets = Zold::SyncWallets.new(Zold::CachedWallets.new(Zold::Wallets.new(home)))
51
+ Zold::Create.new(wallets: wallets, log: test_log).run(
52
+ ['create', '--public-key=fixtures/id_rsa.pub', Zold::Id::ROOT.to_s, '--network=test']
53
+ )
54
+ wallets.find(Zold::Id::ROOT) do |w|
55
+ w.add(Zold::Txn.new(1, Time.now, Zold::Amount.new(zld: 1.0), 'NOPREFIX', Zold::Id.new, '-'))
56
+ end
57
+ stats = Zold::Stress::Stats.new
58
+ air = Zold::Stress::Air.new
59
+ batch = 20
60
+ round = Zold::Stress::Round.new(
61
+ pvt: Zold::Key.new(file: 'fixtures/id_rsa'),
62
+ wallets: wallets, remotes: remotes,
63
+ air: air, stats: stats,
64
+ opts: test_opts('--pool=5', "--batch=#{batch}"),
65
+ copies: File.join(home, 'copies'),
66
+ log: test_log, vlog: test_log
67
+ )
68
+ round.update
69
+ round.prepare
70
+ round.send
71
+ attempt = 0
72
+ loop do
73
+ break if air.fetch.empty?
74
+ break if attempt > 50
75
+ round.pull
76
+ round.match
77
+ test_log.info(stats.to_console)
78
+ attempt += 1
79
+ sleep 0.2
80
+ end
81
+ assert(air.fetch.empty?)
82
+ assert_equal(batch, stats.total('arrived'))
83
+ end
84
+ end
85
+ end
86
+ end