Skip to content

Commit

Permalink
Updated Step Type and Execution Flow [#5]
Browse files Browse the repository at this point in the history
Merge pull request #5 from TanmoySG/dynamic-steps
  • Loading branch information
TanmoySG authored Jul 11, 2023
2 parents 9624cf3 + 384f548 commit b71aa4f
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 158 deletions.
98 changes: 60 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,48 @@ The `Step` type contains the requirments to execute a step function and move to

```go
type Step struct {
Name StepName
Function interface{}
AdditionalArgs []interface{}
NextSteps []Step
NextStepResolver interface{}
ErrorsToRetry []error
StrictErrorCheck bool
SkipRetry bool
MaxAttempts int
RetrySleep time.Duration
Name StepName
Function interface{}
AdditionalArgs []interface{}
NextStep *Step
PossibleNextSteps PossibleNextSteps
NextStepResolver interface{}
ErrorsToRetry []error
StrictErrorCheck bool
SkipRetry bool
MaxAttempts int
RetrySleep time.Duration
}

```

| Field | Description |
|------------------|--------------------------------------------------------------------------------------------------------------------------|
| Name | Name of step |
| Function | The function to execute |
| AdditionalArgs | any additional arguments need to pass to te step |
| NextSteps | Candidate functions for next step (multiple next steps in-case of condition based execution) |
| NextStepResolver | A function that returns the step name, based on conditions, that is used to pick the nextStep from NextSteps |
| ErrorsToRetry | A list of error to retry step for |
| StrictErrorCheck | If set to `true` exact error is matched, else only presence of error is checked |
| SkipRetry | If set to `true` step is not retried for any error |
| MaxAttempts | Max attempts are the number of times the step is tried (first try + subsequent retries). If not set, it'll run 100 times |
| RetrySleep | Sleep duration (type time.Duration) between each re-attempts |
| Field | Description |
|-------------------|------------------------------------------------------------------------------------------------------------------------------|
| Name | Name of step |
| Function | The function to execute |
| AdditionalArgs | any additional arguments need to pass to te step |
| NextStep | Next Step for the current step. If next step needs to be conditional dont set this and use `PossibleNextSteps` field instead |
| PossibleNextSteps | Candidate functions for next step (pick from multiple possible next steps based on condition) |
| NextStepResolver | A function that returns the step name, based on conditions, that is used to pick the NextStep from PossibleNextSteps |
| ErrorsToRetry | A list of error to retry step for |
| StrictErrorCheck | If set to `true` exact error is matched, else only presence of error is checked |
| SkipRetry | If set to `true` step is not retried for any error |
| MaxAttempts | Max attempts are the number of times the step is tried (first try + subsequent retries). If not set, it'll run 100 times |
| RetrySleep | Sleep duration (type time.Duration) between each re-attempts |

### Defining Steps

To define steps, use the `gosteps.Steps` type and link the next steps in the `NextSteps` field as follows
To define steps, use the `gosteps.Steps` type and link the next steps in the `NextStep` field as follows

```go
var steps = gosteps.Steps{
{
Name: "add",
Function: funcs.Add,
AdditionalArgs: []interface{}{2, 3},
NextSteps: gosteps.Steps{
{
Name: "sub",
Function: funcs.Sub,
AdditionalArgs: []interface{}{4},
},
},
var steps = gosteps.Step{
Name: "add",
Function: funcs.Add,
AdditionalArgs: []interface{}{2, 3},
NextStep: gosteps.Steps{
Name: "sub",
Function: funcs.Sub,
AdditionalArgs: []interface{}{4},
},
}
```
Expand All @@ -68,7 +66,31 @@ Here the first step is `Add` and next step (and final) is `Sub`, so the output o

### Conditional Steps

Some steps might have multiple candidates for next step and the executable next step is to be picked based on the output of the current step. To do so, steps with multiple next step candidates must use the `NextStepResolver` field passing a resolver function that returns the Name of the function to use as next step.
Some steps might have multiple candidates for next step and the executable next step is to be picked based on the output of the current step. Define the possible next steps in the `PossibleNextSteps` field, as an array of Steps.

```go
PossibleNextSteps: gosteps.Step{
Function: funcs.Add,
AdditionalArgs: []interface{}{2},
NextStep: &gosteps.Step{
Function: funcs.Sub,
AdditionalArgs: []interface{}{4},
NextStepResolver: nextStepResolver,
PossibleNextSteps: gosteps.PossibleNextSteps{
{
Name: "multiply",
Function: funcs.Multiply,
},
{
Name: "divide",
Function: funcs.Divide,
},
}
}
}
```

To pick the required next step based on conditions, we must use the `NextStepResolver` field passing a resolver function that returns the Name of the function to use as next step.

The resolver function should be of type `func(args ...any) string`, where `args` are the output of current step and returned string is the name of the step to use.

Expand Down Expand Up @@ -147,9 +169,9 @@ If you want to help fix the above constraint or other bugs/issues, feel free to

## Example

In [this example](./example/main.go), we've used a set of complex steps with conditional step and retry. The flow of the same is
In [this example](./example/multistep-example/main.go), we've used a set of complex steps with conditional step and retry. The flow of the same is

![flow](./example/diag.png)
![flow](./example/multistep-example/diag.png)

Execute the example steps

Expand Down
File renamed without changes
46 changes: 46 additions & 0 deletions example/dynamic-steps-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"

gosteps "github.com/TanmoySG/go-steps"
"github.com/TanmoySG/go-steps/example/funcs"
)

