Simple Posting Example#

_images/posting_example.svg

Here we see an active object. To understand it, lets take the following steps:

  1. Look at the states and see how they are related to one another in a hierarchy.

  2. What external events exist and how the relate to the states.

  3. What custom code has been placed within the hierarchy.

In the above example we see three different states interacting with 4 different user defined events. The outer state contains a middle state which contains an inner state.

The event with signal name A will cause a transition from the middle state to the inner state. The event with signal name B will cause the outer event to exit, print “flash b!” then enter back into the outer state. Finally, the event with the signal name D will cause the recall method to be triggered in the outer state.

Now that we have a feeling for how the states relate and how they react to events, lets look at the custom code that has been placed within the hierarchical structure.

We see that, as previously mentioned, the outer state contains some signal handling for the D named event.

The middle state has some code that is run upon entering the state. It looks like we are posting an event to the signal named A, that we want it to happen 3 times, with a period of 1 [second] and that we would like it to be deferred. The entry condition also contains code which augments the chart with something.

The middle state has some exit code, where we are canceling an event.

Upon entering, the inner state, calls the defer method with an Event named B. When the innner state is initialized, it prints “”flash B!” to the terminal, then stops processing.

Generally speaking we have an idea about what is going on with the chart, now let’s look how to implement this design using the miros library, then start it up in the outer state. We will do this in three steps:

  1. We will define the state methods and fill them with the custom code in the diagram.

  2. We will create an active object

  3. We will tie our active object to the state methods, by starting it within the outer state.

  4. We will trigger an event and watch the chart react.

The outer state method would look like this:

 1@spy_on
 2def outer(ao, e):
 3  status = state.UNHANDLED
 4  if(e.signal == signals.ENTRY_SIGNAL):
 5    ao.recall()
 6    status = state.HANDLED
 7  elif(e.signal == signals.EXIT_SIGNAL):
 8    status = state.HANDLED
 9  if(e.signal == signals.INIT_SIGNAL):
10    status = state.HANDLED
11  elif(e.signal == signals.D):
12    ao.recall()
13    status = state.HANDLED
14  elif(e.signal == signals.B):
15    print("flash B!")
16    status = ao.trans(outer)
17  else:
18    status, ao.temp.fun = state.SUPER, ao.top
19  return status

On line 1 we see we are using the @spy_on decorator. This will allow us to look at how the chart behaves in a log once we start sending events to it.

Line 2 has the state name, and the method signature. The ao object will be overwritten with self when this method is used by an active object. However, it can be used by other active objects as well, in that it is a kind of slide-method that can be shared between objects. So long as we don’t mark the function’s name space directly we could share this method between as many active objects as we construct in our system. In this example we only use this method within one active object. The second argument in the method signature contains the event that is being sent to this state method.

On line 3 we see the status variable defined. Pay special attention to this status variable, because it is used by the event processor to determine what to do as it searches the chart hierarchy while responding to various events. We will talk about this in greater detail as we move through the example.

On lines 4-6, we see the entry handling for this state. If we are entering the state (line 4), then call the recall method of the active object (line 5) and finally set the status to HANDLED, or tell the event processor that we know what to do with this event and it does not have to recurse the chart to respond to the event anymore (line 6).

On line 7-8 we are saying, run no custom code upon exiting the state. On line 9-10 we are saying, no custom code needs to be run to initialize this state.

Lines 11-13 describe this state’s reaction to an event with the D signal. It runs the recall method of the active object (line 12) then sets the status variable to HANDLED. If there is an event in the deferred queue of the active object it will be placed into the fifo for the next run to completion event. This probably won’t make sense to you yet, don’t worry when we move through the dynamics of the chart, we will see what this means. Line 13 tells the event processor that the D signal was handled. This means that if the D signal was received by the chart while it was in another state, that the code on line 12 was run, then control was passed back to the state which received the event. This behavior is an example of the ultimate hook pattern which you can read about in the section titled Ultimate Hook.

Lines 14-16 describe the B arrow on the diagram. If any state within our state chart receives an event called B we would like it to pass control to the outer state (line 14,16), then exit the outer state, run print(flash B) (line 15) then re-enter the outer state (line 16). Pay special attention to line 16. Here we are using the trans method, which will return the status. Unlike the other parts of this method, we are not saying that the status is HANDLED, here we let the trans method decide how to set this variable. This is important since it allows the event processor to perform the work required by our hierarchical topology.

