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
@@ -62,6 +62,7 @@ module Zold
62
62
  threads: @threads.map do |t|
63
63
  "#{t.name}/#{t.status}/#{t.alive? ? 'A' : 'D'}"
64
64
  end.join(', '),
65
+ cleanup: @cleanup.status,
65
66
  pipeline: @pipeline.size,
66
67
  best: best.map(&:to_mnemo).join(', ')
67
68
  }
@@ -81,9 +82,10 @@ module Zold
81
82
  end
82
83
  end
83
84
  end
84
- @threads << Thread.new do
85
+ alive = true
86
+ @cleanup = Thread.new do
85
87
  Thread.current.name = 'cleanup'
86
- loop do
88
+ while alive
87
89
  sleep(60) unless strength == 1 # which will only happen in tests
88
90
  VerboseThread.new(@log).run do
89
91
  cleanup(host, port, strength, threads)
@@ -93,21 +95,25 @@ module Zold
93
95
  @log.info("Farm started with #{@threads.count} threads at #{host}:#{port}, strength is #{strength}")
94
96
  return unless block_given?
95
97
  begin
96
- yield
98
+ yield(self)
97
99
  ensure
98
- stop
99
- end
100
- end
101
-
102
- def stop
103
- @log.info("Terminating the farm with #{@threads.count} threads...")
104
- start = Time.now
105
- @threads.each do |t|
106
- tstart = Time.now
107
- t.exit
108
- @log.info("Thread #{t.name} terminated in #{(Time.now - tstart).round(2)}s")
100
+ @log.info("Terminating the farm with #{@threads.count} threads...")
101
+ start = Time.now
102
+ alive = false
103
+ if strength == 1
104
+ @cleanup.join
105
+ @log.info("Cleanup thread finished in #{(Time.now - start).round(2)}s")
106
+ else
107
+ @cleanup.exit
108
+ @log.info("Cleanup thread killed in #{(Time.now - start).round(2)}s")
109
+ end
110
+ @threads.each do |t|
111
+ tstart = Time.now
112
+ t.exit
113
+ @log.info("Thread #{t.name} terminated in #{(Time.now - tstart).round(2)}s")
114
+ end
115
+ @log.info("Farm stopped in #{(Time.now - start).round(2)}s")
109
116
  end
110
- @log.info("Farm stopped in #{(Time.now - start).round(2)}s")
111
117
  end
112
118
 
113
119
  private
@@ -43,7 +43,7 @@ module Zold
43
43
  configure do
44
44
  set :bind, '0.0.0.0'
45
45
  set :suppress_messages, true
46
- set :dump_errors, false
46
+ set :dump_errors, true
47
47
  set :start, Time.now
48
48
  set :lock, false
49
49
  set :show_exceptions, false
@@ -58,6 +58,7 @@ module Zold
58
58
  set :farm, nil? # to be injected at node.rb
59
59
  set :metronome, nil? # to be injected at node.rb
60
60
  set :entrance, nil? # to be injected at node.rb
61
+ set :network, nil? # to be injected at node.rb
61
62
  set :wallets, nil? # to be injected at node.rb
62
63
  set :remotes, nil? # to be injected at node.rb
63
64
  set :copies, nil? # to be injected at node.rb
@@ -65,22 +66,26 @@ module Zold
65
66
  use Rack::Deflater
66
67
 
67
68
  before do
68
- name = "HTTP-#{Http::SCORE_HEADER}".upcase.tr('-', '_')
69
- header = request.env[name]
70
- return unless header
71
- if settings.remotes.all.empty?
72
- settings.log.debug("#{request.url}: we are in standalone mode, won't update remotes")
69
+ check_header(Http::NETWORK_HEADER) do |header|
70
+ if header != settings.network
71
+ raise "Network name mismatch, you are in '#{header}', we are in '#{settings.network}'"
72
+ end
73
73
  end
74
- s = Score.parse_text(header)
75
- error(400, 'The score is invalid') unless s.valid?
76
- error(400, 'The score is weak') if s.strength < Score::STRENGTH && !settings.ignore_score_weakness
77
- if s.value > 3
78
- require_relative '../commands/remote'
79
- Remote.new(remotes: settings.remotes, log: settings.log).run(
80
- ['remote', 'add', s.host, s.port.to_s, '--force']
81
- )
82
- else
83
- settings.log.debug("#{request.url}: the score is too weak: #{s}")
74
+ check_header(Http::SCORE_HEADER) do |header|
75
+ if settings.remotes.all.empty?
76
+ settings.log.debug("#{request.url}: we are in standalone mode, won't update remotes")
77
+ end
78
+ s = Score.parse_text(header)
79
+ error(400, 'The score is invalid') unless s.valid?
80
+ error(400, 'The score is weak') if s.strength < Score::STRENGTH && !settings.ignore_score_weakness
81
+ if s.value > 3
82
+ require_relative '../commands/remote'
83
+ Remote.new(remotes: settings.remotes, log: settings.log).run(
84
+ ['remote', 'add', s.host, s.port.to_s, '--force']
85
+ )
86
+ else
87
+ settings.log.debug("#{request.url}: the score is too weak: #{s}")
88
+ end
84
89
  end
85
90
  end
86
91
 
@@ -121,6 +126,7 @@ module Zold
121
126
  content_type 'application/json'
122
127
  JSON.pretty_generate(
123
128
  version: settings.version,
129
+ network: settings.network,
124
130
  score: score.to_h,
125
131
  pid: Process.pid,
126
132
  cpus: Concurrent.processor_count,
@@ -181,7 +187,7 @@ module Zold
181
187
  )
182
188
  end
183
189
  settings.log.info("Wallet #{id} is new: #{before.length}b != #{after.length}b")
