Skip to main content

Status Communication

Actors in ControlBee communicate state through broadcast and targeted status. Broadcast status is visible to all related actors, while targeted status enables private coordination between specific actor pairs.

The Problem

Actors need different communication scopes for different situations. Sometimes you want all related actors to see your state (like _ready or _error). Other times, you need to coordinate a multi-step handshake with a specific partner where broadcasting every signal would flood unrelated actors with meaningless noise. A signal like ReleasedClamp between two specific actors has no meaning to anyone else.

Broadcast vs. Targeted Status

ControlBee provides two scopes for status communication:

MethodScopeAnalogy
SetStatus(key, val)All related actors can read itShouting to the room
SetStatusByActor(target, key, val)Only target can read itPassing a note to one person
// Broadcast — every related actor sees this
SetStatus("_ready", true);

// Targeted — only actorB sees this
SetStatusByActor(actorB, "ReadyForHandoff", true);

Reading follows the same split:

// Read broadcast status from a peer
Actor.GetPeerStatus(actorA, "_ready")

// Read targeted status that a peer wrote specifically for me
Actor.GetPeerStatusByActor(actorA, "ReadyForHandoff")

When to Use Each

Use Broadcast Status when:

  • Multiple actors need to know your state (e.g., "_ready", "_busy", "_error")
  • You want to enable monitoring and observability across the system
  • Other actors may discover and react to your state dynamically
  • The information is generally useful context (position, mode, capacity)

Use Targeted Status when:

  • Coordinating a specific multi-step handshake between two actors
  • The signal is only meaningful to one specific partner
  • You want to avoid flooding unrelated actors with coordination chatter
  • Building bilateral protocols (supplier/receiver, lock/unlock sequences)

Both patterns are essential. A typical actor uses broadcast for its general state and targeted for specific coordination protocols.

Broadcast Example

// Robot arm broadcasts its readiness — any supervisor or coordinator can see this
Actor.SetStatus("_ready", true);
Actor.SetStatus("_position", currentPosition);
Actor.SetStatus("_holdingPart", partId);

// Multiple other actors can monitor this state
if (Actor.GetPeerStatus(robotArm, "_ready") is true)
{
var position = Actor.GetPeerStatus(robotArm, "_position");
var partId = Actor.GetPeerStatus(robotArm, "_holdingPart");
// Decide whether to request work from this robot
}

Targeted Example (detailed in sections below)

// Supplier and Receiver coordinate a handoff using targeted status
// Only these two actors see these specific coordination signals
Actor.SetStatusByActor(receiver, "ReadyForHandoff", true);
if (Actor.GetPeerStatusByActor(supplier, "Supplying") is true) { ... }

Reactive Execution via _status and Scan()

The mechanism is reactive, not procedural — and this applies to both broadcast and targeted status. You don't write a linear script that blocks and waits. Instead:

  1. Actor A sets a status (broadcast or targeted)
  2. The framework automatically delivers a _status message to relevant actors
  3. Each actor's current state handles _status by calling its Scan() method
  4. Scan() checks conditions using GetPeerStatus() or GetPeerStatusByActor() and advances when met
// Actor A — signal that it's ready
Actor.SetStatusByActor(actorB, "DataReady", true);

// Actor B — in its state's Scan(), reacting to the signal
private bool Scan()
{
if (Actor.GetPeerStatusByActor(actorA, "DataReady") is true)
{
ProcessData();
return true;
}
return false;
}

Neither actor blocks. Neither actor spins in a polling loop. The framework's _status message is the trigger that causes Scan() to re-evaluate.

Typical ProcessMessage Structure

public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case StateEntryMessage.MessageName:
Initialize();
Scan(); // Check conditions on entry
return true;
case "_status":
return Scan(); // Re-check whenever any peer status changes
}
return false;
}

The Handshake Pattern

Almost every bilateral coordination follows the same shape:

Actor A                              Actor B
──────── ────────
SetStatusByActor(B, "signal1", true) →
Scan() sees signal1
[does work]
SetStatusByActor(A, "signal2", true)
← Scan() sees signal2
[does work]
SetStatusByActor(B, "signal3", true) →
...

Each step is a condition in Scan(), not a line in a sequential function. The state tracks which step it's on (via fields like _step or boolean flags), and Scan() checks the right condition for the current step.

Multi-Step Example

// Supplier side — UnloadingState
private bool Scan()
{
if (_waitingForReceiver)
{
if (Actor.GetPeerStatusByActor(receiver, "WaitingSupply") is true)
{
MoveToHandoffPosition();
Actor.SetStatusByActor(receiver, "Supplying", true);
_waitingForReceiver = false;
_waitingForConfirmation = true;
}
}

if (_waitingForConfirmation)
{
if (Actor.GetPeerStatusByActor(receiver, "ReceivedProduct") is true)
{
CompleteUnloading();
return true;
}
}

return false;
}

// Receiver side — LoadingState
private bool Scan()
{
if (_waitingForSupply)
{
Actor.SetStatusByActor(supplier, "WaitingSupply", true);
_waitingForSupply = false;
}

if (Actor.GetPeerStatusByActor(supplier, "Supplying") is true)
{
ReceiveProduct();
Actor.SetStatusByActor(supplier, "ReceivedProduct", true);
return true;
}

return false;
}

Why Not Just Send Messages?

Direct messages (actor.Send(new Message(...))) are also used in ControlBee, but status communication (both broadcast and targeted) has key advantages for coordination:

  • Statuses are persistent and queryable. If Actor B hasn't entered the right state yet when Actor A sets the flag, it doesn't matter — the flag stays set. When B eventually enters its state and calls Scan(), it sees the flag. A message would arrive and be dropped if B wasn't ready.

  • Statuses survive re-scans. Every time any peer's status changes, Scan() re-runs. Persistent flags mean you can check multiple conditions from different actors in a single Scan() without worrying about ordering or race conditions.

  • Statuses are inspectable. The current state of all flags is visible for debugging and monitoring. Messages are fire-and-forget.

Use messages for one-shot commands and request/response pairs. Use broadcast status for general state that multiple actors need to observe. Use targeted status for bilateral coordination protocols where both sides need to see each other's progress.

Cleanup in Dispose()

The flip side of persistence is that you must clean up. When a state exits, its Dispose() method resets all the status flags it set:

public override void Dispose()
{
// Reset broadcast statuses
Actor.SetStatus("_processing", false);

// Reset targeted statuses
Actor.SetStatusByActor(actorB, "DataReady", false);
Actor.SetStatusByActor(actorB, "Completed", false);
}

Without this, stale flags from a previous cycle would trick the next cycle's Scan() into advancing prematurely. Every SetStatus() or SetStatusByActor() call in a state should have a corresponding reset in Dispose().

Best Practices

1. Clean Up Status Flags in Dispose()

// ✅ Good - Resets all status flags
public override void Dispose()
{
Actor.SetStatus("_processing", false);
Actor.SetStatusByActor(receiver, "DataReady", false);
}

// ❌ Bad - Stale flags remain
public override void Dispose()
{
// Missing cleanup - next cycle may see old flags
}

2. Use Broadcast for General State

// ✅ Good - State visible to all actors
Actor.SetStatus("_ready", true);
Actor.SetStatus("_position", currentPosition);

// ❌ Bad - Using targeted for general state
Actor.SetStatusByActor(actorB, "_ready", true); // Only B can see
Actor.SetStatusByActor(actorC, "_ready", true); // Must repeat for each

3. Use Targeted for Bilateral Coordination

// ✅ Good - Private handshake between two actors
Actor.SetStatusByActor(receiver, "ReadyForHandoff", true);
if (Actor.GetPeerStatusByActor(supplier, "Supplying") is true) { ... }

// ❌ Bad - Broadcasting coordination chatter
Actor.SetStatus("ReadyForHandoff", true); // All actors see this noise

4. React to _status in Scan()

// ✅ Good - Reactive pattern
public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case StateEntryMessage.MessageName:
Scan();
return true;
case "_status":
return Scan(); // Re-check when peer status changes
}
return false;
}

// ❌ Bad - Polling pattern
public override bool ProcessMessage(Message message)
{
if (message.Name == TimerMessage.MessageName)
{
// Unnecessary polling - use _status instead
CheckPeerStatus();
}
return false;
}

5. Track Handshake Steps with Flags

// ✅ Good - Clear state tracking
private bool _waitingForReceiver = true;
private bool _waitingForConfirmation = false;

private bool Scan()
{
if (_waitingForReceiver)
{
if (Actor.GetPeerStatusByActor(receiver, "Ready") is true)
{
_waitingForReceiver = false;
_waitingForConfirmation = true;
}
}
return false;
}

// ❌ Bad - Unclear progression
private bool Scan()
{
// Checking all conditions without tracking progress
if (Actor.GetPeerStatusByActor(receiver, "Ready") is true) { ... }
if (Actor.GetPeerStatusByActor(receiver, "Done") is true) { ... }
}

6. Prefer Status over Messages for State Queries

// ✅ Good - Direct status read
bool ready = Actor.GetPeerStatus(stage, "_ready") is true;

// ❌ Bad - Round-trip message query
stage.Publish(new Message(this, "AreYouReady"));
// ... wait for response message ...

API Summary

MethodPurpose
SetStatus(key, val)Write broadcast status visible to all related actors
GetPeerStatus(actor, key)Read broadcast status from another actor
SetStatusByActor(target, key, val)Write targeted status visible only to target
GetPeerStatusByActor(sender, key)Read targeted status that sender wrote for me

The combination of persistent status flags (broadcast and targeted), automatic _status notifications, and reactive Scan() methods creates a coordination model that is race-condition-free, inspectable, and naturally handles timing differences between actors.

See Also