On lines 17-18 we are telling the event processor that if we haven’t managed this signal pass it onto our outer state, in this case it is the top state which means that it is unhandled.

Finally on line 19 we return the status.

Anyone familiar with the event processors described in the Miro Samek tradition of dealing with hierarchical state machines will recognize the structure of this method. This is because the event processor used by the miros library is a port of his work which has been written about in papers in embedded journals and books. I think it is important to keep the same structure and semantics since many in our industry have become familiar with them. It will also ensure that if you port your work into the quantum framework, the code will look about the same there as it does here.

Now let’s move on to the construction of the middle state:

 1@spy_on
 2def middle(ao, e):
 3  status = state.UNHANDLED
 4  if(e.signal == signals.ENTRY_SIGNAL):
 5    multi_shot_thread = \
 6      ao.post_fifo(Event(signal=signals.A),
 7                      times=3,
 8                      period=1.0,
 9                      deferred=True)
10    # We mark up the ao with this id, so that
11    # this state function can be used by many different aos
12    ao.augment(other=multi_shot_thread,
13                  name='multi_shot_thread')
14    status = state.HANDLED
15
16  elif(e.signal == signals.EXIT_SIGNAL):
17    ao.cancel_event(ao.multi_shot_thread)
18    status = state.HANDLED
19
20  if(e.signal == signals.INIT_SIGNAL):
21    status = state.HANDLED
22  elif(e.signal == signals.A):
23    status = ao.trans(inner)
24  else:
25    status, ao.temp.fun = state.SUPER, outer
26  return status

This method generally has the same structure as the outer state method. Line 1 instruments the method. Line 2 has the same method signature. Line 3 uses the same way to set up are return variable.

On lines 4-14 we see the code which will be run when this state is entered. Line 5 stores the multi_shot_thread id which is produced in the call to post_fifo on line 6. The post_fifo call creates a little parallel thread which will make events then send them back at our statechart with no regard to what state our active object is in, it will just place the event into the active object’s first in first out buffer.

We see on lines 12-13 that we augment our ao with the attribute called multi_shot_thread and give it the contents that was returned on line 6. This was done to salt away this information so that it can be used in the exit condition of this state method. Now lets jump back to how the post_fifo event was called:

ao.post_fifo(Event(signal=signals.A),
                times=3,
                period=1.0,
                deferred=True)

Here we see that it will be posting an Event with the signal name A to our chart 3 times, with a period of 1 second and that it is deferred. Here the deferred input means that our parallel thread will wait the period duration (1 second) before beginning its little job of posting the A event 3 times, at a frequency of once per second. There are lots of different ways to post events, if you would like to investigate the other ways, look at the Posting Events recipes.

When this thread source has finished its job it will just stop running. However, if the chart exits our middle state prior to our thread source exhausting itself, it would start posting the A signal to the outer state. This wouldn’t be a big deal, since our state chart would just ignore the A signal, but it would mean that we would be wasting cycles by making our event processor search the chart’s hierarchy with no hope of finding any useful work.

Let’s talk about how this little thread can be canceled upon exiting our state.

On lines 10-11 we see this comment: “We mark up the ao with this id, so that this state function can be used by many different aos.” Then we see some code where the multi_shot_thread attribute is created an given the id of the thread used to post the A events. Remember, the ao variable represents the self of your active object. Here we are creating code that could be written as this instead:

# Re-writing lines 12-13 as if they were in the active object class
this.multi_shot_thread = multi_shot_thread

All we are doing is storing the multi_shot_thread id into the active object that is using it, so that it can be canceled by the exit handler of the middle state. Now what is up with that comment? When I first wrote the example I wrote the thread id into the middle function’s name space. This was a bug, since this middle state method could be used by many different active objects. When one exited it would use an id associate with a different one. Since this code can be re-used by many different active objects we need to mark up those object’s namespace and leave this functions’ name space as is. Never use static variables in the state method state space.

So we have created a little thread that can post events, we have stored its id into a variable within the name space of the active object calling this state method, so we can cancel it if we want to. Now let’s move on.

