zold 0.18.6 → 0.18.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cef493a42202430232e9c904a25258bdf6c59336c3f6732687b1412713f93d1
4
- data.tar.gz: 8ff76340ba5810cf0ccb241c61505fcc1c8f9b0c7efa49b40f49fb312ff0370f
3
+ metadata.gz: e805665d05301f84e70c8e280530b3a9b7cd195b63165a1a3eefda7eb72f509f
4
+ data.tar.gz: 42113e608d1c76f7b3ed5ace35ff36456566b708a9779e6c52e866a6c189416d
5
5
  SHA512:
6
- metadata.gz: 5ffcaa34424091b2f97ce0f3a12cca71b983c32332a7ddeef69d6cdfd89ed507a8539f4baa2cf909ec30675721aea4b7add652fba881192113df3b97b6fd1cd2
7
- data.tar.gz: b1e80e8a217ba00c6a1c6b15a8efc48c1687f7295607c529a2d322b763fd001d019bb87411e3815532f7a9117f7e41f88a137e3e666c74c20fb42db401e14986
6
+ metadata.gz: d7e2b7606454ca38aac815c9c43a280d753845ab8301ac4e0055894a640e6f7c81185c8c23445d776875c215b7d614c8abcaa118133eaab41dc81778f0d44bb4
7
+ data.tar.gz: 1223bc3bee736ccc949ae1f663f25286498b07f1595370a870c65a65ca036477e6800b021caca6b44bf118f3424a3b611d7560c3bf8858c2e034be11f964e5d8
@@ -22,6 +22,7 @@
22
22
 
23
23
  require 'zache'
24
24
  require_relative 'endless'
25
+ require_relative 'thread_pool'
25
26
 
26
27
  # Cached collection of wallets.
27
28
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -33,7 +34,8 @@ module Zold
33
34
  def initialize(wallets)
34
35
  @wallets = wallets
35
36
  @zache = Zache.new
36
- @clean = Thread.start do
37
+ @clean = ThreadPool.new('cached-wallets')
38
+ @clean.add do
37
39
  Endless.new('cached_wallets').run do
38
40
  sleep 60
39
41
  @zache.clean
@@ -32,6 +32,7 @@ require 'concurrent'
32
32
  require 'parallel'
33
33
  require_relative 'thread_badge'
34
34
  require_relative 'args'
35
+ require_relative '../thread_pool'
35
36
  require_relative '../log'
36
37
  require_relative '../age'
37
38
  require_relative '../http'
@@ -77,9 +78,9 @@ Available options:"
77
78
  o.bool '--help', 'Print instructions'
78
79
  end
79
80
  mine = Args.new(opts, @log).take || return
80
- Parallel.map((mine.empty? ? @wallets.all : mine.map { |i| Id.new(i) }), in_threads: opts[:threads]) do |id|
81
+ list = mine.empty? ? @wallets.all : mine.map { |i| Id.new(i) }
82
+ ThreadPool.new('fetch', log: @log).run(opts['threads'], list) do |id|
81
83
  fetch(id, Copies.new(File.join(@copies, id)), opts)
82
- @log.debug("Worker: #{Parallel.worker_number} has fetched wallet #{id}")
83
84
  end
84
85
  end
85
86
 
@@ -28,6 +28,7 @@ require 'concurrent'
28
28
  require 'parallel'
29
29
  require_relative 'thread_badge'
30
30
  require_relative 'args'
31
+ require_relative '../thread_pool'
31
32
  require_relative '../age'
32
33
  require_relative '../size'
33
34
  require_relative '../log'
@@ -66,9 +67,9 @@ Available options:"
66
67
  o.bool '--help', 'Print instructions'
67
68
  end
68
69
  mine = Args.new(opts, @log).take || return
69
- Parallel.map((mine.empty? ? @wallets.all : mine.map { |i| Id.new(i) }), in_threads: opts[:threads]) do |id|
70
+ list = mine.empty? ? @wallets.all : mine.map { |i| Id.new(i) }
71
+ ThreadPool.new('push', log: @log).run(opts['threads'], list) do |id|
70
72
  push(id, opts)
71
- @log.debug("Worker: #{Parallel.worker_number} has pushed wallet #{id}")
72
73
  end
73
74
  end
74
75
 
@@ -229,7 +229,7 @@ Available options:"
229
229
  else
230
230
  scores.each { |s| @log.info("Elected: #{s.reduced(4)}") }
