github.com/anycable/anycable-go@v1.5.1/features/runner.rb (about)

     1  # frozen_string_literal: true
     2  
     3  retried = false
     4  require "bundler/inline"
     5  
     6  begin
     7    gemfile(retried, quiet: true) do
     8      source "https://rubygems.org"
     9  
    10      gem "childprocess", "~> 4.1"
    11      gem "jwt"
    12      gem "activesupport", "~> 7.0.0"
    13    end
    14  rescue
    15    raise if retried
    16    retried = true
    17    retry
    18  end
    19  
    20  require "socket"
    21  require "time"
    22  require "json"
    23  
    24  require "active_support/message_verifier"
    25  
    26  class BenchRunner
    27    LOG_LEVEL_TO_NUM = {
    28      error: 0,
    29      warn: 1,
    30      info: 2,
    31      debug: 3
    32    }.freeze
    33  
    34    # CI machines could be slow
    35    RUN_TIMEOUT = (ENV["CI"] || ENV["CODESPACES"]) ? 120 : 30
    36  
    37    def initialize
    38      @processes = {}
    39      @pipes = {}
    40      @log_level = ENV["DEBUG"] == "true" ? LOG_LEVEL_TO_NUM[:debug] : LOG_LEVEL_TO_NUM[:info]
    41    end
    42  
    43    def load(script, path)
    44      instance_eval script, path, 0
    45    end
    46  
    47    def launch(name, cmd, env: {}, debug: ENV["DEBUG"] == "true", capture_output: false)
    48      log(:info) { "Launching background process: #{cmd}"}
    49  
    50      process = ChildProcess.build(*cmd.split(/\s+/))
    51      # set process environment variables
    52      process.environment.merge!(env)
    53  
    54      if capture_output
    55        r, w = IO.pipe
    56        process.io.stdout = w
    57        process.io.stderr = w
    58        pipes[name] = {r: r, w: w}
    59      else
    60        process.io.inherit! if debug
    61      end
    62  
    63      process.detach = true
    64  
    65      processes[name] = process
    66      process.start
    67    end
    68  
    69    def run(name, cmd, timeout: RUN_TIMEOUT)
    70      log(:info) { "Running command: #{cmd}" }
    71  
    72      r, w = IO.pipe
    73  
    74      process = ChildProcess.build(*cmd.split(/\s+/))
    75      process.io.stdout = w
    76      process.io.stderr = w
    77  
    78      processes[name] = process
    79      pipes[name] = {r: r, w: w}
    80  
    81      process.start
    82  
    83      w.close
    84  
    85      begin
    86        process.poll_for_exit(timeout)
    87      rescue ChildProcess::TimeoutError
    88        process.stop
    89        log(:debug) { "Output:\n#{stdout(name)}" }
    90        fail "Command expected to finish in #{timeout}s but is still running"
    91      end
    92  
    93      log(:info) { "Finished" }
    94      log(:debug) { "Output:\n#{stdout(name)}" }
    95    end
    96  
    97    def gops(pid)
    98      log(:info) { "Fetching Go process #{pid} stats... "}
    99  
   100      `gops stats #{pid}`.lines.each_with_object({}) do |line, acc|
   101        key, val = line.split(/:\s+/)
   102        acc[key] = val.to_i
   103      end
   104    end
   105  
   106    def wait_tcp(port, host: "127.0.0.1", timeout: 10)
   107      log(:info) { "Waiting for TCP server to start at #{port}" }
   108  
   109      listening = false
   110      while timeout > 0
   111        begin
   112          Socket.tcp(host, port, connect_timeout: 1).close
   113          listening = true
   114          log(:info) { "TCP server is listening at #{port}" }
   115          break
   116        rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
   117        end
   118  
   119        Kernel.sleep 0.5
   120        timeout -= 0.5
   121      end
   122  
   123      fail "No server is listening at #{port}" unless listening
   124    end
   125  
   126    def pid(name)
   127      processes.fetch(name).pid
   128    end
   129  
   130    def stop(name)
   131      processes.fetch(name).stop
   132      pipes[name]&.fetch(:w)&.close
   133    end
   134  
   135    def stdout(name)
   136      pipes.fetch(name).then do |pipe|
   137        pipe[:data] ||= pipe[:r].read
   138      end
   139    end
   140  
   141    def sleep(time)
   142      log(:info) { "Wait for #{time}s" }
   143      Kernel.sleep time
   144    end
   145  
   146    def shutdown
   147      processes.each_value do |process|
   148        process.stop
   149      end
   150    end
   151  
   152    def retrying(delay: 1, attempts: 2, &block)
   153      begin
   154        block.call
   155      rescue => e
   156        attempts -= 1
   157        raise if attempts <= 0
   158  
   159        log(:info) { "Retrying after error: #{e.message}" }
   160  
   161        sleep delay
   162        retry
   163      end
   164    end
   165  
   166    private
   167  
   168    attr_reader :processes, :pipes, :log_level
   169  
   170    def log(level, &block)
   171      return unless log_level >= LOG_LEVEL_TO_NUM[level]
   172  
   173      $stdout.puts "[#{level}] [#{Time.now.iso8601}]  #{block.call}"
   174    end
   175  end
   176  
   177  if ARGF
   178    begin
   179      scripts = ARGF.each.group_by { ARGF.filename }
   180      scripts.each do |filename, lines|
   181        puts "\n--- RUN: #{filename} ---\n\n" if scripts.size > 1
   182        script = lines.join
   183        runner = BenchRunner.new
   184  
   185        begin
   186          runner.load(script, filename)
   187          puts "All OK 👍"
   188        rescue => e
   189          $stderr.puts e.message
   190          exit(1)
   191        ensure
   192          runner.shutdown
   193        end
   194      end
   195    rescue Errno::ENOENT
   196      puts "\n--- NOTHINIG TO EXECUTE ---\n\n"
   197    end
   198  end