Line 14 tells the event processor that we have handled this signal and it does not have to recurse the outer states of the chart.

Lines 16-18 describes what we want to do when this state is being exited. On line 17 we see that we are using the thread id of our little event posting thread to cancel that thread. The cancel_event method needs a specific thread id. If you wanted to avoid all of this trouble of storing event source ids into your active object, you could use the cancel_events method instead. See the Canceling Event Source By Signal Name recipe.

From line 20-21 we see that we don’t have any special handling for the initialization event for this state.

On lines 22-23 we see that when this state sees an A event it must transition into the inner state.

On lines 24-25 we see how this state method handle’s signals it does not know what to do with, it sets the status to SUPER and sets the ao.temp.fun to the outer function.

With these bread crumbs the event processor will know what to do so that our architecture can give us the dynamics of the Harel statechart formalism.

It is easy to forget that our statecharts are just programs that repeatedly call methods with arguments. They are structured programs pretending to be in a different programming paradigm. It is the event processor that allows this to happen, the trade off is that we have to pepper our state methods with what looks like strange syntax to give the event processor the ability to traverse any of the topologies that we might want to build.

It is the event processor that calls our state methods over and over again to build up lists of what functions should be called when and with what arguments.

This is what Miro Samek called an inversion of control. By embedding his event processing algorithm into their design, a developer can quickly construct any sort of state chart topology knowing that the dynamics of the how and the when things are called, will behave as they would expect them to. By placing the control of how things happen into the event processor, a developer can unload their cognition, focusing on the design itself rather than how they are going to implement it.

Let’s describe the inner state as a state method:

 1def inner(ao, e):
 2  status = state.UNHANDLED
 3  if(e.signal == signals.ENTRY_SIGNAL):
 4    ao.defer(Event(signal=signals.B))
 5    status = state.HANDLED
 6  elif(e.signal == signals.EXIT_SIGNAL):
 7    status = state.HANDLED
 8  if(e.signal == signals.INIT_SIGNAL):
 9    print("charging with B")
10    status = state.HANDLED
11  else:
12    status, ao.temp.fun = state.SUPER, middle
13  return status

We understand most of this code now, with the exception of line 4. We see that it happens upon entering the state and that we are deferring an event with the signal name B, but what does this mean?

To understand this, we have to know that an active object has a kind of savings-account queue. You can put things into it and nothing will happen. The active object won’t react to them until you ask it to react to them with a call to the recall method. The recall method moves an item out of the deferred queue and places it into the fifo queue. The active object reacts to elements in the fifo so when you call the recall method you are asking the chart to react to the oldest thing that was placed into the deferred queue.

Ok, so defer stores an Event, so who recalls the event? By examining our state diagram, we see that the outer state has a recall method that it calls upon receiving the event named D. The entry of the inner entry handler also has the recall method. That’s kind of strange, but this will make more sense once we reflect upon the dynamics of the active object.

Before we do that, let’s look at lines 8-9. Here we see that once the state is initialized we print, “charging with B” to the terminal. Once again, this is kind of strange. On the diagram we see this expressed as the bit black dot (the init signal) with an arrow labeled with the code we want to run, running into a big black line. This black line means stop there, you have done enough processing. This is the equivalent to line 10 in the above code snippet.

If you understand active objects look at the diagram and ask yourself, what happens if I start this chart in the middle state, then what happens if I wait about 4 seconds and then send an event named D?

_images/posting_example.svg

Hint: I modeled the diagram on a tazor.

Let’s see what happens using our state methods within an active object, then reflecting upon its behavior.

1import time
2ao = ActiveObject()
3ao.start_at(outer)
4ao.post_fifo(Event(signal=signals.C))
5time.sleep(4.0)
6ao.post_fifo(Event(signal=signals.D))
7time.sleep(0.1)
8
9print(ao.spy_full)

On line 1 we create an active object. On line 2 we start it in the outer state method. The active object’s event processor can now reach all of the state methods (even though they are defined outside of its class) because the state methods reference each other. On line 3 we transition into the middle state. We wait for a while; 4 and then we send an event with the D signal to the chart, line 6.

Pay special attention to line 7, because if you don’t you might end up thinking this whole example doesn’t work at all. I did this when I was constructing the example and began a senseless investigation trying to figure out what was wrong.

You need to wait for the active object threads to react to the items placed in their queues. All of the threads used within the miros library are daemonic meaning that when your main program loop stops running, all of the threads it created also stop running. So, if you don’t wait, the program will exit, killing all of the threads before they can do anything useful.

Now let’s break it down, thinking about a tazor as a metaphor. A tazor is a device that contains a small low voltage battery, a voltage amplifier circuit and a capacitor. You turn it on and it starts to whine.

This is the sound of a charge transfer from the small battery to the voltage amplifier which separates the charge at a high voltage across the capacitor. After this capacitor is charged up, you can zap somebody; the charge is coming out of the capacitor in a hurry.

Line 9 shows us the action:

 1['START',
 2 'SEARCH_FOR_SUPER_SIGNAL:middle',
 3 'SEARCH_FOR_SUPER_SIGNAL:outer',
 4 'ENTRY_SIGNAL:outer',
 5 'ENTRY_SIGNAL:middle',
 6 'INIT_SIGNAL:middle',
 7 '<- Queued:(0) Deferred:(0)',
 8 'A:middle',
 9 'SEARCH_FOR_SUPER_SIGNAL:inner',
10 'ENTRY_SIGNAL:inner',
11 'POST_DEFERRED:B',
12 'INIT_SIGNAL:inner',
13 '<- Queued:(0) Deferred:(1)',
14 'A:inner',
15 'A:middle',
16 'EXIT_SIGNAL:inner',
17 'SEARCH_FOR_SUPER_SIGNAL:inner',
18 'ENTRY_SIGNAL:inner',
19 'POST_DEFERRED:B',
20 'INIT_SIGNAL:inner',
21 '<- Queued:(0) Deferred:(2)',
22 'A:inner',
23 'A:middle',
24 'EXIT_SIGNAL:inner',
25 'SEARCH_FOR_SUPER_SIGNAL:inner',
26 'ENTRY_SIGNAL:inner',
27 'POST_DEFERRED:B',
28 'INIT_SIGNAL:inner',
29 '<- Queued:(0) Deferred:(3)',
30 'D:inner',
31 'D:middle',
32 'D:outer',
33 'POST_FIFO:B',
34 'D:outer:HOOK',
35 '<- Queued:(1) Deferred:(2)',
36 'B:inner',
37 'B:middle',
38 'B:outer',
39 'EXIT_SIGNAL:inner',
40 'EXIT_SIGNAL:middle',
41 'EXIT_SIGNAL:outer',
42 'ENTRY_SIGNAL:outer',
43 'POST_FIFO:B',
44 'RECALL:B',
45 'INIT_SIGNAL:outer',
46 '<- Queued:(1) Deferred:(1)',
47 'B:outer',
48 'EXIT_SIGNAL:outer',
49 'ENTRY_SIGNAL:outer',
50 'POST_FIFO:B',
51 'RECALL:B',
52 'INIT_SIGNAL:outer',
53 '<- Queued:(1) Deferred:(0)',
54 'B:outer',
55 'EXIT_SIGNAL:outer',
56 'ENTRY_SIGNAL:outer',
57 'INIT_SIGNAL:outer',
58 '<- Queued:(0) Deferred:(0)']

I have emphasized the beginning and ends of each run to completion event. This should make things easier to talk about. So we entered our chart, waited then sent a single event to it, and we got all of this action.

Lines 1-7 occurred as a result of us starting up the active object in the middle state. We entered the outer state, ran its entry code, then entered the middle state and ran its entry code, then its init code.

The entry code for the middle state started up or post_fifo thread, which would post an A signal to the chart once a second for 3 seconds. We are charging the capacitor. To see how, look at lines 7-13, we see that an A event was fired, the chart transitioned into the inner state, the entry condition for the inner placed the B event into the active objects deferred queue. Think of this as the battery pumping up the capacitor’s voltage with some charge. It can only happen a little bit at a time.

One second later we see the next pumping event on lines 13-21, and then one more time over lines 21-29. Notice that our Deferred queue is getting bigger.

Now it is time to zap someone, so we would hold our tazor close to our unsuspecting victim and trigger the D signal. We can see what happens in the rest of the spy output.

