Skip to content

Commit

Permalink
Gracefully degrade application failure error when deserializing (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
drewhoskins-stripe authored Oct 16, 2021
1 parent 71355c8 commit cccf233
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 4 deletions.
31 changes: 27 additions & 4 deletions lib/temporal/workflow/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,35 @@ class Errors
def self.generate_error(failure, default_exception_class = StandardError)
case failure.failure_info
when :application_failure_info
exception_class = safe_constantize(failure.application_failure_info.type)
exception_class ||= default_exception_class
message = from_details_payloads(failure.application_failure_info.details)
backtrace = failure.stack_trace.split("\n")

exception_class.new(message).tap do |exception|
exception_class = safe_constantize(failure.application_failure_info.type)
if exception_class.nil?
Temporal.logger.error(
"Could not find original error class. Defaulting to StandardError.",
{original_error: failure.application_failure_info.type},
)
message = "#{failure.application_failure_info.type}: #{message}"
exception_class = default_exception_class
end


begin
exception = exception_class.new(message)
rescue ArgumentError => deserialization_error
# We don't currently support serializing/deserializing exceptions with more than one argument.
message = "#{exception_class}: #{message}"
exception = default_exception_class.new(message)
Temporal.logger.error(
"Could not instantiate original error. Defaulting to StandardError.",
{
original_error: failure.application_failure_info.type,
instantiation_error_message: deserialization_error.message,
},
)
end
exception.tap do |exception|
backtrace = failure.stack_trace.split("\n")
exception.set_backtrace(backtrace) if !backtrace.empty?
end
when :timeout_failure_info
Expand Down
16 changes: 16 additions & 0 deletions spec/fabricators/grpc/application_failure_fabricator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'temporal/concerns/payloads'
class TestDeserializer
include Temporal::Concerns::Payloads
end
# Simulates Temporal::Connection::Serializer::Failure
Fabricator(:api_application_failure, from: Temporal::Api::Failure::V1::Failure) do
transient :error_class, :backtrace
message { |attrs| attrs[:message] }
stack_trace { |attrs| attrs[:backtrace].join("\n") }
application_failure_info do |attrs|
Temporal::Api::Failure::V1::ApplicationFailureInfo.new(
type: attrs[:error_class],
details: TestDeserializer.new.to_details_payloads(attrs[:message]),
)
end
end
82 changes: 82 additions & 0 deletions spec/unit/lib/temporal/workflow/errors_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'temporal/workflow/errors'

class ErrorWithTwoArgs < StandardError
def initialize(message, another_argument); end
end

class SomeError < StandardError; end

describe Temporal::Workflow::Errors do
describe '.generate_error' do
it "instantiates properly when the client has the error" do
message = "An error message"
stack_trace = ["a fake backtrace"]
failure = Fabricate(
:api_application_failure,
message: message,
backtrace: stack_trace,
error_class: SomeError.to_s
)

e = Temporal::Workflow::Errors.generate_error(failure)
expect(e).to be_a(SomeError)
expect(e.message).to eq(message)
expect(e.backtrace).to eq(stack_trace)

end

it "falls back to StandardError when the client doesn't have the error class" do
allow(Temporal.logger).to receive(:error)

message = "An error message"
stack_trace = ["a fake backtrace"]
failure = Fabricate(
:api_application_failure,
message: message,
backtrace: stack_trace,
error_class: 'NonexistentError',
)

e = Temporal::Workflow::Errors.generate_error(failure)
expect(e).to be_a(StandardError)
expect(e.message).to eq("NonexistentError: An error message")
expect(e.backtrace).to eq(stack_trace)
expect(Temporal.logger)
.to have_received(:error)
.with(
'Could not find original error class. Defaulting to StandardError.',
{original_error: "NonexistentError"},
)

end


it "falls back to StandardError when the client can't initialize the error class" do
allow(Temporal.logger).to receive(:error)

message = "An error message"
stack_trace = ["a fake backtrace"]
failure = Fabricate(
:api_application_failure,
message: message,
backtrace: stack_trace,
error_class: ErrorWithTwoArgs.to_s,
)

e = Temporal::Workflow::Errors.generate_error(failure)
expect(e).to be_a(StandardError)
expect(e.message).to eq("ErrorWithTwoArgs: An error message")
expect(e.backtrace).to eq(stack_trace)
expect(Temporal.logger)
.to have_received(:error)
.with(
'Could not instantiate original error. Defaulting to StandardError.',
{
original_error: "ErrorWithTwoArgs",
instantiation_error_message: "wrong number of arguments (given 1, expected 2)",
},
)
end

end
end

0 comments on commit cccf233

Please sign in to comment.