zold 0.13.34 → 0.13.35

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/README.md +6 -5
  4. data/bin/zold +6 -1
  5. data/fixtures/scripts/_head.sh +4 -2
  6. data/fixtures/scripts/push-and-pull.sh +1 -1
  7. data/lib/zold/amount.rb +5 -0
  8. data/lib/zold/atomic_file.rb +3 -3
  9. data/lib/zold/backtrace.rb +7 -1
  10. data/lib/zold/commands/clean.rb +1 -1
  11. data/lib/zold/commands/create.rb +2 -2
  12. data/lib/zold/commands/diff.rb +2 -2
  13. data/lib/zold/commands/fetch.rb +8 -4
  14. data/lib/zold/commands/merge.rb +3 -3
  15. data/lib/zold/commands/node.rb +32 -11
  16. data/lib/zold/commands/propagate.rb +1 -1
  17. data/lib/zold/commands/push.rb +14 -9
  18. data/lib/zold/commands/remote.rb +4 -2
  19. data/lib/zold/commands/routines/bonuses.rb +9 -5
  20. data/lib/zold/commands/routines/spread.rb +4 -1
  21. data/lib/zold/commands/show.rb +2 -2
  22. data/lib/zold/commands/taxes.rb +2 -1
  23. data/lib/zold/http.rb +9 -1
  24. data/lib/zold/id.rb +4 -0
  25. data/lib/zold/json_page.rb +43 -0
  26. data/lib/zold/metronome.rb +15 -15
  27. data/lib/zold/node/async_entrance.rb +81 -0
  28. data/lib/zold/node/entrance.rb +26 -91
  29. data/lib/zold/node/farm.rb +21 -15
  30. data/lib/zold/node/front.rb +30 -17
  31. data/lib/zold/node/safe_entrance.rb +78 -0
  32. data/lib/zold/node/spread_entrance.rb +105 -0
  33. data/lib/zold/patch.rb +7 -5
  34. data/lib/zold/remotes.rb +19 -7
  35. data/lib/zold/txn.rb +3 -1
  36. data/lib/zold/version.rb +1 -1
  37. data/lib/zold/wallet.rb +3 -3
  38. data/lib/zold/wallets.rb +2 -0
  39. data/test/commands/routines/test_bonuses.rb +2 -2
  40. data/test/commands/test_fetch.rb +9 -35
  41. data/test/commands/test_merge.rb +1 -1
  42. data/test/commands/test_node.rb +4 -2
  43. data/test/commands/test_push.rb +4 -2
  44. data/test/node/fake_entrance.rb +41 -0
  45. data/test/node/fake_node.rb +37 -29
  46. data/test/node/test_async_entrance.rb +88 -0
  47. data/test/node/test_entrance.rb +1 -19
  48. data/test/node/test_front.rb +3 -2
  49. data/test/node/test_safe_entrance.rb +56 -0
  50. data/test/node/test_spread_entrance.rb +60 -0
  51. data/test/test__helper.rb +0 -1
  52. data/test/test_atomic_file.rb +39 -0
  53. data/test/test_backtrace.rb +40 -0
  54. data/test/test_metronome.rb +0 -1
  55. data/test/test_patch.rb +17 -0
  56. data/test/test_remotes.rb +21 -0
  57. data/test/test_wallet.rb +14 -1
  58. data/test/test_zold.rb +1 -0
  59. metadata +16 -2
@@ -51,10 +51,13 @@ module Zold
51
51
  (@opts['ignore-score-weakness'] ? ['--ignore-score-weakness'] : [])
52
52
  )
53
53
  return if winners.empty?
54
- winners.each do |score|
54
+ unless @wallets.find(Id.new(@opts['bonus-wallet'])).exists?
55
55
  Pull.new(wallets: @wallets, remotes: @remotes, copies: @copies, log: @log).run(
56
- ['pull', @opts['bonus-wallet']]
56
+ ['pull', @opts['bonus-wallet']] +
57
+ (@opts['ignore-score-weakness'] ? ['--ignore-score-weakness'] : [])
57
58
  )
