diff --git a/README.md b/README.md index 3c7a6db..57f1eac 100644 --- a/README.md +++ b/README.md @@ -2,120 +2,15 @@ ![TUI Debugger](assets/am-dbg.gif) -**asyncmachine-go** is a minimal implementation of [AsyncMachine](https://github.com/TobiaszCudnik/asyncmachine) in -Golang using **channels and context**. It aims at simplicity and speed. - -It can be used as a lightweight in-memory [Temporal](https://github.com/temporalio/temporal) alternative, worker for -[Asynq](https://github.com/hibiken/asynq), or to create simple consensus engines, stateful firewalls, telemetry, bots, -etc. - > **asyncmachine-go** is a general purpose state machine for managing complex asynchronous workflows in a safe and -> structured way. - -
- -See am-dbg's states structure and relations - -```go -var States = am.Struct{ - - ///// Input events - - ClientMsg: {Multi: true}, - ConnectEvent: {Multi: true}, - DisconnectEvent: {Multi: true}, - - // user scrolling tx / steps - UserFwd: { - Add: S{Fwd}, - Remove: GroupPlaying, - }, - UserBack: { - Add: S{Back}, - Remove: GroupPlaying, - }, - UserFwdStep: { - Add: S{FwdStep}, - Require: S{ClientSelected}, - Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), - }, - UserBackStep: { - Add: S{BackStep}, - Require: S{ClientSelected}, - Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), - }, - - ///// External state (eg UI) - - TreeFocused: {Remove: GroupFocused}, - LogFocused: {Remove: GroupFocused}, - SidebarFocused: {Remove: GroupFocused}, - TimelineTxsFocused: {Remove: GroupFocused}, - TimelineStepsFocused: {Remove: GroupFocused}, - MatrixFocused: {Remove: GroupFocused}, - DialogFocused: {Remove: GroupFocused}, - StateNameSelected: {Require: S{ClientSelected}}, - HelpDialog: {Remove: GroupDialog}, - ExportDialog: { - Require: S{ClientSelected}, - Remove: GroupDialog, - }, - LogUserScrolled: {}, - Ready: {Require: S{Start}}, - - ///// Actions +> structured way - Start: {}, - TreeLogView: { - Auto: true, - Remove: GroupViews, - }, - MatrixView: {Remove: GroupViews}, - TreeMatrixView: {Remove: GroupViews}, - TailMode: { - Require: S{ClientSelected}, - Remove: GroupPlaying, - }, - Playing: { - Require: S{ClientSelected}, - Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), - }, - Paused: { - Auto: true, - Require: S{ClientSelected}, - Remove: GroupPlaying, - }, +**asyncmachine-go** can be used as a lightweight in-memory [Temporal](https://github.com/temporalio/temporal) +alternative, worker for [Asynq](https://github.com/hibiken/asynq), or to create simple consensus engines, stateful +firewalls, telemetry, bots, etc. - // tx / steps back / fwd - Fwd: { - Require: S{ClientSelected}, - Remove: S{Playing}, - }, - Back: { - Require: S{ClientSelected}, - Remove: S{Playing}, - }, - FwdStep: { - Require: S{ClientSelected}, - Remove: S{Playing}, - }, - BackStep: { - Require: S{ClientSelected}, - Remove: S{Playing}, - }, - - ScrollToTx: {Require: S{ClientSelected}}, - - // client - SelectingClient: {Remove: S{ClientSelected}}, - ClientSelected: { - Remove: S{SelectingClient, LogUserScrolled}, - }, - RemoveClient: {Require: S{ClientSelected}}, -} -``` - -
+The project itself is a minimal implementation of [AsyncMachine](https://github.com/TobiaszCudnik/asyncmachine) +in Golang using **channels and context**. It aims at simplicity and speed. ## Comparison @@ -125,8 +20,8 @@ Common differences with other state machines: - transitions between all the states are allowed - unless constrained - states are connected by relations -- every mutation can be rejected -- error is a state +- every transition can be rejected +- `panic()` is a state ## Usage @@ -176,13 +71,15 @@ type Handlers struct { // negotiation handler func (h *Handlers) ProcessingFileEnter(e *am.Event) bool { // read-only ops (decide if moving fwd is ok) + // no blocking // lock-free critical zone return true } // final handler func (h *Handlers) ProcessingFileState(e *am.Event) { - // read & write ops (but no blocking) + // read & write ops + // no blocking // lock-free critical zone mach := e.Machine // tick-based context @@ -198,7 +95,7 @@ func (h *Handlers) ProcessingFileState(e *am.Event) { mach.AddErr(err) return } - // re-check the ctx after a blocking call + // re-check the tick ctx after a blocking call if stateCtx.Err() != nil { return // expired } @@ -368,9 +265,10 @@ var States = am.Struct{ -### [Temporal FileProcessing workflow](/examples/temporal-fileprocessing/fileprocessing.go) +### [Temporal FileProcessing Workflow](/examples/temporal-fileprocessing/fileprocessing.go) - [origin](https://github.com/temporalio/samples-go/blob/main/fileprocessing/) +- [Asynq worker version](examples/asynq-fileprocessing/fileprocessing_task.go)
@@ -398,8 +296,6 @@ var States = am.Struct{
-### [AM as an Asynq worker](examples/asynq-fileprocessing/fileprocessing_task.go) - ## Documentation - [godoc](https://godoc.org/github.com/pancsta/asyncmachine-go/pkg/machine) @@ -423,7 +319,7 @@ var States = am.Struct{
-Example template for Foo and Bar +Check the example template for Foo and Bar ```go package states @@ -555,8 +451,8 @@ or the [pdf results](https://github.com/pancsta/go-libp2p-pubsub-benchmark/raw/m State-only machine (no handlers, no goroutine). States represent correlations with peer machines. See -[github.com/pancsta/**go-libp2p-pubsub-benchmark**](https://github.com/pancsta/go-libp2p-pubsub-benchmark/tree/main/internal/sim) -and the [pubsub machines](https://github.com/pancsta/go-libp2p-pubsub/tree/psmon-states/states) for more info. +[github.com/pancsta/**go-libp2p-pubsub-benchmark**](https://github.com/pancsta/go-libp2p-pubsub-benchmark?tab=readme-ov-file#libp2p-pubsub-simulator) +for more info. ### am-dbg @@ -566,9 +462,119 @@ am-dbg is a [tview](https://github.com/rivo/tview/) TUI app with a single machin - external state (11 states) - actions (14 states) -This machine features a decent amount of relations within a large number od states and 4 state groups. It's also a good +This machine features a decent amount of relations within a large number of states and 4 state groups. It's also a good example to see how easily an AM-based program can be controller with a script in [tools/cmd/am-dbg-demo](tools/cmd/am-dbg-demo/main.go#L68). +
+ +Check am-dbg's states structure and relations + +```go +// States map defines relations and properties of states. +var States = am.Struct{ + ///// Input events + + ClientMsg: {Multi: true}, + ConnectEvent: {Multi: true}, + DisconnectEvent: {Multi: true}, + + // user scrolling tx / steps + UserFwd: { + Add: S{Fwd}, + Remove: GroupPlaying, + }, + UserBack: { + Add: S{Back}, + Remove: GroupPlaying, + }, + UserFwdStep: { + Add: S{FwdStep}, + Require: S{ClientSelected}, + Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), + }, + UserBackStep: { + Add: S{BackStep}, + Require: S{ClientSelected}, + Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), + }, + + ///// External state (eg UI) + + // focus group + + TreeFocused: {Remove: GroupFocused}, + LogFocused: {Remove: GroupFocused}, + SidebarFocused: {Remove: GroupFocused}, + TimelineTxsFocused: {Remove: GroupFocused}, + TimelineStepsFocused: {Remove: GroupFocused}, + MatrixFocused: {Remove: GroupFocused}, + DialogFocused: {Remove: GroupFocused}, + + StateNameSelected: {Require: S{ClientSelected}}, + HelpDialog: {Remove: GroupDialog}, + ExportDialog: { + Require: S{ClientSelected}, + Remove: GroupDialog, + }, + LogUserScrolled: {}, + Ready: {Require: S{Start}}, + + ///// Actions + + Start: {}, + TreeLogView: { + Auto: true, + Remove: GroupViews, + }, + MatrixView: {Remove: GroupViews}, + TreeMatrixView: {Remove: GroupViews}, + TailMode: { + Require: S{ClientSelected}, + Remove: GroupPlaying, + }, + Playing: { + Require: S{ClientSelected}, + Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), + }, + Paused: { + Auto: true, + Require: S{ClientSelected}, + Remove: GroupPlaying, + }, + + // tx / steps back / fwd + + Fwd: { + Require: S{ClientSelected}, + Remove: S{Playing}, + }, + Back: { + Require: S{ClientSelected}, + Remove: S{Playing}, + }, + FwdStep: { + Require: S{ClientSelected}, + Remove: S{Playing}, + }, + BackStep: { + Require: S{ClientSelected}, + Remove: S{Playing}, + }, + + ScrollToTx: {Require: S{ClientSelected}}, + + // client selection + + SelectingClient: {Remove: S{ClientSelected}}, + ClientSelected: { + Remove: S{SelectingClient, LogUserScrolled}, + }, + RemoveClient: {Require: S{ClientSelected}}, +} +``` + +
+ See [tools/debugger/states](tools/debugger/states) for more info. ## Roadmap @@ -599,8 +605,6 @@ See also [issues](https://github.com/pancsta/asyncmachine-go/issues). Latest release: `v0.5.0` -## [v0.5.0](https://github.com/pancsta/asyncmachine-go/tree/v0.5.0) (2024-06-06) - - feat: add tools/cmd/am-gen [\#63](https://github.com/pancsta/asyncmachine-go/pull/63) (@pancsta) - feat\(am-dbg\): add `--select-connected` and `--clean-on-connect` [\#62](https://github.com/pancsta/asyncmachine-go/pull/62) (@pancsta) @@ -613,7 +617,7 @@ Latest release: `v0.5.0` - feat\(machine\): add empty roadmap methods [\#55](https://github.com/pancsta/asyncmachine-go/pull/55) (@pancsta) - feat\(machine\): add Eval [\#54](https://github.com/pancsta/asyncmachine-go/pull/54) (@pancsta) - refac\(pkg/machine\): rename many identifiers, shorten [\#53](https://github.com/pancsta/asyncmachine-go/pull/53) (@pancsta) -- feat\(machine\): drop add dependencies \(lo, uuid\) [\#52](https://github.com/pancsta/asyncmachine-go/pull/52) (@pancsta) +- feat\(machine\): drop all dependencies \(lo, uuid\) [\#52](https://github.com/pancsta/asyncmachine-go/pull/52) (@pancsta) - feat\(machine\): alloc handler goroutine on demand [\#51](https://github.com/pancsta/asyncmachine-go/pull/51) (@pancsta) - feat\(machine\): add Transition.ClocksAfter [\#50](https://github.com/pancsta/asyncmachine-go/pull/50) (@pancsta) - feat\(machine\): add HasStateChangedSince [\#49](https://github.com/pancsta/asyncmachine-go/pull/49) (@pancsta) diff --git a/assets/prometheus-grafana.light.png b/assets/prometheus-grafana.light.png index 71fd714..07cf91d 100644 Binary files a/assets/prometheus-grafana.light.png and b/assets/prometheus-grafana.light.png differ diff --git a/pkg/machine/machine.go b/pkg/machine/machine.go index f77cf18..45d0231 100644 --- a/pkg/machine/machine.go +++ b/pkg/machine/machine.go @@ -1,12 +1,12 @@ -// Package machine is a dependency-free implementation of AsyncMachine in Golang -// using channels and context. It aims at simplicity and speed. +// Package machine is a general purpose state machine for managing complex +// async workflows in a safe and structured way. // // It can be used as a lightweight in-memory Temporal [1] alternative, worker // for Asynq [2], or to write simple consensus engines, stateful firewalls, // telemetry, bots, etc. // -// asyncmachine-go is a general purpose state machine for managing complex -// async workflows in a safe and structured way. +// The project itself is a minimal implementation of AsyncMachine in Golang +// using channels and context. It aims at simplicity and speed. // // [1]: https://github.com/temporalio/temporal // [2]: https://github.com/hibiken/asynq diff --git a/pkg/machine/machine_test.go b/pkg/machine/machine_test.go index 1d62c02..1ba9ee8 100644 --- a/pkg/machine/machine_test.go +++ b/pkg/machine/machine_test.go @@ -10,25 +10,23 @@ import ( "github.com/stretchr/testify/assert" ) -// TODO ExampleNew func ExampleNew() { t := &testing.T{} // Replace this with actual *testing.T in real test cases initialState := S{"A"} mach := NewNoRels(t, initialState) - // Use the machine m here + // machine mach ready _ = mach } -// TODO ExampleNewCommon func ExampleNewCommon() { t := &testing.T{} // Replace this with actual *testing.T in real test cases initialState := S{"A"} mach := NewNoRels(t, initialState) - // Use the machine m here + // machine mach ready _ = mach } diff --git a/pkg/machine/misc.go b/pkg/machine/misc.go index 4e3ef24..48ea8f0 100644 --- a/pkg/machine/misc.go +++ b/pkg/machine/misc.go @@ -217,8 +217,8 @@ type Mutation struct { Eval func() } -// StateWasCalled returns true if the Mutation was called with the passed -// state (or does it come from relations). +// StateWasCalled returns true if the Mutation was called (directly) with the +// passed state (in opposite to it coming from an `Add` relation). func (m Mutation) StateWasCalled(state string) bool { return slices.Contains(m.CalledStates, state) } diff --git a/pkg/machine/transition.go b/pkg/machine/transition.go index 3edf1ba..52f41d8 100644 --- a/pkg/machine/transition.go +++ b/pkg/machine/transition.go @@ -26,7 +26,7 @@ func newSteps(from string, toStates S, stepType StepType, return ret } -// Transition represents processing of a single mutation withing a machine. +// Transition represents processing of a single mutation within a machine. type Transition struct { ID string // List of steps taken by this transition (so far). @@ -41,15 +41,15 @@ type Transition struct { // clocks of the states from after the transition // TODO timeAfter, produce Clocks via ClockAfter(), add index diffs ClocksAfter Clocks - // Struct with "enter" handlers to execute + // State names with "enter" handlers to execute Enters S - // Struct with "exit" handlers to executed + // State names with "exit" handlers to executed Exits S // target states after parsing the relations TargetStates S // was the transition accepted (during the negotiation phase) Accepted bool - // Mutation call which cased this transition + // Mutation call which caused this transition Mutation *Mutation // Parent machine Machine *Machine diff --git a/tools/debugger/states/ss_dbg.go b/tools/debugger/states/ss_dbg.go index c5e1d00..426cf1e 100644 --- a/tools/debugger/states/ss_dbg.go +++ b/tools/debugger/states/ss_dbg.go @@ -8,7 +8,7 @@ type S = am.S // States map defines relations and properties of states. var States = am.Struct{ - ///// Input events + // /// Input events ClientMsg: {Multi: true}, ConnectEvent: {Multi: true}, @@ -34,7 +34,9 @@ var States = am.Struct{ Remove: am.SMerge(GroupPlaying, S{LogUserScrolled}), }, - ///// External state (eg UI) + // /// External state (eg UI) + + // focus group TreeFocused: {Remove: GroupFocused}, LogFocused: {Remove: GroupFocused}, @@ -43,8 +45,9 @@ var States = am.Struct{ TimelineStepsFocused: {Remove: GroupFocused}, MatrixFocused: {Remove: GroupFocused}, DialogFocused: {Remove: GroupFocused}, - StateNameSelected: {Require: S{ClientSelected}}, - HelpDialog: {Remove: GroupDialog}, + + StateNameSelected: {Require: S{ClientSelected}}, + HelpDialog: {Remove: GroupDialog}, ExportDialog: { Require: S{ClientSelected}, Remove: GroupDialog, @@ -52,7 +55,7 @@ var States = am.Struct{ LogUserScrolled: {}, Ready: {Require: S{Start}}, - ///// Actions + // /// Actions Start: {}, TreeLogView: { @@ -76,6 +79,7 @@ var States = am.Struct{ }, // tx / steps back / fwd + Fwd: { Require: S{ClientSelected}, Remove: S{Playing}, @@ -95,7 +99,8 @@ var States = am.Struct{ ScrollToTx: {Require: S{ClientSelected}}, - // client + // client selection + SelectingClient: {Remove: S{ClientSelected}}, ClientSelected: { Remove: S{SelectingClient, LogUserScrolled}, @@ -121,7 +126,7 @@ var ( } ) -//#region boilerplate defs +// #region boilerplate defs // Names of all the states (pkg enum). @@ -176,7 +181,7 @@ const ( // Names is an ordered list of all the state names. var Names = S{ - ///// Input events + // /// Input events ClientMsg, ConnectEvent, @@ -188,7 +193,7 @@ var Names = S{ UserFwdStep, UserBackStep, - ///// External state (eg UI) + // /// External state (eg UI) TreeFocused, LogFocused, @@ -203,7 +208,7 @@ var Names = S{ LogUserScrolled, Ready, - ///// Actions + // /// Actions Start, TreeLogView, @@ -229,4 +234,4 @@ var Names = S{ am.Exception, } -//#endregion +// #endregion