class Zold::Remote

Remote command

Public Class Methods

new(remotes:, farm: Farm::Empty.new, log: Log::NULL) click to toggle source
# File lib/zold/commands/remote.rb, line 50
def initialize(remotes:, farm: Farm::Empty.new, log: Log::NULL)
  @remotes = remotes
  @farm = farm
  @log = log
end

Public Instance Methods

run(args = []) click to toggle source
# File lib/zold/commands/remote.rb, line 56
    def run(args = [])
      opts = Slop.parse(args, help: true, suppress_errors: true) do |o|
        o.banner = "Usage: zold remote <command> [options]
Available commands:
    #{Rainbow('remote show').green}
      Show all registered remote nodes
    #{Rainbow('remote clean').green}
      Remove all registered remote nodes
    #{Rainbow('remote reset').green}
      Restore it back to the default list of nodes
    #{Rainbow('remote masters').green}
      Add all \"master\" nodes to the list
    #{Rainbow('remote add').green} host [port]
      Add a new remote node
    #{Rainbow('remote remove').green} host [port]
      Remove the remote node
    #{Rainbow('remote elect').green}
      Pick a random remote node as a target for a bonus awarding
    #{Rainbow('remote trim').green}
      Remove the least reliable nodes
    #{Rainbow('remote select [options]').green}
      Select the most reliable N nodes
    #{Rainbow('remote update').green}
      Check each registered remote node for availability
Available options:"
        o.integer '--tolerate',
          "Maximum level of errors we are able to tolerate (default: #{Remotes::TOLERANCE})",
          default: Remotes::TOLERANCE
        o.bool '--ignore-score-weakness',
          'Don\'t complain when their score is too weak',
          default: false
        o.bool '--ignore-score-value',
          'Don\'t complain when their score is too small',
          default: false
        o.array '--ignore-node',
          'Ignore this node and never add it to the list',
          default: []
        o.bool '--ignore-if-exists',
          'Ignore the node while adding if it already exists in the list',
          default: false
        o.bool '--ignore-masters',
          'Don\'t elect master nodes, only edges',
          default: false
        o.bool '--masters-too',
          'Give no priviledges to masters, treat them as other nodes',
          default: false
        o.integer '--min-score',
          "The minimum score required for winning the election (default: #{Tax::EXACT_SCORE})",
          default: Tax::EXACT_SCORE
        o.integer '--max-winners',
          'The maximum amount of election winners the election (default: 1)',
          default: 1
        o.integer '--retry',
          'How many times to retry each node before reporting a failure (default: 2)',
          default: 2
        o.bool '--skip-ping',
          'Don\'t ping back the node when adding it (not recommended)',
          default: false
        o.bool '--ignore-ping',
          'Don\'t fail if ping fails, just report the problem in the log',
          default: false
        o.integer '--depth',
          'The amount of update cycles to run, in order to fetch as many nodes as possible (default: 2)',
          default: 2
        o.integer '--threads',
          "How many threads to use for updating, electing, etc. (default: #{[Concurrent.processor_count, 4].min})",
          default: [Concurrent.processor_count, 4].min
        o.string '--network',
          "The name of the network we work in (default: #{Wallet::MAINET})",
          required: true,
          default: Wallet::MAINET
        o.bool '--reboot',
          'Exit if any node reports version higher than we have',
          default: false
        # @todo #292:30min Group options by subcommands
        #  Having all the options in one place _rather than grouping them by subcommands_
        #  makes the help totally misleading and hard to read.
        #  Not all the options are valid for every command - that's the key here.
        #  The option below (`--max-nodes`) is an example.
        #  **Next actions:**
        #  - Implement the suggestion above.
        #  - Remove note from the --max-nodes option saying that it applies to the select
        #  subcommand only.
        o.integer '--max-nodes',
          "Number of nodes to limit to (default: #{Remotes::MAX_NODES})",
          default: Remotes::MAX_NODES
        o.bool '--help', 'Print instructions'
      end
      mine = Args.new(opts, @log).take || return
      command = mine[0]
      raise "A command is required, try 'zold remote --help'" unless command
      case command
      when 'show'
        show
      when 'clean'
        clean
      when 'reset'
        reset(opts)
      when 'masters'
        masters(opts)
      when 'add'
        add(mine[1], mine[2] ? mine[2].to_i : Remotes::PORT, opts)
      when 'remove'
        remove(mine[1], mine[2] ? mine[2].to_i : Remotes::PORT, opts)
      when 'elect'
        elect(opts)
      when 'trim'
        trim(opts)
      when 'update'
        update(opts)
      when 'select'
        select(opts)
      else
        raise "Unknown command '#{command}'"
      end
    end