231
231
  end
232
- scores
232
+ scores.sort_by(&:value).reverse
233
233
  end
234
234
 
235
235
  def trim(opts)
@@ -257,15 +257,22 @@ Available options:"
257
257
  r.assert_score_ownership(score)
258
258
  r.assert_score_strength(score) unless opts['ignore-score-weakness']
259
259
  @remotes.rescore(score.host, score.port, score.value)
260
- gem = Zold::Gem.new
261
- if Semantic::Version.new(VERSION) < Semantic::Version.new(json['version']) ||
262
- Semantic::Version.new(VERSION) < Semantic::Version.new(gem.last_version)
260
+ if Semantic::Version.new(VERSION) < Semantic::Version.new(json['version'])
263
261
  if opts['reboot']
264
262
  @log.info("#{r}: their version #{json['version']} is higher than mine #{VERSION}, reboot! \
265
263
  (use --never-reboot to avoid this from happening)")
266
264
  terminate
267
265
  end
268
266
  @log.debug("#{r}: their version #{json['version']} is higher than mine #{VERSION}, \
267
+ it's recommended to reboot, but I don't do it because of --never-reboot")
268
+ end
269
+ if Semantic::Version.new(VERSION) < Semantic::Version.new(Zold::Gem.new.last_version)
270
+ if opts['reboot']
271
+ @log.info("#{r}: the version of the gem is higher than mine #{VERSION}, reboot! \
272
+ (use --never-reboot to avoid this from happening)")
273
+ terminate
274
+ end
275
+ @log.debug("#{r}: gem version is higher than mine #{VERSION}, \
269
276
  it's recommended to reboot, but I don't do it because of --never-reboot")
270
277
  end
271
278
  if cycle.positive?
@@ -51,7 +51,7 @@ module Zold
51
51
  cmd.run(args + [id.to_s])
52
52
  removed += 1
53
53
  end
54
- @log.info("Removed #{removed} empty+old wallets out of #{seen} total")
54
+ @log.info("Removed #{removed} empty+old wallets out of #{seen} total") unless removed.zero?
55
55
  end
56
56
  end
57
57
  end
data/lib/zold/endless.rb CHANGED
@@ -39,7 +39,6 @@ module Zold
39
39
  def run
40
40
  start = Time.now
41
41
  Thread.current.name = @title
42
- Thread.current.abort_on_exception = true
43
42
  begin
44
43
  loop do
45
44
  VerboseThread.new(@log).run(true) do
data/lib/zold/http.rb CHANGED
@@ -98,7 +98,7 @@ module Zold
98
98
 
99
99
  private
100
100
 
101
- # Some clients waits for status method in respons
101
+ # Some clients waits for status method in response
102
102
  class Response < SimpleDelegator
103
103
  def status
104
104
  code.zero? ? 599 : code
@@ -24,6 +24,7 @@ require 'backtrace'
24
24
  require_relative 'log'
25
25
  require_relative 'age'
26
26
  require_relative 'verbose_thread'
27
+ require_relative 'thread_pool'
27
28
 
28
29
  # Background routines.
29
30
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -35,8 +36,7 @@ module Zold
35
36
  def initialize(log = Log::NULL)
36
37
  @log = log
37
38
  @routines = []
38
- @threads = []
39
- @starts = {}
39
+ @threads = ThreadPool.new('metronome', log: log)
40
40
  @failures = {}
41
41
  end
42
42
 
@@ -44,13 +44,7 @@ module Zold
44
44
  [
45
45
  Time.now.utc.iso8601,
46
46
  'Current threads:',
47
- @threads.map do |t|
48
- [
49
- "#{t.name}: status=#{t.status}; alive=#{t.alive?}",
50
- "Most recent start: #{Age.new(@starts[t])} ago",
51
- t.backtrace.nil? ? 'NO BACKTRACE' : " #{t.backtrace.join("\n ")}"
52
- ].join("\n")
53
- end,
47
+ @threads.to_s,
54
48
  'Failures:',
55
49
  @failures.map { |r, f| "#{r}\n#{f}\n" }
56
50
  ].flatten.join("\n\n")
@@ -63,18 +57,19 @@ module Zold
63
57
 
64
58
  def start
65
59
  @routines.each_with_index do |r, idx|
66
- @threads << Thread.start do
67
- Thread.current.abort_on_exception = true
60
+ @threads.add do
68
61
  Thread.current.name = "#{r.class.name}-#{idx}"
69
62
  step = 0
70
63
  loop do
71
- @starts[Thread.current] = Time.now
64
+ Thread.current.thread_variable_set(:start, Time.now)
72
65
  begin
73
66
  r.exec(step)
74
- @log.debug("Routine #{r.class.name} ##{step} done in #{Age.new(@starts[Thread.current])}")
67
+ @log.debug("Routine #{r.class.name} ##{step} done \
68
+ in #{Age.new(Thread.current.thread_variable_get(:start))}")
75
69
  rescue StandardError => e
76
70
  @failures[r.class.name] = Time.now.utc.iso8601 + "\n" + Backtrace.new(e).to_s
77
- @log.error("Routine #{r.class.name} ##{step} failed in #{Age.new(@starts[Thread.current])}")
71
+ @log.error("Routine #{r.class.name} ##{step} failed \
72
+ in #{Age.new(Thread.current.thread_variable_get(:start))}")
78
73
  @log.error(Backtrace.new(e).to_s)
79
74
  end
80
75
  step += 1
@@ -85,14 +80,7 @@ module Zold
85
80
  begin
86
81
  yield(self)
87
82
  ensure
88
- start = Time.now
89
- unless @threads.empty?
90
- @log.info("Stopping the metronome with #{@threads.count} threads...")
91
- @threads.each(&:kill)
92
- @threads.each(&:join)
93
- @log.info("Metronome #{@threads.count} threads killed")
94
- end
95
- @log.info("Metronome stopped in #{Age.new(start)}, #{@failures.count} failures")
83
+ @threads.kill
96
84
  end
97
85
  end
98
86
  end
@@ -27,6 +27,7 @@ require_relative '../age'
27
27
  require_relative '../size'
28
28
  require_relative '../id'
29
29
  require_relative '../endless'
30
+ require_relative '../thread_pool'
30
31
  require_relative '../dir_items'
31
32
  require_relative 'soft_error'
32
33
 
@@ -42,7 +43,8 @@ module Zold
42
43
  @entrance = entrance
43
44
  @dir = File.expand_path(dir)
44
45
  @log = log
45
- @total = threads
46
+ @threads = threads
47
+ @pool = ThreadPool.new('async-entrance', log: log)
46
48
  @queue = Queue.new
47
49
  @queue_limit = queue_limit
48
50
  end
@@ -50,7 +52,7 @@ module Zold
50
52
  def to_json
51
53
  @entrance.to_json.merge(
52
54
  'queue': @queue.size,
53
- 'threads': @threads.count,
55
+ 'threads': @pool.count,
54
56
  'queue_limit': @queue_limit
55
57
  )
56
58
  end
@@ -65,8 +67,8 @@ module Zold
65
67
  end
66
68
  @log.info("#{@queue.size} wallets pre-loaded into async_entrace from #{@dir}") unless @queue.size.zero?
67
69
  @entrance.start do
68
- @threads = (0..@total - 1).map do |i|
69
- Thread.start do
70
+ (0..@threads).map do |i|
71
+ @pool.add do
70
72
  Endless.new("async-e##{i}", log: @log).run do
71
73
  take
72
74
  end
@@ -75,10 +77,7 @@ module Zold
75
77
  begin
76
78
  yield(self)
77
79
  ensure
78
- @log.info("AsyncEntrance stopping and killing #{@threads.count} threads...")
79
- @threads.each(&:kill)
80
- @threads.each(&:join)
81
- @log.info("AsyncEntrance stopped, #{@threads.count} threads killed")
80
+ @pool.kill
82
81
  end
83
82
  end
84
83
  end
@@ -28,6 +28,7 @@ require 'concurrent'
28
28
  require 'json'
29
29
  require 'zold/score'
30
30
  require_relative '../log'
31
+ require_relative '../thread_pool'
31
32
  require_relative '../age'
32
33
  require_relative '../endless'
33
34
  require_relative 'farmers'
@@ -64,7 +65,7 @@ module Zold
64
65
  @invoice = invoice
65
66
  @pipeline = Queue.new
66
67
  @farmer = farmer
67
- @threads = []
68
+ @threads = ThreadPool.new('farm')
68
69
  @lifetime = lifetime
69
70
  @strength = strength
70
71
  end
@@ -82,21 +83,14 @@ module Zold
82
83
  "Current time: #{Time.now.utc.iso8601}",
83
84
  "Ruby processes: #{`ps ax | grep zold | wc -l`}",
84
85
  JSON.pretty_generate(to_json),
85
- @threads.map do |t|
86
- trace = t.backtrace || []
87
- [
88
- "#{t.name}: status=#{t.status}; alive=#{t.alive?}",
89
- 'Vars: ' + t.thread_variables.map { |v| "#{v}=\"#{t.thread_variable_get(v)}\"" }.join('; '),
90
- " #{trace.join("\n ")}"
91
- ].join("\n")
92
- end
86
+ @threads.to_s
93
87
  ].flatten.join("\n\n")
94
88
  end
95
89
 
96
90
  # Renders the Farm into JSON to show for the end-user in front.rb.
97
91
  def to_json
98
92
  {
99
- threads: @threads.map { |t| "#{t.name}/#{t.status}/#{t.alive? ? 'alive' : 'dead'}" }.join(', '),
93
+ threads: @threads.to_json,
100
94
  pipeline: @pipeline.size,
101
95
  best: best.map(&:to_mnemo).join(', '),
102
96
  farmer: @farmer.class.name
@@ -121,8 +115,8 @@ module Zold
121
115
  else
122
116
  @log.info("#{best.size} scores pre-loaded from #{@cache}, the best is: #{best[0]}")
123
117
  end
124
- @threads = (1..threads).map do |t|
125
- Thread.start do
118
+ (1..threads).map do |t|
119
+ @threads.add do
126
120
  Thread.current.thread_variable_set(:tid, t.to_s)
127
121
  Endless.new("f#{t}", log: @log).run do
128
122
  cycle(host, port, threads)
@@ -131,7 +125,7 @@ module Zold
131
125
  end
132
126
  unless threads.zero?
133
127
  ready = false
134
- @threads << Thread.start do
128
+ @threads.add do
135
129
  Endless.new('cleanup', log: @log).run do
136
130
  cleanup(host, port, threads)
137
131
  ready = true
@@ -140,7 +134,7 @@ module Zold
140
134
  end
141
135
  loop { break if ready }
142
136
  end
143
- if @threads.empty?
137
+ if threads.zero?
144
138
  cleanup(host, port, threads)
145
139
  @log.info("Farm started with no threads (there will be no score) at #{host}:#{port}")
146
140
  else
@@ -150,10 +144,7 @@ at #{host}:#{port}, strength is #{@strength}")
150
144
  begin
151
145
  yield(self)
152
146
  ensure
153
- @log.info("Farm stopping with #{threads} threads...")
154
- @threads.each(&:kill)
155
- @threads.each(&:join)
156
- @log.info("Farm stopped, #{threads} threads killed")
147
+ @threads.kill
157
148
  end
158
149
  end
159
150
 
@@ -164,11 +155,11 @@ at #{host}:#{port}, strength is #{@strength}")
164
155
  before = scores.map(&:value).max.to_i
165
156
  save(threads, [Score.new(host: host, port: port, invoice: @invoice, strength: @strength)])
166
157
  scores = load
167
- free = scores.reject { |s| @threads.find { |t| t.name == s.to_mnemo } }
158
+ free = scores.reject { |s| @threads.exists?(s.to_mnemo) }
168
159
  @pipeline << free[0] if @pipeline.size.zero? && !free.empty?
169
160
  after = scores.map(&:value).max.to_i
170
161
  return unless before != after && !after.zero?
171
- @log.debug("#{Thread.current.name}: best score of #{scores.count} is #{scores[0]}")
162
+ @log.debug("#{Thread.current.name}: best score of #{scores.count} is #{scores[0].reduced(4)}")
172
163
  end
173
164
 
174
165
  def cycle(host, port, threads)
@@ -28,6 +28,7 @@ require_relative '../remotes'
28
28
  require_relative '../copies'
29
29
  require_relative '../endless'
30
30
  require_relative '../tax'
31
+ require_relative '../thread_pool'
31
32
  require_relative '../commands/merge'
32
33
  require_relative '../commands/fetch'
33
34
  require_relative '../commands/push'
@@ -48,12 +49,13 @@ module Zold
48
49
  @log = log
49
50
  @ignore_score_weakeness = ignore_score_weakeness
50
51
  @mutex = Mutex.new
52
+ @push = ThreadPool.new('spread-entrance')
51
53
  end
52
54
 
53
55
  def to_json
54
56
  @entrance.to_json.merge(
55
57
  'modified': @modified.size,
56
- 'push': @push.status
58
+ 'push': @push.to_json
57
59
  )
58
60
  end
59
61
 
@@ -62,7 +64,7 @@ module Zold
62
64
  @entrance.start do
63
65
  @seen = Set.new
64
66
  @modified = Queue.new
65
- @push = Thread.start do
67
+ @push.add do
66
68
  Endless.new('push', log: @log).run do
67
69
  id = @modified.pop
68
70
  if @remotes.all.empty?
@@ -82,11 +84,8 @@ module Zold
82
84
  begin
83
85
  yield(self)
84
86
  ensure
85
- @log.info('Waiting for spread entrance to finish...')
86
87
  @modified.clear
87
88
  @push.kill
88
- @push.join
89
- @log.info('Spread entrance finished, thread killed')
90
89
  end
91
90
  end
92
91
  end
data/lib/zold/remotes.rb CHANGED
@@ -31,6 +31,7 @@ require 'backtrace'
31
31
  require 'zold/score'
32
32
  require_relative 'age'
33
33
  require_relative 'http'
34
+ require_relative 'thread_pool'
34
35
  require_relative 'node/farm'
35
36
 
36
37
  # The list of remotes.
@@ -94,7 +95,7 @@ module Zold
94
95
  def assert_code(code, response)
95
96
  msg = response.status_line.strip
96
97
  return if response.status.to_i == code
97
- if response.headers['X-Zold-Error']
98
+ if response.headers && response.headers['X-Zold-Error']
98
99
  raise "Error ##{response.status} \"#{response.headers['X-Zold-Error']}\"
99
100
  at #{response.headers['X-Zold-Path']}"
100
101
  end
@@ -182,40 +183,32 @@ module Zold
182
183
  end
183
184
  end
184
185
 
186
+ # Go through the list of remotes and call a provided block for each
187
+ # of them. See how it's used, for example, in fetch.rb.
185
188
  def iterate(log, farm: Farm::Empty.new)
186
189
  raise 'Log can\'t be nil' if log.nil?
187
190
  raise 'Farm can\'t be nil' if farm.nil?
188
- list = all
189
- return if list.empty?
190
- best = farm.best[0]
191
- score = best.nil? ? Score::ZERO : best
192
- idx = Concurrent::AtomicFixnum.new
193
- pool = Concurrent::FixedThreadPool.new([list.count, Concurrent.processor_count * 4].min, max_queue: 0)
194
- list.each do |r|
195
- pool.post do
196
- Thread.current.abort_on_exception = true
197
- Thread.current.name = "remotes-#{idx.value}@#{r[:host]}:#{r[:port]}"
198
- start = Time.now
199
- begin
200
- yield Remotes::Remote.new(
201
- host: r[:host],
202
- port: r[:port],
203
- score: score,
204
- idx: idx.increment - 1,
205
- log: log,
206
- network: @network
207
- )
208
- raise 'Took too long to execute' if (Time.now - start).round > @timeout
209
- rescue StandardError => e
210
- error(r[:host], r[:port])
211
- log.info("#{Rainbow("#{r[:host]}:#{r[:port]}").red}: #{e.message} in #{Age.new(start)}")
212
- log.debug(Backtrace.new(e).to_s)
213
- remove(r[:host], r[:port]) if errors > TOLERANCE
214
- end
191
+ ThreadPool.new('remotes', log: log).run(Concurrent.processor_count * 4, all) do |r, idx|
192
+ Thread.current.name = "remotes-#{idx}@#{r[:host]}:#{r[:port]}"
193
+ start = Time.now
194
+ best = farm.best[0]
195
+ begin
196
+ yield Remotes::Remote.new(
197
+ host: r[:host],
198
+ port: r[:port],
199
+ score: best.nil? ? Score::ZERO : best,
200
+ idx: idx,
201
+ log: log,
202
+ network: @network
203
+ )
204
+ raise 'Took too long to execute' if (Time.now - start).round > @timeout
205
+ rescue StandardError => e
206
+ error(r[:host], r[:port])
207
+ log.info("#{Rainbow("#{r[:host]}:#{r[:port]}").red}: #{e.message} in #{Age.new(start)}")
208
+ log.debug(Backtrace.new(e).to_s)
209
+ remove(r[:host], r[:port]) if r[:errors] > TOLERANCE
215
210
  end
216
211
  end
217
- pool.shutdown
218
- pool.kill unless pool.wait_for_termination(5 * 60)
219
212
  end
220
213
 
221
214
  def error(host, port = PORT)
@@ -0,0 +1,132 @@
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 'concurrent'
24
+ require_relative 'age'
25
+ require_relative 'verbose_thread'
26
+
27
+ # Thread pool.
28
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
30
+ # License:: MIT
31
+ module Zold
32
+ # Thread pool
33
+ class ThreadPool
34
+ def initialize(title, log: Log::NULL)
35
+ @title = title
36
+ @log = log
37
+ @threads = []
38
+ @start = Time.now
39
+ end
40
+
41
+ # Run this code in many threads
42
+ def run(threads, set = (0..threads - 1).to_a)
43
+ raise "Number of threads #{threads} has to be positive" unless threads.positive?
44
+ raise 'Set of data can\'t be empty' if set.empty?
45
+ idx = Concurrent::AtomicFixnum.new
46
+ mutex = Mutex.new
47
+ list = set.dup
48
+ [threads, set.count].min.times do
49
+ add do
50
+ loop do
51
+ r = mutex.synchronize { list.pop }
52
+ break if r.nil?
53
+ yield(r, idx.increment - 1)
54
+ end
55
+ end
56
+ end
57
+ @threads.each(&:join)
58
+ @threads.clear
59
+ end
60
+
61
+ # Add a new thread
62
+ def add
63
+ raise 'Block must be given to start()' unless block_given?
64
+ latch = Concurrent::CountDownLatch.new(1)
65
+ thread = Thread.start do
66
+ VerboseThread.new(@log).run do
67
+ latch.count_down
68
+ yield
69
+ end
70
+ end
71
+ latch.wait
72
+ Thread.current.thread_variable_set(
73
+ :kids,
74
+ (Thread.current.thread_variable_get(:kids) || []) + [thread]
75
+ )
76
+ @threads << thread
77
+ end
78
+
79
+ # Kill them all immediately and close the pool
80
+ def kill
81
+ if @threads.empty?
82
+ @log.debug("Thread pool \"#{@title}\" terminated with no threads")
83
+ return
84
+ end
85
+ @log.info("Stopping \"#{@title}\" thread pool with #{@threads.count} threads: \
86
+ #{@threads.map { |t| "#{t.name}/#{t.status}" }.join(', ')}...")
87
+ start = Time.new
88
+ @threads.each do |t|
89
+ (t.thread_variable_get(:kids) || []).each(&:kill)
90
+ t.kill
91
+ raise "Failed to join the thread \"#{t.name}\" in \"#{@title}\" pool" unless t.join(0.1)
92
+ (Thread.current.thread_variable_get(:kids) || []).delete(t)
93
+ end
94
+ @log.info("Thread pool \"#{@title}\" terminated all threads in #{Age.new(start)}, \
95
+ it was alive for #{Age.new(@start)}: #{@threads.map { |t| "#{t.name}/#{t.status}" }.join(', ')}")
96
+ @threads.clear
97
+ end
98
+
99
+ # How many threads are in there
100
+ def count
101
+ @threads.count
102
+ end
103
+
104
+ # A thread with this name exists?
105
+ def exists?(name)
106
+ !@threads.find { |t| t.name == name }.nil?
107
+ end
108
+
109
+ # As a hash map
110
+ def to_json
111
+ @threads.map do |t|
112
+ {
113
+ name: t.name,
114
+ status: t.status,
115
+ alive: t.alive?,
116
+ vars: t.thread_variables.map { |v| { k: v, v: t.thread_variable_get(v) } }
117
+ }
118
+ end
119
+ end
120
+
121
+ # As a text
122
+ def to_s
123
+ @threads.map do |t|
124
+ [
125
+ "#{t.name}: status=#{t.status}; alive=#{t.alive?}",
126
+ 'Vars: ' + t.thread_variables.map { |v| "#{v}=\"#{t.thread_variable_get(v)}\"" }.join('; '),
127
+ t.backtrace.nil? ? 'NO BACKTRACE' : " #{t.backtrace.join("\n ")}"
128
+ ].join("\n")
129
+ end
130
+ end
131
+ end
132
+ end
data/lib/zold/version.rb CHANGED
@@ -25,7 +25,7 @@
25
25
  # Copyright:: Copyright (c) 2018 Yegor Bugayenko
26
26
  # License:: MIT
27
27
  module Zold
28
- VERSION = '0.18.6'
28
+ VERSION = '0.18.7'
29
29
  PROTOCOL = 2
30
30
  REPO = 'zold-io/zold'
31
31
  end
@@ -43,6 +43,7 @@ class TestRemote < Zold::Test
43
43
  zero = Zold::Score::ZERO
44
44
  stub_request(:get, "http://#{zero.host}:#{zero.port}/remotes").to_return(
45
45
  status: 200,
46
+ headers: {},
46
47
  body: {
47
48
  version: Zold::VERSION,
48
49
  score: zero.to_h,
@@ -63,6 +64,7 @@ class TestRemote < Zold::Test
63
64
  )
64
65
  stub_request(:get, 'https://rubygems.org/api/v1/versions/zold/latest.json').to_return(
65
66
  status: 200,
67
+ headers: {},
66
68
  body: '{"version": "0.0.0"}'
67
69
  )
68
70
  cmd = Zold::Remote.new(remotes: remotes, log: test_log)
data/test/test__helper.rb CHANGED
@@ -21,6 +21,7 @@
21
21
  # SOFTWARE.
22
22
 
23
23
  gem 'openssl'
24
+ require 'eventmachine'
24
25
  require 'openssl'
25
26
  require 'minitest/autorun'
26
27
  require 'minitest/hooks/test'
data/test/test_remotes.rb CHANGED
@@ -82,13 +82,46 @@ class TestRemotes < Zold::Test
82
82
  end
83
83
  end
84
84
 
85
- def test_iterates_them_all
85
+ def test_iterates_all_failures
86
+ Dir.mktmpdir do |dir|
87
+ file = File.join(dir, 'remotes')
88
+ FileUtils.touch(file)
89
+ remotes = Zold::Remotes.new(file: file)
90
+ 5.times { |i| remotes.add("0.0.0.#{i}", 9999) }
91
+ total = 0
92
+ remotes.iterate(Zold::Log::NULL) do
93
+ total += 1
94
+ raise 'Intended'
95
+ end
96
+ assert_equal(5, total)
97
+ end
98
+ end
99
+
100
+ def test_closes_threads_carefully
101
+ Dir.mktmpdir do |dir|
102
+ file = File.join(dir, 'remotes')
103
+ FileUtils.touch(file)
104
+ remotes = Zold::Remotes.new(file: file)
105
+ 5.times { |i| remotes.add("0.0.0.#{i}", 9999) }
106
+ total = 0
107
+ remotes.iterate(Zold::Log::NULL) do
108
+ sleep 0.25
109
+ total += 1
110
+ end
111
+ assert_equal(5, total)
112
+ end
113
+ end
114
+
115
+ def test_iterates_them_all_even_with_delays
86
116
  Dir.mktmpdir do |dir|
87
117
  remotes = Zold::Remotes.new(file: File.join(dir, 'rrr.csv'))
88
118
  remotes.clean
89
119
  5.times { |i| remotes.add("0.0.0.#{i}", 8080) }
90
120
  total = 0
91
- remotes.iterate(test_log) { total += 1 }
121
+ remotes.iterate(test_log) do
122
+ sleep 0.25
123
+ total += 1
124
+ end
92
125
  assert_equal(5, total)
93
126
  end
94
127
  end
@@ -0,0 +1,98 @@
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 'concurrent'
25
+ require_relative 'test__helper'
26
+ require_relative '../lib/zold/thread_pool'
27
+
28
+ # ThreadPool test.
29
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
30
+ # Copyright:: Copyright (c) 2018 Yegor Bugayenko
31
+ # License:: MIT
32
+ class TestThreadPool < Zold::Test
33
+ def test_closes_all_threads_right
34
+ pool = Zold::ThreadPool.new('test', log: test_log)
35
+ idx = Concurrent::AtomicFixnum.new
36
+ threads = 50
37
+ threads.times do
38
+ pool.add do
39
+ idx.increment
40
+ end
41
+ end
42
+ pool.kill
43
+ assert_equal(threads, idx.value)
44
+ end
45
+
46
+ def test_runs_in_many_threads
47
+ idx = Concurrent::AtomicFixnum.new
48
+ threads = 50
49
+ Zold::ThreadPool.new('test', log: test_log).run(threads) do
50
+ idx.increment
51
+ end
52
+ assert_equal(threads, idx.value)
53
+ end
54
+
55
+ def test_runs_with_index
56
+ idx = Concurrent::AtomicFixnum.new
57
+ indexes = Set.new
58
+ Zold::ThreadPool.new('test', log: test_log).run(10, %w[a b c]) do |_, i|
59
+ idx.increment
60
+ indexes << i
61
+ end
62
+ assert_equal(3, idx.value)
63
+ assert_equal('0 1 2', indexes.to_a.sort.join(' '))
64
+ end
65
+
66
+ def test_adds_and_stops
67
+ pool = Zold::ThreadPool.new('test', log: test_log)
68
+ pool.add do
69
+ sleep 60 * 60
70
+ end
71
+ pool.kill
72
+ end
73
+
74
+ def test_stops_stuck_threads
75
+ pool = Zold::ThreadPool.new('test', log: test_log)
76
+ pool.add do
77
+ loop do
78
+ # forever
79
+ end
80
+ end
81
+ pool.kill
82
+ end
83
+
84
+ def test_stops_empty_pool
85
+ pool = Zold::ThreadPool.new('test', log: test_log)
86
+ pool.kill
87
+ end
88
+
89
+ def test_prints_to_json
90
+ pool = Zold::ThreadPool.new('test', log: test_log)
91
+ assert(pool.to_json.is_a?(Array))
92
+ end
93
+
94
+ def test_prints_to_text
95
+ pool = Zold::ThreadPool.new('test', log: test_log)
96
+ assert(!pool.to_s.nil?)
97
+ end
98
+ end
data/zold.gemspec CHANGED
@@ -70,7 +70,6 @@ and suggests a different architecture for digital wallet maintenance.'
70
70
  s.add_runtime_dependency 'json', '2.1.0'
71
71
  s.add_runtime_dependency 'memory_profiler', '0.9.12'
72
72
  s.add_runtime_dependency 'openssl', '2.1.2'
73
- s.add_runtime_dependency 'parallel', '1.12.1'
74
73
  s.add_runtime_dependency 'rainbow', '3.0.0'
75
74
  s.add_runtime_dependency 'rake', '12.3.1' # has to stay here for Heroku
76
75
  s.add_runtime_dependency 'rubocop', '0.60.0' # has to stay here for Heroku
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zold
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.6
4
+ version: 0.18.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-12 00:00:00.000000000 Z
11
+ date: 2018-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backtrace
@@ -136,20 +136,6 @@ dependencies:
136
136
  - - '='
137
137
  - !ruby/object:Gem::Version
138
138
  version: 2.1.2
139
- - !ruby/object:Gem::Dependency
140
- name: parallel
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - '='
144
- - !ruby/object:Gem::Version
145
- version: 1.12.1
146
- type: :runtime
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - '='
151
- - !ruby/object:Gem::Version
152
- version: 1.12.1
153
139
  - !ruby/object:Gem::Dependency
154
140
  name: rainbow
155
141
  requirement: !ruby/object:Gem::Requirement
@@ -614,6 +600,7 @@ files:
614
600
  - lib/zold/size.rb
615
601
  - lib/zold/sync_wallets.rb
616
602
  - lib/zold/tax.rb
603
+ - lib/zold/thread_pool.rb
617
604
  - lib/zold/tree_wallets.rb
618
605
  - lib/zold/txn.rb
619
606
  - lib/zold/txns.rb
@@ -681,6 +668,7 @@ files:
681
668
  - test/test_size.rb
682
669
  - test/test_sync_wallets.rb
683
670
  - test/test_tax.rb
671
+ - test/test_thread_pool.rb
684
672
  - test/test_tree_wallets.rb
685
673
  - test/test_txn.rb
686
674
  - test/test_upgrades.rb
@@ -701,7 +689,7 @@ licenses:
701
689
  - MIT
702
690
  metadata: {}
703
691
  post_install_message: |-
704
- Thanks for installing Zold 0.18.6!
692
+ Thanks for installing Zold 0.18.7!
705
693
  Study our White Paper: https://papers.zold.io/wp.pdf
706
694
  Read our blog posts: https://blog.zold.io
707
695
  Try online wallet at: https://wts.zold.io
@@ -789,6 +777,7 @@ test_files:
789
777
  - test/test_size.rb
790
778
  - test/test_sync_wallets.rb
791
779
  - test/test_tax.rb
780
+ - test/test_thread_pool.rb
792
781
  - test/test_tree_wallets.rb
793
782
  - test/test_txn.rb
794
783
  - test/test_upgrades.rb