Architecture 2

Arch 11 – Event Addressing and Delegation

Concurrent HSM Architecture

Let’s recap what we have achieved so far. In the last episode, we introduced our extended framework based on the Quantum Platform (QP), which provides an intermediate base class Active derived from QActive and class Region derived from QHsm. An Active object is a hierarchical state machine (HSM) by itself with the addition of a thread-context and a main event queue. A Region object is a plain HSM which can be attached to an Active object to share its thread-context and event queue.

From the perspective of event interface, there isn’t really much difference between an Active object and a Region object, since they use similar APIs for posting and dispatching events. As a result, the overall system architecture can be represented by a simplified view of multiple concurrent hierarchical state machines communicating with each other via events:

This architecture provides a simple and unified model for asynchronous software. Grouping of HSMs by the associated Active objects allows for the customization of thread priorities, but it shouldn’t affect the event-driven behaviors of each HSM as defined by its statechart.

Direct HSM Addressing

While publish-subscribe offers a complete decoupling between event originators and recipients, it makes it much harder to trace event paths and to enforce reliable communication among state machines. It is therefore sometimes called publish-and-forget!

For real-time embedded systems in which system components are known at design time and reliability is critical, a direct addressing communication model may be more suitable. In our enhanced framework, we associate an HSM Number (HSMN) with each state machine instance, which can be used to designate the originator and recipient of any events. The framework maintains an array to map from HSMNs (indices to array) to active object pointers (QActive *) to which an event can be posted. This hybrid approach avoids any direct linkage and reference among state machines, yet provides explicit traceability of event paths and allows return paths from event recipients back to the originators.

The following diagram illustrates the data structure mapping an HSMN to the associated state machine attribute object (Hsm *) and active object (QActive *). The former provides attributes of the HSM (e.g. name and state) for logging purposes while the latter provides an API for posting events to the state machine.

The method Fw::Post() illustrates the latter case. e->GetTo() returns the HSMN of the recipient of the event. GetByIndex() returns the row (key-value pair) of the table indexed by the HSMN. GetValue() returns the active object to which the event is posted.

typedef Map<Hsm *, QP::QActive *> HsmActMap;

class Fw {
public:
    ...
    void Fw::Add(Hsmn hsmn, Hsm *hsm, QActive *container) {
        FW_ASSERT(hsmn != HSM_UNDEF && hsm && container);
        QF_CRIT_STAT_TYPE crit;
        QF_CRIT_ENTRY(crit);
        HsmAct *existing = m_hsmActMap.GetByIndex(hsmn);
        FW_ASSERT(existing && (existing->GetKey() == NULL));
        m_hsmActMap.Put(hsmn, HsmAct(hsm, container));
        QF_CRIT_EXIT(crit);
    }
    // If the HSM to post to is invalid, e.g. HSM_UNDEF, the event will be discarded.
    void Fw::Post(Evt const *e) {
        FW_ASSERT(e);
        QActive *act = m_hsmActMap.GetByIndex(e->GetTo())->GetValue();
        if (act) {
            act->post_(e, QF_NO_MARGIN);
        } else {
            QF::gc(e);
        }
    }
protected:
    ...
    static HsmActMap m_hsmActMap;
}

Automatic Event Delegation

An important enhancement of our event framework is that each hierarchical state machine (HSM) is an individual addressable unit, regardless to whether the HSM is an active object by itself or a region contained directly or indirectly by an active object.

This feature enables a scalable system architecture, as for many real-time operating systems (RTOS) the number of threads (as required by active objects) is bound by a hard ceiling but the level of object composition is only limited by the amount of memory available. This allows us, for example, to model each TCP connection of a server with an HSM of class TcpConnection, each being a region contained by an active object of class NetworkManager. As a result, we have a single thread shared by one NetworkManager instance and an arbitrary number (e.g. 10k) of TcpConnection instances running concurrently. This kind of sharing is possible because all hierarchical state machine instances are non-blocking by nature.

We may iterate this pattern to attach multiple levels of regions as illustrated in the block diagram below. Each HSM instance is uniquely identified by its HSMN, e.g. NETWORK_MANAGER and TCP_CONNECTION + i where i is the index to an TcpConnection instance.

Upon initialization, regions register themselves to the containing active object which maintains a map from HSMNs to the corresponding region instances. When an active object receives an event, it automatically delegates the event to the addressed hierarchical state machine according to the recipient HSMN specified in the event. The event may be addressed (1) to the active object itself or (2) to one of the regions attached directly or indirectly to the active object.

With automatic event delegation to any addressed HSMs, we can truly achieve our simple and unified architectural model of multiple concurrent hierarchical state machines communicating with each other via events. The code fragments below illustrate the implementation of region registration and event delegation within our framework.

void Region::Init(Active *container) {
    FW_ASSERT(container);
    m_container = container;
    container->Add(this);
    m_hsm.Init(container);
    QHsm::init(0);
}

void Active::Add(Region *reg) {
    FW_ASSERT(reg);
    Hsmn regHsmn = reg->GetHsmn();
    FW_ASSERT(regHsmn != HSM_UNDEF);
    HsmnReg *existing = m_hsmnRegMap.GetByKey(regHsmn);
    FW_ASSERT(existing == NULL);
    m_hsmnRegMap.Save(HsmnReg(regHsmn, reg));
    Fw::Add(regHsmn, &reg->GetHsm(), this);
}

void Active::dispatch(QEvt const * const e, std::uint_fast8_t const qs_id) {
    (void)qs_id;
    Hsmn hsmn;
    // Discards event if it is associated with an undefined HSM.
    if (!IS_EVT_HSMN_VALID(e->sig)) {
        return;
    }
    if (IS_TIMER_EVT(e->sig)) {
        Timer const *timerEvt = static_cast<Timer const *>(e);
        hsmn = timerEvt->GetHsmn();
    } else {
        Evt const *evt = static_cast<Evt const *>(e);
        hsmn = evt->GetTo();
    }
    if (hsmn == m_hsm.GetHsmn()) {
        // Case (1) Event addressed to active object itself.
        QHsm::dispatch(e, 0);
        // Handles all reminder events generated as a result of e.
        m_hsm.DispatchReminder();
    } else {
        // Case (2) Event addressed to an attached region.
        HsmnReg *hsmnReg = m_hsmnRegMap.GetByKey(hsmn);
        if (hsmnReg && hsmnReg->GetValue()) {
            hsmnReg->GetValue()->Dispatch(e);
        }
    }
}