Actor Framework Discussions

cancel
Showing results for 
Search instead for 
Did you mean: 

Actor Principles, Complexity and Class Count

I have been studying actor programming best practices and reviewing the presentations Dave (aka: Daklu) has made on this topic.  One area that I am having the most trouble with is the 'rule' that an actor should never be blocked.  I interpret this as the message handler should execute messages quickly and we should not put code in the message execution that has an unknown amount of execution time.  This seems simular to the rules around producer/consumer architecture where you have a UI producer event structure that you want to quickly process UI events and hand off any lenghy processes to the consumer.  So this made sense to me at first.

But, the problem I run into is while the actor's message hander is the 'producer' in this scenario there is no 'consumer' in an Actor.  So, I am left with either handing off the lenghty process to a 'helper loop' or to another actor.  This is where the complexity problem arises.

Lets take the example of an actor that has an initialization process.  This process consists of 3 steps.  Each step requires accessing an external resource (device, file, database, whatever).  The point is accessing the external resource will require an unknown amount of time to execute.  So, my first inclination is this should not be done in the message execution but rather handed off to a child actor or helper loop.  The second part of this example is each of the 3 steps is an action we may want to repeat individually after initialization.  So, we need a means to start each action in the Actor and a means to tell the actor the action is complete.  If we use a child actor, we need a means to perform the action in the child.  This amounts to 9 messages.

Helper Loop Example.png

And, by placing the lenghy processes in the message execution of the child actor, we are violating the same rule for the child that prompted us to move the actions outside of the parent message execution.

If, instead we use a helper loop and decide to not call it an 'actor', we still need the 6 messages in the parent actor to start and complete the actions.  We then need to create a new message passing method (queue, user event, etc) for the parent actor to command the helper loop to execute each step.  And since the helper loop normally resides on the same block diagram as the parent actor's message hander (running in parallel), we still prevent the actor from exiting completely if the helper loop is blocked for some reason when executing the lengthy process.

In my opinion, there does not seem to be much advantage to moving the lenghy process outside of the message execution in the parent actor.  If you were to leave it there, 9 messages would be reduced to 3.  The complexity of the application would be reduced and the ability to understand and debug it improved.  The class count in your project would be reduced, thus reliving slowdown issues with the IDE.  And in the end, there does not seem to be a way to satisfy the requirement of non-blocking since we can only move the blocking action down to a lower level.

I guess what I am asking is if this is worth it?  What is the argument to justify the costs?

Lastly, I am curious about the best way to implement the multi-step process.  Where do you put the logic to tell the system to proceed from Action A to Action B to Action C when you are performing the initilization vs only executing one Action when you are not performing initialization?  Should it be within the Action Complete messages or should it return to the initialize message that uses state data to process each Action's completion and determine next steps?  Should we pass information about how to report completion to the message that performs the Action?  I want to make this flexible enough so I can apply this to several areas but I also do not want to make it any more complex than it needs to be.

Thanks for reading all that and for your feedback,

-John

-John
------------------------
Certified LabVIEW Architect
Message 1 of 47
(15,437 Views)

Hi John,

Very interesting post! I also always have a no-blocked-actors rule on my mind when programming with the framework. To mean there is a difference between a blocked actor and a busy actor. If an actor is actually processing message, he is busy. If an actor is waiting for data to process a message, he is blocked. So I try to avoid waiting actors, but I don't have a problem with busy actors.

Another question which I would consider in your example is, if outsourcing action a, b or c could create race conditions. Lets say action a changes the way following messages are processed. Is it required that this change happens after action a is requested or after it is processed? And if the following messages require that action a is processed, can they eventually be ignored while action a is executing? If they require action a to be processed but can't be ignored, maybe your actor should be blocked!

Best regards,

Mike

Message 2 of 47
(4,799 Views)

Thanks for starting this topic, this has been an issue i have struggled with as well.  I often work with hardware and at some point you have to actually "do the work".  You can keep abstracting things lower and lower but i agree with you, where does it stop.  Technically you could abstract out all the way down to the  vi.lib level, such as visa or tcp functions. 

For the time being i generally have been writing a driver for hardware components and putting that in a helper loop.  I guess i agree with you in that i'm not sure exactly what i'm gaining from that.  If the hardware is blocking in the helper, that actor will not shut down as you mention, until it unblocks.  If my actor is solely responsible for this one item that has the potential to block, then why abstract it to a helper? 

As far as the IDE issues, the heaviness of the command pattern is one reason that i shy away from it.  Especially in large projects.  I have been using Daklu's MHL actor pattern using Lapdog.  What you give up in flexibility of expandable messages, i think you gain in debugging and general headaches, such as renaming messages or broken dynamic dispatch overrides. 

