diff --git a/CHANGELOG.md b/CHANGELOG.md index 9628c18..dd5b5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/hotcell-client/lib/hot_cell/client.rb b/hotcell-client/lib/hot_cell/client.rb index 27a07ff..b40364d 100644 --- a/hotcell-client/lib/hot_cell/client.rb +++ b/hotcell-client/lib/hot_cell/client.rb @@ -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. @@ -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) diff --git a/hotcell-client/test/classification_test.rb b/hotcell-client/test/classification_test.rb index 32aa6dd..b24a9c8 100644 --- a/hotcell-client/test/classification_test.rb +++ b/hotcell-client/test/classification_test.rb @@ -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") @@ -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]) @@ -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