DI: document relation to lifecylce and lifecycle events in general

This commit is contained in:
Fischlurch 2018-03-26 02:27:30 +02:00
parent 942bad5d0a
commit f4195c102a
7 changed files with 250 additions and 22 deletions

View file

@ -3,8 +3,9 @@ Layers -- Subsystems -- Lifecycle
:Author: Hermann Voßeler
:Email: <Ichthyostega@web.de>
:Date: 2018
:Toc:
WARNING: under construction [red]#TO BE WRITTEN#
WARNING: under construction -- [red]#some parts to be filled in#
Terminology
@ -31,21 +32,205 @@ Service::
A component within some subsystem is termed _Service_
+
--
* when it exposes an interfaces with an associated contract (informal rules about usage pattern)
* when it accepts invocations from arbitrary other components without specific wiring hard coded knowledge
* when it exposes an interfaces with an associated contract
(informal rules about usage pattern and expectations)
* when it accepts invocations from arbitrary other components
without specific wiring or hard coded knowledge
--
+
The service lifecycle is tied to the lifecycle of the related subsystem; whenever the subsystem is ``up and running'',
any contained serivces can be accessed and used. Within Lumiera, there is no _service broker_ or any similar kind
any contained services can be accessed and used. Within Lumiera, there is no _service broker_ or any similar kind
of elaborate _service discovery_ -- rather, services are accessed *by name*, where ``name'' is the _type name_
of the service interface.
Dependency::
A relation at implementation level and thus a local property of an individual component. A dependency is something
we need to complete the task at hand, yet a dependency lies beyond that task and relates to concerns outside the scope
and theme of this actual task. Wich means, a dependency is not brought in by the task or part of the task, rather
the task is the reason why some entity dealing with that task needs to _pull_ the dependency, in order to be able
to handle the task. So essentially, dependencies are accessed on-demand. Dependencies might be other components
or services, and typically the user (consumer) of a dependency just relies on the corresponding interface and
remains agnostic with respect to the dependencie's actual implementation or lifecycle details.
A relation at implementation level and thus a local property of an individual component. A dependency
is something we need in order to complete the task at hand, yet a dependency lies beyond that task and
relates to concerns outside the scope and theme of this actual task. Wich means, a dependency is not
introduced by the task or part of the task, rather the task is the reason why some entity dealing with
it needs to _pull_ the dependency, in order to be able to handle the task. So essentially, dependencies
are accessed on-demand. Dependencies might be other components or services, and typically the user
(consumer) of a dependency just relies on the corresponding interface and remains agnostic with respect
to the dependency's actual implementation or lifecycle details.
Subsystems
----------
As a coherent part of the application, a subsystem can be started into running state. Several subsystems
can reside within a single layer, which leads to rather tight coupling. We do not define boundaries between
subsystems in a strict way (as we do with layers) -- rather some component is associated with a subsystem
when it relies on services of the subsystem to be ``just available''. However, the grouping into subsystems
is often also a thematic one, and related to a specific abstraction level. To give an example, the Player
deals with _calculation streams,_ while the engine handles individual _render jobs,_ which together form
a calculation stream. So there is a considerable grey area in between. Any code related with defining and
then dispatching frame jobs needs at least some knowledge about the presence of calculation streams; yet
it depends and relies on the scheduling service of the engine. In the end, it remains a question of
architecture to keep those dependency chains ordered in a way to form a one-way route: when we start
the engine, it must not instantiate a component which _requires the player_ in order to be operative.
Yet we can not start the player without having started the engine beforehand; if we do, its services
will throw exceptions due to missing dependencies on first use.
However, subsystems as such are not dynamically configured. This was a clear cut design decision (and the
result of a heated debate within the Lumiera team at that time). We do _not expect_ to load just some plug-in
dynamically, inserted via an UI-action at runtime, which then installs a new subsystem and hooks it into the
existing architecture. The flexibility lies in what we can do with the _contents_ of the session -- yet the
application itself is not conceived as set of Lego(TM) bricks. Rather, we identify a small number of coherent
parts of the application, each with its own theme, style, relations and contingencies.
Engine
~~~~~~
_tbw_
Player
~~~~~~
_tbw_
Session
~~~~~~~
_tbw_
User Interface
~~~~~~~~~~~~~~
_tbw_
Script Runner
~~~~~~~~~~~~~
_tbw_
Net Node
~~~~~~~~
_tbw_
Lifecycle
---------
Dependencies and abstraction through interfaces are ways to deal with complexity getting out of hand.
When done well, we can avoid adding _accidental complexity_ -- yet essential complexity as such can not
be removed, but with the help of abstractions it can be raised to another level.footnote:[Irony tags here.
There is a lot of hostility towards abstractions, because it is quite natural to conflate the abstraction
with the essential complexity it has to deal with. It seems compelling to kill the abstraction, in the
hope to kill the complexities as well -- an attitude rather effective, in practice...].
When components express their external needs in the form of dependency on an interface, the immediate tangling
at the code level is resolved, however, someone needs to implement that interface, and this other entity needs
to be _available_. It is now an architecture challenge to get those dependency chains ordered. A way to
circumvent this problem is to rely on a _lifecycle_ with several _phases._
This is the idea behind the subsystems and the subsystem runner.
. First we define an ordering between the subsystems. The most basic subsystem (the Engine) is started first.
. Within a subsystem, components may be mutually dependent. However, we establish a new rule, dictating that
during the _startup phase_ only local operations within a single component are allowed. The component need
to be written in a way that it does not need the help of anything ``remote'' in order to get its inner
workings up and ready. The component may rely on its members and on other services it created, _owns and
manages._ And sometimes we do need to rely on a more low-level service in another subsystem or in the
application core.footnote:[A typical example would be the reliance on threading, locking or application
configuration.] -- which then creates a hard dependency on _architecture level_
. Moreover, we ensure that all operational activity is generated by actual work tasks, and that such tasks
in turn may be initiated _solely through official interfaces._ Such interfaces are to be _opened explicitly_
at the end of the startup phase.
. In operational mode, any part of the system can now assume for its dependencies to be ``just available''.
. And finally we establish a further rule to _disallow extended clean-up._ Everything must be written in a
way such that when a task is done, it is _really done_ and settled. (Obviously there are some fine points
to consider here, like caching or elaborate buffer and I/O management). The rationale is that after leaving
the operational phase at the end of `main()` the application is able to unwind in any arbitrary way.
The problem with emergencies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This concept has a weak spot however: A catastrophic failure might cause any subsystem to break down immediately.
The handler within the subsystem's primary component will hopefully detect the corresponding exception and signal
emergency to the subsystem runner. However, the working services of that subsystem are already gone at that point.
And even while other subsystems get the (emergency) shutdown trigger, some working parts may be already failing
catastrophically due to their dependencies being dragged away suddenly.
Lumiera is not written for exceptional resilience or high availability. Our attitude towards such failures can
be summarised as ``Let it crash''. And this is another rationale for the ruling against extended clean-up.
Any valuable work done by the user should be accepted and recorded persistently right away. Actions on the
session are logged, like in a database. The user may still save snapshots, but basically any actual change
is immediately recorded persistently. And thus we may crash without remorse.
Lifecycle Events
~~~~~~~~~~~~~~~~
The Application as a whole conducts a well defined lifecycle; whenever transitioning to the next phase,
a _Lifecycle Event_ is issued. Components may register a notification hook with the central _Lifecycle Manager_
(see 'include/lifecycle.h) to be invoked whenever a specific event is emitted. The process of registration
can be simplified by planting a static variable of type `lumiera::LifecycleHook`.
WARNING: A callback enrolled this way needs to be callable at the respective point in the lifecycle,
otherwise the application will crash.
`ON_BASIC_INIT`::
Invoked as early as possible, somewhere in the static initialisation phase prior to entering `main()`.
In order to install a callback hook for this event, the client must plant a static variable somewhere.
`AppState`::
This is the Lumiera »Application Object«. It is a singleton, and should be used by `main()` solely.
While not a lifecycle event in itself, it serves to bring up some very fundamental application services:
+
--
- the plug-in loader
- the application configuration
--
+
After starting those services within the `AppState::init()` function,
the event `ON_GLOBAL_INIT` is emitted.
`ON_GLOBAL_INIT`::
When this event occurs, the start-up phase of the application has commenced. The command line was already
parsed and the basic application configuration is loaded, but the subsystems are not yet initialised.
`Subsys::start()`::
By evaluation of the command line, the application object determines what subsystems actually need to
be started; those will receive the `start()` call, prompting them to enter their startup phase, to
instantiate all service objects and open their business façade when ready
`ON_SESSION_START`::
When this hook is activated, the session implementation facilities are available and the corresponding
interfaces are already opened and accessible, but the session itself is completely pristine and empty.
Basic setup of the session can be performed at that point. Afterwards, the session contents will be
populated.
`ON_SESSION_INIT`::
At this point, all specific session content and configuration has already be loaded. Any subsystems
in need to build some indices or to establish additional wiring to keep track of the session's content
should register here.
`ON_SESSION_READY`::
Lifecycle hook to perform post loading tasks, which require an already completely usable and configured
session to be in place. When activated, the session is completely restored according to the defaulted or
persisted definition, and any access interfaces are already opened and enabled. Scripts and the GUI might
even be accessing the session in parallel already. Subsystems intending to perform additional processing
should register here, when requiring fully functional client side APIs. Examples would be statistics gathering,
validation or auto-correction of the session's contents.
`ON_SESSION_CLOSE`::
This event indicates to commence any activity relying on an opened and fully operative session.
When invoked, the session is still in fully operative state, all interfaces are open and the render engine
is available. However, after issuing this event, the session shutdown sequence will be initiated, by detaching
the engine interfaces and signalling the scheduler to cease running render jobs.
`ON_SESSION_END`::
This is the point to perform any state saving, deregistration or de-activation necessary before
an existing session can be brought down. When invoked, the session is still fully valid and functional,
but the GUI/external access has already been closed. Rendering tasks might be running beyond this point,
since the low-level session data is maintained by reference count.
`Subsys::triggerShutdown()`::
While not a clear cut lifecycle event, this call prompts any subsystem to close external interfaces
and cease any activity. Especially the GUI will signal the UI toolkit set to end the event loop and
then to destroy all windows and widgets.
`ON_GLOBAL_SHUTDOWN`::
Issued when the control flow is about to leave `main()` regularly to proceed into the shutdown and
unwinding phase. All subsystems have already signalled termination at that point. So this is the right
point to perform any non-trivial clean-up, since, on a language level, all service objects (especially
the singletons) are still alive, but all actual application activity has ceased.
`ON_EMERGENCY_EXIT`::
As notification of emergency shutdown, this event is issued _instead of_ `ON_GLOBAL_SHUTDOWN`, whenever
some subsystem collapsed irregularly with a top-level exception.
NOTE: all lifecycle hooks installed on those events are _blocking_. This is intentionally so, since any
lifecycle event is a breaking point, after which some assumptions can or can not be made further on.
However, care should be taken not to block unconditionally from within such a callback, since this
would freeze the whole application. Moreover, implementers should be careful not to make too much
assumptions regarding the actual thread of invocation; we only affirm that it will be _that specific_
thread responsible for bringing the global lifecycle ahead at this point.

View file

@ -36,6 +36,36 @@ Performance
~~~~~~~~~~~
_tbw_
Architecture
------------
Dependency management does not define the architecture, nor can it solve architecture problems.
Rather, its purpose is to _enact_ the architecture. A dependency is something we need in order to perform
the task at hand, yet essence of a dependency lies outside the scope and relates to concerns beyond and theme
of this actual task. A naïve functional approach -- pass everything you need as argument -- would be as harmful
as thoughtlessly manipulating some off-site data to fit current needs. The local function would be splendid,
strict and referentially transparent -- yet anyone using it would be infected with issues of tangling and
tight coupling. As remedy, a _global context_ can be introduced, which works well as long as this global
context does not exhibit any other state than ``being available''. The root of those problems however
lies in the drive to conceive matters simpler as they are.
- collaboration typically leads to indirect mutual dependency.
We can only define precisely _what is required locally,_ and then _pull our requirements_ on demand.
- a given local action can be part of a process, or a conversation or interaction chain, which in turn
might originate from various, quite distinct contexts. At _that level,_ we might find a simpler structure
to hinge questions of lifecycle on.
In Lumiera we encounter both these kinds of circumstances. On a global level, we have a simple and well defined
order of dependencies, cast into link:{ldoc}/design/architecture/Subsystems.html[Subsystem relations].
We know e.g. that mutating changes to the session can originate from scripts or from UI interactions.
It suffices thus, when the _leading subsystem_ (the UI or the script runner) refrains from emitting any further
external activities, _prior_ to reaching that point in the lifecycle where everything is ``basically set''.
Yet however self evident this insight might be, it yields some unsettling and challenging consequences:
The UI _must not assume_ the presence of specific data structures within the lower layers, nor is it allowed to
``pull'' session contents while starting up. Rather the UI-Layer is bound to bootstrap itself into completely
usable and operative state, without the ability to attach anything onto existing tangible content structures.
This runs completely counter common practice of UI programming, where it is customary to wire most of the
application internals somehow directly below the UI ``shell''. Rather, in Lumiera the UI must be conceived
as a _collection of services_ -- and when running, a _population request_ can be issued to fill the prepared
UI framework with content. This is Inversion-of-Control at work.

View file

@ -430,8 +430,10 @@ to be invoked when this happens. Currently (2018) the following events are known
-> see here for
link:{ldoc}/design/application/SubsystemLifecycle.html#_initialisation_and_lifecycle[explanation in detail]
[small]#(somewhat preliminary and messy, yet still valid as of [yellow-background]##2018##)#
[small]#(somewhat preliminary and messy, yet still valid as of [yellow-background]##2018##)# +
-> more
link:{ldoc}/design/architecture/Subsystems.html#_lifecycle[documentation]
[small]#(likewise incomplete, but we're getting there, eventually [yellow-background]##2018##)#
Interface system
~~~~~~~~~~~~~~~~

View file

@ -21,7 +21,7 @@
*/
/** @file dependency-factory.hpp
/** @file depend-inject.hpp
** Per type specific configuration of instances created as service dependencies.
** This is the _"Backstage Area"_ of lib::Depend, where the actual form and details
** of instance creation can be configured in various ways. Client code typically

View file

@ -146,6 +146,7 @@ namespace mobject {
extern const char* ON_SESSION_START; ///< triggered before loading any content into a newly created session
extern const char* ON_SESSION_INIT; ///< triggered when initialising a new session, after adding content
extern const char* ON_SESSION_READY; ///< triggered after session is completely functional and all APIs are open.
extern const char* ON_SESSION_CLOSE; ///< triggered before initiating the session shutdown sequence
extern const char* ON_SESSION_END; ///< triggered before discarding an existing session

View file

@ -111,6 +111,7 @@ namespace session {
void
shutDown()
{
emitEvent (ON_SESSION_CLOSE);
closeSessionInterface();
disconnectRenderProcesses();
emitEvent (ON_SESSION_END);

View file

@ -82,7 +82,7 @@ namespace mobject {
/** \par
/**
* LifecycleHook, to perform all the basic setup for a new session,
* prior to adding any specific data, configuration or content. Any subsystems
* requiring to (re)-initialise for a new session should register here. When this
@ -93,17 +93,17 @@ namespace mobject {
* session should register their basic setup functions using this hook, which can be
* done via the C interface functions defined in lifecycle.h
*/
const char* ON_SESSION_START ("ON_SESSION_START");
const char* ON_SESSION_START = "ON_SESSION_START";
/** \par
/**
* LifecycleHook, to perform any initialisation, wiring and registrations necessary
* to get the session into a usable state. When activated, the specific session content
* and configuration has already be loaded. Any subsystems requiring to build some indices
* or wiring to keep track of the session's content should register here.
*/
const char* ON_SESSION_INIT ("ON_SESSION_INIT");
const char* ON_SESSION_INIT = "ON_SESSION_INIT";
/** \par
/**
* LifecycleHook, to perform post loading tasks, requiring an already completely usable
* and configured session to be in place. When activated, the session is completely restored
* according to the standard or persisted definition and any access interfaces are already
@ -112,9 +112,18 @@ namespace mobject {
* fully functional client side APIs. Examples would be statistics gathering, validation
* or auto-correction of the session's contents.
*/
const char* ON_SESSION_READY ("ON_SESSION_READY");
const char* ON_SESSION_READY = "ON_SESSION_READY";
/** \par
/**
* LifecycleHook, to commence any activity relying on an opened and fully operative session.
* When invoked, the session is still in fully operative state, all interfaces are open and
* the render engine is available. However, after issuing this event, the session shutdown
* sequence will be initiated, by detaching the engine interfaces and signalling the
* scheduler to cease running render jobs.
*/
const char* ON_SESSION_CLOSE ="ON_SESSION_CLOSE";
/**
* LifecycleHook, to perform any state saving, deregistration or de-activation necessary
* before bringing down an existing session. When invoked, the session is still fully valid
* and functional, but the GUI/external access has already been closed.
@ -122,7 +131,7 @@ namespace mobject {
* specific/internal information into the persisted state, besides actually attaching
* data to objects within the session?
*/
const char* ON_SESSION_END ("ON_SESSION_END");
const char* ON_SESSION_END ="ON_SESSION_END";