I am curious how others approach hardware blocking. 

Message 3 of 47
(4,799 Views)

jlokanis wrote:

...by placing the lenghy processes in the message execution of the child actor, we are violating the same rule for the child that prompted us to move the actions outside of the parent message execution.

I don't buy that as a rule that should be applied to all actors. However I do believe that it should be applied to any actor that acts as a public interface. I use the term actor to mean not only literal implementations of Actor.lvclass, but also include any asynchronous messaging system.

If I have an asynchronous interface I'm operating on its sole job is to respond in a reasonable time to requests. Really, that's it. How it does the job is irrelevant to me as the consumer of this interface. Note a "reasonable time" may mean different things depending on context (think networking verus real-time).

If this interface internally has an Actor proper (capital "A", as in Actor.lvclass) implementing some lengthly blocking routine it doesn't affect me. I can't interact with this Actor as I know nothing about it. As long as my interface is designed to accomidate this blocking Actor the fact that it is blocking shouldn't matter.

Now I personally don't think an Actor implementation should ever serve as a public interface, which brings up something interesting. If I think this non-blocking rule only applies to public interfaces, and I don't think an Actor proper (as in Actor.lvclass) belongs as an interface, I don't see much point to the non-blocking rule to begin with. If all my running Actors are private members of a library that have no visibility outside the library, let them be blocking if they need to be. It's the job of whatever is serving as the interface to those Actors to mediate that blocking.

0 Kudos
Message 4 of 47
(4,799 Views)

John,

Rules I live by:

1. Keep the UI responsive. I separate the UI from the system using a Model View Controller (MVC) architecture. I use helper loops in my UI actors that are event driven.

2. Use a hierarchy to simplify logic and to basically have a bunch of parallel helper loops. The top level and anything that connects directly to a user interface should be responsive. Lower level will often times have time consuming processes.

3. If you need a bunch of the data contained in the actor's private data don't make a helper loop. Do the work in the actor. If the work takes a long time this actor should probably not connect directly to a UI.

4. Calculate state. Convert values to states and then you have a nice clean interface. "Go to this state." "I am in this state."

4. Model your system. What data is needed to determine the state of something? How atomic can you go here? What models are aggregates of other models?

A concrete example:

My machine has a UI "View" that connects to a 3 level hierarchy. System, subsystem, component. The UI makes direct connections to the system and subsystem level but not direct to the components. The components in my case are real physical equipment (power supplies: analog on the FPGA, serial, EthernetIP, etc.) The components have a logic model which determines their state. The state and actual data values are sent to the subsystem.

Some of my components need to be run in a "polling" mode. Here I use a helper loop to send a message to the main loop to perform the repetitive action. I have made a genuine actor for this that you give an update rate and a message when you launch it and it sends that to whoever called it. I call this my "parallel repeating loop".

One of my components (EthernetIP Allen Bradley controller for a 300kV power supply) takes different amounts of time to do its tag reads and writes. This one I can't use a parallel loop at a regular interval or messages will stack up. Here I reseed the message to update itself after each method.

The subsystem pools the data from all of its components and calculates its own subsystem state. The calculation is just math and boolean logic so I do it right there in the subsystem. The subsystem sends the data to the UI. The UI fires a user event so that any open front panels (different views) can be registered for the event and then update their view.

The subsystems send their state to the system level. System determines its state as a function of subsystem states. Again math and boolean logic, so no helper loop. The system also connects to the UI.

UI helper loops are all about displaying data that the system and subsystems send and handling user commands. They use the event structure and my UIs are very processor friendly.

Hope that helps!

Casey

Casey Lamers


Phoenix, LLC


casey.lamers@phoenixwi.com


CLA, LabVIEW Champion


Check Out the Software Engineering Processes, Architecture, and Design track at NIWeek. 2018 I guarantee you will learn things you can use daily! I will be presenting!

0 Kudos
Message 5 of 47
(4,799 Views)

Thants for the responses!  I have been thinking about my specific situation in this context to see if any of your ideas will help.  I am leaning toward eliminating the helper and performing the actions within the main (parent) actor message execution.  These operations are required to setup my actor and they must be done in order at initialization.  I also realized that while I may want to repeat some of the actions later (to re-load data from my data source), not all of the actions fit this catagory.

Also, I want to trap errors that occur within the actions and take appropriate steps.  If they are separated from the main actor, that just complicates the interface because I need to communicate the error back instead of just the data.

I can still make these actions into separate messages within the parent, but I am leaning toward a different scheme for performing them.  I am thinking of adding an init flag to the actor and having each message call the appropriate next message in the sequence if the flag is set.  Once I get to the last action, I will clear the init flag.  This way if I call an action and init is not set, it will perform the action, update the actor state data but not contine to the next action in the sequence.  This seems like a cleaner way to do this type of operation.  The one thing I do not like about this design is the need  for each action message to know what action should follow when doing an init.  It seems like it would be better to keep that information in one place instead.  Any ideas or examples of how to do this cleaner?

