go.uber.org/cadence@v1.2.9/workflow/doc.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 /* 22 Package workflow contains functions and types used to implement Cadence workflows. 23 24 A workflow is an implementation of coordination logic. The Cadence programming framework (aka client library) allows 25 you to write the workflow coordination logic as simple procedural code that uses standard Go data modeling. The client 26 library takes care of the communication between the worker service and the Cadence service, and ensures state 27 persistence between events even in case of worker failures. Any particular execution is not tied to a 28 particular worker machine. Different steps of the coordination logic can end up executing on different worker 29 instances, with the framework ensuring that necessary state is recreated on the worker executing the step. 30 31 In order to facilitate this operational model both the Cadence programming framework and the managed service impose 32 some requirements and restrictions on the implementation of the coordination logic. The details of these requirements 33 and restrictions are described in the "Implementation" section below. 34 35 # Overview 36 37 The sample code below shows a simple implementation of a workflow that executes one activity. The workflow also passes 38 the sole parameter it receives as part of its initialization as a parameter to the activity. 39 40 package sample 41 42 import ( 43 "time" 44 45 "go.uber.org/cadence/workflow" 46 ) 47 48 func init() { 49 workflow.Register(SimpleWorkflow) 50 } 51 52 func SimpleWorkflow(ctx workflow.Context, value string) error { 53 ao := workflow.ActivityOptions{ 54 TaskList: "sampleTaskList", 55 ScheduleToCloseTimeout: time.Second * 60, 56 ScheduleToStartTimeout: time.Second * 60, 57 StartToCloseTimeout: time.Second * 60, 58 HeartbeatTimeout: time.Second * 10, 59 WaitForCancellation: false, 60 } 61 ctx = workflow.WithActivityOptions(ctx, ao) 62 63 future := workflow.ExecuteActivity(ctx, SimpleActivity, value) 64 var result string 65 if err := future.Get(ctx, &result); err != nil { 66 return err 67 } 68 workflow.GetLogger(ctx).Info(“Done”, zap.String(“result”, result)) 69 return nil 70 } 71 72 The following sections describe what is going on in the above code. 73 74 # Declaration 75 76 In the Cadence programing model a workflow is implemented with a function. The function declaration specifies the 77 parameters the workflow accepts as well as any values it might return. 78 79 func SimpleWorkflow(ctx workflow.Context, value string) error 80 81 The first parameter to the function is ctx workflow.Context. This is a required parameter for all workflow functions 82 and is used by the Cadence client library to pass execution context. Virtually all the client library functions that 83 are callable from the workflow functions require this ctx parameter. This **context** parameter is the same concept as 84 the standard context.Context provided by Go. The only difference between workflow.Context and context.Context is that 85 the Done() function in workflow.Context returns workflow.Channel instead of the standard go chan. 86 87 The second string parameter is a custom workflow parameter that can be used to pass in data into the workflow on start. 88 A workflow can have one or more such parameters. All parameters to an workflow function must be serializable, which 89 essentially means that params can’t be channels, functions, variadic, or unsafe pointer. 90 91 Since it only declares error as the return value it means that the workflow does not return a value. The error return 92 value is used to indicate an error was encountered during execution and the workflow should be terminated. 93 94 # Implementation 95 96 In order to support the synchronous and sequential programming model for the workflow implementation there are certain 97 restrictions and requirements on how the workflow implementation must behave in order to guarantee correctness. The 98 requirements are that: 99 100 - Execution must be deterministic 101 - Execution must be idempotent 102 103 A simplistic way to think about these requirements is that the workflow code: 104 105 - Can only read and manipulate local state or state received as return values 106 from Cadence client library functions 107 - Should really not affect changes in external systems other than through 108 invocation of activities 109 - Should interact with time only through the functions provided by the 110 Cadence client library (i.e. workflow.Now(), workflow.Sleep()) 111 - Should not create and interact with goroutines directly, it should instead 112 use the functions provided by the Cadence client library. (i.e. 113 workflow.Go() instead of go, workflow.Channel instead of chan, 114 workflow.Selector instead of select) 115 - Should do all logging via the logger provided by the Cadence client 116 library (i.e. workflow.GetLogger()) 117 - Should not iterate over maps using range as order of map iteration is 118 randomized 119 120 Now that we laid out the ground rules we can take a look at how to implement some common patterns inside workflows. 121 122 # Special Cadence client library functions and types 123 124 The Cadence client library provides a number of functions and types as alternatives to some native Go functions and 125 types. Usage of these replacement functions/types is necessary in order to ensure that the workflow code execution is 126 deterministic and repeatable within an execution context. 127 128 Coroutine related constructs: 129 130 - workflow.Go : This is a replacement for the the go statement 131 - workflow.Channel : This is a replacement for the native chan type. Cadence 132 provides support for both buffered and unbuffered channels 133 - workflow.Selector : This is a replacement for the select statement 134 135 Time related functions: 136 137 - workflow.Now() : This is a replacement for time.Now() 138 - workflow.Sleep() : This is a replacement for time.Sleep() 139 140 # Failing a Workflow 141 142 To mark a workflow as failed, return an error from your workflow function via the err return value. 143 Note that failed workflows do not record the non-error return's value: you cannot usefully return both a 144 value and an error, only the error will be recorded. 145 146 # Ending a Workflow externally 147 148 Inside a workflow, to end you must finish your function by returning a result or error. 149 150 Externally, two tools exist to stop workflows from outside the workflow itself, by using the CLI or RPC client: 151 cancellation and termination. Termination is forceful, cancellation allows a workflow to exit gracefully. 152 153 Workflows can also time out, based on their ExecutionStartToClose duration. A timeout behaves the same as 154 termination (it is a hard deadline on the workflow), but a different close status and final event will be reported. 155 156 # Terminating a Workflow 157 158 Terminating is roughly equivalent to using `kill -9` on a process - the workflow will be ended immediately, 159 and no further decisions will be made. It cannot be prevented or delayed by the workflow, or by any configuration. 160 Any in-progress decisions or activities will fail whenever they next communicate with Cadence's servers, i.e. when 161 they complete or when they next heartbeat. 162 163 Because termination does not allow for any further code to be run, this also means your workflow has no 164 chance to clean up after itself (e.g. running a cleanup Activity to adjust a database record). 165 If you need to run additional logic when your workflow, use cancellation instead. 166 167 # Canceling a Workflow 168 169 Canceling marks a workflow as canceled (this is a one-time, one-way operation), and immediately wakes the workflow 170 up to process the cancellation (schedules a new decision task). When the workflow resumes after being canceled, 171 the context that was passed into the workflow (and thus all derived contexts) will be canceled, which changes the 172 behavior of many workflow.* functions. 173 174 # Canceled workflow.Context behavior 175 176 A workflow's context can be canceled by either canceling the workflow, or calling the cancel-func returned from 177 a worfklow.WithCancel(ctx) call. Both behave identically. 178 179 At any time, you can convert a canceled (or could-be-canceled) context into a non-canceled context by using 180 workflow.NewDisconnectedContext. The resulting context will ignore cancellation from the context it is derived from. 181 Disconnected contexts like this can be created before or after a context has been canceled, and it does not matter 182 how the cancellation occurred. 183 Because this context will not be canceled, this can be useful for using context cancellation as a way to request that 184 some behavior be shut down, while allowing you to run cleanup logic in activities or elsewhere. 185 186 As a general guideline, doing anything with I/O with a canceled context (e.g. executing an activity, starting a 187 child workflow, sleeping) will fail rather than cause external changes. Detailed descriptions are available in 188 documentation on functions that change their behavior with a canceled context; if it does not mention canceled-context 189 behavior, its behavior does not change. 190 For exact behavior, make sure to read the documentation on functions that you are calling. 191 192 As an incomplete summary, these actions will all fail immediately, and the associated error returns (possibly within 193 a Future) will be a workflow.CanceledError: 194 195 - workflow.Await 196 - workflow.Sleep 197 - workflow.Timer 198 199 Child workflows will: 200 201 - ExecuteChildWorkflow will synchronously fail with a CanceledError if canceled before it is called 202 (in v0.18.4 and newer. See https://github.com/uber-go/cadence-client/pull/1138 for details.) 203 - be canceled if the child workflow is running 204 - wait to complete their future.Get until the child returns, and the future will contain the final result 205 (which may be anything that was returned, not necessarily a CanceledError) 206 207 Activities have configurable cancellation behavior. For workflow.ExecuteActivity and workflow.ExecuteLocalActivity, 208 see the activity package's documentation for details. In summary though: 209 210 - ExecuteActivity will synchronously fail with a CanceledError if canceled before it is called 211 - the activity's future.Get will by default return a CanceledError immediately when canceled, 212 unless activityoptions.WaitForCancellation is true 213 - the activity's context will be canceled at the next heartbeat event, or not at all if that does not occur 214 215 And actions like this will be completely unaffected: 216 217 - future.Get 218 (futures derived from the calls above may return a CanceledError, but this is not guaranteed for all futures) 219 - selector.Select 220 (Select is completely unaffected, similar to a native select statement. if you wish to unblock when your 221 context is canceled, consider using an AddReceive with the context's Done() channel, as with a native select) 222 - channel.Send, channel.Receive, and channel.ReceiveAsync 223 (similar to native chan read/write operations, use a selector to wait for send/receive or some other action) 224 - workflow.Go 225 (the context argument in the callback is derived and may be canceled, but this does not stop the goroutine, 226 nor stop new ones from being started) 227 - workflow.GetVersion, workflow.GetLogger, workflow.GetMetricsScope, workflow.Now, many others 228 229 # Execute Activity 230 231 The primary responsibility of the workflow implementation is to schedule activities for execution. The most 232 straightforward way to do that is via the library method workflow.ExecuteActivity: 233 234 ao := workflow.ActivityOptions{ 235 TaskList: "sampleTaskList", 236 ScheduleToCloseTimeout: time.Second * 60, 237 ScheduleToStartTimeout: time.Second * 60, 238 StartToCloseTimeout: time.Second * 60, 239 HeartbeatTimeout: time.Second * 10, 240 WaitForCancellation: false, 241 } 242 ctx = workflow.WithActivityOptions(ctx, ao) 243 244 future := workflow.ExecuteActivity(ctx, SimpleActivity, value) 245 var result string 246 if err := future.Get(ctx, &result); err != nil { 247 return err 248 } 249 250 Before calling workflow.ExecuteActivity(), ActivityOptions must be configured for the invocation. These are for the 251 most part options to customize various execution timeouts. These options are passed in by creating a child context from 252 the initial context and overwriting the desired values. The child context is then passed into the 253 workflow.ExecuteActivity() call. If multiple activities are sharing the same exact option values then the same context 254 instance can be used when calling workflow.ExecuteActivity(). 255 256 The first parameter to the call is the required workflow.Context object. This type is an exact copy of context.Context 257 with the Done() method returning workflow.Channel instead of native go chan. 258 259 The second parameter is the function that we registered as an activity function. This parameter can also be the a 260 string representing the fully qualified name of the activity function. The benefit of passing in the actual function 261 object is that in that case the framework can validate activity parameters. 262 263 The remaining parameters are the parameters to pass to the activity as part of the call. In our example we have a 264 single parameter: **value**. This list of parameters must match the list of parameters declared by the activity 265 function. Like mentioned above the Cadence client library will validate that this is indeed the case. 266 267 The method call returns immediately and returns a workflow.Future. This allows for more code to be executed without 268 having to wait for the scheduled activity to complete. 269 270 When we are ready to process the results of the activity we call the Get() method on the future object returned. The 271 parameters to this method are the ctx object we passed to the workflow.ExecuteActivity() call and an output parameter 272 that will receive the output of the activity. The type of the output parameter must match the type of the return value 273 declared by the activity function. The Get() method will block until the activity completes and results are available. 274 275 The result value returned by workflow.ExecuteActivity() can be retrieved from the future and used like any normal 276 result from a synchronous function call. If the result above is a string value we could use it as follows: 277 278 var result string 279 if err := future.Get(ctx1, &result); err != nil { 280 return err 281 } 282 283 switch result { 284 case “apple”: 285 // do something 286 case “bannana”: 287 // do something 288 default: 289 return err 290 } 291 292 In the example above we called the Get() method on the returned future immediately after workflow.ExecuteActivity(). 293 However, this is not necessary. If we wish to execute multiple activities in parallel we can repeatedly call 294 workflow.ExecuteActivity() store the futures returned and then wait for all activities to complete by calling the 295 Get() methods of the future at a later time. 296 297 To implement more complex wait conditions on the returned future objects, use the workflow.Selector class. Take a look 298 at our Pickfirst sample for an example of how to use of workflow.Selector. 299 300 # Child Workflow 301 302 workflow.ExecuteChildWorkflow enables the scheduling of other workflows from within a workflow's implementation. The 303 parent workflow has the ability to "monitor" and impact the life-cycle of the child workflow in a similar way it can do 304 for an activity it invoked. 305 306 cwo := workflow.ChildWorkflowOptions{ 307 // Do not specify WorkflowID if you want cadence to generate a unique ID for child execution 308 WorkflowID: "BID-SIMPLE-CHILD-WORKFLOW", 309 ExecutionStartToCloseTimeout: time.Minute * 30, 310 } 311 childCtx = workflow.WithChildOptions(ctx, cwo) 312 313 var result string 314 future := workflow.ExecuteChildWorkflow(childCtx, SimpleChildWorkflow, value) 315 if err := future.Get(ctx, &result); err != nil { 316 workflow.GetLogger(ctx).Error("SimpleChildWorkflow failed.", zap.Error(err)) 317 return err 318 } 319 320 Before calling workflow.ExecuteChildWorkflow(), ChildWorkflowOptions must be configured for the invocation. These are 321 for the most part options to customize various execution timeouts. These options are passed in by creating a child 322 context from the initial context and overwriting the desired values. The child context is then passed into the 323 workflow.ExecuteChildWorkflow() call. If multiple activities are sharing the same exact option values then the same 324 context instance can be used when calling workflow.ExecuteChildWorkflow(). 325 326 The first parameter to the call is the required workflow.Context object. This type is an exact copy of context.Context 327 with the Done() method returning workflow.Channel instead of the native go chan. 328 329 The second parameter is the function that we registered as a workflow function. This parameter can also be a string 330 representing the fully qualified name of the workflow function. What's the benefit? When you pass in the actual 331 function object, the framework can validate workflow parameters. 332 333 The remaining parameters are the parameters to pass to the workflow as part of the call. In our example we have a 334 single parameter: value. This list of parameters must match the list of parameters declared by the workflow function. 335 336 The method call returns immediately and returns a workflow.Future. This allows for more code to be executed without 337 having to wait for the scheduled workflow to complete. 338 339 When we are ready to process the results of the workflow we call the Get() method on the future object returned. The 340 parameters to this method are the ctx object we passed to the workflow.ExecuteChildWorkflow() call and an output 341 parameter that will receive the output of the workflow. The type of the output parameter must match the type of the 342 return value declared by the workflow function. The Get() method will block until the workflow completes and results 343 are available. 344 345 The workflow.ExecuteChildWorkflow() function is very similar to the workflow.ExecuteActivity() function. All the 346 patterns described for using the workflow.ExecuteActivity() apply to the workflow.ExecuteChildWorkflow() function as 347 well. 348 349 Child workflows can also be configured to continue to exist once their parent workflow is closed. When using this 350 pattern, extra care needs to be taken to ensure the child workflow is started before the parent workflow finishes. 351 352 cwo := workflow.ChildWorkflowOptions{ 353 // Do not specify WorkflowID if you want cadence to generate a unique ID for child execution 354 WorkflowID: "BID-SIMPLE-CHILD-WORKFLOW", 355 ExecutionStartToCloseTimeout: time.Minute * 30, 356 357 // Do not terminate when parent closes. 358 ParentClosePolicy: client.ParentClosePolicyAbandon, 359 } 360 childCtx = workflow.WithChildOptions(ctx, cwo) 361 362 future := workflow.ExecuteChildWorkflow(childCtx, SimpleChildWorkflow, value) 363 364 // Wait for the child workflow to start 365 if err := future.GetChildWorkflowExecution().Get(ctx, nil); err != nil { 366 // Problem starting workflow. 367 return err 368 } 369 370 # Error Handling 371 372 Activities and child workflows can fail. You could handle errors differently based on different error cases. If the 373 activity returns an error as errors.New() or fmt.Errorf(), those errors will be converted to workflow.GenericError. If the 374 activity returns an error as workflow.NewCustomError("err-reason", details), that error will be converted to 375 *workflow.CustomError. There are other types of errors like workflow.TimeoutError, workflow.CanceledError and workflow.PanicError. 376 So the error handling code would look like: 377 378 err := workflow.ExecuteActivity(ctx, YourActivityFunc).Get(ctx, nil) 379 switch err := err.(type) { 380 case *workflow.CustomError: 381 switch err.Reason() { 382 case "err-reason-a": 383 // handle error-reason-a 384 var details YourErrorDetailsType 385 err.Details(&details) 386 // deal with details 387 case "err-reason-b": 388 // handle error-reason-b 389 default: 390 // handle all other error reasons 391 } 392 case *workflow.GenericError: 393 switch err.Error() { 394 case "err-msg-1": 395 // handle error with message "err-msg-1" 396 case "err-msg-2": 397 // handle error with message "err-msg-2" 398 default: 399 // handle all other generic errors 400 } 401 case *workflow.TimeoutError: 402 switch err.TimeoutType() { 403 case shared.TimeoutTypeScheduleToStart: 404 // handle ScheduleToStart timeout 405 case shared.TimeoutTypeStartToClose: 406 // handle StartToClose timeout 407 case shared.TimeoutTypeHeartbeat: 408 // handle heartbeat timeout 409 default: 410 } 411 case *workflow.PanicError: 412 // handle panic error 413 case *workflow.CanceledError: 414 // handle canceled error 415 default: 416 // all other cases (ideally, this should not happen) 417 } 418 419 # Signals 420 421 Signals provide a mechanism to send data directly to a running workflow. Previously, you had two options for passing 422 data to the workflow implementation: 423 424 - Via start parameters 425 - As return values from activities 426 427 With start parameters, we could only pass in values before workflow execution begins. 428 429 Return values from activities allowed us to pass information to a running workflow, but this approach comes with its 430 own complications. One major drawback is reliance on polling. This means that the data needs to be stored in a 431 third-party location until it's ready to be picked up by the activity. Further, the lifecycle of this activity requires 432 management, and the activity requires manual restart if it fails before acquiring the data. 433 434 Signals, on the other hand, provides a fully asynch and durable mechanism for providing data to a running workflow. 435 When a signal is received for a running workflow, Cadence persists the event and the payload in the workflow history. 436 The workflow can then process the signal at any time afterwards without the risk of losing the information. The 437 workflow also has the option to stop execution by blocking on a signal channel. 438 439 var signalVal string 440 signalChan := workflow.GetSignalChannel(ctx, signalName) 441 442 s := workflow.NewSelector(ctx) 443 s.AddReceive(signalChan, func(c workflow.Channel, more bool) { 444 c.Receive(ctx, &signalVal) 445 workflow.GetLogger(ctx).Info("Received signal!", zap.String("signal", signalName), zap.String("value", signalVal)) 446 }) 447 s.Select(ctx) 448 449 if len(signalVal) > 0 && signalVal != "SOME_VALUE" { 450 return errors.New("signalVal") 451 } 452 453 In the example above, the workflow code uses workflow.GetSignalChannel to open a workflow.Channel for the named signal. 454 We then use a workflow.Selector to wait on this channel and process the payload received with the signal. 455 456 # ContinueAsNew Workflow Completion 457 458 Workflows that need to rerun periodically could naively be implemented as a big for loop with a sleep where the entire 459 logic of the workflow is inside the body of the for loop. The problem with this approach is that the history for that 460 workflow will keep growing to a point where it reaches the maximum size enforced by the service. 461 462 ContinueAsNew is the low level construct that enables implementing such workflows without the risk of failures down the 463 road. The operation atomically completes the current execution and starts a new execution of the workflow with the same 464 workflow ID. The new execution will not carry over any history from the old execution. To trigger this behavior, the 465 workflow function should terminate by returning the special ContinueAsNewError error: 466 467 func SimpleWorkflow(workflow.Context ctx, value string) error { 468 ... 469 return workflow.NewContinueAsNewError(ctx, SimpleWorkflow, value) 470 } 471 472 For a complete example implementing this pattern please refer to the Cron example. 473 474 # SideEffect API 475 476 workflow.SideEffect executes the provided function once, records its result into the workflow history, and doesn't 477 re-execute upon replay. Instead, it returns the recorded result. Use it only for short, nondeterministic code snippets, 478 like getting a random value or generating a UUID. It can be seen as an "inline" activity. However, one thing to note 479 about workflow.SideEffect is that whereas for activities Cadence guarantees "at-most-once" execution, no such guarantee 480 exists for workflow.SideEffect. Under certain failure conditions, workflow.SideEffect can end up executing the function 481 more than once. 482 483 The only way to fail SideEffect is to panic, which causes decision task failure. The decision task after timeout is 484 rescheduled and re-executed giving SideEffect another chance to succeed. Be careful to not return any data from the 485 SideEffect function any other way than through its recorded return value. 486 487 encodedRandom := SideEffect(func(ctx workflow.Context) interface{} { 488 return rand.Intn(100) 489 }) 490 491 var random int 492 encodedRandom.Get(&random) 493 if random < 50 { 494 .... 495 } else { 496 .... 497 } 498 499 # Query API 500 501 A workflow execution could be stuck at some state for longer than expected period. Cadence provide facilities to query 502 the current call stack of a workflow execution. You can use cadence-cli to do the query, for example: 503 504 cadence-cli --domain samples-domain workflow query -w my_workflow_id -r my_run_id -qt __stack_trace 505 506 The above cli command uses __stack_trace as the query type. The __stack_trace is a built-in query type that is 507 supported by cadence client library. You can also add your own custom query types to support thing like query current 508 state of the workflow, or query how many activities the workflow has completed. To do so, you need to setup your own 509 query handler using workflow.SetQueryHandler in your workflow code: 510 511 func MyWorkflow(ctx workflow.Context, input string) error { 512 currentState := "started" // this could be any serializable struct 513 err := workflow.SetQueryHandler(ctx, "state", func() (string, error) { 514 return currentState, nil 515 }) 516 if err != nil { 517 return err 518 } 519 // your normal workflow code begins here, and you update the currentState as the code makes progress. 520 currentState = "waiting timer" 521 err = NewTimer(ctx, time.Hour).Get(ctx, nil) 522 if err != nil { 523 currentState = "timer failed" 524 return err 525 } 526 currentState = "waiting activity" 527 ctx = WithActivityOptions(ctx, myActivityOptions) 528 err = ExecuteActivity(ctx, MyActivity, "my_input").Get(ctx, nil) 529 if err != nil { 530 currentState = "activity failed" 531 return err 532 } 533 currentState = "done" 534 return nil 535 } 536 537 The above sample code sets up a query handler to handle query type "state". With that, you should be able to query with 538 cli: 539 540 cadence-cli --domain samples-domain workflow query -w my_workflow_id -r my_run_id -qt state 541 542 Besides using cadence-cli, you can also issue query from code using QueryWorkflow() API on cadence Client object. 543 544 # Registration 545 546 For some client code to be able to invoke a workflow type, the worker process needs to be aware of all the 547 implementations it has access to. A workflow is registered with the following call: 548 549 workflow.Register(SimpleWorkflow) 550 551 This call essentially creates an in memory mapping inside the worker process between the fully qualified function name 552 and the implementation. It is safe to call this registration method from an **init()** function. If the worker 553 receives tasks for a workflow type it does not know it will fail that task. However, the failure of the task will not 554 cause the entire workflow to fail. 555 556 # Testing 557 558 The Cadence client library provides a test framework to facilitate testing workflow implementations. The framework is 559 suited for implementing unit tests as well as functional tests of the workflow logic. 560 561 The code below implements the unit tests for the SimpleWorkflow sample. 562 563 package sample 564 565 import ( 566 "errors" 567 "testing" 568 569 "code.uber.internal/devexp/cadence-worker/activity" 570 571 "github.com/stretchr/testify/mock" 572 "github.com/stretchr/testify/suite" 573 574 "go.uber.org/cadence/testsuite" 575 ) 576 577 type UnitTestSuite struct { 578 suite.Suite 579 testsuite.WorkflowTestSuite 580 581 env *testsuite.TestWorkflowEnvironment 582 } 583 584 func (s *UnitTestSuite) SetupTest() { 585 s.env = s.NewTestWorkflowEnvironment() 586 } 587 588 func (s *UnitTestSuite) AfterTest(suiteName, testName string) { 589 s.env.AssertExpectations(s.T()) 590 } 591 592 func (s *UnitTestSuite) Test_SimpleWorkflow_Success() { 593 s.env.ExecuteWorkflow(SimpleWorkflow, "test_success") 594 595 s.True(s.env.IsWorkflowCompleted()) 596 s.NoError(s.env.GetWorkflowError()) 597 } 598 599 func (s *UnitTestSuite) Test_SimpleWorkflow_ActivityParamCorrect() { 600 s.env.OnActivity(SimpleActivity, mock.Anything, mock.Anything).Return(func(ctx context.Context, value string) (string, error) { 601 s.Equal("test_success", value) 602 return value, nil 603 }) 604 s.env.ExecuteWorkflow(SimpleWorkflow, "test_success") 605 606 s.True(s.env.IsWorkflowCompleted()) 607 s.NoError(s.env.GetWorkflowError()) 608 } 609 610 func (s *UnitTestSuite) Test_SimpleWorkflow_ActivityFails() { 611 s.env.OnActivity(SimpleActivity, mock.Anything, mock.Anything).Return("", errors.New("SimpleActivityFailure")) 612 s.env.ExecuteWorkflow(SimpleWorkflow, "test_failure") 613 614 s.True(s.env.IsWorkflowCompleted()) 615 616 s.NotNil(s.env.GetWorkflowError()) 617 _, ok := s.env.GetWorkflowError().(*workflow.GenericError) 618 s.True(ok) 619 s.Equal("SimpleActivityFailure", s.env.GetWorkflowError().Error()) 620 } 621 622 func TestUnitTestSuite(t *testing.T) { 623 suite.Run(t, new(UnitTestSuite)) 624 } 625 626 # Setup 627 628 First, we define a "test suite" struct that absorbs both the basic suite functionality from testify 629 http://godoc.org/github.com/stretchr/testify/suite via suite.Suite and the suite functionality from the Cadence test 630 framework via testsuite.WorkflowTestSuite. Since every test in this suite will test our workflow we add a property to 631 our struct to hold an instance of the test environment. This will allow us to initialize the test environment in a 632 setup method. For testing workflows we use a testsuite.TestWorkflowEnvironment. 633 634 We then implement a SetupTest method to setup a new test environment before each test. Doing so ensure that each test 635 runs in it's own isolated sandbox. We also implement an AfterTest function where we assert that all mocks we setup were 636 indeed called by invoking s.env.AssertExpectations(s.T()). 637 638 Finally, we create a regular test function recognized by "go test" and pass the struct to suite.Run. 639 640 # A Simple Test 641 642 The simplest test case we can write is to have the test environment execute the workflow and then evaluate the results. 643 644 func (s *UnitTestSuite) Test_SimpleWorkflow_Success() { 645 s.env.ExecuteWorkflow(SimpleWorkflow, "test_success") 646 647 s.True(s.env.IsWorkflowCompleted()) 648 s.NoError(s.env.GetWorkflowError()) 649 } 650 651 Calling s.env.ExecuteWorkflow(...) will execute the workflow logic and any invoked activities inside the test process. 652 The first parameter to s.env.ExecuteWorkflow(...) is the workflow functions and any subsequent parameters are values 653 for custom input parameters declared by the workflow function. An important thing to note is that unless the activity 654 invocations are mocked or activity implementation replaced (see next section), the test environment will execute the 655 actual activity code including any calls to outside services. 656 657 In the example above, after executing the workflow we assert that the workflow ran through to completion via the call 658 to s.env.IsWorkflowComplete(). We also assert that no errors where returned by asserting on the return value of 659 s.env.GetWorkflowError(). If our workflow returned a value, we we can retrieve that value via a call to 660 s.env.GetWorkflowResult(&value) and add asserts on that value. 661 662 # Activity Mocking and Overriding 663 664 When testing workflows, especially unit testing workflows, we want to test the workflow logic in isolation. 665 Additionally, we want to inject activity errors during our tests runs. The test framework provides two mechanisms that 666 support these scenarios: activity mocking and activity overriding. Both these mechanisms allow you to change the 667 behavior of activities invoked by your workflow without having to modify the actual workflow code. 668 669 Lets first take a look at a test that simulates a test failing via the "activity mocking" mechanism. 670 671 func (s *UnitTestSuite) Test_SimpleWorkflow_ActivityFails() { 672 s.env.OnActivity(SimpleActivity, mock.Anything, mock.Anything).Return("", errors.New("SimpleActivityFailure")) 673 s.env.ExecuteWorkflow(SimpleWorkflow, "test_failure") 674 675 s.True(s.env.IsWorkflowCompleted()) 676 677 s.NotNil(s.env.GetWorkflowError()) 678 _, ok := s.env.GetWorkflowError().(*workflow.GenericError) 679 s.True(ok) 680 s.Equal("SimpleActivityFailure", s.env.GetWorkflowError().Error()) 681 } 682 683 In this test we want to simulate the execution of the activity SimpleActivity invoked by our workflow SimpleWorkflow 684 returning an error. We do that by setting up a mock on the test environment for the SimpleActivity that returns an 685 error. 686 687 s.env.OnActivity(SimpleActivity, mock.Anything, mock.Anything).Return("", errors.New("SimpleActivityFailure")) 688 689 With the mock set up we can now execute the workflow via the s.env.ExecuteWorkflow(...) method and assert that the 690 workflow completed successfully and returned the expected error. 691 692 Simply mocking the execution to return a desired value or error is a pretty powerful mechanism to isolate workflow 693 logic. However, sometimes we want to replace the activity with an alternate implementation to support a more complex 694 test scenario. For our simple workflow lets assume we wanted to validate that the activity gets called with the 695 correct parameters. 696 697 func (s *UnitTestSuite) Test_SimpleWorkflow_ActivityParamCorrect() { 698 s.env.OnActivity(SimpleActivity, mock.Anything, mock.Anything).Return(func(ctx context.Context, value string) (string, error) { 699 s.Equal("test_success", value) 700 return value, nil 701 }) 702 s.env.ExecuteWorkflow(SimpleWorkflow, "test_success") 703 704 s.True(s.env.IsWorkflowCompleted()) 705 s.NoError(s.env.GetWorkflowError()) 706 } 707 708 In this example, we provide a function implementation as the parameter to Return. This allows us to provide an 709 alternate implementation for the activity SimpleActivity. The framework will execute this function whenever the 710 activity is invoked and pass on the return value from the function as the result of the activity invocation. 711 Additionally, the framework will validate that the signature of the "mock" function matches the signature of the 712 original activity function. 713 714 Since this can be an entire function, there really is no limitation as to what we can do in here. In this example, to 715 assert that the "value" param has the same content to the value param we passed to the workflow. 716 */ 717 package workflow