Skip to content

Commit 16337b2

Browse files
authored
Creating cadence.IsWorkflowError helper (#1145)
We have a variety of from-workflow errors that are relevant for users calling `client.ExecuteWorkflow(...).Get(...)` and `client.GetWorkflow(...).Get(...)`. Since they do not share any common wrapper or "parent type" that would allow easy comparison with an error type check or `errors.As`, they are rather annoying to differentiate from general RPC errors like timeouts or connection failures. So this adds a top-level "check all relevant types" helper, which is intended to be a stable target for users to rely on, even if we add more types later. It intentionally does not tell *which* kind of error it is, in case there are multiple types in the wrapped chain, as then behavior would be order-dependent and any change risks breaking someone's code.
1 parent 6e90393 commit 16337b2

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

client/client.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ package client
2525

2626
import (
2727
"context"
28+
"errors"
2829

30+
"go.uber.org/cadence"
2931
"go.uber.org/cadence/.gen/go/cadence/workflowserviceclient"
3032
s "go.uber.org/cadence/.gen/go/shared"
3133
"go.uber.org/cadence/encoded"
@@ -450,3 +452,54 @@ func NewValue(data []byte) encoded.Value {
450452
func NewValues(data []byte) encoded.Values {
451453
return internal.NewValues(data)
452454
}
455+
456+
// IsWorkflowError returns true if an error is a known returned-by-the-workflow error type, when it comes from
457+
// WorkflowRun from either Client.ExecuteWorkflow or Client.GetWorkflow. If it returns false, the error comes from
458+
// some other source, e.g. RPC request failures or bad arguments.
459+
// Using it on errors from any other source is currently undefined.
460+
//
461+
// This checks for known types via errors.As, so it will check recursively if it encounters wrapped errors.
462+
// Note that this is different than the various `cadence.Is[Type]Error` checks, which do not unwrap.
463+
//
464+
// Currently the complete list of errors checked is:
465+
// *cadence.CustomError, *cadence.CanceledError, *workflow.ContinueAsNewError,
466+
// *workflow.GenericError, *workflow.TimeoutError, *workflow.TerminatedError,
467+
// *workflow.PanicError, *workflow.UnknownExternalWorkflowExecutionError
468+
//
469+
// See documentation for each error type for details.
470+
func IsWorkflowError(err error) bool {
471+
var custom *cadence.CustomError
472+
if errors.As(err, &custom) {
473+
return true
474+
}
475+
var cancel *cadence.CanceledError
476+
if errors.As(err, &cancel) {
477+
return true
478+
}
479+
480+
var generic *workflow.GenericError
481+
if errors.As(err, &generic) {
482+
return true
483+
}
484+
var timeout *workflow.TimeoutError
485+
if errors.As(err, &timeout) {
486+
return true
487+
}
488+
var terminate *workflow.TerminatedError
489+
if errors.As(err, &terminate) {
490+
return true
491+
}
492+
var panicked *workflow.PanicError
493+
if errors.As(err, &panicked) {
494+
return true
495+
}
496+
var can *workflow.ContinueAsNewError
497+
if errors.As(err, &can) {
498+
return true
499+
}
500+
var unknown *workflow.UnknownExternalWorkflowExecutionError
501+
if errors.As(err, &unknown) {
502+
return true
503+
}
504+
return false
505+
}

test/integration_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func (ts *IntegrationTestSuite) TearDownSuite() {
140140
}
141141

142142
func (ts *IntegrationTestSuite) SetupTest() {
143+
ts.Assertions = require.New(ts.T())
143144
ts.seq++
144145
ts.activities.clearInvoked()
145146
ts.taskListName = fmt.Sprintf("tl-%v", ts.seq)
@@ -252,6 +253,7 @@ func (ts *IntegrationTestSuite) TestCancellation() {
252253
ts.Error(err)
253254
_, ok := err.(*cadence.CanceledError)
254255
ts.True(ok)
256+
ts.Truef(client.IsWorkflowError(err), "err from canceled workflows should be a workflow error: %#v", err)
255257
}
256258

257259
func (ts *IntegrationTestSuite) TestStackTraceQuery() {
@@ -316,6 +318,7 @@ func (ts *IntegrationTestSuite) TestWorkflowIDReuseRejectDuplicate() {
316318
gerr, ok := err.(*workflow.GenericError)
317319
ts.True(ok)
318320
ts.True(strings.Contains(gerr.Error(), "WorkflowExecutionAlreadyStartedError"))
321+
ts.Truef(client.IsWorkflowError(err), "already-started child error should be a workflow error: %#v", err)
319322
}
320323

321324
func (ts *IntegrationTestSuite) TestWorkflowIDReuseAllowDuplicateFailedOnly1() {
@@ -333,6 +336,7 @@ func (ts *IntegrationTestSuite) TestWorkflowIDReuseAllowDuplicateFailedOnly1() {
333336
gerr, ok := err.(*workflow.GenericError)
334337
ts.True(ok)
335338
ts.True(strings.Contains(gerr.Error(), "WorkflowExecutionAlreadyStartedError"))
339+
ts.Truef(client.IsWorkflowError(err), "already-started child error should be a workflow error: %#v", err)
336340
}
337341

338342
func (ts *IntegrationTestSuite) TestWorkflowIDReuseAllowDuplicateFailedOnly2() {
@@ -365,15 +369,39 @@ func (ts *IntegrationTestSuite) TestWorkflowIDReuseAllowDuplicate() {
365369
ts.Equal("HELLOWORLD", result)
366370
}
367371

372+
func (ts *IntegrationTestSuite) TestWorkflowIDReuseErrorViaStartWorkflow() {
373+
duplicatedWID := "test-workflowidreuse-duplicate-start-error"
374+
// setup: run any workflow once to consume the ID
375+
err := ts.executeWorkflow(
376+
duplicatedWID,
377+
ts.workflows.SimplestWorkflow,
378+
nil,
379+
)
380+
ts.NoError(err, "basic workflow should succeed")
381+
382+
// a second attempt should fail
383+
ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout)
384+
defer cancel()
385+
opts := ts.startWorkflowOptions(duplicatedWID)
386+
opts.WorkflowIDReusePolicy = client.WorkflowIDReusePolicyRejectDuplicate
387+
exec, err := ts.libClient.StartWorkflow(ctx, opts, ts.workflows.SimplestWorkflow)
388+
ts.Nil(exec)
389+
ts.Error(err)
390+
ts.IsType(&shared.WorkflowExecutionAlreadyStartedError{}, err, "should be the known already-started error type")
391+
ts.False(client.IsWorkflowError(err), "start-workflow rejected errors should not be workflow errors")
392+
}
393+
368394
func (ts *IntegrationTestSuite) TestChildWFRetryOnError() {
369395
err := ts.executeWorkflow("test-childwf-retry-on-error", ts.workflows.ChildWorkflowRetryOnError, nil)
370396
ts.Error(err)
397+
ts.Truef(client.IsWorkflowError(err), "child error should be a workflow error: %#v", err)
371398
ts.EqualValues([]string{"toUpper", "toUpper", "toUpper"}, ts.activities.invoked())
372399
}
373400

374401
func (ts *IntegrationTestSuite) TestChildWFRetryOnTimeout() {
375402
err := ts.executeWorkflow("test-childwf-retry-on-timeout", ts.workflows.ChildWorkflowRetryOnTimeout, nil)
376403
ts.Error(err)
404+
ts.Truef(client.IsWorkflowError(err), "child-timeout error should be a workflow error: %#v", err)
377405
ts.EqualValues([]string{"sleep", "sleep", "sleep"}, ts.activities.invoked())
378406
}
379407

@@ -434,6 +462,7 @@ func (ts *IntegrationTestSuite) TestLargeQueryResultError() {
434462
ts.Nil(err)
435463
value, err := ts.libClient.QueryWorkflow(ctx, "test-large-query-error", run.GetRunID(), "large_query")
436464
ts.Error(err)
465+
ts.False(client.IsWorkflowError(err), "query errors should not be workflow errors, as they are request-related")
437466

438467
queryErr, ok := err.(*shared.QueryFailedError)
439468
ts.True(ok)

0 commit comments

Comments
 (0)