class Zold::Node
NODE command
Public Class Methods
new(wallets:, remotes:, copies:, log: Log::NULL)
click to toggle source
# File lib/zold/commands/node.rb, line 68 def initialize(wallets:, remotes:, copies:, log: Log::NULL) @remotes = remotes @copies = copies @log = log @wallets = wallets end
Public Instance Methods
run(args = [])
click to toggle source
# File lib/zold/commands/node.rb, line 75 def run(args = []) opts = Slop.parse(args, help: true, suppress_errors: true) do |o| o.banner = 'Usage: zold node [options]' o.string '--invoice', 'The invoice you want to collect money to or the wallet ID', required: true o.integer '--port', "TCP port to open for the Net (default: #{Remotes::PORT})", default: Remotes::PORT o.integer '--bind-port', "TCP port to listen on (default: #{Remotes::PORT})", default: Remotes::PORT o.string '--host', 'Host name (will attempt to auto-detect it, if not specified)' o.integer '--strength', "The strength of the score (default: #{Score::STRENGTH})", default: Score::STRENGTH o.integer '--threads', "How many threads to use for scores finding (default: #{[Concurrent.processor_count / 2, 2].max})", default: [Concurrent.processor_count / 2, 2].max o.bool '--dump-errors', 'Make HTTP front-end errors visible in the log (false by default)', default: false o.bool '--standalone', 'Never communicate with other nodes (mostly for testing)', default: false o.bool '--ignore-score-weakness', 'Ignore score weakness of incoming requests and register those nodes anyway', default: false o.bool '--tolerate-edges', 'Don\'t fail if only "edge" (not "master" ones) nodes accepted/have the wallet', default: false o.integer '--tolerate-quorum', 'The minimum number of nodes required for a successful fetch (default: 4)', default: 4 o.boolean '--nohup', 'Run it in background, rebooting when a higher version is available in the network', default: false o.string '--nohup-command', 'The command to run in server "nohup" mode (default: "gem install zold")', default: 'gem install zold' o.string '--nohup-log', 'The file to log output into (default: zold.log)', default: 'zold.log' o.integer '--nohup-log-truncate', 'The maximum amount of bytes to keep in the file, and truncate it in half if it grows bigger', default: 1024 * 1024 o.string '--halt-code', 'The value of HTTP query parameter "halt," which will cause the front-end immediate termination', default: '' o.integer '--trace-length', 'Maximum length of the trace to keep in memory (default: 4096)', default: 4096 o.string '--save-pid', 'The file to save process ID into right after start (only in NOHUP mode)' o.bool '--never-reboot', 'Don\'t reboot when a new version shows up in the network', default: false o.bool '--routine-immediately', 'Run all routines immediately, without waiting between executions (for testing mostly)', default: false o.bool '--no-cache', 'Skip caching of front JSON pages (will seriously slow down, mostly useful for testing)', default: false o.boolean '--skip-audit', 'Don\'t report audit information to the console every minute', default: false o.boolean '--skip-reconnect', 'Don\'t reconnect to the network every minute (for testing)', default: false o.boolean '--not-hungry', 'Don\'t do hugry pulling of missed nodes (mostly for testing)', default: false o.bool '--allow-spam', 'Don\'t filter the incoming spam via PUT requests (duplicate wallets)', default: false o.bool '--ignore-empty-remotes', 'Don\'t fail if the list of remotes is empty (for testing mostly)', default: false o.bool '--skip-oom', 'Skip Out Of Memory check and never exit, no matter how much RAM is consumed', default: false o.integer '--oom-limit', "Maximum amount of memory we can consume, quit if we take more than that, in Mb (default: #{oom_limit})", default: oom_limit o.integer '--queue-limit', 'The maximum number of wallets to be accepted via PUSH and stored in the queue (default: 256)', default: 256 o.bool '--skip-gc', 'Don\'t run garbage collector and never remove any wallets from the disk', default: false o.integer '--gc-age', 'Maximum time in seconds to keep an empty and unused wallet on the disk', default: 60 * 60 * 24 * 10 o.string '--expose-version', "The version of the software to expose in JSON (default: #{VERSION})", default: VERSION o.string '--private-key', 'The location of RSA private key (default: ~/.ssh/id_rsa)', default: '~/.ssh/id_rsa' o.string '--network', "The name of the network (default: #{Wallet::MAINET})", default: Wallet::MAINET o.integer '--nohup-max-cycles', 'Maximum amount of nohup re-starts (-1 by default, which means forever)', default: -1 o.string '--home', "Home directory (default: #{Dir.pwd})", default: Dir.pwd o.bool '--no-metronome', 'Don\'t run the metronome', default: false o.bool '--disable-push', 'Prohibit all PUSH requests', default: false o.bool '--disable-fetch', 'Prohibit all FETCH requests', default: false o.string '--alias', 'The alias of the node (default: host:port)' o.string '--farmer', 'The name of the farmer, e.g. "plain", "spawn", "fork" (default: "plain")', default: 'plain' o.bool '--help', 'Print instructions' end if opts.help? @log.info(opts.to_s) return end raise '--invoice is mandatory' unless opts['invoice'] if opts['nohup'] if @remotes.all.empty? && !opts['standalone'] && !opts['ignore-empty-remotes'] raise 'There are no remote nodes in the list and you are not running in --standalone mode; the node won\'t connect to the network like that; try to do "zold remote reset" first' end pid = nohup(opts) IO.write(opts['save-pid'], pid) if opts['save-pid'] @log.debug("Process ID #{pid} saved into \"#{opts['save-pid']}\"") @log.info(pid) return end @log = Trace.new(@log, opts['trace-length']) Front.set(:log, @log) Front.set(:logger, @log) Front.set(:trace, @log) Front.set(:nohup_log, opts['nohup-log']) if opts['nohup-log'] Front.set(:protocol, Zold::PROTOCOL) Front.set(:logging, @log.debug?) home = File.expand_path(opts['home']) Front.set(:home, home) @log.info("Time: #{Time.now.utc.iso8601}; CPUs: #{Concurrent.processor_count}") @log.info("Home directory: #{home}") @log.info("Ruby version: #{RUBY_VERSION}/#{RUBY_PLATFORM}") @log.info("Zold gem version: #{Zold::VERSION}") @log.info("Zold protocol version: #{Zold::PROTOCOL}") @log.info("Network ID: #{opts['network']}") @log.info('Front caching is disabled via --no-cache') if opts['no-cache'] host = opts[:host] || ip port = opts[:port] address = "#{host}:#{port}".downcase @log.info("Node location: #{address}") @log.info("Local address: http://127.0.0.1:#{opts['bind-port']}/") @log.info("Remote nodes (#{@remotes.all.count}): \ #{@remotes.all.map { |r| "#{r[:host]}:#{r[:port]}" }.join(', ')}") @log.info("Wallets at: #{@wallets.path}") if opts['standalone'] @remotes = Remotes::Empty.new @log.info('Running in standalone mode! (will never talk to other remotes)') elsif @remotes.exists?(host, port) Remote.new(remotes: @remotes).run(['remote', 'remove', host, port.to_s]) @log.info("Removed current node (#{address}) from list of remotes") end if File.exist?(@copies) FileUtils.rm_rf(@copies) @log.info("Directory #{@copies} deleted") end wts = @wallets if opts['not-hungry'] @log.info('Hungry pulling disabled because of --not-hungry') else hungry = ThreadPool.new('hungry', log: @log) wts = HungryWallets.new(@wallets, @remotes, @copies, hungry, log: @log, network: opts['network']) end Front.set(:zache, Zache.new(dirty: true)) Front.set(:wallets, wts) Front.set(:remotes, @remotes) Front.set(:copies, @copies) Front.set(:address, address) Front.set(:root, home) ledger = File.join(home, 'ledger.csv') Front.set(:ledger, ledger) Front.set(:opts, opts) Front.set(:dump_errors, opts['dump-errors']) Front.set(:port, opts['bind-port']) async_dir = File.join(home, '.zoldata/async-entrance') FileUtils.mkdir_p(async_dir) Front.set(:async_dir, async_dir) journal_dir = File.join(home, '.zoldata/journal') FileUtils.mkdir_p(journal_dir) Front.set(:journal_dir, journal_dir) Front.set(:node_alias, node_alias(opts, address)) entrance = SafeEntrance.new( NoSpamEntrance.new( NoDupEntrance.new( AsyncEntrance.new( SpreadEntrance.new( SyncEntrance.new( Entrance.new( wts, JournaledPipeline.new( Pipeline.new( @remotes, @copies, address, ledger: ledger, network: opts['network'] ), journal_dir ), log: @log ), File.join(home, '.zoldata/sync-entrance'), log: @log ), wts, @remotes, address, log: @log, ignore_score_weakeness: opts['ignore-score-weakness'], tolerate_edges: opts['tolerate-edges'] ), async_dir, log: @log, queue_limit: opts['queue-limit'] ), wts, log: @log ), period: opts['allow-spam'] ? 0 : 60 * 60, log: @log ), network: opts['network'] ) entrance.start do |ent| Front.set(:entrance, ent) farm = Farm.new( invoice(opts), File.join(home, 'farm'), log: @log, farmer: farmer(opts), strength: opts[:strength] ) farm.start(host, opts[:port], threads: opts[:threads]) do |f| Front.set(:farm, f) metronome(f, opts, host, port).start do |metronome| Front.set(:metronome, metronome) @log.info("Starting up the web front at http://#{host}:#{opts[:port]}...") Front.run! @log.info("The web front stopped at http://#{host}:#{opts[:port]}") end end end hungry.kill unless opts['not-hungry'] @log.info('Thanks for helping Zold network!') end
Private Instance Methods
exec(cmd, nohup_log)
click to toggle source
Returns exit code
# File lib/zold/commands/node.rb, line 374 def exec(cmd, nohup_log) start = Time.now Open3.popen2e({ 'MALLOC_ARENA_MAX' => '2' }, cmd) do |stdin, stdout, thr| nohup_log.print("Started process ##{thr.pid} from process ##{Process.pid}: #{cmd}\n") stdin.close until stdout.eof? begin line = stdout.gets rescue IOError => e line = Backtrace.new(e).to_s end nohup_log.print(line) end nohup_log.print("Nothing else left to read from ##{thr.pid}\n") code = thr.value.to_i nohup_log.print("Exit code of process ##{thr.pid} is #{code}, was alive for #{Age.new(start)}: #{cmd}\n") code end end
farmer(opts)
click to toggle source
# File lib/zold/commands/node.rb, line 357 def farmer(opts) case opts['farmer'].downcase.strip when 'plain' @log.debug('"Plain" farmer is used, only one CPU core will be utilized') Farmers::Plain.new when 'fork' @log.debug('"Fork" farmer is used') Farmers::Fork.new(log: @log) when 'spawn' @log.debug('"Spawn" farmer is used') Farmers::Spawn.new(log: @log) else raise "Farmer name is not recognized: #{opts['farmer']}" end end
invoice(opts)
click to toggle source
# File lib/zold/commands/node.rb, line 336 def invoice(opts) invoice = opts['invoice'] unless invoice.include?('@') require_relative 'invoice' invoice = Invoice.new(wallets: @wallets, remotes: @remotes, copies: @copies, log: @log).run( ['invoice', invoice, "--network=#{Shellwords.escape(opts['network'])}"] + ["--tolerate-quorum=#{Shellwords.escape(opts['tolerate-quorum'])}"] + (opts['tolerate-edges'] ? ['--tolerate-edges'] : []) ) end invoice end
ip()
click to toggle source
# File lib/zold/commands/node.rb, line 472 def ip addr = Socket.ip_address_list.detect do |i| i.ipv4? && !i.ipv4_loopback? && !i.ipv4_multicast? && !i.ipv4_private? end raise 'Can\'t detect your IP address, you have to specify it in --host' if addr.nil? addr.ip_address end
metronome(farm, opts, host, port)
click to toggle source
# File lib/zold/commands/node.rb, line 432 def metronome(farm, opts, host, port) metronome = Metronome.new(@log) if opts['no-metronome'] @log.info("Metronome hasn't been started because of --no-metronome") return metronome end if opts['skip-gc'] @log.info('Garbage collection is disabled because of --skip-gc') else require_relative 'routines/gc' metronome.add(Routines::Gc.new(opts, @wallets, log: @log)) end if opts['skip-audit'] @log.info('Audit is disabled because of --skip-audit') else require_relative 'routines/audit' metronome.add(Routines::Audit.new(opts, @wallets, log: @log)) end unless opts['standalone'] if opts['skip-reconnect'] @log.info('Reconnect is disabled because of --skip-reconnect') else require_relative 'routines/reconnect' metronome.add(Routines::Reconnect.new(opts, @remotes, farm, log: @log)) end end require_relative 'routines/spread' metronome.add(Routines::Spread.new(opts, @wallets, @remotes, @copies, log: @log)) require_relative 'routines/retire' metronome.add(Routines::Retire.new(opts, log: @log)) if @remotes.master?(host, port) require_relative 'routines/reconcile' metronome.add(Routines::Reconcile.new(opts, @wallets, @remotes, @copies, "#{host}:#{port}", log: @log)) else @log.info('This is not master, no need to reconcile') end @log.info('Metronome started (use --no-metronome to disable it)') metronome end
node_alias(opts, address)
click to toggle source
# File lib/zold/commands/node.rb, line 349 def node_alias(opts, address) a = opts[:alias] || address unless a.eql?(address) || a =~ /^[A-Za-z0-9]{4,16}$/ raise "Alias should be a 4 to 16 char long alphanumeric string: #{a}" end a end
nohup(opts)
click to toggle source
# File lib/zold/commands/node.rb, line 394 def nohup(opts) pid = fork do nohup_log = NohupLog.new(opts['nohup-log'], opts['nohup-log-truncate']) Signal.trap('HUP') do nohup_log.print("Received HUP, ignoring...\n") end Signal.trap('TERM') do nohup_log.print("Received TERM, terminating...\n") exit(-1) end myself = File.expand_path($PROGRAM_NAME) args = ARGV.delete_if { |a| a.start_with?('--home') || a == '--nohup' } cycle = 0 loop do begin code = exec("#{myself} #{args.join(' ')}", nohup_log) raise "Exit code is #{code}" if code != 0 exec(opts['nohup-command'], nohup_log) rescue StandardError => e nohup_log.print(Backtrace.new(e).to_s) if cycle < opts['nohup-max-cycles'] nohup_log.print("Let's wait for a minutes, because of the exception...") sleep(60) end end next if opts['nohup-max-cycles'].negative? cycle += 1 if cycle > opts['nohup-max-cycles'] nohup_log.print("There are no more nohup cycles left, after the cycle no.#{cycle}") break end nohup_log.print("Going for nohup cycle no.#{cycle}") end end Process.detach(pid) pid end
oom_limit()
click to toggle source
# File lib/zold/commands/node.rb, line 480 def oom_limit require 'total' Total::Mem.new.bytes / (1024 * 1024) / 2 rescue Total::CantDetect => e @log.error(e.message) 512 end