http_loader 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require 'openssl'
8
+ require 'async'
9
+ require 'async/semaphore'
10
+ require 'time'
11
+ require_relative 'client/config'
12
+ require_relative 'client/logger'
13
+ require_relative 'client/target_manager'
14
+ require_relative 'client/slowloris'
15
+ require_relative 'client/http_session'
16
+ require_relative 'client/error_handler'
17
+
18
+ module HttpLoader
19
+ # Master client class coordinating connection pools through Async Engine.
20
+ class Client
21
+ extend T::Sig
22
+ include ErrorHandler
23
+
24
+ sig { params(config: Config).void }
25
+ def initialize(config)
26
+ @config = config
27
+ @logger = T.let(Logger.new(config.verbose), Logger)
28
+ @target_manager = T.let(TargetManager.new(config), TargetManager)
29
+ @slow_sess = T.let(Slowloris.new(config, @logger), Slowloris)
30
+ @http_sess = T.let(HttpSession.new(config, @logger), HttpSession)
31
+ end
32
+
33
+ sig { void }
34
+ def start
35
+ log_startup_message
36
+ trap('INT') { exit(0) }
37
+ @logger.setup_files!
38
+
39
+ Async { |task| run_engine(task) }
40
+ ensure
41
+ @logger.flush_synchronously!
42
+ end
43
+
44
+ private
45
+
46
+ sig { params(task: T.untyped).void }
47
+ def run_engine(task)
48
+ logger_t = @logger.run_task(task)
49
+ sem = Async::Semaphore.new(@config.max_concurrent_connections, parent: task)
50
+
51
+ conn_tasks = Array.new(@config.connections) do |i|
52
+ perform_sleep(calc_ramp, task: task) if calc_ramp.positive?
53
+ sem.async { exec_conn(i) }
54
+ end
55
+
56
+ conn_tasks.each(&:wait)
57
+ logger_t.stop
58
+ end
59
+
60
+ sig { void }
61
+ def log_startup_message
62
+ puts "[Client] Starting #{@config.connections} #{@target_manager.protocol_label} connections to targeted urls..."
63
+ puts '[Client] Note: Output of individual pings is suppressed.' unless @config.verbose
64
+ end
65
+
66
+ sig { returns(Float) }
67
+ def calc_ramp
68
+ if @config.ramp_up.positive?
69
+ @config.ramp_up.to_f / @config.connections
70
+ elsif @config.connections_per_second.positive?
71
+ 1.0 / @config.connections_per_second
72
+ else
73
+ 0.0
74
+ end
75
+ end
76
+
77
+ sig { params(dur: Float, task: T.untyped).void }
78
+ def perform_sleep(dur, task: nil)
79
+ task ? task.sleep(dur) : sleep(dur)
80
+ end
81
+
82
+ sig { params(base: Float).returns(Float) }
83
+ def calc_sleep(base)
84
+ return base if @config.jitter.zero?
85
+
86
+ v = base * @config.jitter
87
+ [0.0, base + rand(-v..v)].max
88
+ end
89
+
90
+ sig { params(idx: Integer).void }
91
+ def exec_conn(idx)
92
+ loop do
93
+ run_session(idx, Time.now)
94
+ break unless @config.reopen_closed_connections
95
+
96
+ sleep(calc_sleep(@config.reopen_interval))
97
+ end
98
+ end
99
+
100
+ sig { params(idx: Integer, start_t: Time).void }
101
+ def run_session(idx, start_t)
102
+ ctx = @target_manager.context_for(idx)
103
+ uri = T.cast(ctx[:uri], URI::Generic)
104
+
105
+ start_http(uri, fetch_opts(idx, ctx)) do |http|
106
+ http.max_retries = 0 if http.respond_to?(:max_retries=)
107
+ @logger.info("[Client #{idx}] Connection established to #{uri.host}.")
108
+ dispatch_sess(idx, uri, http, start_t)
109
+ end
110
+ @logger.info("[Client #{idx}] Connection gracefully closed.")
111
+ rescue StandardError => e
112
+ handle_err(idx, e)
113
+ end
114
+
115
+ sig { params(idx: Integer, ctx: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
116
+ def fetch_opts(idx, ctx)
117
+ args = T.cast(ctx[:http_args], T::Hash[Symbol, T.untyped])
118
+ @target_manager.http_opts_for(idx, args)
119
+ end
120
+
121
+ sig { params(uri: URI::Generic, opts: T::Hash[Symbol, T.untyped], block: T.proc.params(arg: Net::HTTP).void).void }
122
+ def start_http(uri, opts, &block)
123
+ Net::HTTP.start(T.must(uri.host), uri.port, **opts, &block)
124
+ end
125
+
126
+ sig { params(idx: Integer, uri: URI::Generic, http: Net::HTTP, start_t: Time).void }
127
+ def dispatch_sess(idx, uri, http, start_t)
128
+ if @config.slowloris_delay.positive?
129
+ @slow_sess.run(idx, uri, http, start_t)
130
+ else
131
+ @http_sess.run(idx, uri, http, start_t)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,18 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module HttpLoader
7
+ class Harness
8
+ # Config parameters for Harness manager execution natively.
9
+ class Config < T::Struct
10
+ const :connections, Integer
11
+ const :target_urls, T::Array[String], default: []
12
+ const :use_https, T::Boolean, default: false
13
+ const :client_args, T::Array[String], default: []
14
+ const :export_json, T.nilable(String), default: nil
15
+ const :target_duration, Float, default: 0.0
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module HttpLoader
7
+ class Harness
8
+ # Formatter handles printing load test statistics dynamically and robustly.
9
+ module Formatter
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ requires_ancestor { HttpLoader::Harness }
14
+
15
+ sig { void }
16
+ def print_startup_banner
17
+ msg = if @config.target_urls.size > 1
18
+ "**MULTIPLE TARGETS** (#{@config.target_urls.size} URLs)."
19
+ elsif @config.target_urls.size == 1
20
+ "**EXTERNAL URL** #{@config.target_urls.first}."
21
+ elsif @config.use_https
22
+ '**HTTPS**.'
23
+ else
24
+ '**HTTP**.'
25
+ end
26
+ puts "[Harness] Starting test with #{@config.connections} connections to #{msg}"
27
+ end
28
+
29
+ sig { void }
30
+ def print_table_header
31
+ puts '[Harness] Monitoring resources (Press Ctrl+C to stop)...'
32
+ puts '-' * 125
33
+ puts 'Time (UTC) | Real Conns | Srv CPU/Thrds | Srv Mem | Srv Mem/Conn | ' \
34
+ 'Cli CPU/Thrds | Cli Mem | Cli Mem/Conn '
35
+ puts '-' * 125
36
+ end
37
+
38
+ sig { params(params: T::Hash[Symbol, T.untyped]).void }
39
+ def log_table_row(params)
40
+ puts format('%<t>-10s | %<ac>-11s | %<sc>-16s | %<sm>-14s | %<sk>-14s | %<cc>-16s | %<cm>-14s | %<ck>-14s',
41
+ t: params[:t], ac: params[:ac], sc: params[:sc], sm: params[:sm],
42
+ sk: params[:sk], cc: params[:cc], cm: params[:cm], ck: params[:ck])
43
+ end
44
+
45
+ sig { params(kilo: Float, connections: Integer, pid: T.nilable(Integer)).returns(String) }
46
+ def format_kb_conn(kilo, connections, pid)
47
+ return 'EXTERNAL' if pid.nil?
48
+ return 'N/A' if connections.zero? || connections.negative?
49
+
50
+ "#{(kilo / connections).round(2)} KB"
51
+ end
52
+
53
+ sig { params(active: Integer, c_cpu: String, c_th: Integer, c_m: String).void }
54
+ def print_combined_stats(active, c_cpu, c_th, c_m)
55
+ s_cpu, s_mem, s_th, s_conn = extract_server_stats
56
+ _cc, _cm, c_kb, _ct = @monitor.process_stats(@pm.client_pid)
57
+ c_conn = format_kb_conn(c_kb, active, @pm.client_pid)
58
+
59
+ log_table_row(
60
+ t: Time.now.utc.strftime('%H:%M:%S'), ac: active.to_s,
61
+ sc: "#{s_cpu}% / #{s_th}", sm: s_mem, sk: s_conn,
62
+ cc: "#{c_cpu}% / #{c_th}", cm: c_m, ck: c_conn
63
+ )
64
+ end
65
+
66
+ sig { returns([String, String, Integer, String]) }
67
+ def extract_server_stats
68
+ s_cpu, s_mem, s_kb, s_th = @monitor.process_stats(@pm.server_pid)
69
+ active_s = @monitor.count_established_connections(@pm.server_pid)
70
+ s_conn = format_kb_conn(s_kb, active_s, @pm.server_pid)
71
+ [s_cpu, s_mem, s_th, s_conn]
72
+ end
73
+
74
+ sig { returns([Integer, String, Integer, String]) }
75
+ def extract_client_stats
76
+ c_cpu, c_mem, _c_kb, c_th = @monitor.process_stats(@pm.client_pid)
77
+ active_c = @monitor.count_established_connections(@pm.client_pid)
78
+ [active_c, c_cpu, c_th, c_mem]
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,81 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module HttpLoader
7
+ class Harness
8
+ # Manages child process lifecycles natively and checks status dynamically.
9
+ class ProcessManager
10
+ extend T::Sig
11
+
12
+ sig { returns(T.nilable(Integer)) }
13
+ attr_reader :server_pid, :client_pid
14
+
15
+ sig { params(config: HttpLoader::Harness::Config).void }
16
+ def initialize(config)
17
+ @config = config
18
+ @server_pid = T.let(nil, T.nilable(Integer))
19
+ @client_pid = T.let(nil, T.nilable(Integer))
20
+ @log_dir = T.let(File.expand_path('../../../logs', __dir__), String)
21
+ end
22
+
23
+ sig { void }
24
+ def spawn_processes
25
+ FileUtils.mkdir_p(@log_dir)
26
+ spawn_server unless @config.target_urls.any?
27
+ spawn_client
28
+ end
29
+
30
+ sig { void }
31
+ def cleanup
32
+ begin
33
+ Process.kill('INT', T.must(@server_pid)) if @server_pid
34
+ rescue StandardError; nil
35
+ end
36
+ begin
37
+ Process.kill('INT', T.must(@client_pid)) if @client_pid
38
+ rescue StandardError; nil
39
+ end
40
+ end
41
+
42
+ sig { returns(T::Boolean) }
43
+ def missing_process?
44
+ return true if @client_pid && dead?(@client_pid)
45
+ return true if @server_pid && dead?(@server_pid)
46
+
47
+ false
48
+ end
49
+
50
+ private
51
+
52
+ sig { void }
53
+ def spawn_server
54
+ server_cmd = ['ruby', 'bin/http_loader', 'server']
55
+ server_cmd << '--https' if @config.use_https
56
+ @server_pid = Process.spawn(*server_cmd, out: File.join(@log_dir, 'server.log'),
57
+ err: File.join(@log_dir, 'server.err'))
58
+ puts "[Harness] Started server with PID #{@server_pid}"
59
+ sleep(2)
60
+ end
61
+
62
+ sig { void }
63
+ def spawn_client
64
+ client_cmd = ['ruby', 'bin/http_loader', 'client']
65
+ client_cmd += @config.client_args.empty? ? ["--connections_count=#{@config.connections}"] : @config.client_args
66
+
67
+ @client_pid = Process.spawn(*client_cmd, out: File.join(@log_dir, 'client.log'),
68
+ err: File.join(@log_dir, 'client.err'))
69
+ puts "[Harness] Started client with PID #{@client_pid}"
70
+ end
71
+
72
+ sig { params(pid: Integer).returns(T::Boolean) }
73
+ def dead?(pid)
74
+ Process.getpgid(pid)
75
+ false
76
+ rescue Errno::ESRCH
77
+ true
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,132 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'open3'
6
+
7
+ module HttpLoader
8
+ class Harness
9
+ # Monitors CPU, memory, and established connections for running processes.
10
+ class ResourceMonitor
11
+ extend T::Sig
12
+
13
+ sig { void }
14
+ def initialize
15
+ @cpu_prev = T.let({}, T::Hash[Integer, T::Hash[Symbol, T.untyped]])
16
+ end
17
+
18
+ sig { params(pid: T.nilable(Integer)).returns([String, String, Float, Integer]) }
19
+ def process_stats(pid)
20
+ return ['EXTERNAL', 'EXTERNAL', 0.0, 0] if pid.nil?
21
+
22
+ if File.exist?("/proc/#{pid}/stat") && File.exist?("/proc/#{pid}/statm")
23
+ native_linux_stats(pid)
24
+ else
25
+ fallback_ps_stats(pid)
26
+ end
27
+ rescue StandardError
28
+ fallback_ps_stats(pid)
29
+ end
30
+
31
+ sig { params(pid: T.nilable(Integer)).returns(Integer) }
32
+ def count_established_connections(pid)
33
+ return 0 if pid.nil?
34
+
35
+ if File.directory?("/proc/#{pid}/fd")
36
+ count_linux_connections(pid)
37
+ else
38
+ count_lsof_connections(pid)
39
+ end
40
+ rescue StandardError
41
+ 0
42
+ end
43
+
44
+ private
45
+
46
+ sig { params(pid: Integer).returns([String, String, Float, Integer]) }
47
+ def native_linux_stats(pid)
48
+ rss_kb = read_rss_kb(pid)
49
+ cpu_perc = read_cpu_perc(pid)
50
+ [cpu_perc.to_s, "#{(rss_kb / 1024.0).round(2)} MB", rss_kb, read_threads(pid)]
51
+ end
52
+
53
+ sig { params(pid: Integer).returns(Float) }
54
+ def read_rss_kb(pid)
55
+ rss_pages = T.must(File.read("/proc/#{pid}/statm").split[1]).to_i
56
+ (rss_pages * fetch_page_size) / 1024.0
57
+ end
58
+
59
+ sig { params(pid: Integer).returns(Float) }
60
+ def read_cpu_perc(pid)
61
+ stat_array = File.read("/proc/#{pid}/stat").split
62
+ calculate_cpu(pid, T.must(stat_array[13]).to_f + T.must(stat_array[14]).to_f)
63
+ end
64
+
65
+ sig { params(pid: Integer).returns(Integer) }
66
+ def read_threads(pid)
67
+ File.read("/proc/#{pid}/status").match(/Threads:\s+(\d+)/)&.[](1)&.to_i || 1
68
+ end
69
+
70
+ sig { returns(Integer) }
71
+ def fetch_page_size
72
+ return 4096 unless File.exist?('/usr/bin/getconf')
73
+
74
+ out, _s = Open3.capture2('getconf PAGE_SIZE')
75
+ out.to_i.positive? ? out.to_i : 4096
76
+ rescue StandardError
77
+ 4096
78
+ end
79
+
80
+ sig { params(pid: Integer, total_ticks: Float).returns(Float) }
81
+ def calculate_cpu(pid, total_ticks)
82
+ prev = @cpu_prev[pid]
83
+ now = Time.now.utc
84
+ cpu_perc = 0.0
85
+
86
+ if prev && (time_diff = now - prev[:time]).positive?
87
+ cpu_perc = (((total_ticks - prev[:ticks]) / 100.0) / time_diff * 100).round(1)
88
+ end
89
+
90
+ @cpu_prev[pid] = { ticks: total_ticks, time: now }
91
+ cpu_perc
92
+ end
93
+
94
+ sig { params(pid: Integer).returns([String, String, Float, Integer]) }
95
+ def fallback_ps_stats(pid)
96
+ out, _s = Open3.capture2('ps', '-o', '%cpu,rss', '-p', pid.to_s)
97
+ lines = out.strip.split("\n")
98
+ return ['N/A', 'N/A', 0.0, 0] if lines.size < 2
99
+
100
+ cpu, rss_kb_str = T.must(lines[1]).strip.split(/\s+/)
101
+ rss_kb_val = T.must(rss_kb_str).to_f
102
+
103
+ [T.must(cpu), "#{(rss_kb_val / 1024.0).round(2)} MB", rss_kb_val, fallback_threads_count(pid)]
104
+ rescue StandardError
105
+ ['N/A', 'N/A', 0.0, 0]
106
+ end
107
+
108
+ sig { params(pid: Integer).returns(Integer) }
109
+ def fallback_threads_count(pid)
110
+ out, _s = Open3.capture2("ps -M -p #{pid}")
111
+ [out.strip.split("\n").size - 1, 0].max
112
+ rescue StandardError
113
+ 1
114
+ end
115
+
116
+ sig { params(pid: Integer).returns(Integer) }
117
+ def count_linux_connections(pid)
118
+ Dir.glob("/proc/#{pid}/fd/*").count do |fd_path|
119
+ File.readlink(fd_path).start_with?('socket:[')
120
+ rescue StandardError
121
+ false
122
+ end
123
+ end
124
+
125
+ sig { params(pid: Integer).returns(Integer) }
126
+ def count_lsof_connections(pid)
127
+ out, _s = Open3.capture2("lsof -p #{pid} -n -P")
128
+ out.scan('ESTABLISHED').count
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,76 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'json'
6
+
7
+ module HttpLoader
8
+ class Harness
9
+ # Exports telemetry and logs bottlenecks asynchronously and synchronously at termination.
10
+ class Telemetry
11
+ extend T::Sig
12
+
13
+ sig { params(log_dir: String, export_json: T.nilable(String)).void }
14
+ def initialize(log_dir, export_json)
15
+ @log_dir = log_dir
16
+ @export_json = export_json
17
+ end
18
+
19
+ sig { params(peak_connections: Integer, start_time: Time).void }
20
+ def export!(peak_connections, start_time)
21
+ return unless @export_json
22
+
23
+ payload = generate_payload(peak_connections, start_time, count_bottlenecks(read_logs))
24
+ File.write(@export_json, JSON.generate(payload))
25
+ puts "[Harness] Telemetry JSON securely sinked to #{@export_json}."
26
+ end
27
+
28
+ sig { void }
29
+ def check_bottlenecks!
30
+ errors = build_bottleneck_messages(count_bottlenecks(read_logs))
31
+ puts format(' => BOTTLENECK ACTIVE: %s', errors.join(' | ')) if errors.any?
32
+ rescue StandardError
33
+ nil
34
+ end
35
+
36
+ private
37
+
38
+ sig { params(peak: Integer, start: Time, counts: T::Hash[Symbol, Integer]).returns(T::Hash[Symbol, T.untyped]) }
39
+ def generate_payload(peak, start, counts)
40
+ {
41
+ peak_connections: peak, test_duration_seconds: (Time.now.utc - start).round(2),
42
+ errors: { emfile: counts[:emfile], eaddrnotavail: counts[:eaddr_count], thread_limit: counts[:thread_errors] }
43
+ }
44
+ end
45
+
46
+ sig { returns(String) }
47
+ def read_logs
48
+ begin
49
+ File.read(File.join(@log_dir, 'client.log'))
50
+ rescue StandardError; ''
51
+ end + begin
52
+ File.read(File.join(@log_dir, 'client.err'))
53
+ rescue StandardError; ''
54
+ end
55
+ end
56
+
57
+ sig { params(log_text: String).returns(T::Hash[Symbol, Integer]) }
58
+ def count_bottlenecks(log_text)
59
+ {
60
+ emfile: log_text.scan('ERROR_EMFILE').size,
61
+ eaddr_count: log_text.scan('ERROR_EADDRNOTAVAIL').size,
62
+ thread_errors: log_text.scan('ERROR_THREADLIMIT').size
63
+ }
64
+ end
65
+
66
+ sig { params(counts: T::Hash[Symbol, Integer]).returns(T::Array[String]) }
67
+ def build_bottleneck_messages(counts)
68
+ errors = []
69
+ errors << "[OS FDs Limit: #{counts[:emfile]} EMFILE]" if counts[:emfile].positive?
70
+ errors << "[OS Ports Limit: #{counts[:eaddr_count]} EADDRNOTAVAIL]" if counts[:eaddr_count].positive?
71
+ errors << "[OS Thread Limit: #{counts[:thread_errors]} ThreadError]" if counts[:thread_errors].positive?
72
+ errors
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'fileutils'
6
+ require_relative 'harness/config'
7
+ require_relative 'harness/resource_monitor'
8
+ require_relative 'harness/telemetry'
9
+ require_relative 'harness/process_manager'
10
+ require_relative 'harness/formatter'
11
+
12
+ module HttpLoader
13
+ # Harness orchestrates the entire load testing lifecycle seamlessly and securely.
14
+ class Harness
15
+ extend T::Sig
16
+ include Formatter
17
+
18
+ sig { params(config: Config).void }
19
+ def initialize(config)
20
+ @config = config
21
+ @start_time = T.let(Time.now.utc, Time)
22
+ @peak_connections = T.let(0, Integer)
23
+ log_dir = T.let(File.expand_path('../../logs', __dir__), String)
24
+ @telemetry = T.let(Telemetry.new(log_dir, config.export_json), Telemetry)
25
+ @pm = T.let(ProcessManager.new(config), ProcessManager)
26
+ @monitor = T.let(ResourceMonitor.new, ResourceMonitor)
27
+ end
28
+
29
+ sig { void }
30
+ def start
31
+ $stdout.sync = true
32
+ print_startup_banner
33
+ bump_file_limits
34
+
35
+ run_lifecycle
36
+ end
37
+
38
+ private
39
+
40
+ sig { void }
41
+ def run_lifecycle
42
+ @pm.spawn_processes
43
+ trap('INT') do
44
+ puts "\n[Harness] Caught interrupt, cleaning up processes..."
45
+ @pm.cleanup
46
+ exit(0)
47
+ end
48
+ monitor_resources
49
+ ensure
50
+ @telemetry.export!(@peak_connections, @start_time)
51
+ @pm.cleanup
52
+ end
53
+
54
+ sig { void }
55
+ def monitor_resources
56
+ print_table_header
57
+ @start_time = Time.now.utc
58
+
59
+ loop do
60
+ break if duration_exceeded?
61
+
62
+ tick_result = tick_failed?
63
+ break if tick_result
64
+
65
+ @telemetry.check_bottlenecks!
66
+ sleep(2)
67
+ end
68
+ end
69
+
70
+ sig { returns(T::Boolean) }
71
+ def duration_exceeded?
72
+ elapsed = Time.now.utc - @start_time
73
+ return false unless @config.target_duration.positive? && elapsed >= @config.target_duration
74
+
75
+ puts "[Harness] Target duration mathematically reached (#{@config.target_duration}s). auto-shutdown."
76
+ true
77
+ end
78
+
79
+ sig { returns(T::Boolean) }
80
+ def tick_failed?
81
+ active_c, c_cpu, c_th, c_m = extract_client_stats
82
+ @peak_connections = [@peak_connections, active_c].max
83
+
84
+ return true if missing_socket?(active_c, c_cpu, c_th, c_m)
85
+ return true if @pm.missing_process?
86
+
87
+ print_combined_stats(active_c, c_cpu, c_th, c_m)
88
+ false
89
+ end
90
+
91
+ sig { void }
92
+ def bump_file_limits
93
+ Process.setrlimit(Process::RLIMIT_NOFILE, @config.connections + 1024)
94
+ rescue Errno::EPERM
95
+ puts '[Harness] Warning: Could not set RLIMIT_NOFILE automatically.'
96
+ end
97
+
98
+ sig { params(active: Integer, c_cpu: String, c_th: Integer, c_m: String).returns(T::Boolean) }
99
+ def missing_socket?(active, c_cpu, c_th, c_m)
100
+ return false unless @config.target_urls.any? && @peak_connections.positive? && active.zero?
101
+
102
+ log_table_row(
103
+ t: Time.now.utc.strftime('%H:%M:%S'), ac: active, sc: 'EXTERNAL', sm: 'N/A',
104
+ sk: 'N/A', cc: "#{c_cpu}% / #{c_th}T", cm: c_m, ck: 'N/A'
105
+ )
106
+ puts "\n[Harness] \u26A0\uFE0F EXTERNAL SERVER DISCONNECTED!"
107
+ true
108
+ end
109
+ end
110
+ end