zold 0.20.1 → 0.20.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/bin/zold +23 -7
  3. data/fixtures/merge/into-no-wallet/copies/0123456789abcdef/scores.zc +1 -1
  4. data/fixtures/merge/random-expenses/copies/0123456789abcdef/scores.zc +5 -5
  5. data/fixtures/merge/simple-case/copies/0123456789abcdef/scores.zc +1 -1
  6. data/lib/zold/age.rb +2 -1
  7. data/lib/zold/amount.rb +3 -0
  8. data/lib/zold/cached_wallets.rb +2 -2
  9. data/lib/zold/commands/calculate.rb +1 -1
  10. data/lib/zold/commands/clean.rb +6 -1
  11. data/lib/zold/commands/fetch.rb +5 -4
  12. data/lib/zold/commands/merge.rb +2 -1
  13. data/lib/zold/commands/node.rb +46 -10
  14. data/lib/zold/commands/push.rb +4 -2
  15. data/lib/zold/commands/routines/audit.rb +53 -0
  16. data/lib/zold/commands/routines/gc.rb +1 -1
  17. data/lib/zold/copies.rb +15 -8
  18. data/lib/zold/head.rb +15 -14
  19. data/lib/zold/hungry_wallets.rb +2 -2
  20. data/lib/zold/id.rb +4 -8
  21. data/lib/zold/metronome.rb +4 -4
  22. data/lib/zold/node/async_entrance.rb +7 -3
  23. data/lib/zold/node/farm.rb +1 -1
  24. data/lib/zold/node/front.rb +8 -5
  25. data/lib/zold/node/sync_entrance.rb +4 -0
  26. data/lib/zold/remotes.rb +57 -55
  27. data/lib/zold/tax.rb +2 -2
  28. data/lib/zold/thread_pool.rb +18 -14
  29. data/lib/zold/tree_wallets.rb +1 -1
  30. data/lib/zold/txn.rb +32 -3
  31. data/lib/zold/txns.rb +14 -14
  32. data/lib/zold/verbose_thread.rb +7 -0
  33. data/lib/zold/version.rb +1 -1
  34. data/lib/zold/wallet.rb +2 -2
  35. data/test/commands/routines/test_audit.rb +41 -0
  36. data/test/commands/test_clean.rb +3 -3
  37. data/test/node/fake_node.rb +2 -1
  38. data/test/node/test_async_entrance.rb +3 -1
  39. data/test/node/test_front.rb +1 -0
  40. data/test/node/test_sync_entrance.rb +2 -2
  41. data/test/test_copies.rb +14 -5
  42. data/test/test_tree_wallets.rb +17 -7
  43. data/test/test_txn.rb +8 -0
  44. data/test/test_wallet.rb +17 -0
  45. data/zold.gemspec +1 -1
  46. metadata +8 -5
data/lib/zold/head.rb CHANGED
@@ -40,26 +40,27 @@ module Zold
40
40
  def fetch
41
41
  raise "Wallet file '#{@file}' is absent" unless File.exist?(@file)
42
42
  lines = IO.read(@file).split(/\n/)
43
+ # lines = ['', '', '0123456701234567', '']
43
44
  raise "Not enough lines in #{@file}, just #{lines.count}" if lines.count < 4
44
45
  lines.take(4)
45
46
  end
47
+ end
46
48
 
47
- # Cached head.
48
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
49
- # Copyright:: Copyright (c) 2018 Yegor Bugayenko
50
- # License:: MIT
51
- class Cached
52
- def initialize(txns)
53
- @txns = txns
54
- end
49
+ # Cached head.
50
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
51
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
52
+ # License:: MIT
53
+ class CachedHead
54
+ def initialize(head)
55
+ @head = head
56
+ end
55
57
 
56
- def flush
57
- @fetch = nil
58
- end
58
+ def flush
59
+ @fetch = nil
60
+ end
59
61
 
60
- def fetch
61
- @fetch ||= @txns.fetch
62
- end
62
+ def fetch
63
+ @fetch ||= @head.fetch
63
64
  end
64
65
  end
65
66
  end
@@ -58,7 +58,7 @@ module Zold
58
58
  @mutex.synchronize do
59
59
  unless @queue.include?(id)
60
60
  @queue << id
61
- @log.debug("Hungry queue got #{id}, at the pos no.#{@queue.size}")
61
+ @log.debug("Hungry queue got #{id}, at the pos no.#{@queue.size - 1}")
62
62
  end
63
63
  end
64
64
  end
@@ -76,7 +76,7 @@ module Zold
76
76
  end
77
77
  begin
78
78
  Pull.new(wallets: @wallets, remotes: @remotes, copies: @copies, log: @log).run(
79
- ['pull', id.to_s, "--network=#{@network}"]
79
+ ['pull', id.to_s, "--network=#{@network}", '--tolerate-edges', '--tolerate-quorum=1']
80
80
  )
81
81
  rescue Fetch::EdgesOnly => e
82
82
  @log.error("Can't hungry-pull #{id}: #{e.message}")
data/lib/zold/id.rb CHANGED
@@ -31,14 +31,10 @@ module Zold
31
31
  PTN = Regexp.new('^[0-9a-fA-F]{16}$')
32
32
  private_constant :PTN
33
33
 
34
- def initialize(id = nil)
35
- if id.nil?
36
- @id = rand(2**32..2**64 - 1)
37
- else
38
- raise "Invalid wallet ID type: #{id.class.name}" unless id.is_a?(String)
39
- raise "Invalid wallet ID: #{id}" unless id =~ PTN
40
- @id = Integer("0x#{id}", 16)
41
- end
34
+ def initialize(id = format('%016x', rand(2**32..2**64 - 1)))
35
+ raise "Invalid wallet ID type: #{id.class.name}" unless id.is_a?(String)
36
+ raise "Invalid wallet ID: #{id}" unless PTN.match?(id)
37
+ @id = Integer("0x#{id}", 16)
42
38
  end
43
39
 
44
40
  # The ID of the root wallet.
@@ -23,6 +23,7 @@
23
23
  require 'backtrace'
24
24
  require_relative 'log'
25
25
  require_relative 'age'
26
+ require_relative 'endless'
26
27
  require_relative 'verbose_thread'
27
28
  require_relative 'thread_pool'
28
29
 
@@ -58,10 +59,10 @@ module Zold
58
59
  def start
59
60
  @routines.each_with_index do |r, idx|
60
61
  @threads.add do
61
- Thread.current.name = "#{r.class.name}-#{idx}"
62
62
  step = 0
63
- loop do
63
+ Endless.new("#{r.class.name}-#{idx}", log: @log).run do
64
64
  Thread.current.thread_variable_set(:start, Time.now)
65
+ step += 1
65
66
  begin
66
67
  r.exec(step)
67
68
  @log.debug("Routine #{r.class.name} ##{step} done \
@@ -70,9 +71,8 @@ in #{Age.new(Thread.current.thread_variable_get(:start))}")
70
71
  @failures[r.class.name] = Time.now.utc.iso8601 + "\n" + Backtrace.new(e).to_s
71
72
  @log.error("Routine #{r.class.name} ##{step} failed \
72
73
  in #{Age.new(Thread.current.thread_variable_get(:start))}")
73
- @log.error(Backtrace.new(e).to_s)
74
+ raise e
74
75
  end
75
- step += 1
76
76
  sleep(1)
77
77
  end
78
78
  end
@@ -60,10 +60,14 @@ module Zold
60
60
  def start
61
61
  raise 'Block must be given to start()' unless block_given?
62
62
  FileUtils.mkdir_p(@dir)
63
- DirItems.new(@dir).fetch.select { |f| f =~ /^[0-9a-f]{16}-/ }.each do |f|
63
+ DirItems.new(@dir).fetch.each do |f|
64
64
  file = File.join(@dir, f)
65
- id = f.split('-')[0]
66
- @queue << { id: Id.new(id), file: file }
65
+ if /^[0-9a-f]{16}-/.match?(f)
66
+ id = f.split('-')[0]
67
+ @queue << { id: Id.new(id), file: file }
68
+ else
69
+ File.delete(file)
70
+ end
67
71
  end
68
72
  @log.info("#{@queue.size} wallets pre-loaded into async_entrace from #{@dir}") unless @queue.size.zero?
69
73
  @entrance.start do
@@ -53,7 +53,7 @@ module Zold
53
53
  #
54
54
  # <tt>cache</tt> is the file where the farm will keep all the scores it
55
55
  # manages to find. If the file is absent, it will be created, together with
56
- # the necesary parent directories.
56
+ # the necessary parent directories.
57
57
  #
58
58
  # <tt>lifetime</tt> is the amount of seconds for a score to live in the farm, by default
59
59
  # it's the entire day, since the Score expires in 24 hours; can be decreased for the
@@ -48,7 +48,7 @@ require_relative 'soft_error'
48
48
  module Zold
49
49
  # Web front
50
50
  class Front < Sinatra::Base
51
- # The minimum score required in order to recongnize a requestor
51
+ # The minimum score required in order to recognize a requester
52
52
  # as a valuable node and add it to the list of remotes.
53
53
  MIN_SCORE = 4
54
54
 
@@ -208,17 +208,19 @@ from #{request.ip} in #{Age.new(@start, limit: 1)}")
208
208
  cpus: settings.zache.get(:cpus) do
209
209
  Concurrent.processor_count
210
210
  end,
211
- memory: settings.zache.get(:memory, lifetime: 5 * 60) do
211
+ memory: settings.zache.get(:memory, lifetime: settings.opts['no-cache'] ? 0 : 60) do
212
212
  mem = GetProcessMem.new.bytes.to_i
213
213
  if mem > settings.opts['oom-limit'] * 1024 * 1024 &&
214
214
  !settings.opts['skip-oom'] && !settings.opts['never-reboot']
215
- settings.log.error("We are too big in memory (#{Size.new(mem)}), quitting; use --skip-oom to never quit")
215
+ settings.log.error("We are too big in memory (#{Size.new(mem)}), quitting; \
216
+ use --skip-oom to never quit or --memory-dump to print the entire memory usage summary on exit; \
217
+ this is not a normal behavior, you may want to report a bug to our GitHub repository")
216
218
  Front.stop!
217
219
  end
218
220
  mem
219
221
  end,
220
222
  platform: RUBY_PLATFORM,
221
- load: settings.zache.get(:load, lifetime: 5 * 60) do
223
+ load: settings.zache.get(:load, lifetime: settings.opts['no-cache'] ? 0 : 60) do
222
224
  require 'usagewatch_ext'
223
225
  Object.const_defined?('Usagewatch') ? Usagewatch.uw_load.to_f : 0.0
224
226
  end,
@@ -248,7 +250,7 @@ from #{request.ip} in #{Age.new(@start, limit: 1)}")
248
250
  digest: wallet.digest,
249
251
  copies: Copies.new(File.join(settings.copies, wallet.id)).all.count,
250
252
  balance: wallet.balance.to_i,
251
- body: File.new(wallet.path).read
253
+ body: IO.read(wallet.path)
252
254
  )
253
255
  end
254
256
  end
@@ -460,6 +462,7 @@ time to stop; use --skip-oom to never quit")
460
462
  end
461
463
 
462
464
  def total_wallets
465
+ return 256 unless settings.opts['no-cache']
463
466
  settings.zache.get(:wallets, lifetime: settings.opts['no-cache'] ? 0 : 60) do
464
467
  settings.wallets.count
465
468
  end
@@ -46,6 +46,10 @@ module Zold
46
46
 
47
47
  def start
48
48
  raise 'Block must be given to start()' unless block_given?
49
+ if File.exist?(@dir)
50
+ FileUtils.rm_rf(@dir)
51
+ @log.info("Directory #{@dir} deleted")
52
+ end
49
53
  @entrance.start do
50
54
  yield(self)
51
55
  end
data/lib/zold/remotes.rb CHANGED
@@ -39,6 +39,61 @@ require_relative 'node/farm'
39
39
  # Copyright:: Copyright (c) 2018 Yegor Bugayenko
40
40
  # License:: MIT
41
41
  module Zold
42
+ # One remote.
43
+ class RemoteNode
44
+ def initialize(host:, port:, score:, idx:, master:, network: 'test', log: Log::NULL)
45
+ @host = host
46
+ @port = port
47
+ @score = score
48
+ @idx = idx
49
+ @master = master
50
+ @network = network
51
+ @log = log
52
+ end
53
+
54
+ def http(path = '/')
55
+ Http.new(uri: "http://#{@host}:#{@port}#{path}", score: @score, network: @network)
56
+ end
57
+
58
+ def master?
59
+ @master
60
+ end
61
+
62
+ def to_s
63
+ "#{@host}:#{@port}/#{@idx}"
64
+ end
65
+
66
+ def assert_code(code, response)
67
+ msg = response.status_line.strip
68
+ return if response.status.to_i == code
69
+ if response.headers && response.headers['X-Zold-Error']
70
+ raise "Error ##{response.status} \"#{response.headers['X-Zold-Error']}\"
71
+ at #{response.headers['X-Zold-Path']}"
72
+ end
73
+ raise "Unexpected HTTP code #{response.status}, instead of #{code}" if msg.empty?
74
+ raise "#{msg} (HTTP code #{response.status}, instead of #{code})"
75
+ end
76
+
77
+ def assert_valid_score(score)
78
+ raise "Invalid score #{score.reduced(4)}" unless score.valid?
79
+ raise "Expired score (#{Age.new(score.time)}) #{score.reduced(4)}" if score.expired?
80
+ end
81
+
82
+ def assert_score_ownership(score)
83
+ raise "Masqueraded host #{@host} as #{score.host}: #{score.reduced(4)}" if @host != score.host
84
+ raise "Masqueraded port #{@port} as #{score.port}: #{score.reduced(4)}" if @port != score.port
85
+ end
86
+
87
+ def assert_score_strength(score)
88
+ return if score.strength >= Score::STRENGTH
89
+ raise "Score #{score.strength} is too weak (<#{Score::STRENGTH}): #{score.reduced(4)}"
90
+ end
91
+
92
+ def assert_score_value(score, min)
93
+ raise "Score #{score.value} is too small (<#{min}): #{score.reduced(4)}" if score.value < min
94
+ end
95
+ end
96
+
42
97
  # All remotes
43
98
  class Remotes
44
99
  # The default TCP port all nodes are supposed to use.
@@ -73,60 +128,6 @@ module Zold
73
128
  end
74
129
  end
75
130
 
76
- # One remote.
77
- class Remote
78
- def initialize(host:, port:, score:, idx:, network: 'test', log: Log::NULL)
79
- @host = host
80
- @port = port
81
- @score = score
82
- @idx = idx
83
- @network = network
84
- @log = log
85
- end
86
-
87
- def http(path = '/')
88
- Http.new(uri: "http://#{@host}:#{@port}#{path}", score: @score, network: @network)
89
- end
90
-
91
- def master?
92
- !MASTERS.find { |r| r[0] == @host && r[1].to_i == @port }.nil?
93
- end
94
-
95
- def to_s
96
- "#{@host}:#{@port}/#{@idx}"
97
- end
98
-
99
- def assert_code(code, response)
100
- msg = response.status_line.strip
101
- return if response.status.to_i == code
102
- if response.headers && response.headers['X-Zold-Error']
103
- raise "Error ##{response.status} \"#{response.headers['X-Zold-Error']}\"
104
- at #{response.headers['X-Zold-Path']}"
105
- end
106
- raise "Unexpected HTTP code #{response.status}, instead of #{code}" if msg.empty?
107
- raise "#{msg} (HTTP code #{response.status}, instead of #{code})"
108
- end
109
-
110
- def assert_valid_score(score)
111
- raise "Invalid score #{score.reduced(4)}" unless score.valid?
112
- raise "Expired score (#{Age.new(score.time)}) #{score.reduced(4)}" if score.expired?
113
- end
114
-
115
- def assert_score_ownership(score)
116
- raise "Masqueraded host #{@host} as #{score.host}: #{score.reduced(4)}" if @host != score.host
117
- raise "Masqueraded port #{@port} as #{score.port}: #{score.reduced(4)}" if @port != score.port
118
- end
119
-
120
- def assert_score_strength(score)
121
- return if score.strength >= Score::STRENGTH
122
- raise "Score #{score.strength} is too weak (<#{Score::STRENGTH}): #{score.reduced(4)}"
123
- end
124
-
125
- def assert_score_value(score, min)
126
- raise "Score #{score.value} is too small (<#{min}): #{score.reduced(4)}" if score.value < min
127
- end
128
- end
129
-
130
131
  def initialize(file:, network: 'test', timeout: 60)
131
132
  @file = file
132
133
  @network = network
@@ -197,11 +198,12 @@ module Zold
197
198
  start = Time.now
198
199
  best = farm.best[0]
199
200
  begin
200
- yield Remotes::Remote.new(
201
+ yield RemoteNode.new(
201
202
  host: r[:host],
202
203
  port: r[:port],
203
204
  score: best.nil? ? Score::ZERO : best,
204
205
  idx: idx,
206
+ master: master?(r[:host], r[:port]),
205
207
  log: log,
206
208
  network: @network
207
209
  )
data/lib/zold/tax.rb CHANGED
@@ -60,8 +60,8 @@ module Zold
60
60
  # When score strengths were updated. The numbers here indicate the
61
61
  # strengths we accepted before these dates.
62
62
  MILESTONES = {
63
- Time.parse('30-11-2018') => 6,
64
- Time.parse('09-12-2018') => 7
63
+ Txn.parse_time('2018-11-30T00:00:00Z') => 6,
64
+ Txn.parse_time('2018-12-09T00:00:00Z') => 7
65
65
  }.freeze
66
66
  private_constant :MILESTONES
67
67
 
@@ -41,25 +41,29 @@ module Zold
41
41
  # Run this code in many threads
42
42
  def run(threads, set = (0..threads - 1).to_a)
43
43
  raise "Number of threads #{threads} has to be positive" unless threads.positive?
44
- idx = Concurrent::AtomicFixnum.new
45
- mutex = Mutex.new
46
44
  list = set.dup
47
45
  total = [threads, set.count].min
48
- latch = Concurrent::CountDownLatch.new(total)
49
- total.times do |i|
50
- add do
51
- Thread.current.name = "#{@title}-#{i}"
52
- loop do
53
- r = mutex.synchronize { list.pop }
54
- break if r.nil?
55
- yield(r, idx.increment - 1)
46
+ if total == 1
47
+ list.each_with_index { |r, i| yield(r, i) }
48
+ elsif total.positive?
49
+ idx = Concurrent::AtomicFixnum.new
50
+ mutex = Mutex.new
51
+ latch = Concurrent::CountDownLatch.new(total)
52
+ total.times do |i|
53
+ add do
54
+ Thread.current.name = "#{@title}-#{i}"
55
+ loop do
56
+ r = mutex.synchronize { list.pop }
57
+ break if r.nil?
58
+ yield(r, idx.increment - 1)
59
+ end
60
+ ensure
61
+ latch.count_down
56
62
  end
57
- ensure
58
- latch.count_down
59
63
  end
64
+ latch.wait
65
+ kill
60
66
  end
61
- latch.wait
62
- kill
63
67
  end
64
68
 
65
69
  # Add a new thread
@@ -67,7 +67,7 @@ module Zold
67
67
  end
68
68
 
69
69
  def count
70
- `find . -name "*.z" | wc -l`.strip.to_i
70
+ `find #{@dir} -name "*.z" | wc -l`.strip.to_i
71
71
  end
72
72
  end
73
73
  end
data/lib/zold/txn.rb CHANGED
@@ -42,6 +42,14 @@ module Zold
42
42
  RE_PREFIX = '[a-zA-Z0-9]+'
43
43
  private_constant :RE_PREFIX
44
44
 
45
+ # To validate the prefix
46
+ REGEX_PREFIX = Regexp.new("^#{RE_PREFIX}$")
47
+ private_constant :REGEX_PREFIX
48
+
49
+ # To validate details
50
+ REGEX_DETAILS = Regexp.new("^#{RE_DETAILS}$")
51
+ private_constant :REGEX_DETAILS
52
+
45
53
  attr_reader :id, :date, :amount, :prefix, :bnf, :details, :sign
46
54
  attr_writer :sign, :amount, :bnf
47
55
  def initialize(id, date, amount, prefix, bnf, details)
@@ -62,12 +70,12 @@ module Zold
62
70
  raise 'Prefix can\'t be NIL' if prefix.nil?
63
71
  raise "Prefix is too short: \"#{prefix}\"" if prefix.length < 8
64
72
  raise "Prefix is too long: \"#{prefix}\"" if prefix.length > 32
65
- raise "Prefix is wrong: \"#{prefix}\" (#{RE_PREFIX})" unless prefix =~ Regexp.new("^#{RE_PREFIX}$")
73
+ raise "Prefix is wrong: \"#{prefix}\" (#{RE_PREFIX})" unless REGEX_PREFIX.match?(prefix)
66
74
  @prefix = prefix
67
75
  raise 'Details can\'t be NIL' if details.nil?
68
76
  raise 'Details can\'t be empty' if details.empty?
69
77
  raise "Details are too long: \"#{details}\"" if details.length > 512
70
- raise "Wrong details \"#{details}\" (#{RE_DETAILS})" unless details =~ Regexp.new("^#{RE_DETAILS}$")
78
+ raise "Wrong details \"#{details}\" (#{RE_DETAILS})" unless REGEX_DETAILS.match?(details)
71
79
  @details = details
72
80
  end
73
81
 
@@ -149,7 +157,7 @@ module Zold
149
157
  raise "Invalid line ##{idx}: #{line.inspect} #{regex}" unless parts
150
158
  txn = Txn.new(
151
159
  Hexnum.parse(parts[:id]).to_i,
152
- Time.parse(parts[:date]),
160
+ parse_time(parts[:date]),
153
161
  Amount.new(zents: Hexnum.parse(parts[:amount]).to_i),
154
162
  parts[:prefix],
155
163
  Id.new(parts[:bnf]),
@@ -158,5 +166,26 @@ module Zold
158
166
  txn.sign = parts[:sign]
159
167
  txn
160
168
  end
169
+
170
+ ISO8601 = Regexp.new(
171
+ '^' + [
172
+ '(?<year>\d{4})',
173
+ '-(?<month>\d{2})',
174
+ '-(?<day>\d{2})',
175
+ 'T(?<hours>\d{2})',
176
+ ':(?<minutes>\d{2})',
177
+ ':(?<seconds>\d{2})Z'
178
+ ].join
179
+ )
180
+ private_constant :ISO8601
181
+
182
+ def self.parse_time(iso)
183
+ parts = ISO8601.match(iso)
184
+ raise "Invalid ISO 8601 date \"#{iso}\"" if parts.nil?
185
+ Time.gm(
186
+ parts[:year].to_i, parts[:month].to_i, parts[:day].to_i,
187
+ parts[:hours].to_i, parts[:minutes].to_i, parts[:seconds].to_i
188
+ )
189
+ end
161
190
  end
162
191
  end