Lifecycle Service

The lifecycle service is used to orchestrate the workflow and states of a simulation participant. It provides access to the ITimeSyncService, registers callbacks for state changes, queries the participant’s state and issues commands to change the state. For an overview of a participant’s state and its relation to the simulation refer to the participant lifecycle section.

Configuring the Lifecycle Service

To create the lifecycle service, a valid lifecycle configuration must be provided. Currently, only the mode that the lifecycle operates in can be configured. There are two possible operation modes:

  • Coordinated:

    • The lifecycle service will coordinate its state with other participants. It will do so by reacting to changes of the system state, which is based on the list of required participants.

    • A coordinated participant will only work, if any participant set a list of required participants as part of ISystemController::SetWorkflowConfiguration().

    • Coordinated participants will stop, as soon as the system state changes to the stopping state.

    • Coordinated participants can also be terminated externally using the ISystemController::AbortSimulation() call.

  • Autonomous:

    • An autonomous participant will not align its state to the system state or any participant state of other participants.

    • The lifecycle service will run through all participant states until it is running, and it will trigger all callbacks.

    • Autonomous participants must stop themselves by explicitly calling Stop().

    • The only way to stop an autonomous participant externally is the ISystemController::AbortSimulation() call.

Using the Lifecycle Service

An ILifecycleService instance can be retrieved from a Participant. To do so, a lifecycle configuration must be provided:

using SilKit::Services::Orchestration
auto lifecycleConfig = LifecycleConfiguration{OperationMode::Coordinated};
auto* lifecycleService = participant->CreateLifecycleService(lifecycleConfig);

Note

Please note that the lifecycle service and the time synchronization service can only be created once. Calling these methods more than once throws an exception.

Participants can synchronize their time with other participants (see synchronized simulation run for details):

auto* timeSyncService = lifecycleService->CreateTimeSyncService();
(...)
lifecycleService->StartLifecycle();

Alternatively, participants can run asynchronously (regarding the time synchronization). This will happen if CreateTimeSyncService was not called before StartLifecycle.

There are several callbacks, which are triggered on state transitions. They are always executed in the middleware’s worker thread:

lifecycleService->SetCommunicationReadyHandler(
   []() {
        std::cout << "Communication ready" << std::endl;
   }
);

lifecycleService->SetStopHandler(
    []() {
        std::cout << "Stopping" << std::endl;
    }
);

lifecycleService->SetShutdownHandler(
    []() {
        std::cout << "Shutting down" << std::endl;
    }
);

lifecycleService->SetAbortHandler(
    [](auto /*participantState*/) {
        std::cout << "Simulation is aborting" << std::endl;
    }
);

If a participant does not use the virtual time synchronization, a separate callback informs about the transition to the Running state. This can be used to start local timers:

lifecycleService->SetStartingHandler(
    []() {
        std::cout << "Starting" << std::endl;
    }
);

The CommunicationReadyHandler should be used to initialize and configure services and controllers.

Communication guarantees

The following communication guarantees apply to participants that utilize a Lifecycle Service and are limited to RPC and Publish/Subscribe services. Bus systems have definitions on the protocol level for message acknowledgement. For RPC or Publish/Subscribe however, communication guarantees depend on the implementation. In the SIL Kit, the CommunicationReadyHandler handler is the first point at which guarantees can be given. This implies that StartLifecycle has already been called. Remote services that have been created at the point a participant calls StartLifecycle will be available for communication as soon as the CommunicationReadyHandler is triggered. As an example, a call to Publish() on a IDataPublisher on participant A in the CommunicationReadyHandler is guaranteed to arrive at participant B if its IDataPublisher has been created before participant A has called StartLifecycle.

Communication in the CommunicationReadyHandler between participants that use the OperationMode::Coordinated is guaranteed. After StartLifecycle is called, these participants will wait for each other until all reached the ServicesCreated state. Therefore all services have already been created when the CommunicationReadyHandler is triggered.

A common use-case where the guarantee also applies is described as follows: A participant with OperationMode::Autonomous wants to join an already running simulation. In this case, the services of the running participants have been created and the newly joining participant can communicate in the CommunicationReadyHandler.

If several participants are defined with OperationMode::Autonomous and are executed ‘simultaneously’ in a distributed setup, their intention is to proceed through the lifecycle states without coordination. In this case, there is no deterministic execution order of controller creation and calls to StartLifecycle across the processes. Therefore, it is impossible to guarantee that a remote subscriber already exists at the time a participants tries to Publish() in the CommunicationReadyHandler. Without the useage of a lifecycle, there is no way to synchronize controller creation across different participants. In other words, controllers can appear at any time and there is no call to the SIL Kit API to build up a communication guarantee on. In cases were such guarantees are indespensable, it is recommended to use a ILifecycleService.