Lines 29-35 shows the event processor searching for a state method that knows what to do with the D signal. On line 33 we see that the outer state has posted a deferred signal B into our fifo buffer, then on line 34 we see that this was done using a HOOK which means that the code that managed it is an inherited behavior and that we aren’t expected to transition because of the D signal: the signal is HANDLED.

But the resulting B signal is not HANDLED, in fact it is going to create a cascade of activity.

Lines 35-46 show the beginning of this activity. Since the previous D signal was HANDLED (see line 34), the chart is still in its prior inner state. Lines 36-38 show the event processor searching the chart to see if any of the state methods know how to handle the B signal. It finds the trans code in the outer state, builds up a strategy, then starts to act on that strategy from lines 39-46. We see that it runs the exit event against the inner method, then runs the exit event against the middle method (which cancels our post_fifo thread if it is still running), then it posts the exit event against the outer state, then it posts the entry event against the inner state. On lines 43-44 we see that we are posting and recalling the next B signal from our deferred event queue.

Since our statechart is now in the outer state this B signal just leaves and re-enters the chart, triggering the next deferred B event to be posted to the fifo queue of the active object. This dynamic continues until all of the deferred B items in the active object queue are expressed. Your victim should be laying on the floor now.

So, there you have it, a very simple rendition of a tazor, our statechart could have look like this:

_images/tazor.svg

This diagram is almost topologically the same as the one described at the beginning of our Simple Posting Example. The only adjustment was to add a new signal from re-arming our tazor (READY).

Here are the state methods for the diagram:

@spy_on
def tazor_operating(ao, e):
  status = state.UNHANDLED
  if(e.signal == signals.ENTRY_SIGNAL):
    ao.recall()
    status = state.HANDLED
  elif(e.signal == signals.EXIT_SIGNAL):
    status = state.HANDLED
  if(e.signal == signals.INIT_SIGNAL):
    status = state.HANDLED
  elif(e.signal == signals.TRIGGER_PULLED):
    ao.recall()
    status = state.HANDLED
  # added this so we can rearm our tazor
  elif(e.signal == signals.READY):
    status = ao.trans(arming)
  elif(e.signal == signals.CAPACITOR_CHARGE):
    print("zapping")
    status = ao.trans(tazor_operating)
  else:
    status, ao.temp.fun = state.SUPER, ao.top
  return status

@spy_on
def arming(ao, e):
  status = state.UNHANDLED
  if(e.signal == signals.ENTRY_SIGNAL):
    multi_shot_thread = \
      ao.post_fifo(Event(signal=signals.BATTERY_CHARGE),
                      times=3,
                      period=1.0,
                      deferred=True)
    # We mark up the ao with this id, so that
    # state function can be used by many different aos
    ao.augment(other=multi_shot_thread,
                  name='multi_shot_thread')
    status = state.HANDLED

  elif(e.signal == signals.EXIT_SIGNAL):
    ao.cancel_event(ao.multi_shot_thread)
    status = state.HANDLED

  if(e.signal == signals.INIT_SIGNAL):
    status = state.HANDLED
  elif(e.signal == signals.BATTERY_CHARGE):
    status = ao.trans(armed)
  else:
    status, ao.temp.fun = state.SUPER, tazor_operating
  return status


@spy_on
def armed(ao, e):
  status = state.UNHANDLED
  if(e.signal == signals.ENTRY_SIGNAL):
    ao.defer(Event(signal=signals.CAPACITOR_CHARGE))
    status = state.HANDLED
  elif(e.signal == signals.EXIT_SIGNAL):
    status = state.HANDLED
  if(e.signal == signals.INIT_SIGNAL):
    print("charging tazor")
    status = state.HANDLED
  else:
    status, ao.temp.fun = state.SUPER, arming
  return status

Now we will create an active object, link the above state methods into it by starting it in the arming state:

tazor = ActiveObject()
tazor.start_at(arming)
time.sleep(4.0)

Notice that we wait 3 seconds to let it charge up.

Now let’s pull the trigger:

tazor.post_fifo(Event(signal=signals.TRIGGER_PULLED))
time.sleep(0.1)  # if you don't wait it won't look like it is working
print(tazor.trace())

The highlighted code above shows that we used the trace to output a high level view of what happened when we pulled the trigger:

119:51:25.509209 [75c8c] None: top->arming
219:51:26.511506 [75c8c] BATTERY_CHARGE: arming->armed
319:51:27.512153 [75c8c] BATTERY_CHARGE: armed->armed
419:51:28.512604 [75c8c] BATTERY_CHARGE: armed->armed
519:51:29.512080 [75c8c] CAPACITOR_CHARGE: armed->tazor_operating
619:51:29.513081 [75c8c] CAPACITOR_CHARGE: tazor_operating->tazor_operating
719:51:29.514085 [75c8c] CAPACITOR_CHARGE: tazor_operating->tazor_operating

Notice that our TRIGGER_PULL signal did not show up in our trace. We would expect it to occur between lines 4 and 5. This is because the trace only shows signals that cause state transition. The TRIGGER_PULL signal was handled by a HOOK and therefore didn’t directly cause a transition. Instead, it cause the recall method to post a CAPACITOR_CHARGE signal, which in turn causes two more state transitions.

To see our full spy log, we would use the following code:

pp(tazor.spy_full())

Which outputs the full story:

 1['START',
 2'SEARCH_FOR_SUPER_SIGNAL:arming',
 3'SEARCH_FOR_SUPER_SIGNAL:tazor_operating',
 4'ENTRY_SIGNAL:tazor_operating',
 5'ENTRY_SIGNAL:arming',
 6'INIT_SIGNAL:arming',
 7'<- Queued:(0) Deferred:(0)',
 8'BATTERY_CHARGE:arming',
 9'SEARCH_FOR_SUPER_SIGNAL:armed',
10'ENTRY_SIGNAL:armed',
11'POST_DEFERRED:CAPACITOR_CHARGE',
12'INIT_SIGNAL:armed',
13'<- Queued:(0) Deferred:(1)',
14'BATTERY_CHARGE:armed',
15'BATTERY_CHARGE:arming',
16'EXIT_SIGNAL:armed',
17'SEARCH_FOR_SUPER_SIGNAL:armed',
18'ENTRY_SIGNAL:armed',
19'POST_DEFERRED:CAPACITOR_CHARGE',
20'INIT_SIGNAL:armed',
21'<- Queued:(0) Deferred:(2)',
22'BATTERY_CHARGE:armed',
23'BATTERY_CHARGE:arming',
24'EXIT_SIGNAL:armed',
25'SEARCH_FOR_SUPER_SIGNAL:armed',
26'ENTRY_SIGNAL:armed',
27'POST_DEFERRED:CAPACITOR_CHARGE',
28'INIT_SIGNAL:armed',
29'<- Queued:(0) Deferred:(3)',
30'TRIGGER_PULLED:armed',
31'TRIGGER_PULLED:arming',
32'TRIGGER_PULLED:tazor_operating',
33'POST_FIFO:CAPACITOR_CHARGE',
34'TRIGGER_PULLED:tazor_operating:HOOK',
35'<- Queued:(1) Deferred:(2)',
36'CAPACITOR_CHARGE:armed',
37'CAPACITOR_CHARGE:arming',
38'CAPACITOR_CHARGE:tazor_operating',
39'EXIT_SIGNAL:armed',
40'EXIT_SIGNAL:arming',
41'EXIT_SIGNAL:tazor_operating',
42'ENTRY_SIGNAL:tazor_operating',
43'POST_FIFO:CAPACITOR_CHARGE',
44'RECALL:CAPACITOR_CHARGE',
45'INIT_SIGNAL:tazor_operating',
46'<- Queued:(1) Deferred:(1)',
47'CAPACITOR_CHARGE:tazor_operating',
48'EXIT_SIGNAL:tazor_operating',
49'ENTRY_SIGNAL:tazor_operating',
50'POST_FIFO:CAPACITOR_CHARGE',
51'RECALL:CAPACITOR_CHARGE',
52'INIT_SIGNAL:tazor_operating',
53'<- Queued:(1) Deferred:(0)',
54'CAPACITOR_CHARGE:tazor_operating',
55'EXIT_SIGNAL:tazor_operating',
56'ENTRY_SIGNAL:tazor_operating',
57'INIT_SIGNAL:tazor_operating',
58'<- Queued:(0) Deferred:(0)']

back to examples