59
+ end
60
+ winners.each do |score|
58
61
  Pay.new(wallets: @wallets, remotes: @remotes, log: @log).run(
59
62
  [
60
63
  'pay', @opts['bonus-wallet'], score.invoice, @opts['bonus-amount'].to_s,
@@ -62,10 +65,11 @@ module Zold
62
65
  '--private-key', @opts['private-key']
63
66
  ]
64
67
  )
65
- Push.new(wallets: @wallets, remotes: @remotes, log: @log).run(
66
- ['push', @opts['bonus-wallet']]
67
- )
68
68
  end
69
+ Push.new(wallets: @wallets, remotes: @remotes, log: @log).run(
70
+ ['push', @opts['bonus-wallet']] +
71
+ (@opts['ignore-score-weakness'] ? ['--ignore-score-weakness'] : [])
72
+ )
69
73
  end
70
74
  end
71
75
  end
@@ -41,7 +41,10 @@ module Zold
41
41
 
42
42
  def exec(_ = 0)
43
43
  sleep(60) unless @opts['routine-immediately']
44
- @entrance.spread(@wallets.all.sample(10).map { |w| Id.new(w) })
44
+ @wallets.all.sample(10).map do |w|
45
+ id = Id.new(w)
46
+ @entrance.push(id, File.read(@wallets.find(id).path))
47
+ end
45
48
  end
46
49
  end
47
50
  end
@@ -50,8 +50,8 @@ Available options:"
50
50
  List.new(wallets: @wallets, log: @log).run(args)
51
51
  else
52
52
  total = Amount::ZERO
53
- mine.each do |id|
54
- total += show(@wallets.find(Id.new(id)), opts)
53
+ mine.map { |i| Id.new(i) }.each do |id|
54
+ total += show(@wallets.find(id), opts)
55
55
  end
56
56
  total
57
57
  end
@@ -24,6 +24,7 @@ require 'rainbow'
24
24
  require_relative 'args'
25
25
  require_relative 'pay'
26
26
  require_relative '../log'
27
+ require_relative '../json_page'
27
28
  require_relative '../score'
28
29
  require_relative '../id'
29
30
  require_relative '../tax'
@@ -120,7 +121,7 @@ Available options:"
120
121
  @remotes.iterate(@log) do |r|
121
122
  res = r.http.get
122
123
  r.assert_code(200, res)
123
- json = JSON.parse(res.body)
124
+ json = JsonPage.new(res.body).to_hash
124
125
  score = Score.parse_json(json['score'])
125
126
  r.assert_valid_score(score)
126
127
  r.assert_score_strength(score)
data/lib/zold/http.rb CHANGED
@@ -42,11 +42,18 @@ module Zold
42
42
  # reboot itself, if the version is higher.
43
43
  VERSION_HEADER = 'X-Zold-Version'.freeze
44
44
 
45
- def initialize(uri, score = Score::ZERO)
45
+ # HTTP header we add, in order to inform the node about our
46
+ # network. This is done in order to isolate test networks from
47
+ # production one.
48
+ NETWORK_HEADER = 'X-Zold-Network'.freeze
49
+
50
+ def initialize(uri, score = Score::ZERO, network: 'test')
46
51
  raise 'URI can\'t be nil' if uri.nil?
47
52
  @uri = uri.is_a?(String) ? URI(uri) : uri
48
53
  raise 'Score can\'t be nil' if score.nil?
49
54
  @score = score
55
+ raise 'Network can\'t be nil' if network.nil?
56
+ @network = network
50
57
  end
51
58
 
52
59
  def get
@@ -105,6 +112,7 @@ module Zold
105
112
  'Accept-Encoding': 'gzip'
106
113
  }
