package machineimport ()// ///// ///// /////// ///// TIME// ///// ///// /////// Time// Time is machine time, an ordered list of state ticks. It's like Clock, but// indexed by int, instead of string.// TODO use math/big?typeTime []uint64// Increment adds 1 to a state's tick valuefunc ( Time) ( int) Time { := make(Time, len())copy(, )if < len() { []++ }return}// String returns an integer representation of the time slice.func ( Time) () string { := ""for , := range {if != "" { += " " } += strconv.Itoa(int()) }return}// Add sums 2 instances of Time and returns a new one.func ( Time) ( Time) Time { := make(Time, len())iflen() != len() {return }for := range { [] = [] + [] }return}// ToIndex returns a string-indexed version of Time.func ( Time) ( S) *TimeIndex {return &TimeIndex{Time: ,Index: , }}// Filter returns a subset of the Time slice for the given state indexes.// It's not advised to slice an already sliced time slice.func ( Time) ( []int) Time { := make(Time, len())for , := range {if >= len() {continue } [] = [] }return}// Sum returns a sum of ticks for each state in Time, or narrowed down to// [idxs].func ( Time) ( []int) uint64 {// total sumif == nil {varuint64for , := range { += }return }// selective sumvaruint64for , := range {if >= len() {continue } += [] }return}// DiffSince returns the number of ticks for each state in Time since the// passed machine time.func ( Time) ( Time) Time { := make(Time, len())iflen() != len() {return }for := range { [] = [] - [] }return}// NonZeroStates returns a list of state indexes with non-zero ticks in this// machine time slice.func ( Time) () []int { := make([]int, 0, len())for , := range {if != 0 { = append(, ) } }return}// After returns true if at least 1 tick in time1 is after time2, optionally// accepting equal values for true. Requires a deterministic states// order, eg by using [Machine.VerifyStates].func ( Time) ( bool, Time) bool { := len()for , := range {// shorter time2 cant be after time1if <= {break }if < [] || ( == [] && !) {returnfalse } }returntrue}// Before returns true if at least 1 tick in time1 is before time2, optionally// accepting equal values for true. Requires a deterministic states// order, eg by using [Machine.VerifyStates].func ( Time) ( bool, Time) bool { := len()for , := range {// shorter time2 cant be after time1if <= {break }if > [] || ( == [] && !) {returnfalse } }returntrue}// Equal checks if time1 is equal to time2. Requires a deterministic states// order, eg by using [Machine.VerifyStates].func ( Time) ( bool, Time) bool {if && len() != len() {returnfalse }for , := range {if != [] {returnfalse } }returntrue}// Time - state checking// Tick is [Machine.Tick] but for an int-based time slice.func ( Time) ( int) uint64 {// out of bound falls back to 0iflen() <= {return0 }return []}// Is1 is [Machine.Is1] but for an int-based time slice.func ( Time) ( int) bool {if == -1 || >= len() {returnfalse }returnIsActiveTick([])}// Is is [Machine.Is] but for an int-based time slice.func ( Time) ( []int) bool {iflen() == 0 {returnfalse }for , := range {// -1 is not found or mach disposedif == -1 {returnfalse }if !IsActiveTick([]) {returnfalse } }returntrue}// Not is [Machine.Not] but for an int-based time slice.func ( Time) ( []int) bool {iflen() == 0 {returntrue }for , := range {// -1 is not found or mach disposedif != -1 && IsActiveTick([]) {returnfalse } }returntrue}// Not1 is [Machine.Not1] but for an int-based time slice.func ( Time) ( int) bool {if == -1 || >= len() {returnfalse }return !IsActiveTick([])}// Any is [Machine.Any] but for an int-based time slice.func ( Time) ( ...[]int) bool {for , := range {if .Is() {returntrue } }returnfalse}// Any1 is [Machine.Any1] but for an int-based time slice.func ( Time) ( ...int) bool {for , := range {if .Is1() {returntrue } }returnfalse}// ActiveStates returns a list of active state indexes in this machine time// slice. When idxs isn't nil, only the passed indexes are considered.func ( Time) ( []int) []int { := make([]int, 0, len())for , := range {if !IsActiveTick() {continue } = append(, ) }return}// TimeIndex// TimeIndex is [Time] with a bound state index (list of state names). It's not// suitable for storage, use [Time] instead. See [Clock] for a simpler type with// ticks indexes by state names.typeTimeIndexstruct {Time Index S}func ( S, []int) *TimeIndex { := &TimeIndex{Index: ,Time: make(Time, len()), }for , := range { .Time[] = 1 }return}// String returns a string representation of the time slice.func ( TimeIndex) () string {returnj(.ActiveStates(nil))}// StateName returns the name of the state at the given index.func ( TimeIndex) ( int) string {if >= len(.Index) {return"" }return .Index[]}// Sum is [Time.Sum] but for a sting-based time slice.func ( TimeIndex) ( S) uint64 {return .Time.Sum(StatesToIndex(.Index, ))}// Filter is [Time.Filter] but for a sting-based time slice.func ( TimeIndex) ( S) *TimeIndex {return .Time.Filter(StatesToIndex(.Index, )).ToIndex()}// NonZeroStates is [Time.NonZeroStates] but for a sting-based time slice.func ( TimeIndex) () S {returnIndexToStates(.Index, .Time.NonZeroStates())}// TimeIndex - state checking// Is is [Machine.Is] but for a sting-based time slice.func ( TimeIndex) ( S) bool {return .Time.Is(StatesToIndex(.Index, ))}// Is1 is [Machine.Is1] but for a sting-based time slice.func ( TimeIndex) ( string) bool {return .Time.Is(StatesToIndex(.Index, S{}))}// Not is [Machine.Not] but for a sting-based time slice.func ( TimeIndex) ( S) bool {return .Time.Not(StatesToIndex(.Index, ))}// Not1 is [Machine.Not1] but for a sting-based time slice.func ( TimeIndex) ( string) bool {return .Time.Not(StatesToIndex(.Index, S{}))}// Any is [Machine.Any] but for a sting-based time slice.func ( TimeIndex) ( ...string) bool { := make([][]int, len())for , := range { [] = StatesToIndex(.Index, S{}) }return .Time.Any(...)}// Any1 is [Machine.Any1] but for a sting-based time slice.func ( TimeIndex) ( ...string) bool {return .Time.Any1(StatesToIndex(.Index, )...)}// ActiveStates is [Machine.ActiveStates] but for a sting-based time slice.func ( TimeIndex) ( S) S { := S{}for , := range .Time {if !IsActiveTick() {continue } := "unknown" + strconv.Itoa()iflen(.Index) > { = .Index[] }if != nil && !slices.Contains(, ) {continue } = append(, ) }return}// ///// ///// /////// ///// LOGGING, TRACING// ///// ///// /////// Contexttype (CtxKeyNamestruct{}CtxValuestruct { Id string State string Tick uint64 })varCtxKey = &CtxKeyName{}// LoggerFn is a logging function for the machine.typeLoggerFnfunc(level LogLevel, msg string, args ...any)// LogArgsMapperFn is a function that maps arguments to be logged. Useful for// debugging. See NewArgsMapper.typeLogArgsMapperFnfunc(args A) map[string]stringtypeLogEntrystruct { Level LogLevel Text string}// LogLevel enumtypeLogLevelintconst (// LogNothing means no logging, including external msgs.LogNothingLogLevel = iota// LogExternal will show ony external user msgs.LogExternal// LogChanges means logging state changes and external msgs.LogChanges// LogOps means LogChanges + logging all the operations.LogOps// LogDecisions means LogOps + logging all the decisions behind them.LogDecisions// LogEverything means LogDecisions + all event and handler names, and more.LogEverything)func ( LogLevel) () string {switch {caseLogNothing:fallthroughdefault:return"nothing"caseLogExternal:return"external"caseLogChanges:return"changes"caseLogOps:return"ops"caseLogDecisions:return"decisions"caseLogEverything:return"everything" }}// SemLogger is a semantic logger for structured events. It's consist of:// - enable / enabled methods// - text logger utils// - setters for external semantics (eg pipes)// It's WIP, and eventually it will replace (but not remove) the text logger.typeSemLoggerinterface {// TODO implement empty methods // TODO add SetTag, RemoveTag, JoinTopic, LeaveTopic, custom graph // links / edges// graph// AddPipeOut informs that [sourceState] has been piped out into [targetMach]. // The name of the target state is unknown.AddPipeOut(addMut bool, sourceState, targetMach string)// AddPipeIn informs that [targetState] has been piped into this machine from // [sourceMach]. The name of the source state is unknown.AddPipeIn(addMut bool, targetState, sourceMach string)// RemovePipes removes all pipes for the passed machine ID.RemovePipes(machId string)// details// IsCan return true when the machine is logging Can* methods.IsCan() bool// EnableCan enables / disables logging of Can* methods.EnableCan(enable bool)// IsSteps return true when the machine is logging transition steps.IsSteps() bool// EnableSteps enables / disables logging of transition steps.EnableSteps(enable bool)// IsGraph returns true when the machine is logging graph structures.IsGraph() bool// EnableGraph enables / disables logging of graph structures.EnableGraph(enable bool)// EnableId enables or disables the logging of the machine's ID in log // messages.EnableId(val bool)// IsId returns true when the machine is logging the machine's ID in log // messages.IsId() bool// EnableQueued enables or disables the logging of queued mutations.EnableQueued(val bool)// IsQueued returns true when the machine is logging queued mutations.IsQueued() bool// EnableStateCtx enables or disables the logging of active state contexts.EnableStateCtx(val bool)// IsStateCtx returns true when the machine is logging active state contexts.IsStateCtx() bool// EnableWhen enables or disables the logging of "when" methods.EnableWhen(val bool)// IsWhen returns true when the machine is logging "when" methods.IsWhen() bool// EnableArgs enables or disables the logging known args.EnableArgs(val bool)// IsArgs returns true when the machine is logging known args.IsArgs() bool// logger// SetLogger sets a custom logger function.SetLogger(logger LoggerFn)// Logger returns the current custom logger function or nil.Logger() LoggerFn// SetLevel sets the log level of the machine.SetLevel(lvl LogLevel)// Level returns the log level of the machine.Level() LogLevel// SetEmpty creates an empty logger that does nothing and sets the log // level in one call. Useful when combined with am-dbg. Requires LogChanges // log level to produce any output.SetEmpty(lvl LogLevel)// SetSimple takes log.Printf and sets the log level in one // call. Useful for testing. Requires LogChanges log level to produce any // output.SetSimple(logf func(format string, args ...any), level LogLevel)// SetArgsMapper accepts a function which decides which mutation arguments // to log. See NewArgsMapper or create your own manually.SetArgsMapper(mapper LogArgsMapperFn)// ArgsMapper returns the current log args mapper function.ArgsMapper() LogArgsMapperFn}// SemConfig defines a config for SemLogger.typeSemConfigstruct {// TODO Full bool Steps bool Graph bool Can bool Queued bool Args bool When bool StateCtx bool}type semLogger struct { mach *Machine steps atomic.Bool graph atomic.Bool queued atomic.Bool args atomic.Bool can atomic.Bool}// implement [SemLogger]var _ SemLogger = &semLogger{}func ( *semLogger) ( LogArgsMapperFn) { .mach.logArgs.Store(&)}func ( *semLogger) () LogArgsMapperFn { := .mach.logArgs.Load()if == nil {returnnil }return *}func ( *semLogger) ( bool) { .mach.logId.Store()}func ( *semLogger) () bool {return .mach.logId.Load()}func ( *semLogger) ( LoggerFn) {if == nil { .mach.logger.Store(nil)return } .mach.logger.Store(&)}func ( *semLogger) () LoggerFn {if := .mach.logger.Load(); != nil {return * }returnnil}func ( *semLogger) ( LogLevel) { .mach.logLevel.Store(&)}func ( *semLogger) () LogLevel {return *.mach.logLevel.Load()}func ( *semLogger) ( LogLevel) {varLoggerFn = func( LogLevel, string, ...any) {// no-op } .mach.logger.Store(&) .mach.logLevel.Store(&)}func ( *semLogger) (func( string, ...any), LogLevel,) {if == nil {panic("logf cannot be nil") }varLoggerFn = func( LogLevel, string, ...any) { (, ...) } .mach.logger.Store(&) .mach.logLevel.Store(&)}func ( *semLogger) ( bool, , string) { := "remove"if { = "add" } .mach.log(LogOps, "[pipe-out:%s] %s to %s", , , )}func ( *semLogger) ( bool, , string) { := "remove"if { = "add" } .mach.log(LogOps, "[pipe-in:%s] %s from %s", , , )}func ( *semLogger) ( string) { .mach.log(LogOps, "[pipe:gc] %s", )}func ( *semLogger) () bool {return .steps.Load()}func ( *semLogger) ( bool) { .steps.Store()}func ( *semLogger) () bool {return .can.Load()}func ( *semLogger) ( bool) { .can.Store()}func ( *semLogger) () bool {return .graph.Load()}func ( *semLogger) ( bool) { .graph.Store()}func ( *semLogger) () bool {return .queued.Load()}func ( *semLogger) ( bool) { .queued.Store()}func ( *semLogger) () bool {return .args.Load()}func ( *semLogger) ( bool) { .args.Store()}// TODO more data typesfunc ( *semLogger) ( bool) {// TODO}func ( *semLogger) () bool {returnfalse}func ( *semLogger) () bool {returnfalse}func ( *semLogger) ( bool) {// TODO}// LogArgs is a list of common argument names to be logged. Useful for// debugging.varLogArgs = []string{"name", "id", "port", "addr", "err"}// LogArgsMaxLen is the default maximum length of the arg's string// representation.varLogArgsMaxLen = 20// NewArgsMapper returns a matcher function for LogArgs. Useful for debugging// untyped argument maps.//// maxLen: maximum length of the arg's string representation). Default to// LogArgsMaxLen,func ( []string, int) func( A) map[string]string {if == 0 { = LogArgsMaxLen }returnfunc( A) map[string]string { := make([]bool, len()) := 0for , := range { , := [] [] = ++ }if == 0 {returnnil } := make(map[string]string)for , := range {if ![] {continue } [] = TruncateStr(fmt.Sprintf("%v", []), ) }return }}func ( map[string]string) string {iflen() == 0 {return"" }// sort by namevar []string := slices.Collect(maps.Keys())slices.Sort()for , := range { := [] = append(, +"="+) }return" (" + strings.Join(, " ") + ")"}// Tracer is an interface for logging machine transitions and events, used by// Opts.Tracers and Machine.BindTracer.typeTracerinterface {TransitionInit(transition *Transition)TransitionStart(transition *Transition)TransitionEnd(transition *Transition)MutationQueued(machine Api, mutation *Mutation)HandlerStart(transition *Transition, emitter string, handler string)HandlerEnd(transition *Transition, emitter string, handler string)// MachineInit is called only for machines with tracers added via // Opts.Tracers.MachineInit(machine Api) context.ContextMachineDispose(machID string)NewSubmachine(parent, machine Api)Inheritable() boolQueueEnd(machine Api)SchemaChange(machine Api, old Schema)VerifyStates(machine Api)}// NoOpTracer is a no-op implementation of Tracer, used for embedding.// TODO rename to TracerNoOptypeNoOpTracerstruct{}func ( *NoOpTracer) ( *Transition) {}func ( *NoOpTracer) ( *Transition) {}func ( *NoOpTracer) ( *Transition) {}func ( *NoOpTracer) ( Api, *Mutation) {}func ( *NoOpTracer) ( *Transition, string, string) {}func ( *NoOpTracer) ( *Transition, string, string) {}func ( *NoOpTracer) ( Api) context.Context {returnnil}func ( *NoOpTracer) ( string) {}func ( *NoOpTracer) (, Api) {}func ( *NoOpTracer) ( Api) {}func ( *NoOpTracer) ( Api, Schema) {}func ( *NoOpTracer) ( Api) {}func ( *NoOpTracer) () bool { returnfalse }var _ Tracer = &NoOpTracer{}// ///// ///// /////// ///// EVENTS, WHEN, EMITTERS// ///// ///// /////var emitterNameRe = regexp.MustCompile(`/\w+\.go:\d+`)// Event struct represents a single event of a Mutation within a Transition.// One event can have 0-n handlers.typeEventstruct {// Ctx is an optional context this event is constrained by. Ctx context.Context// Name of the event / handler Name string// MachineId is the ID of the parent machine. MachineId string// TransitionId is the ID of the parent transition. TransitionId string// Args is a map of named arguments for a Mutation. Args A// IsCheck is true if this event is a check event, fired by one of Can*() // methods. Useful for avoiding flooding the log with errors. IsCheck bool// Machine is the machine that the event belongs to. It can be used to access // the current Transition and Mutation. machine *Machine}// Mutation returns the Mutation of an Event.func ( *Event) () *Mutation { := .Machine().Transition()if == nil {returnnil }return .Mutation}func ( *Event) () *Machine {return .machine}// Transition returns the Transition of an Event.func ( *Event) () *Transition {if .machine == nil {returnnil }return .Machine().t.Load()}// IsValid confirm this event should still be processed. Useful for negotiation// handlers, which can't use state context.func ( *Event) () bool { := .Transition()if == nil {returnfalse }// optional ctxif .Ctx != nil && .Ctx.Err() != nil {returnfalse }return .TransitionId == .Id && !.IsCompleted.Load() && .IsAccepted.Load()}// Export clones only the essential data of the Event. Useful for tracing vs GC.func ( *Event) () *Event { := .MachineIdif .Machine() == nil { = .Machine().Id() }return &Event{MachineId: ,Name: .Name,TransitionId: .TransitionId,IsCheck: .IsCheck, }}// Clone clones the event struct, making it writable.func ( *Event) () *Event { := .Export()// non-exportable fields .Args = .Args .Ctx = .Ctxreturn}// SwapArgs clone the event and assign new args.func ( *Event) ( A) *Event { := .Clone() .Args = return}func ( *Event) () string { := .Machine()if == nil {return .Mutation().String() }return .Mutation().StringFromIndex(.StateNames())}// ///// ///// /////// ///// ERROR HANDLING// ///// ///// /////// ExceptionArgsPanic is an optional argument ["panic"] for the StateException// state which describes a panic within a Transition handler.typeExceptionArgsPanicstruct { CalledStates S StatesBefore S Transition *Transition LastStep *Step StackTrace string}// ExceptionHandler provide a basic StateException state support, as should be// embedded into handler structs in most of the cases. Because ExceptionState// will be called after [Machine.HandlerDeadline], it should handle locks// on its own (to not race with itself).typeExceptionHandlerstruct{}type recoveryData struct { err any stack string}func captureStackTrace() string { := make([]byte, 4024) := runtime.Stack(, false) := string([:]) := strings.Split(, "\n") := strings.Contains(, "panic")slices.Reverse() := []string{"AddErr", "AddErrState", "Remove", "Remove1", "Add", "Add1", "Set", }// TODO trim tails start at reflect.Value.Call({ // with asyncmachine 2 frames down// trim the head, remove junk := falsefor , := range {if && strings.HasPrefix(, "panic(") { = [:-1]break }for , := range {ifstrings.Contains("machine.(*Machine)."++"(", ) { = [:-1] = truebreak } }if {break } }slices.Reverse() := strings.Join(, "\n")if := os.Getenv(EnvAmTraceFilter); != "" { = strings.ReplaceAll(, , "") }return}// ExceptionState is a final entry handler for the StateException state.// Args:// - err error: The error that caused the StateException state.// - panic *ExceptionArgsPanic: Optional details about the panic.func ( *ExceptionHandler) ( *Event) {// TODO handle ErrHandlerTimeout to ErrHandlerTimeoutState (if present) := ParseArgs(.Args) := .Err := .ErrTrace := .Machine()// errif == nil { = errors.New("missing error in args to ExceptionState") }if .Panic == nil { := strings.TrimSpace(.Error())// TODO more mutation infoif .LogStackTrace && != "" { .log(LogChanges, "[error] %s\n%s", , ) } else { .log(LogChanges, "[error] %s", ) }return }// handler panic info := .Panic.Transition.Mutation.Typeif .LogStackTrace && != "" {// stack trace .log(LogChanges, "[error:%s] %s (%s)\n%s", ,j(.Panic.CalledStates), , ) } else {// no stack trace .log(LogChanges, "[error:%s] %s (%s)", ,j(.Panic.CalledStates), ) }}// NewLastTxTracer returns a Tracer that logs the last transition.func () *LastTxTracer {return &LastTxTracer{}}// TODO add TTL, ctxtypeLastTxTracerstruct { *NoOpTracer lastTx atomic.Pointer[Transition]}func ( *LastTxTracer) ( *Transition) { .lastTx.Store()}// Load returns the last transition.func ( *LastTxTracer) () *Transition {return .lastTx.Load()}func ( *LastTxTracer) () string { := .lastTx.Load()if == nil {return"" }return .String()}func newClosedChan() chanstruct{} { := make(chanstruct{})close()return}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.