zold 0.18.6 → 0.18.7

Sign up to get free protection for your applications and to get access to all the features.
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