class Zold::Farm
Public Class Methods
Makes an instance of a farm. There should be only farm in the entire application, but you can, of course, start as many of them as necessary for the purpose of unit testing.
cache
is the file where the farm will keep all the scores it manages to find. If the file is absent, it will be created, together with the necessary parent directories.
lifetime
is the amount of seconds for a score to live in the farm, by default it's the entire day, since the Score expires in 24 hours; can be decreased for the purpose of unit testing.
# File lib/zold/node/farm.rb, line 61 def initialize(invoice, cache = File.join(Dir.pwd, 'farm'), log: Log::NULL, farmer: Farmers::Plain.new, lifetime: 24 * 60 * 60, strength: Score::STRENGTH) @log = log @cache = File.expand_path(cache) @invoice = invoice @pipeline = Queue.new @farmer = farmer @threads = ThreadPool.new('farm', log: log) @lifetime = lifetime @strength = strength end
Public Instance Methods
Returns the list of best scores the farm managed to find up to now. The list is NEVER empty, even if the farm has just started. If it's empty, it's definitely a bug. If the farm is just fresh start, the list will contain a single score with a zero value.
# File lib/zold/node/farm.rb, line 77 def best load end
Starts a farm, all threads, and yields the block provided. You are supposed to use it only with the block:
Farm.new.start('example.org', 4096) do |farm| score = farm.best[0] # Everything else... end
The farm will stop all its threads and close all resources safely right after the block provided exists.
# File lib/zold/node/farm.rb, line 110 def start(host, port, threads: Concurrent.processor_count) raise 'Block is required for the farm to start' unless block_given? @log.info('Zero-threads farm won\'t score anything!') if threads.zero? if best.empty? @log.info("No scores found in the cache at #{@cache}") else @log.info("#{best.size} scores pre-loaded from #{@cache}, the best is: #{best[0]}") end (1..threads).map do |t| @threads.add do Thread.current.thread_variable_set(:tid, t.to_s) Endless.new("f#{t}", log: @log).run do cycle(host, port, threads) end end end unless threads.zero? ready = false @threads.add do Endless.new('cleanup', log: @log).run do cleanup(host, port, threads) ready = true sleep(1) end end loop { break if ready } end if threads.zero? cleanup(host, port, threads) @log.info("Farm started with no threads (there will be no score) at #{host}:#{port}") else @log.info("Farm started with #{@threads.count} threads (one for cleanup) \ at #{host}:#{port}, strength is #{@strength}") end begin yield(self) ensure @threads.kill end end
Renders the Farm
into JSON to show for the end-user in front.rb.
# File lib/zold/node/farm.rb, line 91 def to_json { threads: @threads.to_json, pipeline: @pipeline.size, best: best.map(&:to_mnemo).join(', '), farmer: @farmer.class.name } end
# File lib/zold/node/farm.rb, line 81 def to_text [ "Current time: #{Time.now.utc.iso8601}", "Ruby processes: #{`ps ax | grep zold | wc -l`}", JSON.pretty_generate(to_json), @threads.to_s ].flatten.join("\n\n") end
Private Instance Methods
# File lib/zold/node/farm.rb, line 153 def cleanup(host, port, threads) scores = load before = scores.map(&:value).max.to_i save(host, port, threads, [Score.new(host: host, port: port, invoice: @invoice, strength: @strength)]) scores = load free = scores.reject { |s| @threads.exists?(s.to_mnemo) } @pipeline << free[0] if @pipeline.size.zero? && !free.empty? after = scores.map(&:value).max.to_i return unless before != after && !after.zero? @log.debug("#{Thread.current.name}: best score of #{scores.count} is #{scores[0].reduced(4)}") end
# File lib/zold/node/farm.rb, line 165 def cycle(host, port, threads) s = [] loop do begin s << @pipeline.pop(true) rescue ThreadError sleep(0.25) end s.compact! break unless s.empty? end s = s[0] return unless s.valid? return unless s.host == host return unless s.port == port return unless s.strength >= @strength Thread.current.name = s.to_mnemo Thread.current.thread_variable_set(:start, Time.now.utc.iso8601) score = @farmer.up(s) @log.debug("New score discovered: #{score}") if @strength > 4 save(host, port, threads, [score]) cleanup(host, port, threads) end
# File lib/zold/node/farm.rb, line 208 def load return [] unless File.exist?(@cache) Futex.new(@cache).open(false) { |f| IO.readlines(f, "\n") }.reject(&:empty?).map do |t| Score.parse(t) rescue StandardError => e @log.error(Backtrace.new(e).to_s) nil end.compact end
# File lib/zold/node/farm.rb, line 189 def save(host, port, threads, list = []) scores = load + list period = @lifetime / [threads, 1].max body = scores.select(&:valid?) .reject(&:expired?) .reject { |s| s.strength < @strength } .select { |s| s.host == host } .select { |s| s.port == port } .select { |s| s.invoice == @invoice } .sort_by(&:value) .reverse .uniq(&:time) .uniq { |s| (s.age / period).round } .map(&:to_s) .uniq .join("\n") Futex.new(@cache).open { |f| IO.write(f, body) } end