zold 0.20.1 → 0.20.2

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.
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