hastci 0.1.0
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.
- checksums.yaml +7 -0
- data/.envrc +5 -0
- data/.rspec +3 -0
- data/.standard.yml +7 -0
- data/.zed/settings.json +24 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +17 -0
- data/bin/console +11 -0
- data/bin/hastci-rspec +5 -0
- data/bin/setup +8 -0
- data/devenv.lock +171 -0
- data/devenv.nix +23 -0
- data/devenv.yaml +6 -0
- data/lib/hastci/ack_worker.rb +146 -0
- data/lib/hastci/adapters/rspec/runner.rb +205 -0
- data/lib/hastci/api_client.rb +310 -0
- data/lib/hastci/api_error.rb +13 -0
- data/lib/hastci/claim_result.rb +27 -0
- data/lib/hastci/cli.rb +101 -0
- data/lib/hastci/config.rb +112 -0
- data/lib/hastci/configuration_error.rb +5 -0
- data/lib/hastci/error.rb +5 -0
- data/lib/hastci/error_collector.rb +18 -0
- data/lib/hastci/exit_codes.rb +13 -0
- data/lib/hastci/fatal_api_error.rb +5 -0
- data/lib/hastci/heartbeat.rb +84 -0
- data/lib/hastci/pact.rb +11 -0
- data/lib/hastci/queue_drained.rb +5 -0
- data/lib/hastci/retry_exhausted_error.rb +12 -0
- data/lib/hastci/retryable_error.rb +5 -0
- data/lib/hastci/session.rb +259 -0
- data/lib/hastci/task.rb +23 -0
- data/lib/hastci/task_buffer.rb +207 -0
- data/lib/hastci/task_result.rb +37 -0
- data/lib/hastci/version.rb +7 -0
- data/lib/hastci.rb +35 -0
- data/sig/hastci.rbs +4 -0
- data/spec/pacts/hastci_rspec-hastci_api.json +385 -0
- metadata +112 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module HastCI
|
|
6
|
+
class Config
|
|
7
|
+
DEFAULT_API_BASE_URL = "https://hastci.com/api"
|
|
8
|
+
DEFAULT_CLAIM_BATCH_SIZE = 10
|
|
9
|
+
DEFAULT_HEARTBEAT_INTERVAL = 15
|
|
10
|
+
DEFAULT_SEEDING_TIMEOUT = 300
|
|
11
|
+
DEFAULT_LOG_LEVEL = "INFO"
|
|
12
|
+
VALID_LOG_LEVELS = %w[DEBUG INFO WARN ERROR FATAL].freeze
|
|
13
|
+
|
|
14
|
+
private_constant :DEFAULT_API_BASE_URL, :DEFAULT_CLAIM_BATCH_SIZE, :DEFAULT_HEARTBEAT_INTERVAL,
|
|
15
|
+
:DEFAULT_SEEDING_TIMEOUT, :DEFAULT_LOG_LEVEL, :VALID_LOG_LEVELS
|
|
16
|
+
|
|
17
|
+
attr_reader :api_key, :api_base_url, :api_max_retries,
|
|
18
|
+
:run_key, :worker_id, :commit_sha,
|
|
19
|
+
:claim_batch_size, :heartbeat_interval, :seeding_timeout, :log_level, :debug
|
|
20
|
+
|
|
21
|
+
def self.load_from_env(env)
|
|
22
|
+
if env["GITHUB_ACTIONS"] == "true"
|
|
23
|
+
from_github_actions(env)
|
|
24
|
+
else
|
|
25
|
+
from_generic_ci(env)
|
|
26
|
+
end
|
|
27
|
+
rescue KeyError => e
|
|
28
|
+
raise ConfigurationError, "Missing required env var: #{e.key}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.from_github_actions(env)
|
|
32
|
+
run_id = env.fetch("GITHUB_RUN_ID")
|
|
33
|
+
run_attempt = env.fetch("GITHUB_RUN_ATTEMPT")
|
|
34
|
+
commit_sha = env.fetch("GITHUB_SHA")
|
|
35
|
+
node_index = env.fetch("CI_NODE_INDEX")
|
|
36
|
+
|
|
37
|
+
new(
|
|
38
|
+
api_key: env.fetch("HASTCI_API_KEY"),
|
|
39
|
+
api_base_url: env.fetch("HASTCI_API_URL", DEFAULT_API_BASE_URL),
|
|
40
|
+
api_max_retries: env["HASTCI_API_MAX_RETRIES"]&.to_i,
|
|
41
|
+
run_key: "gha-#{run_id}-#{run_attempt}-#{commit_sha}",
|
|
42
|
+
worker_id: "gha-worker-#{node_index}",
|
|
43
|
+
commit_sha: commit_sha,
|
|
44
|
+
claim_batch_size: env.fetch("HASTCI_CLAIM_BATCH_SIZE", DEFAULT_CLAIM_BATCH_SIZE).to_i,
|
|
45
|
+
heartbeat_interval: env.fetch("HASTCI_HEARTBEAT_INTERVAL", DEFAULT_HEARTBEAT_INTERVAL).to_i,
|
|
46
|
+
seeding_timeout: env.fetch("HASTCI_SEEDING_TIMEOUT", DEFAULT_SEEDING_TIMEOUT).to_i,
|
|
47
|
+
log_level: env.fetch("HASTCI_LOG_LEVEL", DEFAULT_LOG_LEVEL),
|
|
48
|
+
debug: env["HASTCI_DEBUG"] == "true"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.from_generic_ci(env)
|
|
53
|
+
worker_id = env["HASTCI_WORKER_ID"] ||
|
|
54
|
+
(env["CI_NODE_INDEX"] && "worker-#{env["CI_NODE_INDEX"]}") ||
|
|
55
|
+
"worker-#{SecureRandom.hex(4)}"
|
|
56
|
+
|
|
57
|
+
new(
|
|
58
|
+
api_key: env.fetch("HASTCI_API_KEY"),
|
|
59
|
+
api_base_url: env.fetch("HASTCI_API_URL", DEFAULT_API_BASE_URL),
|
|
60
|
+
api_max_retries: env["HASTCI_API_MAX_RETRIES"]&.to_i,
|
|
61
|
+
run_key: env.fetch("HASTCI_RUN_KEY"),
|
|
62
|
+
worker_id: worker_id,
|
|
63
|
+
commit_sha: env["HASTCI_COMMIT_SHA"],
|
|
64
|
+
claim_batch_size: env.fetch("HASTCI_CLAIM_BATCH_SIZE", DEFAULT_CLAIM_BATCH_SIZE).to_i,
|
|
65
|
+
heartbeat_interval: env.fetch("HASTCI_HEARTBEAT_INTERVAL", DEFAULT_HEARTBEAT_INTERVAL).to_i,
|
|
66
|
+
seeding_timeout: env.fetch("HASTCI_SEEDING_TIMEOUT", DEFAULT_SEEDING_TIMEOUT).to_i,
|
|
67
|
+
log_level: env.fetch("HASTCI_LOG_LEVEL", DEFAULT_LOG_LEVEL),
|
|
68
|
+
debug: env["HASTCI_DEBUG"] == "true"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private_class_method :from_github_actions, :from_generic_ci
|
|
73
|
+
|
|
74
|
+
def initialize(
|
|
75
|
+
api_key:,
|
|
76
|
+
api_base_url:,
|
|
77
|
+
api_max_retries:,
|
|
78
|
+
run_key:,
|
|
79
|
+
worker_id:,
|
|
80
|
+
commit_sha:,
|
|
81
|
+
claim_batch_size:,
|
|
82
|
+
heartbeat_interval:,
|
|
83
|
+
seeding_timeout:,
|
|
84
|
+
log_level:,
|
|
85
|
+
debug: false
|
|
86
|
+
)
|
|
87
|
+
raise ConfigurationError, "worker_id cannot be blank" if worker_id.nil? || worker_id.empty?
|
|
88
|
+
raise ConfigurationError, "claim_batch_size must be positive" if claim_batch_size <= 0
|
|
89
|
+
raise ConfigurationError, "heartbeat_interval must be positive" if heartbeat_interval <= 0
|
|
90
|
+
raise ConfigurationError, "seeding_timeout must be positive" if seeding_timeout <= 0
|
|
91
|
+
|
|
92
|
+
@api_key = api_key
|
|
93
|
+
@api_base_url = api_base_url
|
|
94
|
+
@api_max_retries = api_max_retries
|
|
95
|
+
@run_key = run_key
|
|
96
|
+
@worker_id = worker_id
|
|
97
|
+
@commit_sha = commit_sha
|
|
98
|
+
@claim_batch_size = claim_batch_size
|
|
99
|
+
@heartbeat_interval = heartbeat_interval
|
|
100
|
+
@seeding_timeout = seeding_timeout
|
|
101
|
+
@log_level = normalize_log_level(log_level)
|
|
102
|
+
@debug = debug
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def normalize_log_level(level)
|
|
108
|
+
normalized = level.to_s.upcase
|
|
109
|
+
VALID_LOG_LEVELS.include?(normalized) ? normalized : DEFAULT_LOG_LEVEL
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/hastci/error.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HastCI
|
|
4
|
+
class ErrorCollector
|
|
5
|
+
def initialize
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
@errors = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def report(error)
|
|
11
|
+
@mutex.synchronize { @errors << error unless @errors.any? }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def first_error
|
|
15
|
+
@mutex.synchronize { @errors.first }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HastCI
|
|
4
|
+
class Heartbeat
|
|
5
|
+
SHUTDOWN_TIMEOUT = 5
|
|
6
|
+
MAX_CONSECUTIVE_FAILURES = 5
|
|
7
|
+
|
|
8
|
+
private_constant :SHUTDOWN_TIMEOUT, :MAX_CONSECUTIVE_FAILURES
|
|
9
|
+
|
|
10
|
+
def initialize(api_client:, error_collector: nil, interval: 15)
|
|
11
|
+
@api_client = api_client
|
|
12
|
+
@error_collector = error_collector
|
|
13
|
+
@interval = interval
|
|
14
|
+
@running = false
|
|
15
|
+
@thread = nil
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@stop_condition = ConditionVariable.new
|
|
18
|
+
@consecutive_failures = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
return if @running
|
|
24
|
+
|
|
25
|
+
@running = true
|
|
26
|
+
@thread = Thread.new { run_loop }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stop
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
return unless @running
|
|
33
|
+
|
|
34
|
+
@running = false
|
|
35
|
+
@stop_condition.signal
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@thread.join(SHUTDOWN_TIMEOUT)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def running?
|
|
42
|
+
@mutex.synchronize { @running }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def run_loop
|
|
48
|
+
while running? && !fatal_error
|
|
49
|
+
send_heartbeat
|
|
50
|
+
wait_for_next_heartbeat
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def wait_for_next_heartbeat
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
return unless @running
|
|
57
|
+
|
|
58
|
+
@stop_condition.wait(@mutex, @interval)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fatal_error
|
|
63
|
+
@error_collector&.first_error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def send_heartbeat
|
|
67
|
+
@api_client.heartbeat
|
|
68
|
+
@consecutive_failures = 0
|
|
69
|
+
rescue RetryExhaustedError, FatalApiError => e
|
|
70
|
+
@error_collector&.report(e)
|
|
71
|
+
HastCI.logger.error("Heartbeat failed: #{e.message}")
|
|
72
|
+
rescue RetryableError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
|
|
73
|
+
Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
|
|
74
|
+
@consecutive_failures += 1
|
|
75
|
+
HastCI.logger.warn("Heartbeat failed (#{@consecutive_failures}/#{MAX_CONSECUTIVE_FAILURES}): #{e.class} - #{e.message}")
|
|
76
|
+
|
|
77
|
+
if @consecutive_failures >= MAX_CONSECUTIVE_FAILURES
|
|
78
|
+
error = FatalApiError.new("Heartbeat failed #{MAX_CONSECUTIVE_FAILURES} consecutive times")
|
|
79
|
+
@error_collector&.report(error)
|
|
80
|
+
HastCI.logger.error("Heartbeat: max consecutive failures reached")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/hastci/pact.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HastCI
|
|
4
|
+
class RetryExhaustedError < ApiError
|
|
5
|
+
attr_reader :original_error
|
|
6
|
+
|
|
7
|
+
def initialize(message, status_code: nil, response_body: nil, original_error: nil)
|
|
8
|
+
@original_error = original_error
|
|
9
|
+
super(message, status_code: status_code, response_body: response_body)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HastCI
|
|
4
|
+
class Session
|
|
5
|
+
DEFAULT_POLL_INTERVAL = 0.5
|
|
6
|
+
DEFAULT_BUFFER_MIN_SIZE = 3
|
|
7
|
+
DEFAULT_BUFFER_MAX_SIZE = 10
|
|
8
|
+
DEFAULT_SEEDING_POLL_INTERVAL = 1.0
|
|
9
|
+
|
|
10
|
+
STOP_REASONS = %i[user_interrupt server_cancelled].freeze
|
|
11
|
+
|
|
12
|
+
private_constant :DEFAULT_POLL_INTERVAL, :DEFAULT_BUFFER_MIN_SIZE, :DEFAULT_BUFFER_MAX_SIZE,
|
|
13
|
+
:DEFAULT_SEEDING_POLL_INTERVAL
|
|
14
|
+
|
|
15
|
+
def self.run(config:, **options)
|
|
16
|
+
session = new(config: config, **options)
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
yield session
|
|
20
|
+
ensure
|
|
21
|
+
begin
|
|
22
|
+
cleanup_result = session.cleanup
|
|
23
|
+
ensure
|
|
24
|
+
session.shutdown!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
cleanup_result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(
|
|
32
|
+
config:,
|
|
33
|
+
api_client: nil,
|
|
34
|
+
ack_worker: nil,
|
|
35
|
+
task_buffer: nil,
|
|
36
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
37
|
+
buffer_min_size: DEFAULT_BUFFER_MIN_SIZE,
|
|
38
|
+
buffer_max_size: DEFAULT_BUFFER_MAX_SIZE,
|
|
39
|
+
sleeper: Kernel.method(:sleep),
|
|
40
|
+
seeding_poll_interval: DEFAULT_SEEDING_POLL_INTERVAL,
|
|
41
|
+
error_collector: ErrorCollector.new
|
|
42
|
+
)
|
|
43
|
+
@config = config
|
|
44
|
+
@api_client = api_client || ApiClient.new(config: config)
|
|
45
|
+
@ack_worker = ack_worker
|
|
46
|
+
@task_buffer = task_buffer
|
|
47
|
+
@poll_interval = poll_interval
|
|
48
|
+
@buffer_min_size = buffer_min_size
|
|
49
|
+
@buffer_max_size = buffer_max_size
|
|
50
|
+
@sleeper = sleeper
|
|
51
|
+
@seeding_poll_interval = seeding_poll_interval
|
|
52
|
+
@error_collector = error_collector
|
|
53
|
+
|
|
54
|
+
@run_id = nil
|
|
55
|
+
@status = nil
|
|
56
|
+
@role = nil
|
|
57
|
+
@heartbeat = nil
|
|
58
|
+
|
|
59
|
+
@stop_reason = nil
|
|
60
|
+
@stop_mutex = Mutex.new
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :run_id, :status, :role, :error_collector, :config
|
|
64
|
+
|
|
65
|
+
def first_error
|
|
66
|
+
@error_collector.first_error
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def request_stop!(reason)
|
|
70
|
+
raise ArgumentError, "Invalid stop reason: #{reason}" unless STOP_REASONS.include?(reason)
|
|
71
|
+
|
|
72
|
+
should_stop_buffer = false
|
|
73
|
+
@stop_mutex.synchronize do
|
|
74
|
+
return if @stop_reason
|
|
75
|
+
|
|
76
|
+
@stop_reason = reason
|
|
77
|
+
should_stop_buffer = user_initiated_stop?(reason)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@task_buffer&.stop if should_stop_buffer
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def stopping?
|
|
84
|
+
@stop_mutex.synchronize { !@stop_reason.nil? }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stop_reason
|
|
88
|
+
@stop_mutex.synchronize { @stop_reason }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def cancelled?
|
|
92
|
+
stop_reason == :server_cancelled
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def start!
|
|
96
|
+
connect! unless @run_id
|
|
97
|
+
start_heartbeat!
|
|
98
|
+
|
|
99
|
+
unless seeder?
|
|
100
|
+
wait_for_ready!
|
|
101
|
+
return if stopping?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
start_task_buffer!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def connect!
|
|
108
|
+
run_state = @api_client.init_run
|
|
109
|
+
|
|
110
|
+
@run_id = run_state.fetch(:run_id)
|
|
111
|
+
@status = run_state.fetch(:status)
|
|
112
|
+
@role = run_state.fetch(:role)
|
|
113
|
+
|
|
114
|
+
HastCI.logger.info("[HastCI] Connected: run_id=#{@run_id} role=#{@role}")
|
|
115
|
+
|
|
116
|
+
@ack_worker ||= AckWorker.new(api_client: @api_client, error_collector: @error_collector)
|
|
117
|
+
@ack_worker.start
|
|
118
|
+
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def start_task_buffer!
|
|
123
|
+
return if stopping?
|
|
124
|
+
|
|
125
|
+
@task_buffer ||= build_task_buffer
|
|
126
|
+
@task_buffer.start
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def wait_for_ready!
|
|
130
|
+
return unless @status == :seeding
|
|
131
|
+
|
|
132
|
+
HastCI.logger.info("[HastCI] Waiting for seeding (timeout: #{@config.seeding_timeout}s)...")
|
|
133
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @config.seeding_timeout
|
|
134
|
+
|
|
135
|
+
while @status == :seeding
|
|
136
|
+
return if stopping?
|
|
137
|
+
|
|
138
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
139
|
+
raise FatalApiError, "Timed out waiting for seeding to complete (#{@config.seeding_timeout}s). " \
|
|
140
|
+
"The seeder may have failed."
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
run_state = @api_client.run_status(run_id: @run_id)
|
|
144
|
+
@status = run_state.fetch(:status)
|
|
145
|
+
|
|
146
|
+
break unless @status == :seeding
|
|
147
|
+
|
|
148
|
+
@sleeper.call(@seeding_poll_interval)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
HastCI.logger.info("[HastCI] Seeding complete, status=#{@status}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def seeder?
|
|
155
|
+
role == :seeder
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def seed(tasks:)
|
|
159
|
+
ensure_connected!
|
|
160
|
+
|
|
161
|
+
HastCI.logger.info("[HastCI] Seeding #{tasks.size} tasks...")
|
|
162
|
+
@api_client.seed(run_id: run_id, tasks: tasks)
|
|
163
|
+
HastCI.logger.info("[HastCI] Seeding complete")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def each_task
|
|
167
|
+
return enum_for(:each_task) unless block_given?
|
|
168
|
+
|
|
169
|
+
ensure_connected!
|
|
170
|
+
|
|
171
|
+
loop do
|
|
172
|
+
return if stopping?
|
|
173
|
+
|
|
174
|
+
check_for_fatal_error!
|
|
175
|
+
|
|
176
|
+
task = @task_buffer.next_task
|
|
177
|
+
return if task.nil?
|
|
178
|
+
|
|
179
|
+
yield task
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def ack(result)
|
|
184
|
+
ensure_connected!
|
|
185
|
+
|
|
186
|
+
@ack_worker.enqueue(result)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def flush_acks!(timeout: 10)
|
|
190
|
+
return true unless @ack_worker
|
|
191
|
+
|
|
192
|
+
@ack_worker.flush(timeout: timeout)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def disconnect!
|
|
196
|
+
@task_buffer&.stop
|
|
197
|
+
@ack_worker&.stop
|
|
198
|
+
@api_client.disconnect!
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def start_heartbeat!
|
|
202
|
+
return if @heartbeat
|
|
203
|
+
|
|
204
|
+
@heartbeat = Heartbeat.new(
|
|
205
|
+
api_client: @api_client,
|
|
206
|
+
interval: @config.heartbeat_interval,
|
|
207
|
+
error_collector: @error_collector
|
|
208
|
+
)
|
|
209
|
+
@heartbeat.start
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def cleanup
|
|
213
|
+
flush_ok = flush_acks!(timeout: 10)
|
|
214
|
+
HastCI.logger.info("[HastCI] Cleanup: flush_ok=#{flush_ok}")
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
flush_ok: flush_ok,
|
|
218
|
+
first_error: first_error,
|
|
219
|
+
stop_reason: stop_reason
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def shutdown!
|
|
224
|
+
@heartbeat&.stop
|
|
225
|
+
disconnect!
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def build_task_buffer
|
|
231
|
+
TaskBuffer.new(
|
|
232
|
+
min_size: @buffer_min_size,
|
|
233
|
+
max_size: @buffer_max_size,
|
|
234
|
+
fetcher: lambda { |limit|
|
|
235
|
+
batch_size = [limit, @config.claim_batch_size].min
|
|
236
|
+
@api_client.claim(batch: batch_size)
|
|
237
|
+
},
|
|
238
|
+
error_collector: @error_collector,
|
|
239
|
+
poll_interval: @poll_interval,
|
|
240
|
+
on_cancelled: -> { request_stop!(:server_cancelled) }
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def check_for_fatal_error!
|
|
245
|
+
error = @error_collector.first_error
|
|
246
|
+
raise error if error
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def ensure_connected!
|
|
250
|
+
return if run_id
|
|
251
|
+
|
|
252
|
+
raise HastCI::Error, "Session is not connected. Call #connect! first."
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def user_initiated_stop?(reason)
|
|
256
|
+
reason == :user_interrupt
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
data/lib/hastci/task.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HastCI
|
|
4
|
+
class Task
|
|
5
|
+
attr_reader :id, :name
|
|
6
|
+
|
|
7
|
+
def initialize(id:, name:)
|
|
8
|
+
@id = id
|
|
9
|
+
@name = name
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
other.is_a?(Task) && other.id == id && other.name == name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
alias_method :eql?, :==
|
|
18
|
+
|
|
19
|
+
def hash
|
|
20
|
+
[id, name].hash
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|