func main() {

intsToAdd := []int{1, 4, 7, 10}

var step *gosteps.Step
for _, val := range intsToAdd {
step = addStepToChain(step, funcs.Add, []interface{}{val})
}

finalOutput, err := step.Execute(1)
if err != nil {
fmt.Printf("error executing steps: %s, final output: [%s]\n", err, finalOutput)
}

fmt.Printf("Final Output: [%v]\n", finalOutput)
}

// step to add new next step to step-chain; basically a linked-list insertion
func addStepToChain(step *gosteps.Step, stepFunc interface{}, additionalArgs []interface{}) *gosteps.Step {
temp := gosteps.Step{
Function: stepFunc,
AdditionalArgs: additionalArgs,
}

if step == nil {
step = &temp
return step
}

curr := step
for curr.NextStep != nil {
curr = curr.NextStep
}

curr.NextStep = &temp
return step
}
93 changes: 0 additions & 93 deletions example/main.go

This file was deleted.

Binary file added example/multistep-example/diag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions example/multistep-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"fmt"
"time"

gosteps "github.com/TanmoySG/go-steps"
"github.com/TanmoySG/go-steps/example/funcs"
)

const (
stepMultiply = "Multiply"
stepDivide = "Divide"
)

// reading/maintaining this is a bit tricky will add
// a functional way to create this in the next version
var steps = gosteps.Step{
Function: funcs.Add,
AdditionalArgs: []interface{}{2},
NextStep: &gosteps.Step{
Function: funcs.Sub,
AdditionalArgs: []interface{}{4},
NextStepResolver: nextStepResolver,
PossibleNextSteps: gosteps.PossibleNextSteps{
{
Name: stepMultiply,
Function: funcs.Multiply,
AdditionalArgs: []interface{}{-5},
NextStep: &gosteps.Step{
Function: funcs.Add,
AdditionalArgs: []interface{}{100},
NextStep: &gosteps.Step{
Function: funcs.StepWillError3Times,
ErrorsToRetry: []error{
fmt.Errorf("error"),
},
NextStep: &gosteps.Step{
Function: funcs.StepWillErrorInfinitely,
ErrorsToRetry: []error{
fmt.Errorf("error"),
},
NextStep: &gosteps.Step{
Function: funcs.Multiply,
},
StrictErrorCheck: false,
MaxAttempts: 5, // use gosteps.MaxMaxAttempts for Maximum Possible reattempts
},
MaxAttempts: 5,
RetrySleep: 1 * time.Second,
},
},
},
{
Name: stepDivide,
Function: funcs.Divide,
AdditionalArgs: []interface{}{-2},
},
},
},
}

func main() {
initArgs := []interface{}{1}
finalOutput, err := steps.Execute(initArgs...)
if err != nil {
fmt.Printf("error executing steps: %s, final output: [%s]\n", err, finalOutput)
}

fmt.Printf("Final Output: [%v]\n", finalOutput)
}

// step resolver
func nextStepResolver(args ...any) string {
if args[0].(int) < 0 {
fmt.Printf("StepResolver [%v]: Arguments is Negative, going with Multiply\n", args)
return stepMultiply
}

fmt.Printf("StepResolver [%v]: Arguments is Positive, going with Divide\n", args)
return stepDivide
}
Loading

0 comments on commit b71aa4f

Please sign in to comment.