Note that a IDataPublisher can be defined with a history (of length 1). This guarantees the delivery of the last publication but is not coupled to the lifecycle states.

Another guarantee can be given for communication in the SimulationStepHandler. All participants that take part in the distributed time algorithm are ready to exchange messages if the first SimulationStepHandler is triggered, regardless of their OperationMode.

Controlling the Participant

After a successful startup, the participant will enter the Running state. State() returns the current state as a plain enumeration, whereas Status() returns additional information such as the participant’s name, the human-readable reason for entering the state, and the wall clock time when the state was entered.

To temporarily pause a simulation task, the Pause() method can be invoked with a human-readable explanation as a string argument. Execution can be resumed using the Continue() method.

To abort the simulation and report an error message, use the ReportError() method. This will change the current participant state to the Error state and report the error message to the SIL Kit runtime system. ReportError() is also called when the invocation of a registered handler throws an exception.

To stop a particular participant, use the Stop() method. This will exit Running or Paused, call the registered StopHandler (if it was set) and switch to the Stopped state. Afterwards, the participant will shut down by first changing to the ShuttingDown state, which triggers the shutdown handler (if it was set) and then finishing at the Shutdown state. At this point, the future provided by StartLifecycle will return.

API Reference

class ILifecycleService

Public Functions

virtual ~ILifecycleService() = default
virtual void SetCommunicationReadyHandler(CommunicationReadyHandler handler) = 0

Register a callback that is executed once all communication channels between participants with a lifecycle have been set up and are ready for communication.

The handler is called when the participant reaches the ParticipantState::CommunicationInitialized. This handler is invoked in an internal thread, and receiving messages during the handler invocation is not possible. For an asynchronous invocation, see SetCommunicationReadyHandlerAsync and CompleteCommunicationReadyHandlerAsync. After the handler has been processed, the participant switches to the ParticipantState::ReadyToRun state.

virtual void SetCommunicationReadyHandlerAsync(CommunicationReadyHandler handler) = 0

Register a callback that is executed once all communication channels between participants with a lifecycle have been set up and are ready for communication.

The handler is called when the participant reaches the ParticipantState::CommunicationInitialized. Even after the callback returns, the participant will stay in the ParticipantState::CommunicationInitialized until the CompleteCommunicationReadyHandlerAsync() method is called. Only then will the participant switch to the ParticipantState::ReadyToRun. Note that CompleteCommunicationReadyHandlerAsync may not be called from within any CommunicationReadyHandler. The CommunicationReadyHandler is executed in an internal thread and must not be blocked by the user.

virtual void CompleteCommunicationReadyHandlerAsync() = 0

Notify that the async CommunicationReadyHandler is completed.

This method must not be invoked from within a CommunicationReadyHandler.

virtual void SetStartingHandler(StartingHandler handler) = 0

(Participants without TimeSyncService only) Register a callback that is executed once directly before the participant enters ParticipantState::Running.

This handler is triggered just before the participant changes to ParticipantState::Running. It is only triggered if the participant does NOT use virtual time synchronization. It does not block other participants from changing to ParticipantState::Running and should only be used for lightweight operations such as starting timers. It is executed in the context of an internal thread that received the command. After the handler has been processed, the participant switches to the ParticipantState::Running state.

virtual void SetStopHandler(StopHandler handler) = 0

Register a callback that is executed on simulation stop.

The handler is called when the participant enters the ParticipantState::Stopping state. It is executed in the context of an internal thread that received the command. After the handler has been processed, the participant switches to the ParticipantState::Stopped state.

Throwing an error inside the handler will cause a call to ReportError().

virtual void SetShutdownHandler(ShutdownHandler handler) = 0

Register a callback that is executed on simulation shutdown.

The handler is called when the participant enters the ParticipantState::ShuttingDown state. It is executed in the context of an internal thread that received the command. After the handler has been processed, the participant switches to the ParticipantState::Shutdown state and is allowed to terminate.

Throwing an error inside the handler will cause a call to ReportError().

virtual void SetAbortHandler(AbortHandler handler) = 0

Register a callback that is executed on simulation abort.

The handler is called when the participant enters the ParticipantState::Aborting. It is executed in the context of an internal thread that received the command. After the handler has been processed, the participant switches to the ParticipantState::Shutdown state and is allowed to terminate.

Throwing an error inside the handler will cause a call to ReportError().

virtual auto StartLifecycle() -> std::future<ParticipantState> = 0

Start non blocking operation; returns immediately.

Starts simulation until the simulation is stopped or aborted.

Returns

Future that will hold the final state of the participant once the LifecycleService finishes operation.

virtual void ReportError(std::string errorMsg) = 0

