Skip to content

pwjazz/gflow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

--------------------------------- OVERVIEW ---------------------------------

   Package gflow provides a mechanism for defining and processing
   event-driven flows, which we'll just call 'flows'. These flows are
   immutable and side-effect free, making them safe to use in multi-threaded
   environments.  Flows can be composed of other flows, making it easy to
   create relatively complex flows from smaller constituent parts.

   A flow is a directed acyclic graph of states, sharing the same start and end
   states and connected by one or more transitions from one state to the next.
   Each transition is governed by a single test, which is specified as a
   function. These tests must be mutually exclusive.

   ---------------------------- STRUCTURE OF FLOWS ----------------------------

   Let a, b, c ... equal a set of tests for advancing a flow
   Let A, B, C ... equal a set of events that pass the respective tests
   Let @ represent a state
   Let --a-->, --b-->, ... represent transitions dependent on events a, b, etc.
   Let A -> B -> C -> ... represent a sequence of events

   For example, the below is a diagram of a 2-state flow with a single test 'a'
   satisfied by a one-event sequence:

   @ --a--> @

   Satisfied by:

   A

   Multiple tests are composed into a flow using the following operators:

   THEN     a sequence of tests
   OR       logical OR (commutative)
   AND      logical AND (commutative)

   The flow a THEN a THEN b is structured as follows:

   @ --a--> @ --a--> @ --b--> @

   Satisfied by:

   A -> A -> B

   Note: each event may trigger only 1 transition at a time, thus the above
   flow is not satisfied by A -> B because A is not double counted.

   Note: events that do not trigger a transition are effectively ignored, thus
   the above flow is satisfied equally well by all three of the following:

   A -> A -> B
   A -> X -> A -> Y -> B
   A -> A -> A -> A -> B

   The flow c AND d is structured as follows:

   |--c--> @ --d-->| 
   @               @
   |--d--> @ --c-->|

   Satisfied by:

   C -> D
   D -> C

   The flow e OR f is structured as follows:

   |--e-->|
   @      @
   |--f-->|

   Satisfied by:

   E
   F

   Flows can be composed by the same operators as tests, for example:

   a THEN a THEN b OR (c AND d)

   Satisfied by:

   A -> A -> B
   C -> D
   D -> C

   ------------------------------- CODING ------------------------------------

   // This example uses the flow a THEN a THEN b OR (c AND d)

   // Define tests.  Tests are functions that accept an EventData and
   // return a bool.  EventData is the empty interface, meaning it can be
   // any type of value.

   // In our example, we're just using strings as our EventData.

   var a gflow.Test = func(data gflow.EventData) bool {
       return "A" == data.(string)
   }

   var b gflow.Test = func(data gflow.EventData) bool {
       return "B" == data.(string)
   }

   // ... and so on ...

   // The flow is defined using the methods THEN OR and AND, which return
   // state objects.

   flow := a.THEN(a).THEN(b).OR(c.AND(d))

   // Each of these compositional methods accepts two tests and returns a new
   // state.  Since states can be composed using these same methods, it is
   // possible to break apart the definition of flows however is convenient.

   subFlow1 := a.THEN(a).THEN(b)
   subFlow2 := c.AND(d)
   redefined := subFlow1.OR(subFlow2)
   // the above redefined is equivalent to the original flow

   // As mentioned previously, events can be anything, and in our case are just
   // strings.

   eventA := "A"
   eventB := "B"

   // To use a flow, call the Build() method and then Advance().
   // Build() basically just returns the root state of the flow so that
   // you start from the beginning.  It also assigns ids to states, which is
   // discussed further below.

   state := flow.Build().Advance(eventA)

   // Advance() returns the next state based on whatever transition fired (or
   // did not fire).  Continue advancing by calling Advance() on the returned
   // states.

   state = state.Advance(eventB)
   state = state.Advance(eventC)
   // ... and so on ...

   // Above we assigned the result from Advance() to a variable named "state".
   // In reality, state and flow are interchangeable - it's all just states.
   // A flow can be referenced by any state in that flow, since they all
   // share the same root.

   state = state.Build().Advance(eventA).Advance(eventB)

   // You can check whether a flow is finished by using the method Finished.

   isFinished := state.Finished()

   // Finished just means that there are no further transitions left.

   // Once a flow is finished, it is legal to keep sending events to it,
   // but this will have no effect.

   // IMPORTANT - gflow states are immutable and their methods are free of
   // side-effects, making them safe to use in a multi-threaded environment.

   // For example, the below line creates a new flow from subFlow1 that
   // requires the additional step f.

   subFlow1 = subFlow1.THEN(f)

   // Sub flow 1 is now @ --a--> @ -->a--> @ --b--> @ --f--> @

   // Note how we assigned the result of THEN() to a variable.  The original
   // state referred to by subFlow1 before the assignment has not changed.  So,
   // any in-flight processes based on that flow can continue unhampered.

   // The same property of thread-safety holds true for Advance().

   state1 := flow.Advance(eventA)
   state2 := flow.Advance(eventA)

   // The above two states are completely independent of each other.

   // The stateless nature of gflow means that the responsibility for managing
   // the current state of flows is entirely the client program's.

   // To help clients manage long running flows, gflow provides an ID property
   // on states and a FindByID method to retrieve a state from a flow by its ID.

   state = flow.Advance(eventA)
   stateId = state.ID
   // Save ID
   // Do some other stuff
   // Later ...
   resumedState := flow.FindByID(stateId)
   resumedState = resumedState.Advance(eventB)

   // IMPORTANT: if the definition of a flow changes, even if it is logically
   // equivalent to the previous definition, saved IDs will no longer be
   // reliable. Therefore, once you have started using a flow, you should
   // NEVER change its definition unless and until you no longer have any such
   // flows in flight.  For example:

   flow1 := a.AND(b).OR(c.AND(d)).Build()
   flow2 := c.AND(d).OR(a.AND(b)).Build()

   s1 := flow1.Advance(eventA)
   id := s1.ID
   s2 := flow2.FindByID(id)

   // Logically, these flows are the same, but the id's from these flows cannot
   // be interchanged.  In this case, s1 and s2 are NOT equivalent!

About

Simple event-driven workflows in Go

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors