Skip to content

Commit 73e664b

Browse files
authored
[Feature] Failure converter (#55)
* Add a dummy implementation of a FailureConverter * Implement FailureConverter::Basic#from_failure * Implement FailureConverter::Basic#to_failure * Support encoded attributes and enums * Fix rubocop violations * Add types for the FailureConverter::Basic * Rubocop fixes * Integrate FailureConverter into other parts of the SDK * Add ExpectedResponse error * Add missing specs * Extend Basic failure converter from Base * Switch to passing payload converter per-call * Skip RBS for now due to an incompatibility
1 parent bc14791 commit 73e664b

28 files changed

+1627
-55
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ jobs:
2929
go-version: "1.19"
3030
- run: bundle install
3131
- run: bundle exec rubocop
32-
- run: bundle exec steep check
32+
# Turned off for now because of an incompatibility in steep
33+
# More info — https://github.com/soutaro/steep/issues/477
34+
# - run: bundle exec steep check
3335
- run: bundle exec rake bridge:lint
3436
- run: bundle exec rake bridge:release
3537
- run: bundle exec rake test_server:build

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Style/IfUnlessModifier:
2929
Style/GuardClause:
3030
Enabled: false
3131

32+
Style/MultilineBlockChain:
33+
Exclude:
34+
- spec/**/*_spec.rb
35+
3236
Style/RedundantReturn:
3337
Enabled: false
3438

lib/temporal/client/implementation.rb

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
require 'socket'
22
require 'temporal/api/workflowservice/v1/request_response_pb'
33
require 'temporal/client/workflow_handle'
4-
require 'temporal/errors'
4+
require 'temporal/error/failure'
5+
require 'temporal/error/workflow_failure'
56
require 'temporal/interceptor/chain'
7+
require 'temporal/timeout_type'
68
require 'temporal/version'
79
require 'temporal/workflow/execution_info'
810
require 'temporal/workflow/id_reuse_policy'
@@ -275,8 +277,7 @@ def process_workflow_result_from(response, follow_runs)
275277
end
276278

277279
event = events.first
278-
# TODO: Use special error type for internal errors
279-
raise Temporal::Error, 'Missing final history event' unless event
280+
raise Temporal::Error::UnexpectedResponse, 'Missing final history event' unless event
280281

281282
case event.event_type
282283
when :EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED
@@ -290,23 +291,39 @@ def process_workflow_result_from(response, follow_runs)
290291
attributes = event.workflow_execution_failed_event_attributes
291292
follow(attributes&.new_execution_run_id) if follow_runs
292293

293-
# TODO: Use more specific error and decode failure
294-
raise Temporal::Error, 'Workflow execution failed'
294+
raise Temporal::Error::WorkflowFailure.new(
295+
cause: converter.from_failure(attributes&.failure),
296+
)
295297

296298
when :EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED
297-
# TODO: Use more specific error and decode failure
298-
raise Temporal::Error, 'Workflow execution cancelled'
299+
attributes = event.workflow_execution_canceled_event_attributes
300+
301+
raise Temporal::Error::WorkflowFailure.new(
302+
cause: Temporal::Error::CancelledError.new(
303+
'Workflow execution cancelled',
304+
details: converter.from_payloads(attributes&.details),
305+
),
306+
)
299307

300308
when :EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED
301-
# TODO: Use more specific error and decode failure
302-
raise Temporal::Error, 'Workflow execution terminated'
309+
attributes = event.workflow_execution_terminated_event_attributes
310+
311+
raise Temporal::Error::WorkflowFailure.new(
312+
cause: Temporal::Error::TerminatedError.new(
313+
attributes&.reason || 'Workflow execution terminated',
314+
),
315+
)
303316

304317
when :EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT
305318
attributes = event.workflow_execution_timed_out_event_attributes
306319
follow(attributes&.new_execution_run_id) if follow_runs
307320

308-
# TODO: Use more specific error and decode failure
309-
raise Temporal::Error, 'Workflow execution timed out'
321+
raise Temporal::Error::WorkflowFailure.new(
322+
cause: Temporal::Error::TimeoutError.new(
323+
'Workflow execution timed out',
324+
type: Temporal::TimeoutType::START_TO_CLOSE,
325+
),
326+
)
310327

311328
when :EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW
312329
attributes = event.workflow_execution_continued_as_new_event_attributes

lib/temporal/data_converter.rb

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
require 'temporal/api/common/v1/message_pb'
22
require 'temporal/errors'
33
require 'temporal/payload_converter'
4+
require 'temporal/failure_converter'
45

56
module Temporal
67
class DataConverter
78
class MissingPayload < Temporal::Error; end
89

9-
def initialize(payload_converter: Temporal::PayloadConverter::DEFAULT, payload_codecs: [])
10+
def initialize(
11+
payload_converter: Temporal::PayloadConverter::DEFAULT,
12+
payload_codecs: [],
13+
failure_converter: Temporal::FailureConverter::DEFAULT
14+
)
1015
@payload_converter = payload_converter
1116
@payload_codecs = payload_codecs
17+
@failure_converter = failure_converter
1218
end
1319

1420
def to_payloads(data)
@@ -28,6 +34,11 @@ def to_payload_map(data)
2834
end
2935
end
3036

37+
def to_failure(error)
38+
failure = failure_converter.to_failure(error, payload_converter)
39+
encode_failure(failure)
40+
end
41+
3142
def from_payloads(payloads)
3243
return unless payloads
3344

@@ -49,11 +60,20 @@ def from_payload_map(payload_map)
4960
# rubocop:enable Style/MapToHash
5061
end
5162

63+
def from_failure(failure)
64+
raise ArgumentError, 'missing a failure to convert from' unless failure
65+
66+
failure = decode_failure(failure)
67+
failure_converter.from_failure(failure, payload_converter)
68+
end
69+
5270
private
5371

54-
attr_reader :payload_converter, :payload_codecs
72+
attr_reader :payload_converter, :payload_codecs, :failure_converter
5573

5674
def encode(payloads)
75+
return [] unless payloads
76+
5777
payload_codecs.each do |codec|
5878
payloads = codec.encode(payloads)
5979
end
@@ -62,6 +82,8 @@ def encode(payloads)
6282
end
6383

6484
def decode(payloads)
85+
return [] unless payloads
86+
6587
payload_codecs.reverse_each do |codec|
6688
payloads = codec.decode(payloads)
6789
end
@@ -76,5 +98,59 @@ def to_payload(data)
7698
def from_payload(payload)
7799
payload_converter.from_payload(payload)
78100
end
101+
102+
def encode_failure(failure)
103+
failure = failure.dup
104+
105+
failure.encoded_attributes = failure.encoded_attributes ? encode([failure.encoded_attributes])&.first : nil
106+
failure.cause = failure.cause ? encode_failure(failure.cause) : nil
107+
108+
if failure.application_failure_info
109+
failure.application_failure_info.details = Temporal::Api::Common::V1::Payloads.new(
110+
payloads: encode(failure.application_failure_info.details&.payloads).to_a,
111+
)
112+
elsif failure.timeout_failure_info
113+
failure.timeout_failure_info.last_heartbeat_details = Temporal::Api::Common::V1::Payloads.new(
114+
payloads: encode(failure.timeout_failure_info.last_heartbeat_details&.payloads).to_a,
115+
)
116+
elsif failure.canceled_failure_info
117+
failure.canceled_failure_info.details = Temporal::Api::Common::V1::Payloads.new(
118+
payloads: encode(failure.canceled_failure_info.details&.payloads).to_a,
119+
)
120+
elsif failure.reset_workflow_failure_info
121+
failure.reset_workflow_failure_info.last_heartbeat_details = Temporal::Api::Common::V1::Payloads.new(
122+
payloads: encode(failure.reset_workflow_failure_info.last_heartbeat_details&.payloads).to_a,
123+
)
124+
end
125+
126+
failure
127+
end
128+
129+
def decode_failure(failure)
130+
failure = failure.dup
131+
132+
failure.encoded_attributes = failure.encoded_attributes ? decode([failure.encoded_attributes])&.first : nil
133+
failure.cause = failure.cause ? decode_failure(failure.cause) : nil
134+
135+
if failure.application_failure_info
136+
failure.application_failure_info.details = Temporal::Api::Common::V1::Payloads.new(
137+
payloads: decode(failure.application_failure_info.details&.payloads).to_a,
138+
)
139+
elsif failure.timeout_failure_info
140+
failure.timeout_failure_info.last_heartbeat_details = Temporal::Api::Common::V1::Payloads.new(
141+
payloads: decode(failure.timeout_failure_info.last_heartbeat_details&.payloads).to_a,
142+
)
143+
elsif failure.canceled_failure_info
144+
failure.canceled_failure_info.details = Temporal::Api::Common::V1::Payloads.new(
145+
payloads: decode(failure.canceled_failure_info.details&.payloads).to_a,
146+
)
147+
elsif failure.reset_workflow_failure_info
148+
failure.reset_workflow_failure_info.last_heartbeat_details = Temporal::Api::Common::V1::Payloads.new(
149+
payloads: decode(failure.reset_workflow_failure_info.last_heartbeat_details&.payloads).to_a,
150+
)
151+
end
152+
153+
failure
154+
end
79155
end
80156
end

lib/temporal/error/failure.rb

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# TODO: Figure out the hierarchy
2+
require 'temporal/errors'
3+
4+
module Temporal
5+
class Error
6+
class Failure < Error
7+
attr_reader :raw
8+
9+
def initialize(message, raw: nil, cause: nil)
10+
super(message)
11+
12+
@raw = raw
13+
@cause = cause
14+
end
15+
16+
def cause
17+
@cause || super
18+
end
19+
end
20+
21+
class ApplicationError < Failure
22+
attr_reader :type, :details, :non_retryable
23+
24+
def initialize(message, type:, details: [], non_retryable: false, raw: nil, cause: nil)
25+
super(message, raw: raw, cause: cause)
26+
27+
@type = type
28+
@details = details
29+
@non_retryable = non_retryable
30+
end
31+
32+
def retryable?
33+
!non_retryable
34+
end
35+
end
36+
37+
class TimeoutError < Failure
38+
attr_reader :type, :last_heartbeat_details
39+
40+
def initialize(message, type:, last_heartbeat_details: [], raw: nil, cause: nil)
41+
super(message, raw: raw, cause: cause)
42+
43+
@type = type
44+
@last_heartbeat_details = last_heartbeat_details
45+
end
46+
end
47+
48+
class CancelledError < Failure
49+
attr_reader :details
50+
51+
def initialize(message, details: [], raw: nil, cause: nil)
52+
super(message, raw: raw, cause: cause)
53+
54+
@details = details
55+
end
56+
end
57+
58+
class TerminatedError < Failure; end
59+
60+
class ServerError < Failure
61+
attr_reader :non_retryable
62+
63+
def initialize(message, non_retryable:, raw: nil, cause: nil)
64+
super(message, raw: raw, cause: cause)
65+
66+
@non_retryable = non_retryable
67+
end
68+
69+
def retryable?
70+
!non_retryable
71+
end
72+
end
73+
74+
class ResetWorkflowError < Failure
75+
attr_reader :last_heartbeat_details
76+
77+
def initialize(message, last_heartbeat_details: [], raw: nil, cause: nil)
78+
super(message, raw: raw, cause: cause)
79+
80+
@last_heartbeat_details = last_heartbeat_details
81+
end
82+
end
83+
84+
class ActivityError < Failure
85+
attr_reader :scheduled_event_id,
86+
:started_event_id,
87+
:identity,
88+
:activity_name,
89+
:activity_id,
90+
:retry_state
91+
92+
def initialize(
93+
message,
94+
scheduled_event_id:,
95+
started_event_id:,
96+
identity:,
97+
activity_name:,
98+
activity_id:,
99+
retry_state:,
100+
raw: nil,
101+
cause: nil
102+
)
103+
super(message, raw: raw, cause: cause)
104+
105+
@scheduled_event_id = scheduled_event_id
106+
@started_event_id = started_event_id
107+
@identity = identity
108+
@activity_name = activity_name
109+
@activity_id = activity_id
110+
@retry_state = retry_state
111+
end
112+
end
113+
114+
class ChildWorkflowError < Failure
115+
attr_reader :namespace,
116+
:workflow_id,
117+
:run_id,
118+
:workflow_name,
119+
:initiated_event_id,
120+
:started_event_id,
121+
:retry_state
122+
123+
def initialize(
124+
message,
125+
namespace:,
126+
workflow_id:,
127+
run_id:,
128+
workflow_name:,
129+
initiated_event_id:,
130+
started_event_id:,
131+
retry_state:,
132+
raw: nil,
133+
cause: nil
134+
)
135+
super(message, raw: raw, cause: cause)
136+
137+
@namespace = namespace
138+
@workflow_id = workflow_id
139+
@run_id = run_id
140+
@workflow_name = workflow_name
141+
@initiated_event_id = initiated_event_id
142+
@started_event_id = started_event_id
143+
@retry_state = retry_state
144+
end
145+
end
146+
end
147+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require 'temporal/errors'
2+
3+
module Temporal
4+
class Error
5+
# Used as a wrapper to perserve failure hierarchy in nested calls
6+
# i.e. WorkflowFailure(ActivityError(WorkflowFailure(CancelledError)))
7+
class WorkflowFailure < Error
8+
attr_reader :cause
9+
10+
def initialize(cause:)
11+
super
12+
13+
@cause = cause
14+
end
15+
end
16+
end
17+
end

lib/temporal/errors.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
module Temporal
22
# Superclass for all Temporal errors
3-
class Error < StandardError; end
3+
class Error < StandardError
4+
# Superclass for RPC and proto related errors
5+
class RPCError < Error; end
6+
class UnexpectedResponse < RPCError; end
7+
end
48
end

0 commit comments

Comments
 (0)