Abort current simulation run due to an error.

Switch to the ParticipantState::Error state and report the error message in the SIL Kit system.

virtual void Pause(std::string reason) = 0

Pause execution of the participant.

Switch to ParticipantState::Paused due to the provided reason.

When a client is in state ParticipantState::Paused, it must not be considered as unresponsive even if a health monitoring related timeout occurs.

Precondition: State() == ParticipantState::Running

virtual void Continue() = 0

Switch back to ParticipantState::Running after having paused.

Precondition: State() == ParticipantState::Paused

virtual void Stop(std::string reason) = 0

Stop execution of the participant.

Allows the participant to exit the RunAsync loop, e.g., if it is unable to further progress its simulation.

Calls the StopHandler and then switches to the ParticipantState::Stopped state.

NB: In general, Stop should not be called by the participants as the end of simulation is governed by the central execution controller. This method should only be used if the client cannot participate in the system simulation anymore.

Precondition: State() == ParticipantState::Running

virtual auto State() const -> ParticipantState = 0

Get the current participant status.

virtual auto Status() const -> const ParticipantStatus& = 0

Get the current participant status.

virtual auto CreateTimeSyncService() -> ITimeSyncService* = 0

Return the ITimeSyncService for the current ILifecycleService.

Handlers

using SilKit::Services::Orchestration::CommunicationReadyHandler = std::function<void()>

This handler is called after ParticipantState::CommunicationInitialized is reached Cf., ILifecycleService::SetCommunicationReadyHandler(CommunicationReadyHandler);

using SilKit::Services::Orchestration::StartingHandler = std::function<void()>

This handler is called before the participant enters ParticipantState::Running. Cf., ILifecycleService::SetStartingHandler(StartingHandler);

using SilKit::Services::Orchestration::StopHandler = std::function<void()>

This handler is called after ParticipantState::Stopping is reached Cf., ILifecycleService::SetStopHandler(StopHandler);

using SilKit::Services::Orchestration::ShutdownHandler = std::function<void()>

This handler is called after ParticipantState::ShuttingDown is reached Cf., ILifecycleService::SetShutdownHandler(ShutdownHandler);

using SilKit::Services::Orchestration::AbortHandler = std::function<void(ParticipantState)>

This handler is called after ParticipantState::Aborting is reached Cf., ILifecycleService::SetAbortHandler(AbortHandler);

Data Structures

struct LifecycleConfiguration

The lifecycle start configuration.

Public Members

OperationMode operationMode
enum SilKit::Services::Orchestration::OperationMode

Available operation modes of the lifecycle service.

Values:

enumerator Invalid

An invalid operation mode.

enumerator Coordinated

The coordinated operation mode.

enumerator Autonomous

The autonomous operation mode.

struct WorkflowConfiguration

Details of the simulation workflow regarding lifecycle and participant coordination.

Public Members

std::vector<std::string> requiredParticipantNames

Participants that are waited for when coordinating the simulation start/stop.

struct ParticipantConnectionInformation

Public Members

std::string participantName

Usage Example

The following example is based on the SilKitCanDemo source code which is distributed with the SIL Kit, and slightly adapted for clarity. It demonstrates how to setup a lifecycle service and register callbacks to monitor participant state changes.

To demonstrate how to properly initialize other services, a CAN controller is initialized within the CommunicationReady callback of the lifecycle service. This is the recommended way to set up controllers before first use.

/* Copyright (c) 2022 Vector Informatik GmbH

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */

auto participant = SilKit::CreateParticipant(config, participantName, registryUri);
auto* lifecycleService = participant->CreateLifecycleService({OperationMode::Autonomous});
auto* canController = participant->CreateCanController("CAN1", "CAN1");

canController->AddFrameTransmitHandler(
	[](Can::ICanController* /*ctrl*/, const Can::CanFrameTransmitEvent& ack) {
		// Asynchroneously handle transmit status
});
canController->AddFrameHandler(
	[](Can::ICanController* /*ctrl*/, const Can::CanFrameEvent& frameEvent) {
		// Asynchroneously handle message reception
});

// Set an Init Handler
lifecycleService->SetCommunicationReadyHandler(
	[canController, &participantName]() {
		std::cout << "Initializing " << participantName << std::endl;
		canController->SetBaudRate(10000, 1000000);
		canController->Start();
});

// Set a Stop Handler
lifecycleService->SetStopHandler([]() {
	std::cout << "Stopping..." << std::endl;
});

// Set a Shutdown Handler
lifecycleService->SetShutdownHandler([]() {
	std::cout << "Shutting down..." << std::endl;
});

auto finalStateFuture = lifecycleService->StartLifecycle();
auto finalState = finalStateFuture.get();