package machine
import (
"fmt"
"maps"
"slices"
"strings"
"sync"
"sync/atomic"
)
type Transition struct {
Id string
Steps []*Step
TimeBefore Time
TimeAfter Time
TargetIndexes []int
Enters S
Exits S
Mutation *Mutation
Machine *Machine
MachApi Api
LogEntries []*LogEntry
PreLogEntries []*LogEntry
QueueLen uint16
InternalLogEntriesLock sync .Mutex
IsCompleted atomic .Bool
IsAccepted atomic .Bool
IsBroken atomic .Bool
IsSettled atomic .Bool
latestHandlerIsEnter bool
latestHandlerIsFinal bool
latestHandlerToState string
cacheTargetStates atomic .Pointer [S ]
cacheStatesBefore atomic .Pointer [S ]
cacheClockBefore atomic .Pointer [Clock ]
cacheActivated S
cacheDeactivated S
cacheSchema Schema
}
func newTransition(m *Machine , mut *Mutation ) *Transition {
m .activeStatesMx .RLock ()
defer m .activeStatesMx .RUnlock ()
index := m .StateNames ()
schema := m .Schema ()
tNow := m .time (nil )
tAfter := slices .Clone (tNow )
semlog := m .SemLogger ()
if !mut .IsCheck {
is := IsActiveTick
for _ , idx := range mut .Called {
name := index [idx ]
switch {
case mut .Type == MutationAdd && is (tAfter [idx ]) && schema [name ].Multi :
tAfter [idx ] += 2
case mut .Type == MutationAdd && !is (tAfter [idx ]):
tAfter [idx ] += 1
case mut .Type == MutationRemove && is (tAfter [idx ]):
tAfter [idx ] += 1
}
}
}
t := &Transition {
Id : randId (),
Mutation : mut ,
TimeBefore : tNow ,
TimeAfter : tAfter ,
Machine : m ,
MachApi : m ,
cacheSchema : schema ,
}
activeStates := slices .Clone (m .activeStates )
t .cacheStatesBefore .Store (&activeStates )
clock := maps .Clone (m .clock )
t .cacheClockBefore .Store (&clock )
t .IsAccepted .Store (true )
m .t .Store (t )
m .tracersMx .RLock ()
for _ , tracer := range m .tracers {
if t .Machine .IsDisposed () {
break
}
tracer .TransitionInit (t )
}
m .tracersMx .RUnlock ()
called := t .CalledStates ()
mutType := t .Type ()
logArgs := ""
if semlog .IsArgs () || semlog .Level () > LogExternal {
logArgs = mut .LogArgs (semlog .ArgsMapper ())
}
if t .isLogSteps () {
t .addSteps (newSteps ("" , called , StepRequested , 0 )...)
}
if mut .IsAuto {
m .log (LogDecisions , "[%s:auto] %s%s" , mutType , j (called ), logArgs )
} else {
m .log (LogOps , "[%s] %s%s" , mutType , j (called ), logArgs )
}
src := mut .Source
if src != nil {
m .log (LogOps , "[source] %s/%s/%d" , src .MachId , src .TxId , src .MachTime )
}
statesToSet := t .statesToSet (mutType , called )
targetStates := m .resolver .TargetStates (t , statesToSet , index )
t .TargetIndexes = m .Index (targetStates )
t .cacheTargetStates .Store (&targetStates )
impliedStates := StatesDiff (targetStates , statesToSet )
if len (impliedStates ) > 0 {
m .log (LogOps , "[implied] %s" , j (impliedStates ))
}
t .setupAccepted ()
if t .IsAccepted .Load () {
t .setupExitEnter ()
}
return t
}
func (t *Transition ) statesToSet (mutType MutationType , states S ) S {
m := t .Machine
switch mutType {
case MutationRemove :
statesToSet := slicesFilter (m .activeStates , func (state string , _ int ) bool {
return !slices .Contains (states , state )
})
if t .isLogSteps () {
t .addSteps (newSteps ("" , states , StepRemove , 0 )...)
}
return statesToSet
case MutationAdd :
statesToSet := slices .Concat (states , m .activeStates )
if t .isLogSteps () {
t .addSteps (newSteps ("" , StatesDiff (statesToSet , m .activeStates ),
StepSet , 0 )...)
}
return statesToSet
case MutationSet :
statesToSet := states
if t .isLogSteps () {
t .addSteps (newSteps ("" , StatesDiff (statesToSet , m .activeStates ),
StepSet , 0 )...)
t .addSteps (newSteps ("" , StatesDiff (m .activeStates , statesToSet ),
StepRemove , 0 )...)
}
return statesToSet
}
return nil
}
func (t *Transition ) CleanCache () {
t .cacheTargetStates .Store (nil )
t .cacheStatesBefore .Store (nil )
t .latestHandlerToState = ""
t .latestHandlerIsEnter = false
t .latestHandlerIsFinal = false
t .cacheClockBefore .Store (nil )
t .Mutation .cacheCalled .Store (nil )
t .cacheSchema = nil
}
func (t *Transition ) StatesBefore () S {
if v := t .cacheStatesBefore .Load (); v != nil {
return *v
}
ret := make (S , 0 , len (t .TimeBefore ))
mach := t .Machine
if mach == nil {
return ret
}
states := mach .StateNames ()
for i := range t .TimeBefore {
if IsActiveTick (t .TimeBefore [i ]) {
ret = append (ret , states [i ])
}
}
return ret
}
func (t *Transition ) TargetStates () S {
if v := t .cacheTargetStates .Load (); v != nil {
return *v
}
ret := make (S , len (t .TargetIndexes ))
mach := t .MachApi
if mach == nil {
return nil
}
states := mach .StateNames ()
for i , idx := range t .TargetIndexes {
if idx == -1 {
return S {}
}
ret [i ] = states [idx ]
}
return ret
}
func (t *Transition ) IsAuto () bool {
return t .Mutation .IsAuto
}
func (t *Transition ) IsHealth () bool {
c := t .CalledStates ()
if len (c ) != 1 || t .Type () != MutationAdd {
return false
}
return c [0 ] == StateHealthcheck || c [0 ] == StateHeartbeat
}
func (t *Transition ) CalledStates () S {
if v := t .Mutation .cacheCalled .Load (); v != nil {
return *v
}
return IndexToStates (t .MachApi .StateNames (), t .Mutation .Called )
}
func (t *Transition ) TimeIndexAfter () TimeIndex {
return TimeIndex {
Time : t .TimeAfter ,
Index : t .MachApi .StateNames (),
}
}
func (t *Transition ) ClockBefore () Clock {
if v := t .cacheClockBefore .Load (); v != nil {
return *v
}
states := t .Machine .StateNames ()
ret := make (Clock , len (states ))
for k , v := range t .TimeBefore {
ret [states [k ]] = v
}
return ret
}
func (t *Transition ) ClockAfter () Clock {
states := t .Machine .StateNames ()
ret := make (Clock , len (states ))
for k , v := range t .TimeAfter {
ret [states [k ]] = v
}
return ret
}
func (t *Transition ) Args () A {
return t .Mutation .Args
}
func (t *Transition ) Type () MutationType {
return t .Mutation .Type
}
func (t *Transition ) String () string {
index := t .Machine .StateNames ()
var lines []string
for _ , step := range t .Steps {
lines = append (lines , step .StringFromIndex (index ))
}
steps := strings .Join (lines , "\n- " )
if steps != "" {
steps = "\n- " + steps
}
source := ""
mutSrc := t .Mutation .Source
if mutSrc != nil {
source = fmt .Sprintf ("\nsource: [%s](%s/%s/t%d)" , mutSrc .TxId ,
mutSrc .MachId , mutSrc .TxId , mutSrc .MachTime )
}
return fmt .Sprintf ("tx#%s\n%s%s%s" , t .Id , t .Mutation .StringFromIndex (index ),
source , steps )
}
func (t *Transition ) addSteps (steps ...*Step ) {
t .Steps = append (t .Steps , steps ...)
}
func (t *Transition ) setupExitEnter () {
m := t .Machine
targetStates := t .TargetStates ()
exits := StatesDiff (m .activeStates , targetStates )
m .resolver .SortStates (exits )
var enters S
for _ , s := range targetStates {
state := m .schema [s ]
if !m .is (S {s }) || (state .Multi && slices .Contains (t .CalledStates (), s )) {
enters = append (enters , s )
}
}
t .Exits = exits
t .Enters = enters
}
func (t *Transition ) emitSelfEvents () Result {
m := t .Machine
ret := Executed
var handlerCalled bool
for _ , s := range t .CalledStates () {
if !t .Machine .Is (S {s }) {
continue
}
name := s + s
t .latestHandlerToState = s
ret , handlerCalled = m .handle (name , t .Mutation .Args , false , false , true )
if handlerCalled && t .isLogSteps () {
step := newStep ("" , s , StepHandler , 0 )
step .IsSelf = true
t .addSteps (step )
}
if ret == Canceled {
break
}
}
return ret
}
func (t *Transition ) emitEnterEvents () Result {
for _ , toState := range t .Enters {
args := t .Mutation .Args
ret := t .emitHandler ("" , toState , false , true , toState +SuffixEnter , args )
if ret == Canceled {
if t .IsAuto () {
targetStates := t .TargetStates ()
idx := slices .Index (targetStates , toState )
t .TargetIndexes = slices .Delete (t .TargetIndexes , idx , idx +1 )
targetStates = slices .Delete (targetStates , idx , idx +1 )
t .cacheTargetStates .Store (&targetStates )
} else {
return ret
}
}
}
return Executed
}
func (t *Transition ) emitExitEvents () Result {
for _ , fromState := range t .Exits {
ret := t .emitHandler (fromState , "" , false , false , fromState +SuffixExit ,
t .Mutation .Args )
if ret == Canceled {
if t .IsAuto () {
targetStates := t .TargetStates ()
idx := slices .Index (targetStates , fromState )
t .TargetIndexes = slices .Delete (t .TargetIndexes , idx , idx +1 )
targetStates = slices .Delete (targetStates , idx , idx +1 )
t .cacheTargetStates .Store (&targetStates )
} else {
return ret
}
}
}
return Executed
}
func (t *Transition ) emitHandler (
from , to string , isFinal , isEnter bool , event string , args A ,
) Result {
t .latestHandlerToState = to
ret , handlerCalled := t .Machine .handle (event , args , isFinal , isEnter , false )
if handlerCalled && t .Machine .semLogger .IsSteps () {
step := newStep (from , to , StepHandler , 0 )
step .IsFinal = isFinal
step .IsEnter = isEnter
t .addSteps (step )
}
return ret
}
func (t *Transition ) emitFinalEvents () Result {
finals := slices .Concat (t .Exits , t .Enters )
for _ , s := range finals {
isEnter := slices .Contains (t .Enters , s )
var handler string
if isEnter {
handler = s + SuffixState
t .latestHandlerToState = s
} else {
handler = s + SuffixEnd
t .latestHandlerToState = ""
}
ret , handlerCalled := t .Machine .handle (handler , t .Mutation .Args ,
true , isEnter , false )
if handlerCalled && t .Machine .semLogger .IsSteps () {
step := newStep ("" , s , StepHandler , 0 )
step .IsFinal = true
step .IsEnter = isEnter
t .addSteps (step )
}
if ret == Canceled {
return ret
}
}
return Executed
}
func (t *Transition ) emitStateStateEvents () Result {
before := t .StatesBefore ()
after := t .TargetStates ()
var newAfter S
for i := range before {
for ii := range after {
if before [i ] == after [ii ] {
continue
}
handler := before [i ] + after [ii ]
t .latestHandlerToState = ""
ret , handlerCalled := t .Machine .handle (handler , t .Mutation .Args , false ,
false , false )
if handlerCalled && t .Machine .semLogger .IsSteps () {
step := newStep (before [i ], after [ii ], StepHandler , 0 )
t .addSteps (step )
}
if ret != Canceled {
continue
}
if t .IsAuto () {
if newAfter == nil {
newAfter = slices .Clone (after )
}
idx := slices .Index (newAfter , after [ii ])
if idx == -1 {
continue
}
t .TargetIndexes = slices .Delete (t .TargetIndexes , idx , idx +1 )
newAfter = slices .Delete (newAfter , idx , idx +1 )
t .cacheTargetStates .Store (&newAfter )
} else {
return ret
}
}
}
return Executed
}
func (t *Transition ) emitEvents () Result {
m := t .Machine
result := Executed
if !t .IsAccepted .Load () {
result = Canceled
}
hasStateChanged := false
called := t .CalledStates ()
hasHandlers := t .Machine .handlerLoopRunning .Load ()
logEverything := m .semLogger .Level () == LogEverything
m .tracersMx .RLock ()
for i := 0 ; !t .Machine .IsDisposed () && i < len (t .Machine .tracers ); i ++ {
t .Machine .tracers [i ].TransitionStart (t )
}
m .tracersMx .RUnlock ()
if hasHandlers || logEverything {
if result != Canceled {
result = t .emitExitEvents ()
}
if result != Canceled {
result = t .emitEnterEvents ()
}
if result != Canceled && t .Type () != MutationRemove {
result = t .emitSelfEvents ()
}
if result != Canceled {
result = t .emitStateStateEvents ()
}
if t .IsAuto () && len (t .TargetIndexes ) == 0 {
result = Canceled
}
if result != Canceled {
result = t .emitHandler (StateAny , StateAny , false , true ,
StateAny +SuffixEnter , t .Mutation .Args )
}
}
if !t .Mutation .IsCheck {
if t .IsAuto () {
rejected := StatesDiff (called , t .TargetStates ())
calledClean := StatesDiff (called , rejected )
toSet := t .statesToSet (MutationAdd , calledClean )
targetStates := m .resolver .TargetStates (t , toSet , m .StateNames ())
t .TargetIndexes = m .Index (targetStates )
t .cacheTargetStates .Store (&targetStates )
}
if result != Canceled {
m .activeStatesMx .Lock ()
m .setActiveStates (called , t .TargetStates (), t .IsAuto ())
m .activeStatesMx .Unlock ()
t .TimeAfter = m .time (nil )
if hasHandlers || logEverything || m .subs .HasWhenArgs () {
result = t .emitFinalEvents ()
}
if result == Canceled {
m .recoverFinalPhase ()
}
hasStateChanged = !m .IsTime (t .TimeBefore , nil )
} else {
t .TimeAfter = m .time (nil )
}
if result != Canceled && (hasHandlers || logEverything ) {
result = t .emitHandler (StateAny , StateAny , true , true ,
StateAny +SuffixState , t .Mutation .Args )
}
if onChange := m .onChange .Load (); onChange != nil {
(*onChange )(m , t .TimeBefore , t .TimeAfter )
}
if result == Canceled {
t .IsAccepted .Store (false )
} else if !m .disposing .Load () && hasStateChanged && !t .IsAuto () &&
!t .IsHealth () {
autoMut , calledStates := m .resolver .NewAutoMutation ()
if autoMut != nil {
m .log (LogOps , "[auto] %s" , j (calledStates ))
m .PrependMut (autoMut )
}
}
if t .IsAuto () {
before := t .StatesBefore ()
t .cacheActivated = StatesDiff (m .activeStates , before )
t .cacheDeactivated = StatesDiff (before , m .activeStates )
} else {
t .cacheActivated = t .Enters
t .cacheDeactivated = t .Exits
}
} else if result == Canceled {
t .IsAccepted .Store (false )
}
m .logEntriesLock .Lock ()
t .PreLogEntries = m .logEntries
t .QueueLen = uint16 (m .queueLen .Load ())
m .logEntries = nil
m .logEntriesLock .Unlock ()
t .IsCompleted .Store (true )
m .tracersMx .RLock ()
for i := 0 ; !t .Machine .IsDisposed () && i < len (t .Machine .tracers ); i ++ {
t .Machine .tracers [i ].TransitionEnd (t )
}
m .tracersMx .RUnlock ()
if result == Canceled {
return Canceled
} else if t .Mutation .IsCheck {
return result
}
if t .Type () == MutationRemove {
if m .Not (called ) {
return Executed
} else {
return Canceled
}
} else {
if m .Is (t .TargetStates ()) {
return Executed
} else {
return Canceled
}
}
}
func (t *Transition ) setupAccepted () {
m := t .Machine
if t .Type () == MutationRemove {
return
}
called := t .CalledStates ()
notAccepted := StatesDiff (called , t .TargetStates ())
if t .IsAuto () {
if len (notAccepted ) < len (called ) {
return
}
t .IsAccepted .Store (false )
}
if len (notAccepted ) <= 0 {
return
}
isMulti := false
for _ , s := range called {
if m .schema [s ].Multi {
isMulti = true
break
}
}
if t .Mutation .IsCheck && isMulti {
return
}
t .IsAccepted .Store (false )
m .log (LogOps , "[cancel:reject] %s" , j (notAccepted ))
if t .isLogSteps () {
t .addSteps (newSteps ("" , notAccepted , StepCancel , 0 )...)
}
}
func (t *Transition ) isLogSteps () bool {
l := t .Machine .semLogger
if t .Mutation .IsCheck && !l .IsCan () {
return false
}
return l .IsSteps ()
}
The pages are generated with Golds v0.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 .