Private Instance Methods

add(host, port, opts) click to toggle source
# File lib/zold/commands/remote.rb, line 206
def add(host, port, opts)
  if opts['ignore-node'].include?("#{host}:#{port}")
    @log.debug("#{host}:#{port} won't be added since it's in the --ignore-node list")
    return
  end
  if opts['ignore-if-exists'] && @remotes.exists?(host, port)
    @log.debug("#{host}:#{port} already exists, won't add because of --ignore-if-exists")
    return
  end
  return unless ping(host, port, opts)
  if @remotes.exists?(host, port)
    @log.debug("#{host}:#{port} already exists among #{@remotes.all.count} others")
  else
    @remotes.add(host, port)
    @log.debug("#{host}:#{port} added to the list, #{@remotes.all.count} total")
  end
end
clean() click to toggle source
# File lib/zold/commands/remote.rb, line 188
def clean
  before = @remotes.all.count
  @remotes.clean
  @log.debug("All #{before} remote nodes deleted")
end
elect(opts) click to toggle source

Returns an array of Zold::Score

# File lib/zold/commands/remote.rb, line 230
def elect(opts)
  scores = []
  @remotes.iterate(@log, farm: @farm, threads: opts['threads']) do |r|
    uri = '/'
    res = r.http(uri).get
    r.assert_code(200, res)
    json = JsonPage.new(res.body, uri).to_hash
    score = Score.parse_json(json['score'])
    r.assert_valid_score(score)
    r.assert_score_ownership(score)
    r.assert_score_strength(score) unless opts['ignore-score-weakness']
    r.assert_score_value(score, opts['min-score']) unless opts['ignore-score-value']
    if r.master? && opts['--ignore-masters']
      @log.debug("#{r} ignored, it's a master node")
      next
    end
    scores << score
  end
  scores = scores.sample(opts['max-winners'])
  if scores.empty?
    @log.info("No winners elected out of #{@remotes.all.count} remotes")
  else
    scores.each { |s| @log.info("Elected: #{s.reduced(4)}") }
  end
  scores.sort_by(&:value).reverse
end
masters(opts) click to toggle source
# File lib/zold/commands/remote.rb, line 199
def masters(opts)
  @remotes.masters do |host, port|
    !opts['ignore-node'].include?("#{host}:#{port}")
  end
  @log.debug("Masters nodes were added to the list, #{@remotes.all.count} total")
end
ping(host, port, opts) click to toggle source
# File lib/zold/commands/remote.rb, line 370
def ping(host, port, opts)
  return true if opts['skip-ping']
  res = Http.new(uri: "http://#{host}:#{port}/version", network: opts['network']).get
  return true if res.status == 200
  raise "The node #{host}:#{port} is not responding, #{res.status}:#{res.status_line}" unless opts['ignore-ping']
  @log.error("The node #{host}:#{port} is not responding but we --ignore-ping, #{res.status}:#{res.status_line}")
  false
end
reboot(r, json, opts) click to toggle source
# File lib/zold/commands/remote.rb, line 329
    def reboot(r, json, opts)
      return unless json['repo'] == Zold::REPO
      mine = Semantic::Version.new(VERSION)
      if mine < Semantic::Version.new(json['version'])
        if opts['reboot']
          @log.info("#{r}: their version #{json['version']} is higher than mine #{VERSION}, reboot! \
(use --never-reboot to avoid this from happening)")
          terminate
        end
        @log.debug("#{r}: their version #{json['version']} is higher than mine #{VERSION}, \
it's recommended to reboot, but I don't do it because of --never-reboot")
      end
      if mine < Semantic::Version.new(Zold::Gem.new.last_version)
        if opts['reboot']
          @log.info("#{r}: the version of the gem is higher than mine #{VERSION}, reboot! \
(use --never-reboot to avoid this from happening)")
          terminate
        end
        @log.debug("#{r}: gem version is higher than mine #{VERSION}, \
it's recommended to reboot, but I don't do it because of --never-reboot")
      end
      @log.debug("#{r}: gem version is lower or equal to mine #{VERSION}, no need to reboot")
    end
remove(host, port, _) click to toggle source
# File lib/zold/commands/remote.rb, line 224
def remove(host, port, _)
  @remotes.remove(host, port)
  @log.debug("#{host}:#{port} removed from the list, #{@remotes.all.count} total")