I agree with the comments about the UI responsiveness.  I use a simular process of handing UI updates to a helper event loop to do the display work.  I also have the UI event loop send a message to the actor when the user takes an action.

As for command-pattern architectures, I am not sure I would have used it if I had known about the IDE issues. I like the elegant nature of the design but it has gotten to the point where I can only work on my code in sections in separate projects setup to limit the number of classes in memory at once.  This does help with keeping things more modular (MVC style) however, so maybe that is not totally a bad thing.

-John
------------------------
Certified LabVIEW Architect
0 Kudos
Message 6 of 47
(4,799 Views)

One of my components (EthernetIP Allen Bradley controller for a 300kV power supply) takes different amounts of time to do its tag reads and writes. This one I can't use a parallel loop at a regular interval or messages will stack up. Here I reseed the message to update itself after each method.

Do you find any issues with this design?  There seems to be a debate about giving any actor(no mater what framework) the ability to message itself and put commands on its own queue.  You seem to have strict design parameters so this may not be an issue for you.  A general "Update" Message seems pretty benign but i think this is why i have pushed so much of my hardware to helper loops.  Soley to eliminate the ability for an actor to message itself. 

I can still make these actions into separate messages within the parent, but I am leaning toward a different scheme for performing them.  I am thinking of adding an init flag to the actor and having each message call the appropriate next message in the sequence if the flag is set.  Once I get to the last action, I will clear the init flag.  This way if I call an action and init is not set, it will perform the action, update the actor state data but not contine to the next action in the sequence.

Have you looked into the State Machine info for the Actor Framework?  This might eliminate your need for flags.  Instead you can just handle message differently depending upon your state.  In addition, you could look into either bundling several messages into a single message or creating a new method/message pair that does all this work.  I will say that the state pattern using the Actor Framework is not going to help your IDE issues.  From an OOP perspective I tend to use the pattern for hardware but not for message handling since i'm not using the command pattern. 

However, I think this issue right here is the heart of your Helper Vs Actor Debate.  The more you start bundling the longer things might take and the less responsive it gets.  A think a general argument around here is that the Actor Queue should not be a job queue, and that if you need to do a sequence of actions that it should be passed to a helper loop that acts as a job queue. 

The debate goes round and round because then you get right back into your responsiveness argument....

0 Kudos
Message 7 of 47
(4,799 Views)

Good questions John.  I'm glad you asked them because it helps me understand what I need to clarify in my presentation.

