An effect of queuing the async event, as if it were an external event, is that it creates a kind of context switch. The current chain of event processing is completed and all Dezyne component queues are emptied before the ack event is fired. This means some interactions become possible which previously were not allowed because of potential circular reference problems. Two such situations are presented in the first 2 examples.

In other situations it is just very handy to schedule some actions at a moment you are sure all event processing has finished. The last examples shows this.

Communication across two provided ports

image

Without async it is not allowed in Dezyne to call an out-event in another provided port than the provided port that was the origin of the event. The following example shows how this can be done using async.

component Async_ForkProvidedIn
{
  provides IA ppa;
  provides IA ppb;

  behaviour
  {
    bool outstanding_a = false;
    bool outstanding_b = false;
    requires dzn.async reflector_a;
    requires dzn.async reflector_b;

    [!outstanding_a] on ppa.ia(): {outstanding_a = true; reflector_a.req();}
    [!outstanding_b] on ppb.ia(): {outstanding_b = true; reflector_b.req();}
    [outstanding_a] {
      on ppa.ia(): {} // ignore second ia
      on reflector_a.ack(): {outstanding_a = false; ppb.oa();}
    }
    [outstanding_b] {
      on ppb.ia(): {} // ignore second ia
      on reflector_b.ack(): {outstanding_b = false; ppa.oa();}
    }
  }
}
interface IA
{
  in void ia();
  out void oa();

  behaviour
  {
    on ia: {}
    on optional: oa;
  }
}

In this example an in-event on one port results in an out-event on the other port. The async only decouples the resulting out-event from the context of the in-event.

image
A provides in-event on one provides port generates an out-event on the other provides port

Fork a required event across two provided ports

image

Async can also be used to fork an event to two different provides ports. A similar argument applies here that without async circular references could be the result. The code is shown below:

component Async_ForkRequiredOut
{
  provides IA ppa;
  provides IA ppb;
  requires IB rp;

  behaviour
  {
    requires dzn.async() reflector;

    on ppa.ia(): rp.ib();
    on ppb.ia(): rp.ib();
    on rp.ob(): {reflector.req(); ppb.oa();}
    on reflector.ack(): ppa.oa();
  }
}

interface IA
{
  in void ia();
  out void oa();

  behaviour
  {
    on ia: {}
    on optional: oa;
  }
}
interface IB
{
  in void ib();
  out void ob();

  behaviour
  {
    bool is_active = false;
    on ib: is_active = true;
    [is_active] on inevitable: {ob; is_active = false;}
  }
}

At the reception of an out-event in the required port directly one of the out-events to a provided port is generated together with the sync.req. At the subsequent firing of the sync.ack the out-event to the other provided port is generated.

image
Forking an event over two provides interfaces triggered by a requires out-event

Armour for a missing synchronous out-event

image

A Dezyne component has a requires port where a synchronous out-event is expected in response to an in-event. The type of this port is the strict interface IStrict. We armour for the situation where the native component that implements this interface potentially violates this requirement by defining a robust interface IRobust that is more forgiving. You can see in the code below that the IRobust interface has one additional statement to describe the situation that the in-event does not result in a synchronous out-event. The Armour component maps the IRobust on the IStrict by simply ignoring the error.

interface IStrict
{
  in void ia();
  out void oa();

  behaviour
  {
    on ia: oa;
  }
}

interface IRobust
{
  in void ia();
  out void oa();

  behaviour
  {
    on ia: oa;
    on ia: {}
  }
}

extern str $char*$;
interface ILogger
{
  in void Log(str msg);

  behaviour {
    on Log: {}
  }
}

component ArmourMSOE {
  provides IStrict pStrict;
  requires IRobust rRobust;
  requires injected ILogger iLog;

  behaviour {
    requires dzn.async p;

    on pStrict.ia(): blocking {rRobust.ia(); p.req();}
    on rRobust.oa(): {pStrict.reply(); pStrict.oa(); p.clr();}
    on p.ack(): {pStrict.reply(); pStrict.oa(); iLog.Log($"Native comp does not send sync out-event oa"$);}
  }
}

component Dezyne {
  provides IA pp;
  requires IStrict rStrict;

  behaviour {
    on pp.ia(): blocking {rStrict.ia();}
    on rStrict.oa(): {pp.reply(); pp.oa();}
  }
}

interface IA
{
  in void ia();
  out void oa();

  behaviour
  {
    on ia: oa;
  }
}

component Native {
  provides IRobust pRobust;
  requires IRobust r;
}

component ArmouredSystemMSOE {
  provides IA pp;
  requires IRobust ir;

  system {
    Dezyne re;
    ArmourMSOE am;
    Native n;

    pp <=> re.pp;
    re.rStrict <=> am.pStrict;
    am.rRobust <=> n.pRobust;
    n.r <=> ir;
  }
}

We use both the blocking and async keyword in this example in order to detect the situation that the synchronous out-event is missing. The blocking keyword is used to guarantee that the armour only gives control back to the caller after it is sure that a potential violation is captured and dealt with. With the async we raise an internal event req’. It fires after all processing of the current context has been completed. So before it fires, a synchronous out-event may happen. In the normal case where the synchronous out-event happens we pass it on through the IStrict interface and cancel the async event. This is the happy flow.

If no such synchronous out-event fires in the armour then eventually the ‘ack’ event fires and the interface violation can be dealt with. In this example the armour simply logs the error and generates a synchronous out-event on its own to satisfy the IStrict requirement.

The reply statement in both situations lifts the blocking situation so after completion of the statement block control is passed back to the caller.

image
Handling the interface violation in the Armour component