107
114
  headers[Http::VERSION_HEADER] = VERSION
115
+ headers[Http::NETWORK_HEADER] = @network
108
116
  headers[Http::SCORE_HEADER] = @score.reduced(4).to_text if @score.valid? && !@score.expired? && @score.value > 3
109
117
  headers
110
118
  end
data/lib/zold/id.rb CHANGED
@@ -51,6 +51,10 @@ module Zold
51
51
  to_s == other.to_s
52
52
  end
53
53
 
54
+ def to_str
55
+ to_s
56
+ end
57
+
54
58
  def to_s
55
59
  format('%016x', @id)
56
60
  end
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require 'json'
22
+
23
+ # JSON page.
24
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
25
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
26
+ # License:: MIT
27
+ module Zold
28
+ # JSON page
29
+ class JsonPage
30
+ def initialize(text)
31
+ raise 'JSON text can\'t be nil' if text.nil?
32
+ raise 'JSON must be of type String' unless text.is_a?(String)
33
+ @text = text
34
+ end
35
+
36
+ def to_hash
37
+ raise 'JSON is empty, can\'t parse' if @text.empty?
38
+ JSON.parse(@text)
39
+ rescue JSON::ParserError => e
40
+ raise "Failed to parse JSON (#{e.message}): #{@text}"
41
+ end
42
+ end
43
+ end
@@ -48,11 +48,12 @@ module Zold
48
48
  end
49
49
 
50
50
  def start
51
+ alive = true
51
52
  @routines.each do |r|
52
53
  @threads << Thread.start do
53
54
  Thread.current.name = r.class.name
54
55
  step = 0
55
- loop do
56
+ while alive
56
57
  start = Time.now
57
58
  begin
58
59
  r.exec(step)
@@ -69,21 +70,20 @@ module Zold
69
70
  begin
70
71
  yield(self)
71
72
  ensure
72
- stop
73
- end
74
- end
75
-
76
- private
77
-
78
- def stop
79
- @log.info("Terminating the metronome with #{@threads.count} threads...")
80
- start = Time.now
81
- @threads.each do |t|
82
- tstart = Time.now
83
- t.exit
84
- @log.info("Thread #{t.name} terminated in #{(Time.now - tstart).round(2)}s")
73
+ alive = false
74
+ @log.info("Stopping the metronome with #{@threads.count} threads: #{@threads.map(&:name).join(', ')}")
75
+ start = Time.now
76
+ @threads.each do |t|
77
+ tstart = Time.now
78
+ if t.join(60)
79
+ @log.info("Thread #{t.name} finished in #{(Time.now - tstart).round(2)}s")
80
+ else
81
+ t.exit
82
+ @log.info("Thread #{t.name} killed in #{(Time.now - tstart).round(2)}s")
83
+ end
84
+ end
85
+ @log.info("Metronome stopped in #{(Time.now - start).round(2)}s, #{@failures.count} failures")
85
86
  end
86
- @log.info("Metronome stopped in #{(Time.now - start).round(2)}s, #{@failures.count} failures")
87
87
  end
88
88
  end
