Skip to content

Commit cccf233

Browse files
Gracefully degrade application failure error when deserializing (#103)
1 parent 71355c8 commit cccf233

File tree

3 files changed

+125
-4
lines changed

3 files changed

+125
-4
lines changed

lib/temporal/workflow/errors.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,35 @@ class Errors
1010
def self.generate_error(failure, default_exception_class = StandardError)
1111
case failure.failure_info
1212
when :application_failure_info
13-
exception_class = safe_constantize(failure.application_failure_info.type)
14-
exception_class ||= default_exception_class
1513
message = from_details_payloads(failure.application_failure_info.details)
16-
backtrace = failure.stack_trace.split("\n")
1714

18-
exception_class.new(message).tap do |exception|
15+
exception_class = safe_constantize(failure.application_failure_info.type)
16+
if exception_class.nil?
17+
Temporal.logger.error(
18+
"Could not find original error class. Defaulting to StandardError.",
19+
{original_error: failure.application_failure_info.type},
20+
)
21+
message = "#{failure.application_failure_info.type}: #{message}"
22+
exception_class = default_exception_class
23+
end
24+
25+
26+
begin
27+
exception = exception_class.new(message)
28+
rescue ArgumentError => deserialization_error
29+
# We don't currently support serializing/deserializing exceptions with more than one argument.
30+
message = "#{exception_class}: #{message}"
31+
exception = default_exception_class.new(message)
32+
Temporal.logger.error(
33+
"Could not instantiate original error. Defaulting to StandardError.",
34+
{
35+
original_error: failure.application_failure_info.type,
36+
instantiation_error_message: deserialization_error.message,
37+
},
38+
)
39+
end
40+
exception.tap do |exception|
41+
backtrace = failure.stack_trace.split("\n")
1942
exception.set_backtrace(backtrace) if !backtrace.empty?
2043
end
2144
when :timeout_failure_info
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'temporal/concerns/payloads'
2+
class TestDeserializer
3+
include Temporal::Concerns::Payloads
4+
end
5+
# Simulates Temporal::Connection::Serializer::Failure
6+
Fabricator(:api_application_failure, from: Temporal::Api::Failure::V1::Failure) do
7+
transient :error_class, :backtrace
8+
message { |attrs| attrs[:message] }
9+
stack_trace { |attrs| attrs[:backtrace].join("\n") }
10+
application_failure_info do |attrs|
11+
Temporal::Api::Failure::V1::ApplicationFailureInfo.new(
12+
type: attrs[:error_class],
13+
details: TestDeserializer.new.to_details_payloads(attrs[:message]),
14+
)
15+
end
16+
end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
require 'temporal/workflow/errors'
2+
3+
class ErrorWithTwoArgs < StandardError
4+
def initialize(message, another_argument); end
5+
end
6+
7+
class SomeError < StandardError; end
8+
9+
describe Temporal::Workflow::Errors do
10+
describe '.generate_error' do
11+
it "instantiates properly when the client has the error" do
12+
message = "An error message"
13+
stack_trace = ["a fake backtrace"]
14+
failure = Fabricate(
15+
:api_application_failure,
16+
message: message,
17+
backtrace: stack_trace,
18+
error_class: SomeError.to_s
19+
)
20+
21+
e = Temporal::Workflow::Errors.generate_error(failure)
22+
expect(e).to be_a(SomeError)
23+
expect(e.message).to eq(message)
24+
expect(e.backtrace).to eq(stack_trace)
25+
26+
end
27+
28+
it "falls back to StandardError when the client doesn't have the error class" do
29+
allow(Temporal.logger).to receive(:error)
30+
31+
message = "An error message"
32+
stack_trace = ["a fake backtrace"]
33+
failure = Fabricate(
34+
:api_application_failure,
35+
message: message,
36+
backtrace: stack_trace,
37+
error_class: 'NonexistentError',
38+
)
39+
40+
e = Temporal::Workflow::Errors.generate_error(failure)
41+
expect(e).to be_a(StandardError)
42+
expect(e.message).to eq("NonexistentError: An error message")
43+
expect(e.backtrace).to eq(stack_trace)
44+
expect(Temporal.logger)
45+
.to have_received(:error)
46+
.with(
47+
'Could not find original error class. Defaulting to StandardError.',
48+
{original_error: "NonexistentError"},
49+
)
50+
51+
end
52+
53+
54+
it "falls back to StandardError when the client can't initialize the error class" do
55+
allow(Temporal.logger).to receive(:error)
56+
57+
message = "An error message"
58+
stack_trace = ["a fake backtrace"]
59+
failure = Fabricate(
60+
:api_application_failure,
61+
message: message,
62+
backtrace: stack_trace,
63+
error_class: ErrorWithTwoArgs.to_s,
64+
)
65+
66+
e = Temporal::Workflow::Errors.generate_error(failure)
67+
expect(e).to be_a(StandardError)
68+
expect(e.message).to eq("ErrorWithTwoArgs: An error message")
69+
expect(e.backtrace).to eq(stack_trace)
70+
expect(Temporal.logger)
71+
.to have_received(:error)
72+
.with(
73+
'Could not instantiate original error. Defaulting to StandardError.',
74+
{
75+
original_error: "ErrorWithTwoArgs",
76+
instantiation_error_message: "wrong number of arguments (given 1, expected 2)",
77+
},
78+
)
79+
end
80+
81+
end
82+
end

0 commit comments

Comments
 (0)