Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ gem but are what an operator runs against their own image.

## next / unreleased

### HotCell::Client

#### Added

* The `perform.hot_cell` event carries `stderr`, the failure's captured stream, beside `signal`. A subscriber can log the diagnosis of a crash — `libgomp: Thread creation failed` — on the same line as its cause. Before, that text survived only in the exception's message, so a failure that was discarded rather than retried lost it. The field is already bounded by `Failure.sanitize`; it is text a tool wrote while processing a hostile file, so write it to a log field and interpolate it nowhere else.

### HotCell::Core

#### Fixed
Expand Down
6 changes: 6 additions & 0 deletions hotcell-client/lib/hot_cell/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ def verify_output(response, outputs)
# the code filed every fsize kill as transient. `permanent` is the Failure's own answer, so no
# subscriber re-derives it.
#
# `stderr` is the diagnosis of a crash — `libgomp: Thread creation failed` — and the event is the only
# place a subscriber can log it: the exception's message carries it too, but only a retry logs that,
# and a discarded failure loses it. It is text a tool wrote while processing a hostile file, bounded
# by `Failure.sanitize` on the way in, and a subscriber should write it to a log field and nowhere else.
#
# A subscriber's own duration minus perform_ms is transport plus queueing, and those want separate
# metrics: a rising perform_ms means the work got more expensive, and a rising difference means the
# cell is saturated.
Expand All @@ -171,6 +176,7 @@ def publish(event, cell, response, inputs, outputs)
event[:code] = failure&.code
event[:cause] = failure&.cause
event[:signal] = failure&.signal
event[:stderr] = failure&.stderr
event[:permanent] = failure&.permanent?
event[:bytes_in] = byte_count(inputs)
event[:bytes_out] = byte_count(outputs)
Expand Down
18 changes: 16 additions & 2 deletions hotcell-client/test/classification_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ def test_the_event_carries_the_cause_and_the_verdict_so_a_subscriber_can_classif
assert_equal true, event[:permanent]
end

# A crash's diagnosis is the tool's stderr — `libgomp: Thread creation failed` — and the event is the
# only place a subscriber can log it, since a discarded failure never reaches the retry line.
def test_the_event_carries_the_stderr_so_a_subscriber_can_log_the_diagnosis_of_a_crash
register_with failed(code: "killed", cause: "crashed", stderr: "libgomp: Thread creation failed\n")

event = events_for do
assert_raises(TemporarilyUnavailable) { Anything.perform_in_hotcell [], [], {} }
end.first.payload

assert_equal "crashed", event[:cause]
assert_equal "libgomp: Thread creation failed\n", event[:stderr]
end

def test_the_event_carries_a_transient_verdict_for_a_deadline_kill
register_with failed(code: "killed", cause: "deadline")

Expand All @@ -154,6 +167,7 @@ def test_the_event_carries_no_code_on_success
assert_nil event[:code]
assert_nil event[:cause]
assert_nil event[:signal]
assert_nil event[:stderr]
assert_nil event[:permanent]
assert_equal 12, event[:perform_ms]
assert_equal({ perform_ms: 12, operation_ms: 9 }, event[:timing])
Expand Down Expand Up @@ -245,9 +259,9 @@ def register_with(response, **options)
end
end

def failed(code:, cause: nil, signal: nil, error_class: nil, message: nil)
def failed(code:, cause: nil, signal: nil, error_class: nil, message: nil, stderr: nil)
HotCell::Response.failed HotCell::Failure.new(code: code, cause: cause, signal: signal,
error_class: error_class, message: message),
error_class: error_class, message: message, stderr: stderr),
timing: { perform_ms: 1 }
end
end