89
89
  end
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2018 Yegor Bugayenko
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the 'Software'), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ require 'concurrent'
22
+ require_relative '../log'
23
+ require_relative '../verbose_thread'
24
+
25
+ # The async entrance of the web front.
26
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
27
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
28
+ # License:: MIT
29
+ module Zold
30
+ # The entrance
31
+ class AsyncEntrance
32
+ def initialize(entrance, log: Log::Quiet.new)
33
+ raise 'Entrance can\'t be nil' if entrance.nil?
34
+ @entrance = entrance
35
+ raise 'Log can\'t be nil' if log.nil?
36
+ @log = log
37
+ end
38
+
39
+ def start
40
+ @entrance.start do
41
+ @pool = Concurrent::FixedThreadPool.new(
42
+ Concurrent.processor_count,
43
+ max_queue: Concurrent.processor_count * 10,
44
+ fallback_policy: :abort
45
+ )
46
+ begin
47
+ yield(self)
48
+ ensure
49
+ @log.info("Stopping async entrance, pool length is #{@pool.length}, queue length is #{@pool.queue_length}")
50
+ @pool.shutdown
51
+ if @pool.wait_for_termination(10)
52
+ @log.info('Async entrance terminated peacefully')
53
+ else
54
+ @pool.kill
55
+ @log.info('Async entrance was killed')
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def to_json
62
+ @entrance.to_json.merge(
63
+ 'pool.completed_task_count': @pool.completed_task_count,
64
+ 'pool.largest_length': @pool.largest_length,
65
+ 'pool.length': @pool.length,
66
+ 'pool.queue_length': @pool.queue_length,
67
+ 'pool.running': @pool.running?
68
+ )
69
+ end
70
+
71
+ def push(id, body)
72
+ @pool.post do
73
+ VerboseThread.new(@log).run(true) do
74
+ @entrance.push(id, body)
75
+ end
76
+ end
77
+ @log.debug("Pushed #{id}/#{body.length}b to #{@entrance.class.name}, \
78
+ pool: #{@pool.length}/#{@pool.queue_length}")
79
+ end
80
+ end
81
+ end
@@ -18,17 +18,15 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  # SOFTWARE.
20
20
 
21
- require 'concurrent'
22
21
  require 'tempfile'
23
- require_relative 'emission'
24
22
  require_relative '../log'
25
23
  require_relative '../remotes'
26
24
  require_relative '../copies'
27
25
  require_relative '../tax'
26
+ require_relative '../commands/clean'
28
27
  require_relative '../commands/merge'
29
28
  require_relative '../commands/fetch'
30
29
  require_relative '../commands/push'
31
- require_relative '../commands/clean'
32
30
 
33
31
  # The entrance of the web front.
34
32
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -38,115 +36,52 @@ module Zold
38
36
  # The entrance
39
37
  class Entrance
40
38
  def initialize(wallets, remotes, copies, address, log: Log::Quiet.new)
39
+ raise 'Wallets can\'t be nil' if wallets.nil?
40
+ raise 'Wallets must be of type Wallets' unless wallets.is_a?(Wallets)
41
41
  @wallets = wallets
42
+ raise 'Remotes can\'t be nil' if remotes.nil?
43
+ raise "Remotes must be of type Remotes: #{remotes.class.name}" unless remotes.is_a?(Remotes)
42
44
  @remotes = remotes
45
+ raise 'Copies can\'t be nil' if copies.nil?
43
46
  @copies = copies
47
+ raise 'Address can\'t be nil' if address.nil?
44
48
  @address = address
49
+ raise 'Log can\'t be nil' if log.nil?
45
50
  @log = log
46
- @semaphores = Concurrent::Map.new
47
- @push_mutex = Mutex.new
48
- @modified = Set.new
49
- @pool = Concurrent::FixedThreadPool.new(16, max_queue: 64, fallback_policy: :abort)
50
- @pushes = Concurrent::FixedThreadPool.new(1, max_queue: 64, fallback_policy: :abort)
51
- end
52
-
53
- def to_json
54
- {
55
- 'semaphores': @semaphores.size,
56
- 'modified': @modified.length,
57
- 'pool': {
58
- 'completed_task_count': @pool.completed_task_count,
59
- 'largest_length': @pool.largest_length,
60
- 'length': @pool.length,
61
- 'queue_length': @pool.queue_length
62
- },
63
- 'pushes': {
64
- 'completed_task_count': @pushes.completed_task_count,
65
- 'largest_length': @pushes.largest_length,
66
- 'length': @pushes.length,
67
- 'queue_length': @pushes.queue_length
68
- }
69
- }
70
- end
71
-
72
- def push(id, body, sync: true)
73
- check(body)
74
- if sync
75
- push_sync(id, body)
76
- else
77
- @pool.post do
78
- push_sync(id, body)
79
- end
80
- end
81
- end
82
-
83
- def check(body)
84
- Tempfile.open do |f|
85
- File.write(f.path, body)
86
- wallet = Wallet.new(f)
87
- break unless wallet.network == Wallet::MAIN_NETWORK
88
- balance = wallet.balance
89
- if balance.negative? && !wallet.root?
90
- raise "The balance #{balance} of #{wallet.id} is negative and it's not a root wallet"
91
- end
92
- Emission.new(wallet).check
93
- tax = Tax.new(wallet)
94
- if tax.in_debt?
95
- raise "Taxes are not paid, can't accept the wallet; the debt is #{tax.debt} (#{tax.debt.to_i} zents)"
96
- end
97
- end
98
51
  end
99
52
 
100
- def spread(ids)
101
- return if ids.empty?
102
- @push_mutex.synchronize { @modified += ids }
103
- @pushes.post { push_one } if @pushes.length < 2
53
+ def start
54
+ yield(self)
104
55
  end
105
56
 
106
- private
107
-
108
- # Returns a list of modifed wallets (as Zold::Id)
109
- def push_sync(id, body)
110
- @semaphores.put_if_absent(id.to_s, Mutex.new)
111
- @semaphores.get(id.to_s).synchronize do
112
- start = Time.now
113
- modified = push_unsafe(id, body)
114
- if modified.empty?
115
- @log.info("Accepted #{id} in #{(Time.now - start).round(2)}s \
116
- and modified nothing (this is most likely a bug!)")
117
- else
118
- @log.info("Accepted #{id} in #{(Time.now - start).round(2)}s and modified #{modified.join(', ')}")
119
- end
120
- modified
121
- end
57
+ def to_json
58
+ {}
122
59
  end
123
60
 
124
61
  # Returns a list of modifed wallets (as Zold::Id)
125
- def push_unsafe(id, body)
62
+ def push(id, body)
63
+ raise 'Id can\'t be nil' if id.nil?
64
+ raise 'Id must be of type Id' unless id.is_a?(Id)
65
+ raise 'Body can\'t be nil' if body.nil?
66
+ start = Time.now
126
67
  copies = Copies.new(File.join(@copies, id.to_s))
127
68
  localhost = '0.0.0.0'
128
69
  copies.add(body, localhost, Remotes::PORT, 0)
129
- Fetch.new(
130
- wallets: @wallets, remotes: @remotes, copies: copies.root, log: @log
131
- ).run(['fetch', id.to_s, "--ignore-node=#{@address}"])
70
+ unless @remotes.all.empty?
71
+ Fetch.new(
72
+ wallets: @wallets, remotes: @remotes, copies: copies.root, log: @log
73
+ ).run(['fetch', id.to_s, "--ignore-node=#{@address}"])
74
+ end
132
75
  modified = Merge.new(
133
76
  wallets: @wallets, copies: copies.root, log: @log
134
77
  ).run(['merge', id.to_s, '--no-baseline'])
135
78
  Clean.new(wallets: @wallets, copies: copies.root, log: @log).run(['clean', id.to_s])
136
79
  copies.remove(localhost, Remotes::PORT)
137
- spread(modified)
138
- modified
139
- end
140
-
141
- def push_one
142
- @push_mutex.synchronize do
143
- id = @modified.to_a[0]
144
- @modified.delete(id)
145
- return if id.nil?
146
- Push.new(
147
- wallets: @wallets, remotes: @remotes, log: @log
148
- ).run(['push', "--ignore-node=#{@address}"] + [id.to_s])
80
+ unless modified.empty?
81
+ @log.info("Accepted #{id} in #{(Time.now - start).round(2)}s \
82
+ and modified #{modified.join(', ')}")
149
83
  end
84
+ modified
150
85
  end
151
86
  end
152
87
  end