Skip to content

Commit

Permalink
GoSteps Initial Library [#1]
Browse files Browse the repository at this point in the history
Merge pull request #1 from TanmoySG/steps-init

In this PR,
* Added GoSteps code to run functions as steps
* Added step Retry-ability
* Added documentation
* Added example
  • Loading branch information
TanmoySG authored Jul 6, 2023
2 parents c940921 + f245dea commit df6e2fe
Show file tree
Hide file tree
Showing 8 changed files with 463 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @TanmoySG
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Go workspace file
go.work

# macOS junk
.DS_Store
175 changes: 173 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,173 @@
# go-steps
[WIP] A Go Library for executing functions-as-steps


# GoSteps

GoSteps is a go library that helps in running functions as steps and reminds you to step out and get active (kidding!).

<p align="center">
<img src="https://codetips.dev/wp-content/uploads/2022/08/gopher-go-run.jpg" alt="Sublime's custom image" width="300" height="300" />
</p>

The idea behind `gosteps` is to define set of functions as steps-chain (kind of a linked list) and execute them in a sequential fashion by piping output (other than error) from previous step, as arguments, into the next steps (not necessarily using the args).

## Usage

The `Step` type contains the requirments to execute a step function and move to next one.

```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
}

```

| 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 |

### Defining Steps

To define steps, use the `gosteps.Steps` type and link the next steps in the `NextSteps` 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},
},
},
},
}
```

Here the first step is `Add` and next step (and final) is `Sub`, so the output of Add is piped to Sub and that gives the final output.

### 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.

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.

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

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

### Executing Steps

To execute steps use the `Execute(initArgs ...any)` method, passing the (optional) initializing arguments.

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

func main() {
initArgs := []interface{}{1, 2}
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)
}
```

### Retrying for Error

To retry a step for particular erors, use the `ErrorsToRetry` field passing the list of errors. To make sure the error matches exactly as that of the Errors to retry, pass `true` for the `StrictErrorCheck` field, otherwise only error-substring presense will be checked.

```go
{
ErrorsToRetry: []error{
fmt.Errorf("error to retry"),
},
StrictErrorCheck: true,
MaxAttempts: 5,
RetrySleep: 1 * time.Second,
}
```

To skip retry on error pass `true` to the `SkipRetry` field.

Additionally,

- To limit the number of tries, use the `MaxAttempts` field, passing the number of max tries. If not set then Default Max Attempts (of 100 tries) is used. To avoid infinite runs due to the MaxAttempts not being set, we're keeping the default attempts to 100. If required, import and use the `gosteps.MaxMaxAttempts`. Please note that the Max value is `9223372036854775807`, which is not infinite but a huge number of attempts, please use cautiously.
- To add sleep between each attempts, use the `RetrySleep` parameter passing the duration of sleep (of type time.Duration) like `2 * time.Second`.

## Constraints

To keep the step execution same, all step functions must be of type `func(args ..any) ([]interface{}, error)`

Here all arguments passed to the step function need to be type asserted within the step function, as

```go
func Multiply(args ...any) ([]interface{}, error) {
return []interface{}{args[0].(int) * args[1].(int)}, nil
}
```

The step function must also return all parameters (other than error) as type `[]interface{ret1, ret2, ...}` and error must be returned for all functions even if nil.

### Help Wanted

If you want to help fix the above constraint or other bugs/issues, feel free to raise an Issue or Pull Request with the changes. It'd be an immense help!

## 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

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

Execute the example steps

```sh
go run example/main.go

// output
Adding [1 2]
Sub [3 4]
StepResolver [[-1]]: Arguments is Negative, going with Multiply
Multiply [-1 -5]
Adding [5 100]
Running fake error function for arg [[105]]
Running fake error function for arg [[105]]
Running fake error function for arg [[105]]
Running fake error function for arg [[105]]
Multiply [3150 5250]
Final Output: [[16537500]]
```

*Please Note:* This example flow might change and can be out-of-date. Please check the example.
Binary file added 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.
42 changes: 42 additions & 0 deletions example/funcs/funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package funcs

import "fmt"

var itr int = 1

func Add(args ...any) ([]interface{}, error) {
fmt.Printf("Adding %v\n", args)
return []interface{}{args[0].(int) + args[1].(int)}, nil
}

func Sub(args ...any) ([]interface{}, error) {
fmt.Printf("Sub %v\n", args)
return []interface{}{args[0].(int) - args[1].(int)}, nil
}

func Multiply(args ...any) ([]interface{}, error) {
fmt.Printf("Multiply %v\n", args)
return []interface{}{args[0].(int) * args[1].(int)}, nil
}

func Divide(args ...any) ([]interface{}, error) {
fmt.Printf("Divide %v\n", args)
return []interface{}{args[0].(int) / args[1].(int)}, nil
}

// Step will error 3times and return arg*30 and arg*31 on the 4th try
func StepWillError3Times(args ...any) ([]interface{}, error) {
fmt.Printf("Running fake error function for arg [%v]\n", args)
if itr == 3 {
return []interface{}{args[0].(int) * 30, args[0].(int) * 50}, nil
}

itr += 1
return nil, fmt.Errorf("error to retry")
}

// Step will error infinitely
func StepWillErrorInfinitely(args ...any) ([]interface{}, error) {
fmt.Printf("Running infinite fake error function for arg [%v]\n", args)
return nil, fmt.Errorf("error to retry")
}
93 changes: 93 additions & 0 deletions example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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.Steps{
{
Function: funcs.Add,
NextSteps: gosteps.Steps{
{
Function: funcs.Sub,
AdditionalArgs: []interface{}{4},
NextStepResolver: nextStepResolver,
NextSteps: gosteps.Steps{
{
Name: stepMultiply,
Function: funcs.Multiply,
AdditionalArgs: []interface{}{-5},
NextSteps: gosteps.Steps{
{
Function: funcs.Add,
AdditionalArgs: []interface{}{100},
NextSteps: gosteps.Steps{
{
Function: funcs.StepWillError3Times,
ErrorsToRetry: []error{
fmt.Errorf("error"),
},
NextSteps: gosteps.Steps{
{
Function: funcs.StepWillErrorInfinitely,
ErrorsToRetry: []error{
fmt.Errorf("error"),
},
NextSteps: gosteps.Steps{
{
Function: funcs.Multiply,
},
},
StrictErrorCheck: true,
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, 2}
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 df6e2fe

Please sign in to comment.