From 33c77c658d22cc2a7f5fb55c2af893116addc573 Mon Sep 17 00:00:00 2001 From: Alex Shtin Date: Thu, 28 May 2020 12:22:48 -0700 Subject: [PATCH] Move all public errors to temporal package (#145) --- error.go | 146 ++++++++++++++++++++++++++++++++++++-- test/activity_test.go | 2 +- test/workflow_test.go | 8 +-- workflow/error.go | 161 ------------------------------------------ 4 files changed, 145 insertions(+), 172 deletions(-) delete mode 100644 workflow/error.go diff --git a/error.go b/error.go index d4d540715..ba0193876 100644 --- a/error.go +++ b/error.go @@ -27,12 +27,93 @@ package temporal import ( "errors" + commonpb "go.temporal.io/temporal-proto/common" "go.temporal.io/temporal-proto/serviceerror" "go.temporal.io/temporal/internal" "go.temporal.io/temporal/workflow" ) +/* +If activity fails then *ActivityTaskError is returned to the workflow code. The error has important information about activity +and actual error which caused activity failure. This internal error can be unwrapped using errors.Unwrap() or checked using errors.As(). +Below are the possible types of internal error: +1) *ApplicationError: (this should be the most common one) + *ApplicationError can be returned in two cases: + - If activity implementation returns *ApplicationError by using NewRetryableApplicationError()/NewNonRetryableApplicationError() API. + The error would contain a message and optional details. Workflow code could extract details to string typed variable, determine + what kind of error it was, and take actions based on it. The details is encoded payload therefore, workflow code needs to know what + the types of the encoded details are before extracting them. + - If activity implementation returns errors other than from NewApplicationError() API. In this case GetOriginalType() + will return orginal type of an error represented as string. Workflow code could check this type to determine what kind of error it was + and take actions based on the type. These errors are retryable by default, unless error type is specified in retry policy. +2) *CanceledError: + If activity was canceled, internal error will be an instance of *CanceledError. When activity cancels itself by + returning NewCancelError() it would supply optional details which could be extracted by workflow code. +3) *TimeoutError: + If activity was timed out (several timeout types), internal error will be an instance of *TimeoutError. The err contains + details about what type of timeout it was. +4) *PanicError: + If activity code panic while executing, temporal activity worker will report it as activity failure to temporal server. + The SDK will present that failure as *PanicError. The err contains a string representation of the panic message and + the call stack when panic was happen. +Workflow code could handle errors based on different types of error. Below is sample code of how error handling looks like. + +err := workflow.ExecuteActivity(ctx, MyActivity, ...).Get(ctx, nil) +if err != nil { + var applicationErr *ApplicationError + if errors.As(err, &applicationError) { + // retrieve error message + fmt.Println(applicationError.Error()) + + // handle activity errors (created via NewApplicationError() API) + var detailMsg string // assuming activity return error by NewApplicationError("message", true, "string details") + applicationErr.Details(&detailMsg) // extract strong typed details + + // handle activity errors (errors created other than using NewApplicationError() API) + switch err.OriginalType() { + case "CustomErrTypeA": + // handle CustomErrTypeA + case CustomErrTypeB: + // handle CustomErrTypeB + default: + // newer version of activity could return new errors that workflow was not aware of. + } + } + + var canceledErr *CanceledError + if errors.As(err, &canceledErr) { + // handle cancellation + } + + var timeoutErr *TimeoutError + if errors.As(err, &timeoutErr) { + // handle timeout, could check timeout type by timeoutErr.TimeoutType() + switch err.TimeoutType() { + case commonpb.ScheduleToStart: + // Handle ScheduleToStart timeout. + case commonpb.StartToClose: + // Handle StartToClose timeout. + case commonpb.Heartbeat: + // Handle heartbeat timeout. + default: + } + } + + var panicErr *PanicError + if errors.As(err, &panicErr) { + // handle panic, message and stack trace are available by panicErr.Error() and panicErr.StackTrace() + } +} +Errors from child workflow should be handled in a similar way, except that instance of *ChildWorkflowExecutionError is returned to +workflow code. It might contain *ActivityTaskError in case if error comes from activity (which in turn will contain on of the errors above), +or *ApplicationError in case if error comes from child workflow itslef. + +When panic happen in workflow implementation code, SDK catches that panic and causing the decision timeout. +That decision task will be retried at a later time (with exponential backoff retry intervals). +Workflow consumers will get an instance of *WorkflowExecutionError. This error will contains one of errors above. +*/ + type ( // ApplicationError returned from activity implementations with message and optional details. ApplicationError = internal.ApplicationError @@ -51,15 +132,37 @@ type ( // WorkflowExecutionError returned from workflow. WorkflowExecutionError = internal.WorkflowExecutionError + + // TimeoutError returned when activity or child workflow timed out. + TimeoutError = internal.TimeoutError + + // TerminatedError returned when workflow was terminated. + TerminatedError = internal.TerminatedError + + // PanicError contains information about panicked workflow/activity. + PanicError = internal.PanicError + + // ContinueAsNewError can be returned by a workflow implementation function and indicates that + // the workflow should continue as new with the same WorkflowID, but new RunID and new history. + ContinueAsNewError = internal.ContinueAsNewError + + // UnknownExternalWorkflowExecutionError can be returned when external workflow doesn't exist + UnknownExternalWorkflowExecutionError = internal.UnknownExternalWorkflowExecutionError ) // ErrNoData is returned when trying to extract strong typed data while there is no data available. var ErrNoData = internal.ErrNoData -// NewApplicationError create new instance of *ApplicationError with reason and optional details. +// NewNonRetryableApplicationError creates new instance of non-retryable *ApplicationError with reason and optional details. +// Use ApplicationError for any use case specific errors that cross activity and child workflow boundaries. +func NewNonRetryableApplicationError(reason string, details ...interface{}) *ApplicationError { + return internal.NewApplicationError(reason, true, details...) +} + +// NewRetryableApplicationError creates new instance of retryable *ApplicationError with reason and optional details. // Use ApplicationError for any use case specific errors that cross activity and child workflow boundaries. -func NewApplicationError(reason string, nonRetryable bool, details ...interface{}) *ApplicationError { - return internal.NewApplicationError(reason, nonRetryable, details...) +func NewRetryableApplicationError(reason string, details ...interface{}) *ApplicationError { + return internal.NewApplicationError(reason, false, details...) } // NewCanceledError creates CanceledError instance. @@ -88,18 +191,49 @@ func IsCanceledError(err error) bool { // IsTimeoutError return if the err is a TimeoutError func IsTimeoutError(err error) bool { - var timeoutError *workflow.TimeoutError + var timeoutError *TimeoutError return errors.As(err, &timeoutError) } // IsTerminatedError return if the err is a TerminatedError func IsTerminatedError(err error) bool { - var terminateError *workflow.TerminatedError + var terminateError *TerminatedError return errors.As(err, &terminateError) } // IsPanicError return if the err is a PanicError func IsPanicError(err error) bool { - var panicError *workflow.PanicError + var panicError *PanicError return errors.As(err, &panicError) } + +// NewContinueAsNewError creates ContinueAsNewError instance +// If the workflow main function returns this error then the current execution is ended and +// the new execution with same workflow ID is started automatically with options +// provided to this function. +// ctx - use context to override any options for the new workflow like execution timeout, decision task timeout, task list. +// if not mentioned it would use the defaults that the current workflow is using. +// ctx := WithWorkflowExecutionTimeout(ctx, 30 * time.Minute) +// ctx := WithWorkflowTaskTimeout(ctx, time.Minute) +// ctx := WithWorkflowTaskList(ctx, "example-group") +// wfn - workflow function. for new execution it can be different from the currently running. +// args - arguments for the new workflow. +// +func NewContinueAsNewError(ctx workflow.Context, wfn interface{}, args ...interface{}) *ContinueAsNewError { + return internal.NewContinueAsNewError(ctx, wfn, args...) +} + +// NewTimeoutError creates TimeoutError instance. +// Use NewHeartbeatTimeoutError to create heartbeat TimeoutError +// WARNING: This function is public only to support unit testing of workflows. +// It shouldn't be used by application level code. +func NewTimeoutError(timeoutType commonpb.TimeoutType, lastErr error, details ...interface{}) *TimeoutError { + return internal.NewTimeoutError(timeoutType, lastErr, details...) +} + +// NewHeartbeatTimeoutError creates TimeoutError instance +// WARNING: This function is public only to support unit testing of workflows. +// It shouldn't be used by application level code. +func NewHeartbeatTimeoutError(details ...interface{}) *TimeoutError { + return internal.NewHeartbeatTimeoutError(details...) +} diff --git a/test/activity_test.go b/test/activity_test.go index fcc0a4e67..61eab9ea0 100644 --- a/test/activity_test.go +++ b/test/activity_test.go @@ -46,7 +46,7 @@ type Activities2 struct { impl *Activities } -var errFailOnPurpose = temporal.NewApplicationError("failing-on-purpose", false) +var errFailOnPurpose = temporal.NewRetryableApplicationError("failing on purpose") func newActivities() *Activities { activities2 := &Activities2{} diff --git a/test/workflow_test.go b/test/workflow_test.go index 644fba460..0962de43f 100644 --- a/test/workflow_test.go +++ b/test/workflow_test.go @@ -125,7 +125,7 @@ func (w *Workflows) ActivityRetryOnTimeout(ctx workflow.Context, timeoutType com return nil, fmt.Errorf("expected activity to be retried on failure, but it was not: %v", elapsed) } - var timeoutErr *workflow.TimeoutError + var timeoutErr *temporal.TimeoutError ok := errors.As(err, &timeoutErr) if !ok { return nil, fmt.Errorf("activity failed with unexpected error: %v", err) @@ -155,7 +155,7 @@ func (w *Workflows) ActivityRetryOnHBTimeout(ctx workflow.Context) ([]string, er return nil, fmt.Errorf("expected activity to be retried on failure, but it was not") } - var timeoutErr *workflow.TimeoutError + var timeoutErr *temporal.TimeoutError ok := errors.As(err, &timeoutErr) if !ok { return nil, fmt.Errorf("activity failed with unexpected error: %v", err) @@ -189,7 +189,7 @@ func (w *Workflows) ContinueAsNew(ctx workflow.Context, count int, taskList stri return 999, nil } ctx = workflow.WithTaskList(ctx, taskList) - return -1, workflow.NewContinueAsNewError(ctx, w.ContinueAsNew, count-1, taskList) + return -1, temporal.NewContinueAsNewError(ctx, w.ContinueAsNew, count-1, taskList) } func (w *Workflows) ContinueAsNewWithOptions(ctx workflow.Context, count int, taskList string) (string, error) { @@ -219,7 +219,7 @@ func (w *Workflows) ContinueAsNewWithOptions(ctx workflow.Context, count int, ta } ctx = workflow.WithTaskList(ctx, taskList) - return "", workflow.NewContinueAsNewError(ctx, w.ContinueAsNewWithOptions, count-1, taskList) + return "", temporal.NewContinueAsNewError(ctx, w.ContinueAsNewWithOptions, count-1, taskList) } func (w *Workflows) IDReusePolicy( diff --git a/workflow/error.go b/workflow/error.go deleted file mode 100644 index 5f5ce9171..000000000 --- a/workflow/error.go +++ /dev/null @@ -1,161 +0,0 @@ -// The MIT License -// -// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. -// -// Copyright (c) 2020 Uber Technologies, Inc. -// -// 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 workflow - -import ( - commonpb "go.temporal.io/temporal-proto/common" - - "go.temporal.io/temporal/internal" -) - -/* -If activity fails then *ActivityTaskError is returned to the workflow code. The error has important information about activity -and actual error which caused activity failure. This internal error can be unwrapped using errors.Unwrap() or checked using errors.As(). -Below are the possible types of internal error: -1) *ApplicationError: (this should be the most common one) - *ApplicationError can be returned in two cases: - - If activity implementation returns *ApplicationError by using NewApplicationError() API. - The err would contain a message, details, and NonRetryable flag. Workflow code could check this flag and details to determine - what kind of error it was and take actions based on it. The details is encoded payload which workflow code could extract - to strong typed variable. Workflow code needs to know what the types of the encoded details are before extracting them. - - If activity implementation returns errors other than from NewApplicationError() API. In this case GetOriginalType() - will return orginal type of an error represented as string. Workflow code could check this type to determine what kind of error it was - and take actions based on the type. These errors are retryable by default, unless error type is specified in retry policy. -2) *CanceledError: - If activity was canceled, internal error will be an instance of *CanceledError. When activity cancels itself by - returning NewCancelError() it would supply optional details which could be extracted by workflow code. -3) *TimeoutError: - If activity was timed out (several timeout types), internal error will be an instance of *TimeoutError. The err contains - details about what type of timeout it was. -4) *PanicError: - If activity code panic while executing, temporal activity worker will report it as activity failure to temporal server. - The SDK will present that failure as *PanicError. The err contains a string representation of the panic message and - the call stack when panic was happen. - -Workflow code could handle errors based on different types of error. Below is sample code of how error handling looks like. - -err := workflow.ExecuteActivity(ctx, MyActivity, ...).Get(ctx, nil) -if err != nil { - var applicationErr *ApplicationError - if errors.As(err, &applicationError) { - // handle activity errors (created via NewApplicationError() API) - if !applicationErr.NonRetryable() { - // manually retry activity - } - var detailMsg string // assuming activity return error by NewApplicationError("message", true, "string details") - applicationErr.Details(&detailMsg) // extract strong typed details - - // handle activity errors (errors created other than using NewApplicationError() API) - switch err.OriginalType() { - case "CustomErrTypeA": - // handle CustomErrTypeA - case CustomErrTypeB: - // handle CustomErrTypeB - default: - // newer version of activity could return new errors that workflow was not aware of. - } - } - - var canceledErr *CanceledError - if errors.As(err, &canceledErr) { - // handle cancellation - } - - var timeoutErr *TimeoutError - if errors.As(err, &timeoutErr) { - // handle timeout, could check timeout type by timeoutErr.TimeoutType() - switch err.TimeoutType() { - case commonpb.ScheduleToStart: - // Handle ScheduleToStart timeout. - case commonpb.StartToClose: - // Handle StartToClose timeout. - case commonpb.Heartbeat: - // Handle heartbeat timeout. - default: - } - } - - var panicErr *PanicError - if errors.As(err, &panicErr) { - // handle panic, message and stack trace are available by panicErr.Error() and panicErr.StackTrace() - } -} - -Errors from child workflow should be handled in a similar way, except that instance of *ChildWorkflowExecutionError is returned to -workflow code. It will contains *ActivityTaskError, which in turn will contains on of the errors above. -When panic happen in workflow implementation code, SDK catches that panic and causing the decision timeout. -That decision task will be retried at a later time (with exponential backoff retry intervals). - -Workflow consumers will get an instance of *WorkflowExecutionError. This error will contains one of errors above. -*/ - -type ( - // TimeoutError returned when activity or child workflow timed out. - TimeoutError = internal.TimeoutError - - // TerminatedError returned when workflow was terminated. - TerminatedError = internal.TerminatedError - - // PanicError contains information about panicked workflow/activity. - PanicError = internal.PanicError - - // ContinueAsNewError can be returned by a workflow implementation function and indicates that - // the workflow should continue as new with the same WorkflowID, but new RunID and new history. - ContinueAsNewError = internal.ContinueAsNewError - - // UnknownExternalWorkflowExecutionError can be returned when external workflow doesn't exist - UnknownExternalWorkflowExecutionError = internal.UnknownExternalWorkflowExecutionError -) - -// NewContinueAsNewError creates ContinueAsNewError instance -// If the workflow main function returns this error then the current execution is ended and -// the new execution with same workflow ID is started automatically with options -// provided to this function. -// ctx - use context to override any options for the new workflow like execution timeout, decision task timeout, task list. -// if not mentioned it would use the defaults that the current workflow is using. -// ctx := WithWorkflowExecutionTimeout(ctx, 30 * time.Minute) -// ctx := WithWorkflowTaskTimeout(ctx, time.Minute) -// ctx := WithWorkflowTaskList(ctx, "example-group") -// wfn - workflow function. for new execution it can be different from the currently running. -// args - arguments for the new workflow. -// -func NewContinueAsNewError(ctx Context, wfn interface{}, args ...interface{}) *ContinueAsNewError { - return internal.NewContinueAsNewError(ctx, wfn, args...) -} - -// NewTimeoutError creates TimeoutError instance. -// Use NewHeartbeatTimeoutError to create heartbeat TimeoutError -// WARNING: This function is public only to support unit testing of workflows. -// It shouldn't be used by application level code. -func NewTimeoutError(timeoutType commonpb.TimeoutType, lastErr error, details ...interface{}) *TimeoutError { - return internal.NewTimeoutError(timeoutType, lastErr, details...) -} - -// NewHeartbeatTimeoutError creates TimeoutError instance -// WARNING: This function is public only to support unit testing of workflows. -// It shouldn't be used by application level code. -func NewHeartbeatTimeoutError(details ...interface{}) *TimeoutError { - return internal.NewHeartbeatTimeoutError(details...) -}