Skip to content

Commit

Permalink
Add support for custom failure converters (#924)
Browse files Browse the repository at this point in the history
Adds an interface called `FailureConverter` that allows users to
customize failure serialization and deserialization.
  • Loading branch information
Quinn-With-Two-Ns committed Sep 30, 2022
1 parent a6a8749 commit e6a06f2
Show file tree
Hide file tree
Showing 22 changed files with 613 additions and 310 deletions.
36 changes: 36 additions & 0 deletions converter/failure_converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// The MIT License
//
// Copyright (c) 2022 Temporal Technologies Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package converter

import failurepb "go.temporal.io/api/failure/v1"

// FailureConverter is used by the sdk to serialize/deserialize errors
// that need to be sent over the wire.
// To use a custom FailureConverter, set FailureConverter in client, through client.Options.
type FailureConverter interface {
// ErrorToFailure converts a error to a Failure proto message.
ErrorToFailure(err error) *failurepb.Failure

// FailureToError converts a Failure proto message to a Go Error.
FailureToError(failure *failurepb.Failure) error
}
10 changes: 10 additions & 0 deletions internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,11 @@ type (
// default: defaultDataConverter, an combination of google protobuf converter, gogo protobuf converter and json converter
DataConverter converter.DataConverter

// Optional: Sets FailureConverter to customize serialization/deserialization of errors.
// default: temporal.DefaultFailureConverter, does not encode any fields of the error. Use temporal.NewDefaultFailureConverter
// options to configure or create a custom converter.
FailureConverter converter.FailureConverter

// Optional: Sets ContextPropagators that allows users to control the context information passed through a workflow
// default: nil
ContextPropagators []ContextPropagator
Expand Down Expand Up @@ -739,6 +744,10 @@ func NewServiceClient(workflowServiceClient workflowservice.WorkflowServiceClien
options.DataConverter = converter.GetDefaultDataConverter()
}

if options.FailureConverter == nil {
options.FailureConverter = GetDefaultFailureConverter()
}

if options.MetricsHandler == nil {
options.MetricsHandler = metrics.NopHandler
}
Expand All @@ -764,6 +773,7 @@ func NewServiceClient(workflowServiceClient workflowservice.WorkflowServiceClien
logger: options.Logger,
identity: options.Identity,
dataConverter: options.DataConverter,
failureConverter: options.FailureConverter,
contextPropagators: options.ContextPropagators,
workerInterceptors: workerInterceptors,
excludeInternalFromRetry: options.ConnectionOptions.excludeInternalFromRetry,
Expand Down
174 changes: 0 additions & 174 deletions internal/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,177 +820,3 @@ func getErrType(err error) string {

return t.Name()
}

// ConvertErrorToFailure converts error to failure.
func ConvertErrorToFailure(err error, dc converter.DataConverter) *failurepb.Failure {
if err == nil {
return nil
}

if fh, ok := err.(failureHolder); ok {
if fh.failure() != nil {
return fh.failure()
}
}

failure := &failurepb.Failure{
Source: "GoSDK",
}

if m, ok := err.(messenger); ok && m != nil {
failure.Message = m.message()
} else {
failure.Message = err.Error()
}

switch err := err.(type) {
case *ApplicationError:
failureInfo := &failurepb.ApplicationFailureInfo{
Type: err.errType,
NonRetryable: err.nonRetryable,
Details: convertErrDetailsToPayloads(err.details, dc),
}
failure.FailureInfo = &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: failureInfo}
case *CanceledError:
failureInfo := &failurepb.CanceledFailureInfo{
Details: convertErrDetailsToPayloads(err.details, dc),
}
failure.FailureInfo = &failurepb.Failure_CanceledFailureInfo{CanceledFailureInfo: failureInfo}
case *PanicError:
failureInfo := &failurepb.ApplicationFailureInfo{
Type: getErrType(err),
}
failure.FailureInfo = &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: failureInfo}
failure.StackTrace = err.StackTrace()
case *workflowPanicError:
failureInfo := &failurepb.ApplicationFailureInfo{
Type: getErrType(&PanicError{}),
NonRetryable: true,
}
failure.FailureInfo = &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: failureInfo}
failure.StackTrace = err.StackTrace()
case *TimeoutError:
failureInfo := &failurepb.TimeoutFailureInfo{
TimeoutType: err.timeoutType,
LastHeartbeatDetails: convertErrDetailsToPayloads(err.lastHeartbeatDetails, dc),
}
failure.FailureInfo = &failurepb.Failure_TimeoutFailureInfo{TimeoutFailureInfo: failureInfo}
case *TerminatedError:
failureInfo := &failurepb.TerminatedFailureInfo{}
failure.FailureInfo = &failurepb.Failure_TerminatedFailureInfo{TerminatedFailureInfo: failureInfo}
case *ServerError:
failureInfo := &failurepb.ServerFailureInfo{
NonRetryable: err.nonRetryable,
}
failure.FailureInfo = &failurepb.Failure_ServerFailureInfo{ServerFailureInfo: failureInfo}
case *ActivityError:
failureInfo := &failurepb.ActivityFailureInfo{
ScheduledEventId: err.scheduledEventID,
StartedEventId: err.startedEventID,
Identity: err.identity,
ActivityType: err.activityType,
ActivityId: err.activityID,
RetryState: err.retryState,
}
failure.FailureInfo = &failurepb.Failure_ActivityFailureInfo{ActivityFailureInfo: failureInfo}
case *ChildWorkflowExecutionError:
failureInfo := &failurepb.ChildWorkflowExecutionFailureInfo{
Namespace: err.namespace,
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: err.workflowID,
RunId: err.runID,
},
WorkflowType: &commonpb.WorkflowType{Name: err.workflowType},
InitiatedEventId: err.initiatedEventID,
StartedEventId: err.startedEventID,
RetryState: err.retryState,
}
failure.FailureInfo = &failurepb.Failure_ChildWorkflowExecutionFailureInfo{ChildWorkflowExecutionFailureInfo: failureInfo}
default: // All unknown errors are considered to be retryable ApplicationFailureInfo.
failureInfo := &failurepb.ApplicationFailureInfo{
Type: getErrType(err),
NonRetryable: false,
}
failure.FailureInfo = &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: failureInfo}
}

