lifecycle - App Lifecycle Management
Overview
This module provides classes and interfaces that allow the user to define and execute the lifecycle of an application.
Application Lifecycle
An application running on a core usually depends on several services like an ethernet stack and other buses, a logger, a non volatile storage among others.
Each of these services as well as the application itself will at some point be initialized, run and eventually shut down. For each service a LifecycleComponent
can be created that controls this service. The LifecycleManager
is there to make sure these transitions are performed on the lifecycle components.
Usually the lifecycle needs to process in a certain order. First the hardware has to be initialized, then logging or networking, and finally the application itself is run.
In order to model simple dependencies between services each lifecycle component is assigned a runlevel at which it shall be initialized, run, or shut down. During startup the lifecycle manager goes through all runlevels in ascending order. At each runlevel, first all its components are initialized, and then all its components are run. During shutdown components are shut down in reverse, i. e. in descending order of their runlevels. Note that even if a component is shut down and run again by a sequence of calls to transitionToLevel()
, each component is initialized at most once by the lifecycle manager the first time its run level is reached.
Asynchronous State Transitions
A component implementing LifecycleComponent
must provide implementations for the three state transition functions init()
, run()
and shutdown()
. After registering a lifecycle component to the lifecycle manager, the lifecycle manager will make sure that these are executed at the appropriate time.
At the end of each state transition implementation the method LifecycleComponent::transitionDone()
must be called to tell the lifecycle manager that the component has completed its transition. This explicit callback allows the implementation to schedule asynchronous operations from within a transition and return early, as long as the asynchronous operation eventually calls transitionDone()
.
All transitions are scheduled asynchronously by the lifecycle manager. For this an implementer of LifecycleComponent
must also provide an implementation of getTransitionContext()
that specifies in which async context to execute which transition. For convenience there are several derived classes that should be used as a base for implementing a lifecycle component. They handle the most common use cases and implement getTransitionContext()
accordingly.
In the simplest case, e. g. when using the SimpleLifecycleComponent
, transitions are scheduled in the same context as the lifecycle manager itself.
The classes SingleContextLifecycle
in include/lifecycle/SingleContextLifecycleComponent.h
and AsyncLifecycleComponent
in include/lifecycle/AsyncLifecycleComponent.h
allow the implementer to specify a context in which to schedule the transitions, or even different contexts for each transition type, respectively.
A small sample lifecycle component could look like this:
// #include <lifecycle/SingleContextLifecycleComponent.h>
// #include <async/Types.h>
namespace lifecycle
{
class ComponentA : public ::lifecycle::SingleContextLifecycleComponent
{
public:
// Inheriting from `SingleContextLifecycleComponent` makes all transitions be
// scheduled within the same `context`.
explicit ComponentA(::async::ContextType const context)
: ::lifecycle::SingleContextLifecycleComponent(context)
{}
void init() override
{
// Perform component specific initialization. Will only be called when this component's
// runlevel is first reached.
transitionDone();
}
void run() override
{
// Perform tasks to run the component.
transitionDone();
}
void shutdown() override
{
// Perform shutdown sequence of the component.
transitionDone();
}
};
Lifecycle Manager
The LifecycleManager
in include/lifecycle/LifecycleManager.h
is the central piece of the lifecycle module. It stores the component registry and triggers initialization, running, and shutdown of these components. It is declared using a declare::LifecycleManager
with static capacities of components, runlevels, and components per runlevel, and a system time callback.
Components can be added via addComponent()
. The function transitionToLevel()
will initialize and then run all components in each runlevel, up to the target level. Or, if a lower level is targeted for a transition, all components in levels above the target level are shut down, in descending order of their runlevel.
Sleep/Wakeup
Sometimes the ECU needs to be suspended, i. e. go to sleep and temporarily cease its function but not be completely shut down. To achieve this, the init()
and run()
methods behave differently with respect to transitionToLevel()
. The init()
method of a lifecycle component is only ever called once when the lifecycle component’s runlevel is reached from a lower level, whereas run()
is called every time the lifecycle component’s runlevel is reached from a lower level.
The lifecycle component can thus implement the run()
and shutdown()
methods to be complementary to each other, so as to start and stop the functionality, respectively. The init()
method however should perform the more fundamental initialization that is not to be redone everytime the lifecycle component is suspended or woken up.
This also implies that if any sleep/wakeup runlevel transition is desired then shutdown()
should not undo the work done in init()
, since init()
will not be called again.
Lifecycle Logger
The logging component ::util::logger::LIFECYCLE
can be used to perform lifecycle transition logging.
Lifecycle Listener
In the LifecycleManager there are methods addLifecycleListener()
and removeLifecycleListener()
to add and remove listeners implementing ILifecycleListener from include/lifecycle/ILifecycleListener.h
. Each listener will be notified when a transition has happened.
Code Example
An example application lifecycle is demonstrated in the following code snippet. The LifecycleManager
itself is scheduled within the managerContext
. There are three lifecycle components. ComponentA
runs its transitions in the ethernetContext
and ComponentB
in the diagnosisContext
, both are in runlevel 1. There is also ComponentC
in runlevel 2, its transitions running also in the ethernetcontext
. The lifecycle first transitions from runlevel 0 to 2, then back to runlevel 1, and then eventually from 2 to 0 again.
// #include <lifecycle/LifecycleManager.h>
// #include <async/Types.h>
::async::ContextType const managerContext(1);
::async::ContextType const ethernetContext(2);
::async::ContextType const diagnosisContext(3);
size_t const MAX_NUM_COMPONENTS = 3;
size_t const MAX_NUM_LEVELS = 2;
size_t const MAX_NUM_COMPONENTS_PER_LEVEL = 2;
::lifecycle::declare::
LifecycleManager<MAX_NUM_COMPONENTS, MAX_NUM_LEVELS, MAX_NUM_COMPONENTS_PER_LEVEL>
lifecycleManager(
managerContext,
::lifecycle::LifecycleManager::GetTimestampType::create<&getSystemTimeUs32Bit>());
// Transitions of components A and C are executed mutually exclusively.
ComponentA componentA(ethernetContext);
ComponentB componentB(diagnosisContext);
ComponentC componentC(ethernetContext);
// Components A and B are at the same run level.
lifecycleManager.addComponent("component A", componentA, 1);
lifecycleManager.addComponent("component B", componentB, 1);
lifecycleManager.addComponent("component C", componentC, 2);
lifecycleManager.transitionToLevel(2); // `init()` then `run()` of all components are called
lifecycleManager.transitionToLevel(1); // `shutdown()` of C is called
lifecycleManager.transitionToLevel(2); // only `run()` of C is called, `init()` already called
lifecycleManager.transitionToLevel(0); // `shutdown()` is called on all components
Simplified Sequence Diagram
The sequence diagram of its complete lifecycle where most of the asynchronicity is simplified is as follows:
Detailed Sequence Diagram
The detailed sequence diagram for the first init()
transitions only with asynchronous calls explicitly displayed is as follows: