Lesson 5: Increasing Concurrency¶
In the previous lesson, we modified ValveControllerImpl in an attempt to prevent it blocking its client while the valve is
moving in order to comply with requirement R3. Our solution only partly meets this goal. To illustrate where our
current solution fails, consider the following Coco snippet of a component implementation of a client using
our ValveControllerImpl component:
state Empty { client.startFillingTank() = { valve.open(); setNextState(Filling); } } state Filling { client.isValveMoving() = !(valve.isOpen() || valve.isClosed()) }
In state Filling, the client could be blocked when executing the transition for isValveMoving(), depending on
whether or not the valve finished moving when isValveMoving() was called. This is because while the valve is moving,
the ValveControllerImpl is in an execution state. If the client attempts a call on ValveControllerImpl while
it is in an execution state, the client will become blocked until the ValveControllerImpl enters an
event state.
The current implementation of the ValveControllerImpl therefore does not guarantee that isOpen() and
isClosed() are always non-blocking, and so requirement R3 is not met.
In this lesson, we will build a solution in which the required non-blocking property of the valve controller is always preserved, and the client can always remain responsive.
We will be introducing the following new language features:
- Nested states.
- Component queues and handling incoming signals from required ports.
- The built-in variable
nextState.
Extending the Architecture¶
We could make the ValveHALBase component more complex to solve the asynchronies there. However, this goes against
our objective of keeping ValveHALBase as a simple, handwritten component that makes the hardware/software interface
accessible to the Coco components. Instead, we are going to define a new level of abstraction called the ValveDriverImpl
with the goal of making a uniform interface defined by the ValveDriver port, which hides as many differences between
different hardware valve types as possible.
The revised architecture is as follows:
In addition to the existing ValveControllerImpl component, we have now introduced the following two new components:
ValveDriverImplis a driver module which abstracts away valve-specific characteristics behind a common interface. The existence of this component, and its provided port are invisible to the client application.ValveGroupis an encapsulating component which consists of aValveControllerImpland aValveDriverImpl, and implementsValveControlleras its provided port. It appears as a single opaque component to other components using it, which it interacts with via its provided port client.
We will abstract away from the details of whether or not the physical valve movement is synchronous or asynchronous, and
the way in which the end of the valve movement is determined. This satisfies the requirement R4, and allows a
single implementation of the ValveControllerImpl to work with more valve types, each of which potentially has its own,
type-specific driver.
Step 1: Creating the ValveDriver Port¶
We start by creating a new module called ValveDriver as follows:
import unqualified ValveHAL
port ValveDriver {
enum Status {
case Unknown
case Opened
case Closed
case Moving
case Error
}
function setup(settings : ValveParameters) : Bool
function open() : Nil
function close() : Nil
outgoing signal endOfMovement(result : Status)
machine {
// Initial position unknown
var status : Status = .Unknown
state Initial {
setup(_) = nondet {
ValidParameters:
{
setNextState(Ready);
true
},
InvalidParameters: false,
}
}
state Ready {
open() = setNextState(Moving(.Opened))
close() = setNextState(Moving(.Closed))
}
state Moving(target : Status) {
spontaneous = {
status = nondet {
MoveSucceeded: target,
MoveFailed: .Error,
};
endOfMovement(status);
setNextState(Ready);
}
}
}
}
We are not introducing any new Coco constructs in this port, so it should be clear to anyone who has worked through the previous lessons.
The complete Coco for this port is in Example_05B/src/ValveDriver.coco.
Step 2: Creating the ValveDriverImpl Component¶
Our next step is to create a component to implement a virtual valve. It will behave asynchronously, and report error
conditions, irrespective of whether or not the underlying hardware has these properties. We have designed this example
to implement asynchronous movement on hardware that behaves synchronously, while modifying the ValveControllerImpl so that
it remains responsive during the valve movement.
Our complete component for ValveDriverImpl is as follows:
import unqualified ValveDriver
import unqualified ValveHAL
@runtime(.MultiThreaded)
component ValveDriverImpl {
val client : Provided<ValveDriver>
val hal : Required<ValveHAL>
machine {
state Initial {
client.setup(settings : ValveParameters) = if (hal.setParameters(settings)) {
setNextState(Ready);
return true;
} else {
return false;
}
}
state Ready {
client.open() = setNextState(Moving(.Open))
client.close() = setNextState(Moving(.Close))
}
// Asynchronously execute the synchronous valve movement
execution state Moving(target : ValveHAL.Moves) {
hal.move(target);
// Signal end of movement - this valve has no errors
client.endOfMovement(match (target) {
.Open => .Opened,
.Close => .Closed,
});
// Ready for next move
setNextState(Ready);
}
}
}
We have not used any new constructs in this component, so it should be clear to anyone who has worked through the previous lessons.
Similar to the previous implementation of the ValveControllerImpl, this component cannot respond to its client, the
ValveControllerImpl, until the execution state has finished, and the state machine has changed to an event state. This
means that for ValveControllerImpl to remain responsive to its client, it must not interact with ValveDriverImpl
while the valve is moving.
The complete Coco for this component is in Example_05B/src/ValveDriver.coco.
Step 3: Modifying ValveControllerImpl to use ValveDriver¶
We now need to modify the ValveControllerImpl component to use the ValveDriverImpl component, instead of directly
accessing the ValveHALBase. We will:
- Replace the current required port instance
halwith a new instance ofValveDriver, calleddriver, and change all references ofhaltodriverthroughout the state machine. - Replace the execution state with an event state, which will handle the
endOfMovement(…)signal fromValveDriver.
While making these changes, we will also take the opportunity to refactor ValveControllerImpl. The modified version looks
like the following:
import unqualified ValveController
import unqualified ValveDriver
import unqualified ValveHAL
@runtime(.MultiThreaded)
component ValveControllerImpl {
val client : Provided<ValveController>
val driver : Required<ValveDriver>
machine {
client.isOpen() = false
client.isClosed() = false
state Initial {
client.setup(settings : ValveParameters) = if (driver.setup(settings)) {
setNextState(Ready.Unknown);
return true;
} else {
return false;
}
}
state Ready {
client.open() = {
driver.open();
setNextState(Moving)
}
client.close() = {
driver.close();
setNextState(Moving)
}
// Valve position unknown until the first move request is received
state Unknown {}
state Opened {
client.isOpen() = true
client.open() = client.moveEnded(true)
}
state Closed {
client.isClosed() = true
client.close() = client.moveEnded(true)
}
}
state Moving {
driver.endOfMovement(result : ValveDriver.Status) = {
setNextState(match (result) {
.Opened => Ready.Opened,
.Closed => Ready.Closed,
_ => abort(),
});
client.moveEnded(true);
}
}
}
}
In the refactored state machine, a new state called Ready is introduced to encapsulate all the
stationary states that are able to receive open() and close() requests, namely Unknown, Opened, and Closed.
These states are known as nested or child states of
Ready, which in turn is referred to as their parent state. Ready has transition declarations for
open() and close() at the top-level, which means that if either of them is called when the state
machine is in one of the nested states with no matching transition enabled, then the corresponding top-level transition
(i.e. in Ready) will be executed. For example, the nested state Unknown does not have any transitions, and therefore
the corresponding transitions in the parent state Ready will be executed in the event that open() or close()
is called.
Note
Coco supports hierarchical state machines. This means that every event state,
except for Terminated, can have nested states to any depth, forming a rooted tree of parent-child relationships with
Main as the root. Event transitions for the same event can be declared in
multiple states across the hierarchy.
When a component state machine receives an incoming function call or signal in a state S, it will execute the
corresponding enabled event transition declared in S. If no such match exists, then the state
machine will execute the corresponding match transition declared in the nearest ancestor of S. If no matching transitions
exist in any of the ancestors of S up to and including Main, then it will execute the illegal handler for that
incoming event. See Dispatching Events in a Component State Machine for further details.
Verifying your Changes (Example_05B)¶
The verification fails because the ValveControllerImpl is no longer well-formed and produces the following counterexample:
Analysis¶
The counterexample shows the required port driver is sending a signal to ValveControllerImpl, but
its queue is full. This is because we have not declared a queue for the ValveControllerImpl
component yet, and therefore it cannot receive any of the signals sent to it by its required port.
Step 4: Adding a Queue to ValveControllerImpl¶
We need to extend the ValveControllerImpl component with a queue that is big enough to store all of the signals that
can be sent by its required ports before they have to be processed. In this case, a queue length of size 1 suffices.
We add a queue by labelling the ValveControllerImpl component declaration with the following @queue
attribute:
@queue(1)
The modified Coco for this component is in Example_05C/src/ValveControllerImpl.coco.
Verifying the Models (Example_05C)¶
During verification, Popili finds an error where ValveControllerImpl calls abort(),
which is not allowed:
Analysis¶
This counterexample illustrates how the last event performed by ValveControllerImpl before the abort() call is
to dequeue an endOfMovement(Status.Error) in state Moving. Examining the corresponding transition in Moving,
we see:
driver.endOfMovement(result : ValveDriver.Status) = {
setNextState(match (result) {
.Opened => Ready.Opened,
.Closed => Ready.Closed,
_ => abort(),
});
client.moveEnded(true);
}
A match expression, matching on the result parameter in the endOfMovement(…) signal, is used to
determine the next state based on the result of the movement. The type of result is Status, which is an enum with five
values, but the match expression only provides logic for two of them, and an abort() for all the others.
Step 5: Adding Missing Match Cases to ValveControllerImpl¶
The solution to this error is to amend the match statement by adding the missing cases as follows:
driver.endOfMovement(result : ValveDriver.Status) = {
setNextState(match (result) {
.Open => Ready.Opened,
.Closed => Ready.Closed,
.Error => Ready.Unknown,
.Unknown => Ready.Unknown,
.Moving => abort(),
});
client.moveEnded(true);
}
In the case of an error, we do not know the position of the valve. For completeness, we will also add
the missing cases for .Unknown and .Moving.
The complete Coco for this component is in Example_05D/src/ValveControllerImpl.coco.
Verifying your Changes (Example_05D)¶
The verification fails because the ValveControllerImpl does not implement its provided port correctly:
Analysis¶
The counterexample shows the client calling open() on the ValveControllerImpl to close the valve, and in turn,
ValveControllerImpl calls open() on its required port driver. driver sends an endOfMovement(.Error)
signal to the ValveControllerImpl indicating that the move has failed, which in turn should send a moveEnded(false)
signal to its client. Instead, it erroneously sends a moveEnded(true) specifying that the move completed without
errors. As a result, the provided port, ValveController, expects ValveControllerImpl to be in state Opened when in
fact, it is in state Unknown. Consequently, the isOpen() returns false when the provided port was expecting it to
return true.
Note
When the verification finds an error, the shortest counterexample that results in the error is displayed. In this
example, the same error can occur when the client calls open() or close(), and since their respective
counterexamples are the same length, the one displayed may vary between releases.
Step 6: Modifying ValveControllerImpl¶
To resolve the error shown in the counterexample above, we will modify the Moving state in ValveControllerImpl to send the
correct signals:
state Moving {
driver.endOfMovement(result : ValveDriver.Status) = {
setNextState(match (result) {
.Opened => Ready.Opened,
.Closed => Ready.Closed,
.Error => Ready.Unknown,
.Unknown => Ready.Unknown,
.Moving => abort(),
});
client.moveEnded(nextState != Ready.Unknown);
}
}
Note
The builtin variable nextState : State is an immutable copy of the parameter in the most recent call to one of the
state transition functions. Its value can only be changed indirectly by calling
one of these state transition functions, such as setNextState.
This nextState variable is used in the following statement:
client.moveEnded(nextState != Ready.Unknown);
ValveControllerImpl sends the moveEnded() signal with an expression as its argument that refers to the value of
nextState. If the value of nextState is Ready.Unknown, then this means that ValveControllerImpl has
received one of the error conditions from the ValveDriverImpl, and sends the signal moveEnded(false). In all other
cases, it sends the signal moveEnded(true).
The complete Coco for this component is in Example_05E/src/ValveControllerImpl.coco.
Verifying your Changes (Example_05E)¶
The project now verifies correctly.
Step 7: Extending ValveControllerImpl to Preserve Signal Data¶
When the valve completes a movement, the ValveDriverImpl component sends an endOfMovement(…) signal that is declared
with the following signature:
outgoing signal endOfMovement(result : Status)
In contrast, when the ValveControllerImpl component sends a signal to its client to indicate the completion
of the valve movement, it does so with a signal declared as follows:
outgoing signal moveEnded(result : Bool)
This loses information that would probably be useful to its client. We will therefore change the moveEnded() signature
to match the corresponding signal sent by ValveDriverImpl by:
- Modifying the
ValveControllerport to change the signal signature. - Modifying the
ValveControllerImplcomponent to solve any static type errors caused by changing its provided port. - Reverifying the project, and fixing any other issues resulting from the changes.
Modifying ValveController¶
We will take the following steps to modify ValveController:
- Import the declarations from
ValveDriver. - Modify the signature in the signal declaration.
- Adapt the transition handlers that send this signal to handle the new signature.
- Modify the transition that sends this signal in state
Movingwith the new arguments. - Delete the
enumPosition.
Your modified ValveController port should look like the following:
import unqualified ValveDriver
import unqualified ValveHAL
port ValveController {
function setup(settings : ValveParameters) : Bool
function open() : Nil
function close() : Nil
function isOpen() : Bool
function isClosed() : Bool
outgoing signal moveEnded(result : ValveDriver.Status)
machine {
var position : ValveDriver.Status = .Unknown
isOpen() = position == .Opened
isClosed() = position == .Closed
state Initial {
setup(_) = nondet {
InvalidParameters:
{
return false;
},
ValidParameters:
{
setNextState(Ready);
return true;
},
}
}
state Ready {
open() = nondet {
{
position = .Opened;
moveEnded(position);
},
{
setNextState(Moving(.Opened))
},
}
close() = nondet {
{
position = .Closed;
moveEnded(position);
},
{
setNextState(Moving(.Closed))
},
}
}
state Moving(target : ValveDriver.Status) {
entry() = {
position = .Unknown;
}
spontaneous = {
position = nondet {
target,
.Unknown,
.Error,
};
setNextState(Ready);
}
exit() = {
moveEnded(position);
}
}
}
}
Let’s step through each change in turn.
The following line imports ValveDriver:
import unqualified ValveDriver
This brings the declaration of Status into scope. The following line is the amended signal
declaration:
outgoing signal moveEnded(result : ValveDriver.Status)
The following lines are changed to use the values of Status:
var position : ValveDriver.Status = .Unknown
isOpen() = position == .Opened
isClosed() = position == .Closed
The following lines are also changed to use Status defined in ValveDriver:
open() = nondet {
{
position = .Opened;
moveEnded(position);
},
{
setNextState(Moving(.Opened))
},
}
close() = nondet {
{
position = .Closed;
moveEnded(position);
},
{
setNextState(Moving(.Closed))
},
}
The parameter of state Moving is modified as follows:
state Moving(target : ValveDriver.Status) {
Finally, the spontaneous transition handler is changed as follows:
position = nondet {
target,
.Unknown,
.Error,
};
The complete Coco for this port is in Example_06A/src/ValveController.coco.
Modifying ValveControllerImpl¶
Your ValveControllerImpl component will now have type errors caused by the changes we have made to the signal declaration
in its provided port ValveController.
To fix this, edit your component so that the state machine looks like the following:
machine {
client.isOpen() = false
client.isClosed() = false
state Initial {
client.setup(settings : ValveParameters) = if (driver.setup(settings)) {
setNextState(Ready.Unknown);
return true;
} else {
return false;
}
}
state Ready {
client.open() = {
driver.open();
setNextState(Moving)
}
client.close() = {
driver.close();
setNextState(Moving)
}
// Valve position unknown until the first move request is received
state Unknown {}
state Opened {
client.isOpen() = true
client.open() = client.moveEnded(.Opened)
}
state Closed {
client.isClosed() = true
client.close() = client.moveEnded(.Closed)
}
}
state Moving {
driver.endOfMovement(result : ValveDriver.Status) = {
setNextState(match (result) {
.Opened => Ready.Opened,
.Closed => Ready.Closed,
.Error => Ready.Unknown,
.Unknown => Ready.Unknown,
.Moving => abort(),
});
client.moveEnded(result);
}
}
}
The complete Coco for this component is in Example_06A/src/ValveControllerImpl.coco.
Step 8: Building an Encapsulating Component ValveGroup¶
As defined in the new architecture at the start of this lesson, some of the functionality of the ValveControllerImpl has
been moved into a new component called ValveDriverImpl. The fact that we have divided the ValveControllerImpl into two
components and how they are connected together at runtime, is private knowledge that should be hidden from the client.
We should be free to refactor the ValveControllerImpl into as many components as we like in order to implement the
specified behaviour, without having any impact on the client.
We will create a new encapsulating component called ValveGroup, which consists of
an instance of ValveControllerImpl and an instance of ValveDriverImpl, and connects them together:
Once we have done this, we will modify the TestValveGroup to instantiate the new ValveGroup component
instead of ValveControllerImpl.
Creating ValveGroup¶
We start by declaring a new component called ValveGroup as follows:
import unqualified ValveController
import unqualified ValveControllerImpl
import unqualified ValveDriverImpl
import unqualified ValveHAL
@runtime(.MultiThreaded)
component ValveGroup {
val client : Provided<ValveController>
val hal : Required<ValveHAL>
val valve : ValveControllerImpl
val driver : ValveDriverImpl
init() = {
connect(client, valve.client);
connect(valve.driver, driver.client);
connect(driver.hal, hal);
}
}
The following lines declare instances of its provided and required ports:
The following lines declare the instances valve and driver of the components ValveControllerImpl and ValveDriverImpl
respectively:
val valve : ValveControllerImpl
val driver : ValveDriverImpl
The following constructor specifies how the component instances are connected together:
Modifying TestValveGroup to use ValveGroup¶
Our next step is to make the following changes to TestValveGroup:
- Import the declaration from
ValveGroupinstead ofValveControllerImpl. - Instantiate
ValveGroupinstead ofValveControllerImpl.
Our modified TestValveGroup is as follows:
import unqualified SimulatorImpl
import unqualified ValveController
import unqualified ValveGroup
@runtime(.MultiThreaded)
component TestValveGroup {
val client : Provided<ValveController>
val valve : ValveGroup
val hal : SimulatorImpl
init() = {
connect(client, valve.client);
connect(valve.hal, hal.client);
}
}
The following architecture diagram is a graphical representation of our updated TestValveGroup:
Verifying your Changes (Example_06A)¶
Your project should now pass the verification without any errors.