First, I need to point out the presentation I gave at the CLA Summit (and at last year's NI Week) is titled "Fundamentals of Actor Oriented Programming."  It's intended to introduce people to actor oriented programming using concepts that are familiar to QSM/QMH programmers.  Unfortunately there isn't enough time to dig into too many nitty gritty details like you are asking about.

You are correct that continuously pushing lengthy processes lower in the hierarchy doesn't gain you much of anything.  ("It's turtles all the way down.")  The real question is, what does "lengthy" mean?  Like kegghead said, it's a relative term.  I consider a process "lengthy" when it causes messages to stack up in the queue.  (Stacked messages are, IMO, generally a bad thing.)  If you have an actor you can guarantee will receive only one message every 10 minutes, there's no problem with having it execute a 9 minute process in its message handling loop.

In your example, the main actor (M) can receive requests to do A, B, or C, but it can also receive requests to do many other things, as well as receive information from other actors that affect how M (or any of M's subactors, S) handles those requests.  By making a subactor (or helper loop) responsible for doing A, B, and C, you free up the message handling loop to handle all the other messages that may arrive while those lengthy processes are executing.  Those processes take just as long in the subactor, but the subactor is responsible for fewer things and is (usually) receiving messages less frequently, so its message queue doesn't stack up while executing the process.  Ultimately it comes down to managing the messages that are flowing to the loop that is executing the long process.

"In my opinion, there does not seem to be much advantage to moving the lenghy process outside of the message execution in the parent actor.  If you were to leave it there, 9 messages would be reduced to 3.  The complexity of the application would be reduced and the ability to understand and debug it improved."

On the surface 3 messages appears simpler than 9, but it really depends on the application and project requirements.  If you have A, B, and C execute in the message handling loop you have two basic options:

1. Have each action enqueue the following action.  In other words, A's last act is to enqueue ExecuteActionB, and B's last act is to enqueue ExecuteActionC.  The disadvantage of this is you have embedded business logic in the independent actions.  You can't execute A without also executing B and C.

2. Enqueue BeginActionA, BeginActionB, and BeginActionC messages all at once.  If each action takes 10 seconds, you've just committed yourself to 30 seconds before that actor can respond to any other messages, like Exit.  Maybe that's okay, maybe it isn't.

To take it a step further, suppose the actor just started initialization and the user did something that causes two BeginActionB and an additional BeginActionC messages to be sent to the actor, then changes his mind and tries to exit the application.  Now the user has to wait 60 seconds for the app to quit.

How do you solve that problem?  Priority messages?  That *can* work, but it creates an entirely new set of issues and adds complexity.  By pushing the lengthy process down to S and having M handle the sequencing you guarantee that the longest you will ever have to wait to do something new (like exit) is the duration of the single longest subprocess.

"The class count in your project would be reduced, thus reliving slowdown issues with the IDE."

Yeah, count that as another reason I prefer name-data messaging over command-pattern messaging.  But you can't claim you weren't warned. 

"Where do you put the logic to tell the system to proceed from Action A to Action B to Action C when you are performing the initilization vs only executing one Action when you are not performing initialization?  Should it be within the Action Complete messages or should it return to the initialize message that uses state data to process each Action's completion and determine next steps?"

I don't see any reason to go back to the Init message.  One way to implement it is to create an IsInitializing flag in M.  When it receives an action complete message from S it checks the flag.  If the flag is true then it sends the next action in the sequence to S.  When the sequence is complete M sets the flag to false.  You'll have to decide what to do with other messages that arrive during the initialization process.

0 Kudos
Message 8 of 47
(4,799 Views)

Jed394 wrote:

I have been using Daklu's MHL actor pattern using Lapdog.

Just as an FYI, I'm calling it "Agile Actors" now.  Presumptuous?  Perhaps.

Jed394 wrote:

What you give up in flexibility of expandable messages, i think you gain in debugging and general headaches, such as renaming messages or broken dynamic dispatch overrides. 

I hope you mean relief from general headaches, not gain general headaches. 

Jed394 wrote:

I am curious how others approach hardware blocking. 

There's not much you can do when your software is blocked by a hardware call other than wait.  When I have an actor directly interacting with hardware, I'll typically put the hardware calls in a helper loop to prevent the MHL from stacking up.  That allows my app to be as responsive as possible given the constraints of the hardware.

You *can* put the hardware calls directly in the MHL, but then your parent actor (assuming you are using a hierarchical design) has to be aware of the hardware actor's state and task progress to avoid stacking up the message queue.  Really all you're doing is pushing that management responsibility up another level.

0 Kudos
Message 9 of 47
(4,799 Views)

"Where do you put the logic to tell the system to proceed from Action A to Action B to Action C when you are performing the initilization vs only executing one Action when you are not performing initialization?  Should it be within the Action Complete messages or should it return to the initialize message that uses state data to process each Action's completion and determine next steps?"

I don't see any reason to go back to the Init message.  One way to implement it is to create an IsInitializing flag in M.  When it receives an action complete message from S it checks the flag.  If the flag is true then it sends the next action in the sequence to S.  When the sequence is complete M sets the flag to false.  You'll have to decide what to do with other messages that arrive during the initialization process.

It seems to me that if the Action Complete message that S sends to M needs to read the flag and then decide to execute the next step in the init sequence or not, that is ebedding the business logic across each Action Complete message.  I was thinking of a different solution that would capture the logic in a single place.  Perhaps having a central message that controls the sequence (like the init message) and then having the Action Complete Messages all call that.  This way we could check the flag in the init message and decide what to do next (continue init or nothing if we are not in init mode).  If I want to change the order or add a step, I only need to modify the init message.  I may want to rename the init message, however.

This breaks the responsibilites down to this:

Init message sets the init flag, clears all the action complete flags and sends the Begin Action message for the first step.

Begin Action (M) clears the Action Complete Flag for that action (if not already cleared) and calls S to do the action.

S returns the data to M using the Action Complete message.

The Action Complete message sets the Action Complete Flag, stores the returned data and checks the init flag to see if it should call the Init message.

The Init message sees that the init flag is set and then checks each action complete flag to see what is done and what needs to be done next.  It then either calls the next Begin Action message or if all actions are complete, clears the init flag.

Any messages that arrive durring this process can see if the data is valid for an action by testing the action's flag and can see if an init is being done to decide if they should continue or ignore the message.

I am sure Dave will pokes some holes in this now.    So, poke away!  I realise this is adding complexity.  I think this situation is generic enough that we should be able to come up with a reusable design pattern here that can be applied to many applications and architectures, regardless if you use AF or AA or something else. 

-John
------------------------
Certified LabVIEW Architect
0 Kudos
Message 10 of 47
(4,799 Views)