end
reset(opts) click to toggle source
# File lib/zold/commands/remote.rb, line 194
def reset(opts)
  clean
  masters(opts)
end
select(opts) click to toggle source
# File lib/zold/commands/remote.rb, line 353
    def select(opts)
      @remotes.all.shuffle.sort_by { |r| r[:errors] }.reverse.each_with_index do |r, idx|
        next if idx < opts['max-nodes']
        next if r[:master] && !opts['masters-too']
        @remotes.remove(r[:host], r[:port])
        @log.debug("Remote #{r[:host]}:#{r[:port]}/#{r[:score]}/#{r[:errors]}e removed from the list")
      end
      @log.info("#{@remotes.all.count} best remote nodes were selected to stay in the list \
(#{@remotes.all.count { |r| r[:master] }} masters)")
    end
show() click to toggle source
# File lib/zold/commands/remote.rb, line 175
def show
  @remotes.all.each do |r|
    score = Rainbow("/#{r[:score]}").color(r[:score].positive? ? :green : :red)
    @log.info(
      [
        "#{r[:host]}:#{r[:port]}#{score}",
        r[:errors].positive? ? " #{r[:errors]} errors" : '',
        r[:master] ? ' [master]' : ''
      ].join
    )
  end
end
terminate() click to toggle source
# File lib/zold/commands/remote.rb, line 364
def terminate
  @log.info("All threads before exit: #{Thread.list.map { |t| "#{t.name}/#{t.status}" }.join(', ')}")
  require_relative '../node/front'
  Front.stop!
end
trim(opts) click to toggle source
# File lib/zold/commands/remote.rb, line 257
    def trim(opts)
      all = @remotes.all
      all.each do |r|
        next if r[:errors] <= opts['tolerate']
        @remotes.remove(r[:host], r[:port]) if !opts['masters-too'] || !r[:master]
        @log.debug("#{r[:host]}:#{r[:port]} removed because of #{r[:errors]} errors (over #{opts['tolerate']})")
      end
      @log.info("The list of #{all.count} remotes trimmed down to #{@remotes.all.count} nodes \
(#{@remotes.all.count { |r| r[:master] }} masters)")
    end
update(opts) click to toggle source
# File lib/zold/commands/remote.rb, line 268
    def update(opts)
      st = Time.now
      seen = Set.new
      capacity = []
      opts['depth'].times do
        @remotes.iterate(@log, farm: @farm, threads: opts['threads']) do |r|
          if seen.include?(r.to_mnemo)
            @log.debug("#{r} seen already, won't check again")
            next
          end
          seen << r.to_mnemo
          start = Time.now
          update_one(r, opts) do |json, score|
            r.assert_valid_score(score)
            r.assert_score_ownership(score)
            r.assert_score_strength(score) unless opts['ignore-score-weakness']
            @remotes.rescore(score.host, score.port, score.value)
            reboot(r, json, opts)
            json['all'].each do |s|
              next if @remotes.exists?(s['host'], s['port'])
              add(s['host'], s['port'], opts)
            end
            capacity << { host: score.host, port: score.port, count: json['all'].count }
            @log.debug("#{r}: the score is #{Rainbow(score.value).green} (#{json['version']}) in #{Age.new(start)}")
          end
        end
      end
      max_capacity = capacity.map { |c| c[:count] }.max || 0
      capacity.each do |c|
        @remotes.error(c[:host], c[:port]) if c[:count] < max_capacity
      end
      total = @remotes.all.size
      if total.zero?
        @log.info("The list of remotes is #{Rainbow('empty').red}, run 'zold remote reset'!")
      else
        @log.info("There are #{total} known remotes \
(#{@remotes.all.count { |r| r[:master] }} masters) \
with the overall score of \
#{@remotes.all.map { |r| r[:score] }.inject(&:+)}, after update in #{Age.new(st)}")
      end
    end
update_one(r, opts) { |json, score| ... } click to toggle source
# File lib/zold/commands/remote.rb, line 310
def update_one(r, opts)
  attempt = 0
  begin
    uri = '/remotes'
    res = r.http(uri).get
    r.assert_code(200, res)
    json = JsonPage.new(res.body, uri).to_hash
    score = Score.parse_json(json['score'])
    yield json, score
  rescue JsonPage::CantParse, Score::CantParse, RemoteNode::CantAssert => e
    attempt += 1
    if attempt < opts['retry']
      @log.debug("#{r} failed to read, trying again (attempt no.#{attempt}): #{e.message}")
      retry
    end
    raise e
  end
end