184
- settings.entrance.push(id, after, sync: !params[:sync].nil?)
190
+ settings.entrance.push(id, after)
185
191
  JSON.pretty_generate(
186
192
  version: settings.version,
187
193
  score: score.to_h
@@ -228,6 +234,13 @@ module Zold
228
234
 
229
235
  private
230
236
 
237
+ def check_header(name)
238
+ name = "HTTP-#{name}".upcase.tr('-', '_')
239
+ header = request.env[name]
240
+ return unless header
241
+ yield header
242
+ end
243
+
231
244
  def score
232
245
  best = settings.farm.best
233
246
  raise 'Score is empty, there is something wrong with the Farm!' if best.empty?
@@ -0,0 +1,78 @@
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 'tempfile'
23
+ require_relative 'emission'
24
+ require_relative '../log'
25
+ require_relative '../remotes'
26
+ require_relative '../copies'
27
+ require_relative '../tax'
28
+ require_relative '../commands/merge'
29
+ require_relative '../commands/fetch'
30
+ require_relative '../commands/push'
31
+
32
+ # The entrance thav validate the incoming wallet first.
33
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
34
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
35
+ # License:: MIT
36
+ module Zold
37
+ # The safe entrance
38
+ class SafeEntrance
39
+ def initialize(entrance, network: 'test')
40
+ raise 'Entrance can\'t be nil' if entrance.nil?
41
+ @entrance = entrance
42
+ raise 'Network can\'t be nil' if network.nil?
43
+ @network = network
44
+ end
45
+
46
+ def start
47
+ @entrance.start { yield(self) }
48
+ end
49
+
50
+ def to_json
51
+ @entrance.to_json
52
+ end
53
+
54
+ # Returns a list of modifed wallets (as Zold::Id)
55
+ def push(id, body)
56
+ raise 'Id can\'t be nil' if id.nil?
57
+ raise 'Id must be of type Id' unless id.is_a?(Id)
58
+ raise 'Body can\'t be nil' if body.nil?
59
+ Tempfile.open do |f|
60
+ File.write(f.path, body)
61
+ wallet = Wallet.new(f)
62
+ unless wallet.network == @network
63
+ raise "The network name mismatch, the wallet is in '#{wallet.network}', we are in '#{@network}'"
64
+ end
65
+ balance = wallet.balance
66
+ if balance.negative? && !wallet.root?
67
+ raise "The balance #{balance} of #{wallet.id} is negative and it's not a root wallet"
68
+ end
69
+ Emission.new(wallet).check
70
+ tax = Tax.new(wallet)
71
+ if tax.in_debt?
72
+ raise "Taxes are not paid, can't accept the wallet; the debt is #{tax.debt} (#{tax.debt.to_i} zents)"
73
+ end
74
+ @entrance.push(id, body)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,105 @@
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 'tempfile'
23
+ require_relative 'emission'
24
+ require_relative '../log'
25
+ require_relative '../remotes'
26
+ require_relative '../copies'
27
+ require_relative '../tax'
28
+ require_relative '../commands/merge'
29
+ require_relative '../commands/fetch'
30
+ require_relative '../commands/push'
31
+ require_relative '../commands/clean'
32
+
33
+ # The entrance that spreads what's been modified.
34
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
35
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
36
+ # License:: MIT
37
+ module Zold
38
+ # The entrance
39
+ class SpreadEntrance
40
+ def initialize(entrance, wallets, remotes, address, log: Log::Quiet.new, ignore_score_weakeness: false)
41
+ raise 'Entrance can\'t be nil' if entrance.nil?
42
+ @entrance = entrance
43
+ raise 'Wallets can\'t be nil' if wallets.nil?
44
+ raise 'Wallets must be of type Wallets' unless wallets.is_a?(Wallets)
45
+ @wallets = wallets
46
+ raise 'Remotes can\'t be nil' if remotes.nil?
47
+ raise 'Remotes must be of type Remotes' unless remotes.is_a?(Remotes)
48
+ @remotes = remotes
49
+ raise 'Address can\'t be nil' if address.nil?
50
+ @address = address
51
+ raise 'Log can\'t be nil' if log.nil?
52
+ @log = log
53
+ @ignore_score_weakeness = ignore_score_weakeness
54
+ end
55
+
56
+ def to_json
57
+ @entrance.to_json.merge(
58
+ 'modified': @modified.size,
59
+ 'push': @push.status
60
+ )
61
+ end
62
+
63
+ def start
64
+ @entrance.start do
65
+ @seen = Set.new
66
+ @modified = Queue.new
67
+ @push = Thread.start do
68
+ Thread.current.abort_on_exception = true
69
+ Thread.current.name = 'push'
70
+ VerboseThread.new(@log).run do
71
+ loop do
72
+ id = @modified.pop
73
+ if @remotes.all.empty?
74
+ @log.info("There are no remotes, won\'t spread #{id}")
75
+ else
76
+ Push.new(wallets: @wallets, remotes: @remotes, log: @log).run(
77
+ ['push', "--ignore-node=#{@address}", id.to_s] +
78
+ (@ignore_score_weakeness ? ['--ignore-score-weakness'] : [])
79
+ )
80
+ end
81
+ @seen.delete(id)
82
+ end
83
+ end
84
+ end
85
+ begin
86
+ yield(self)
87
+ ensure
88
+ @log.info('Waiting for spread entrance to finish...')
89
+ @modified.clear
90
+ @push.exit
91
+ @log.info('Spread entrance finished, thread killed')
92
+ end
93
+ end
94
+ end
95
+
96
+ def push(id, body)
97
+ @entrance.push(id, body).each do |m|
98
+ next if @seen.include?(m)
99
+ @seen << m
100
+ @modified.push(m)
101
+ @log.debug("Push scheduled for #{m}, queue size is #{@modified.size}")
102
+ end
103
+ end
104
+ end
105
+ end
data/lib/zold/patch.rb CHANGED
@@ -66,14 +66,15 @@ module Zold
66
66
  next if @txns.find { |t| t == txn }
67
67
  if txn.amount.negative?
68
68
  if txn.id <= max
69
- @log.error("Transaction ID is less than max #{max}: #{txn.to_text}")
69
+ @log.error("Transaction ID is not greater than max ID #{max}: #{txn.to_text}")
70
70
  next
71
71
  end
72
- if @txns.find { |t| t.id == txn.id }
73
- @log.error("Transaction ##{txn.id} already exists: #{txn.to_text}")
72
+ dup = @txns.find { |t| t.id == txn.id }
73
+ if dup
74
+ @log.error("An attempt to overwrite #{dup.to_text} with this: #{txn.to_text}")
74
75
  next
75
76
  end
76
- if !@txns.empty? && @txns.map(&:amount).inject(&:+) < txn.amount
77
+ if @txns.map(&:amount).map(&:to_i).inject(&:+).to_i < txn.amount.to_i * -1 && !wallet.root?
77
78
  @log.error("Transaction ##{txn.id} attempts to make the balance negative: #{txn.to_text}")
78
79
  next
79
80
  end
@@ -92,7 +93,8 @@ module Zold
92
93
  next
93
94
  end
94
95
  unless payer.has?(txn.id, wallet.id)
95
- @log.error("Paying wallet #{wallet.id} doesn't have transaction ##{txn.id}: #{txn.to_text}")
96
+ @log.error("Paying wallet #{txn.bnf} doesn't have transaction ##{txn.id} \
97
+ among #{payer.txns.count} transactions: #{txn.to_text}")
96
98
  next
97
99
  end
98
100
  end
data/lib/zold/remotes.rb CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  require 'csv'
22
22
  require 'uri'
23
+ require 'time'
23
24
  require 'fileutils'
24
25
  require_relative 'backtrace'
25
26
  require_relative 'node/farm'
@@ -39,7 +40,11 @@ module Zold
39
40
  TOLERANCE = 8
40
41
 
41
42
  # Empty, for standalone mode
42
- class Empty
43
+ class Empty < Remotes
44
+ def initialize
45
+ # Nothing here
46
+ end
47
+
43
48
  def all
44
49
  []
45
50
  end
@@ -52,7 +57,7 @@ module Zold
52
57
  # One remote.
53
58
  class Remote
54
59
  attr_reader :host, :port
55
- def initialize(host, port, score, idx, log: Log::Quiet.new)
60
+ def initialize(host, port, score, idx, log: Log::Quiet.new, network: 'test')
56
61
  @host = host
57
62
  raise 'Post must be Integer' unless port.is_a?(Integer)
58
63
  @port = port
@@ -60,11 +65,13 @@ module Zold
60
65
  @score = score
61
66
  raise 'Idx must be of type Integer' unless idx.is_a?(Integer)
62
67
  @idx = idx
68
+ raise 'Network can\'t be nil' if network.nil?
69
+ @network = network
63
70
  @log = log
64
71
  end
65
72
 
66
73
  def http(path = '/')
67
- Http.new("http://#{@host}:#{@port}#{path}", @score)
74
+ Http.new("http://#{@host}:#{@port}#{path}", @score, network: @network)
68
75
  end
69
76
 
70
77
  def to_s
@@ -90,7 +97,7 @@ module Zold
90
97
  end
91
98
 
92
99
  def assert_score_strength(score)
93
- raise "Score is too weak #{score.strength}: #{score}" if score.strength < Score::STRENGTH
100
+ raise "Score #{score.strength} is too weak (<#{Score::STRENGTH}): #{score}" if score.strength < Score::STRENGTH
94
101
  end
95
102
 
96
103
  def assert_score_value(score, min)
@@ -98,8 +105,11 @@ module Zold
98
105
  end
99
106
  end
100
107
 
101
- def initialize(file)
108
+ def initialize(file, network: 'test')
109
+ raise 'File can\'t be nil' if file.nil?
102
110
  @file = file
111
+ raise 'Network can\'t be nil' if network.nil?
112
+ @network = network
103
113
  end
104
114
 
105
115
  def all
@@ -163,15 +173,17 @@ module Zold
163
173
  best = farm.best[0]
164
174
  require_relative 'score'
165
175
  score = best.nil? ? Score::ZERO : best
176
+ start = Time.now
166
177
  idx = 0
167
178
  all.each do |r|
168
179
  begin
169
- yield Remotes::Remote.new(r[:host], r[:port], score, idx, log: log)
180
+ yield Remotes::Remote.new(r[:host], r[:port], score, idx, log: log, network: @network)
170
181
  idx += 1
171
182
  rescue StandardError => e
172
183
  error(r[:host], r[:port])
173
184
  errors = errors(r[:host], r[:port])
174
- log.info("#{Rainbow("#{r[:host]}:#{r[:port]}").red}: #{e.message}; errors=#{errors}")
185
+ log.info("#{Rainbow("#{r[:host]}:#{r[:port]}").red}: #{e.message} \
186
+ in #{(Time.now - start).round}s; errors=#{errors}")
175
187
  log.debug(Backtrace.new(e).to_s)
176
188
  remove(r[:host], r[:port]) if errors > Remotes::TOLERANCE
177
189
  end
data/lib/zold/txn.rb CHANGED
@@ -68,7 +68,9 @@ module Zold
68
68
  end
69
69
 
70
70
  def ==(other)
71
- id == other.id && bnf == other.bnf
71
+ id == other.id && date == other.date && amount == other.amount &&
72
+ prefix == other.prefix && bnf == other.bnf &&
73
+ details == other.details && sign == other.sign
72
74
  end
73
75
 
74
76
  def to_s