statemachine

package
v0.0.0-...-b078d35 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2024 License: MIT Imports: 11 Imported by: 0

README

StateMachine - The Functional State Machine for Go

GoDoc Go Report Card

Introduction

This package provides an implementation of a routable statemachine.

This package is designed with inspiration from Rob Pike's talk on Lexical Scanning in Go.

This package incorporates support for OTEL tracing.

You can read about the advantages of statemachine design for sequential processing at: https://medium.com/@johnsiilver/go-state-machine-patterns-3b667f345b5e

If you are interested in a parallel and concurrent state machine, checkout the stagedpipe package.

Usage

The import path is github.com/gostdlib/ops/statemachine.

Simple example:


type data struct {
	Name string
}

func printName(r statemachine.Request[data]) statemachine.Request[data] {
	if r.Data.Name == "" {
		r.Err = errors.New("Name is empty")
		return r // <- This will stop the state machine due to the error
	}

	fmt.Println(r.Data.Name)
	r.Next = writeFile // <- Route to the next state
}

func writeFile(r statemachine.Request[data]) statemachine.Request[data] {
	fileName := r.Data.Name + ".txt"
	r.Event("writeFile", "fileName", fileName)
	err := os.WriteFile(fileName, []byte(r.Data.Name), 0644)
	if err != nil {
		// This will write an error event to the OTEL trace, if present.
		// Retruned Errors are automatically recorded in the OTEL trace.
		r.Event("writeFile", "fileName", fileName, "error", err.Error())
		r.Err = err
		return r
	}
	r.Event("writeFile", "fileName", fileName, "success", true)
	return r // <- This will stop the state machine because we did not set .Next
}

func NameHandler(name string) error {
	return stateMachine.Run(statemachine.Request{
		Data: data{
			Name: name,
		},
		Next: printName,
	})
}

This is a simple example of how to use the state machine. The state machine will run the printName function and then the writeFile function. You could route to other states based on the data in the request. You can change the data in the request inside the state functions. This means that you can use stack allocated data instead of heap allocated data.

Use https://pkg.go.dev/github.com/gostdlib/ops/statemachine to view the documentation.

Contributing

This package is a part of the gostdlib project. The gostdlib project is a collection of packages that should useful to many Go projects.

Please see guidelines for contributing to the gostdlib project.

Documentation

Overview

Package statemachine provides a simple routing state machine implementation. This is useful for implementing complex state machines that require routing logic. The state machine is implemented as a series of state functions that take a Request objects and Request object. The Request returned has the next State to execute or an error. An error causes the state machine to stop and return the error. A nil state causes the state machine to stop.

You may build a state machine using either function calls or method calls. The Request.Data object you define can be a stack or heap allocated object. Using a stack allocated object is useful when running a lot of state machines in parallel, as it reduces the amount of memory allocation and garbage collection required.

State machines of this design can reduce testing complexity and improve code readability. You can read about how here: https://medium.com/@johnsiilver/go-state-machine-patterns-3b667f345b5e

This package is has OTEL support built in. If the Context passed to the state machine has a span, the state machine will create a child span for each state. If the state machine returns an error, the span will be marked as an error.

For complex state machines where you want to leverage concurrent and parallel processing, you may want to use the stagedpipe package at: https://pkg.go.dev/github.com/gostdlib/concurrency/pipelines/stagedpipe

Example:

	package main

	import (
		"context"
		"fmt"
		"io"
		"log"
		"net/http"

		"github.com/gostdlib/ops/statemachine"
	)

	var (
		author = flag.String("author", "", "The author of the quote, if not set will choose a random one")
	)

	// Data is the data passed to through the state machine. It can be modified by the state functions.
	type Data struct {
		// This section is data set before starting the state machine.

		// Author is the author of the quote. If not set it will be chosen at random.
		Author string

		// This section is data set during the state machine.

		// Quote is a quote from the author. It is set in the state machine.
		Quote string

		// httpClient is the http client used to make requests.
		httpClient *http.Client
	}

	func Start(req statemachine.Request[Data]) statemachine.Request[Data] {
		if req.Data.httpClient == nil {
			req.Data.httpClient = &http.Client{}
		}

		if req.Data.Author == "" {
			req.Next = RandomAuthor
			return req
		}
		req.Next = RandomQuote
		return req
	}

	func RandomAuthor(req statemachine.Request[Data]) statemachine.Request[Data] {
		const url = "https://api.quotable.io/randomAuthor" // This is a fake URL
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			req.Err = err
			return req
		}

		req = req.WithContext(ctx)
		resp, err := args.Data.httpClient.Do(req)
		if err != nil {
			req.Err = err
			return req
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			req.Err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
			return req
		}
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			req.Err = err
			return req
		}
		args.Data.Author = string(b)
		req.Next = RandomQuote
		return req
	}

	func RandomQuote(req statemachine.Request[Data]) statemachine.Request[Data] {
		const url = "https://api.quotable.io/randomQuote" // This is a fake URL
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			req.Err = err
			return req
		}

		req = req.WithContext(ctx)
		resp, err := args.Data.httpClient.Do(req)
		if err != nil {
			req.Err = err
			return req
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			req.Err = fmt.Errorf("unexpected status code: %d", resp.StatusCode)
			return req
		}
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			req.Err = err
			return req
		}
		args.Data.Quote = string(b)
		req.Next = nil  // This is not needed, but a good way to show that the state machine is done.
		return req
	}

	func main() {
		flag.Parse()

		req := statemachine.Request{
  			Ctx: context.Background(),
     			Data: Data{
				Author: *author,
				httpClient: &http.Client{},
			},
   			Next: Start,
		}

		err := statemachine.Run("Get author quotes", req)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(data.Author, "said", data.Quote)
	}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option[T any] func(Request[T]) (Request[T], error)

Option is an option for the Run() function. This is currently unused, but exists for future expansion.

type Request

type Request[T any] struct {

	// Ctx is the context passed to the state function.
	Ctx context.Context

	// Data is the data to be passed to the next state.
	Data T

	// Err is the error to be returned by the state machine. If Err is not nil, the state machine stops.
	Err error

	// Next is the next state to be executed. If Next is nil, the state machine stops.
	// Must be set to the initial state to execute before calling Run().
	Next State[T]
	// contains filtered or unexported fields
}

Request are the request passed to a state function.

func Run

func Run[T any](name string, req Request[T], options ...Option[T]) (Request[T], error)

Run runs the state machine with the given a Request. name is the name of the statemachine for the purpose of OTEL tracing. An error is returned if the state machine fails, name is empty, the Request Ctx/Next is nil or the Err field is not nil.

func (Request[T]) Event

func (r Request[T]) Event(name string, keyValues ...any) error

Event records an OTEL event into the Request span with name and keyvalues. This allows for stages in your statemachine to record information in each state. keyvalues must be an even number with every even value a string representing the key, with the following value representing the value associated with that key. The following values are supported:

- bool/[]bool - float64/[]float64 - int/[]int - int64/[]int64 - string/[]string - time.Duration/[]time.Duration

Note: This is a no-op if the Request is not recording.

type State

type State[T any] func(req Request[T]) Request[T]

State is a function that takes a Request and returns a Request. If the returned Request has a nil Next, the state machine stops. If the returned Request has a non-nil Err, the state machine stops and returns the error. If the returned Request has a non-nil next, the state machine continues with the next state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL