blob: 3d048bd4797f2dbfe3bffdbf4cbdb8f943c0ae45 [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fsm
import (
"errors"
"sync"
"fmt"
"android.googlesource.com/platform/tools/gpu/framework/log"
"android.googlesource.com/platform/tools/gpu/framework/task"
)
type pending struct {
event Event
state *runningState
hooks []HookFunc
done task.Task
}
type active struct {
state *runningState
ctx log.Context // ctx for the running state
cancel task.CancelFunc // cancel for the running state
}
// Instance is a running copy of a compiled fsm.
// All public methods on this class are concurrent safe.
type Instance struct {
mutex sync.Mutex
data interface{}
next pending
running active
controller task.Handle
}
type fsmKeyType string
var (
fsmKey fsmKeyType = "FSM"
fsmExit = errors.New("fsm exit")
)
// Get returns the running fsm as stored in the context, if there is one.
func Get(ctx log.Context) *Instance {
return ctx.Value(fsmKey).(*Instance)
}
// RunWith executes an FSM by creating a new instance, and running it to completion.
// It passes the supplied data argument to the instance and all it's builders.
// It returns the running error if one occurred.
func RunWith(ctx log.Context, data interface{}, c *Compiled) error {
fsm := &Instance{data: data}
ctx = ctx.HiddenValue(fsmKey, fsm)
// first build the machine structure
if err := fsm.build(ctx, c); err != nil {
return err
}
// and prepare the running information
defer func() {
fsm.next = pending{}
fsm.running = active{}
}()
fsm.running = active{ctx: ctx}
// start the controller if present
fsm.controller = task.Go(ctx, func(log.Context) error {
if c.Controller != nil {
if err := c.Controller(ctx); err != nil {
fsm.Trigger(ctx, controllerError)
return err
}
}
return nil
})
if err := fsm.run(ctx); err == fsmExit {
return nil
} else {
return err
}
}
// Run executes an FSM by creating a new instance, and running it to completion.
// It returns the running error if one occurred.
func Run(ctx log.Context, c *Compiled) error {
return RunWith(ctx, nil, c)
}
// Trigger delivers an event to the fsm, possibly causing it to change state.
func (fsm *Instance) Trigger(ctx log.Context, e Event) (task.Signal, error) {
fsm.mutex.Lock()
defer fsm.mutex.Unlock()
if e == nil {
e = Next
}
err := fsm.transition(ctx, e)
if err != nil {
return nil, err
}
signal, transitioned := task.NewSignal()
fsm.next.done = transitioned
return signal, nil
}
// State returns the current active state of the fsm.
func (fsm *Instance) State() StateID {
fsm.mutex.Lock()
defer fsm.mutex.Unlock()
return fsm.running.state.name
}
// Return the data the fsm was run with.
func (fsm *Instance) Data() interface{} {
return fsm.data
}
func emptyStateFunc(log.Context) error { return nil }
func emptyStateBuilder(log.Context, interface{}) (task.Task, error) {
return emptyStateFunc, nil
}
// build the running structures from the compiled ones
func (fsm *Instance) build(ctx log.Context, c *Compiled) error {
stateList := make([]runningState, len(c.StateList))
stateMap := make(map[StateID]*runningState, len(c.StateList))
for i, name := range c.StateList {
do := c.States[name].Do
if do == nil {
do = emptyStateBuilder
}
stateFunc, err := do(ctx, fsm.data)
if err != nil {
return err
}
if stateFunc == nil {
stateFunc = emptyStateFunc
}
stateList[i] = runningState{
fsm: fsm,
name: name,
do: stateFunc,
transitions: map[Event]runningTransition{},
}
stateMap[name] = &stateList[i]
}
for i, name := range c.StateList {
rs := &stateList[i]
for _, t := range c.States[name].Transitions {
rt := runningTransition{to: stateMap[t.To.Name]}
rt.hooks = make([]HookFunc, len(t.Hooks))
for i, h := range t.Hooks {
hook, err := h(ctx, fsm.data)
if err != nil {
return err
}
rt.hooks[i] = hook
}
rs.transitions[t.Event] = rt
}
}
fsm.next = pending{
event: "initial",
state: stateMap[c.Initial.Name],
}
return nil
}
// run the instance
// the instance mutex must *not* be held when this function is invoked.
func (fsm *Instance) run(ctx log.Context) error {
var from StateID
// loop until the cancel context happens
for !task.Stopped(ctx) {
active, event, hooks, err := fsm.nextState(ctx)
if err != nil {
return err
}
// run transition hooks
for _, hook := range hooks {
hook(ctx, fsm, from, event)
}
// run the current state
if err := active.state.do(active.ctx); err != nil {
return err
}
from = active.state.name
}
return task.StopReason(ctx)
}
// perform the switch to a new state.
// the instance mutex must *not* be held when this function is invoked.
func (fsm *Instance) nextState(ctx log.Context) (active, Event, []HookFunc, error) {
fsm.mutex.Lock()
defer fsm.mutex.Unlock()
next := fsm.next
fsm.next = pending{}
if next.state == nil {
// A state just quit with no queued transition, try the default one
fsm.running.cancel = nil
err := fsm.transition(fsm.running.ctx, Next)
next = fsm.next
fsm.next = pending{}
if err != nil {
return active{}, nil, nil, err
}
}
fsm.running.state = next.state
fsm.running.ctx, fsm.running.cancel = task.WithCancel(ctx.Enter(fmt.Sprint(fsm.running.state.name)))
if next.done != nil {
next.done(ctx)
next.done = nil
}
return fsm.running, next.event, next.hooks, nil
}
// transition is the internal state transition function.
// the machine mutex *must* be held when this function is invoked.
func (fsm *Instance) transition(ctx log.Context, e Event) error {
if fsm.running.state == nil {
return ctx.V("Event", e).AsError("Transition when not running")
}
transition, ok := fsm.running.state.transitions[e]
if !ok {
if e == controllerError {
transition = runningTransition{to: &runningState{do: func(log.Context) error {
return fsm.controller.Result()
}}}
} else {
// event matches no valid transition
return ctx.V("Event", e).AsError("Invalid event")
}
}
// Cancel the running task, if we have not already
if fsm.running.cancel != nil {
// TODO: hand cancel events to a hook?
fsm.running.cancel()
// clear the cancel so we don't invoke it again if another transition interrupts
fsm.running.cancel = nil
}
// If we have a pending transition, mark it complete before building the new signal
if fsm.next.done != nil {
fsm.next.done(ctx)
}
fsm.next = pending{
event: e,
state: transition.to,
hooks: transition.hooks,
}
return nil
}