failure.Cause = ConvertErrorToFailure(errors.Unwrap(err), dc)

return failure
}

// ConvertFailureToError converts failure to error.
func ConvertFailureToError(failure *failurepb.Failure, dc converter.DataConverter) error {
if failure == nil {
return nil
}

var err error

if failure.GetApplicationFailureInfo() != nil {
applicationFailureInfo := failure.GetApplicationFailureInfo()
details := newEncodedValues(applicationFailureInfo.GetDetails(), dc)
switch applicationFailureInfo.GetType() {
case getErrType(&PanicError{}):
err = newPanicError(failure.GetMessage(), failure.GetStackTrace())
default:
err = NewApplicationError(
failure.GetMessage(),
applicationFailureInfo.GetType(),
applicationFailureInfo.GetNonRetryable(),
ConvertFailureToError(failure.GetCause(), dc),
details)
}
} else if failure.GetCanceledFailureInfo() != nil {
details := newEncodedValues(failure.GetCanceledFailureInfo().GetDetails(), dc)
err = NewCanceledError(details)
} else if failure.GetTimeoutFailureInfo() != nil {
timeoutFailureInfo := failure.GetTimeoutFailureInfo()
lastHeartbeatDetails := newEncodedValues(timeoutFailureInfo.GetLastHeartbeatDetails(), dc)
err = NewTimeoutError(
failure.GetMessage(),
timeoutFailureInfo.GetTimeoutType(),
ConvertFailureToError(failure.GetCause(), dc),
lastHeartbeatDetails)
} else if failure.GetTerminatedFailureInfo() != nil {
err = newTerminatedError()
} else if failure.GetServerFailureInfo() != nil {
err = NewServerError(failure.GetMessage(), failure.GetServerFailureInfo().GetNonRetryable(), ConvertFailureToError(failure.GetCause(), dc))
} else if failure.GetResetWorkflowFailureInfo() != nil {
err = NewApplicationError(failure.GetMessage(), "", true, ConvertFailureToError(failure.GetCause(), dc), failure.GetResetWorkflowFailureInfo().GetLastHeartbeatDetails())
} else if failure.GetActivityFailureInfo() != nil {
activityTaskInfoFailure := failure.GetActivityFailureInfo()
err = NewActivityError(
activityTaskInfoFailure.GetScheduledEventId(),
activityTaskInfoFailure.GetStartedEventId(),
activityTaskInfoFailure.GetIdentity(),
activityTaskInfoFailure.GetActivityType(),
activityTaskInfoFailure.GetActivityId(),
activityTaskInfoFailure.GetRetryState(),
ConvertFailureToError(failure.GetCause(), dc),
)
} else if failure.GetChildWorkflowExecutionFailureInfo() != nil {
childWorkflowExecutionFailureInfo := failure.GetChildWorkflowExecutionFailureInfo()
err = NewChildWorkflowExecutionError(
childWorkflowExecutionFailureInfo.GetNamespace(),
childWorkflowExecutionFailureInfo.GetWorkflowExecution().GetWorkflowId(),
childWorkflowExecutionFailureInfo.GetWorkflowExecution().GetRunId(),
childWorkflowExecutionFailureInfo.GetWorkflowType().GetName(),
childWorkflowExecutionFailureInfo.GetInitiatedEventId(),
childWorkflowExecutionFailureInfo.GetStartedEventId(),
childWorkflowExecutionFailureInfo.GetRetryState(),
ConvertFailureToError(failure.GetCause(), dc),
)
}

if err == nil {
// All unknown types are considered to be retryable ApplicationError.
err = NewApplicationError(failure.GetMessage(), "", false, ConvertFailureToError(failure.GetCause(), dc))
}

if fh, ok := err.(failureHolder); ok {
fh.setFailure(failure)
}

return err
}
Loading

0 comments on commit e6a06f2

Please sign in to comment.