From f4195c102a6e3c5babfdd3be52f1cf5f564936a9 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Mon, 26 Mar 2018 02:27:30 +0200 Subject: [PATCH] DI: document relation to lifecylce and lifecycle events in general --- doc/design/architecture/Subsystems.txt | 207 +++++++++++++++++- doc/technical/library/Dependencies.txt | 30 +++ doc/technical/overview.txt | 6 +- src/lib/depend-inject.hpp | 2 +- src/proc/mobject/session.hpp | 1 + .../mobject/session/lifecycle-advisor.hpp | 1 + src/proc/mobject/session/session.cpp | 25 ++- 7 files changed, 250 insertions(+), 22 deletions(-) diff --git a/doc/design/architecture/Subsystems.txt b/doc/design/architecture/Subsystems.txt index e0ade1879..db69d937e 100644 --- a/doc/design/architecture/Subsystems.txt +++ b/doc/design/architecture/Subsystems.txt @@ -3,8 +3,9 @@ Layers -- Subsystems -- Lifecycle :Author: Hermann Voßeler :Email: :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. diff --git a/doc/technical/library/Dependencies.txt b/doc/technical/library/Dependencies.txt index 96932f687..e5837cdba 100644 --- a/doc/technical/library/Dependencies.txt +++ b/doc/technical/library/Dependencies.txt @@ -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. diff --git a/doc/technical/overview.txt b/doc/technical/overview.txt index 81d557607..ac3f5b21c 100644 --- a/doc/technical/overview.txt +++ b/doc/technical/overview.txt @@ -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 ~~~~~~~~~~~~~~~~ diff --git a/src/lib/depend-inject.hpp b/src/lib/depend-inject.hpp index f2e6a17fb..b7d90a424 100644 --- a/src/lib/depend-inject.hpp +++ b/src/lib/depend-inject.hpp @@ -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 diff --git a/src/proc/mobject/session.hpp b/src/proc/mobject/session.hpp index 85904e9cd..1a05fedb8 100644 --- a/src/proc/mobject/session.hpp +++ b/src/proc/mobject/session.hpp @@ -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 diff --git a/src/proc/mobject/session/lifecycle-advisor.hpp b/src/proc/mobject/session/lifecycle-advisor.hpp index 5a8adc11f..dc123fa44 100644 --- a/src/proc/mobject/session/lifecycle-advisor.hpp +++ b/src/proc/mobject/session/lifecycle-advisor.hpp @@ -111,6 +111,7 @@ namespace session { void shutDown() { + emitEvent (ON_SESSION_CLOSE); closeSessionInterface(); disconnectRenderProcesses(); emitEvent (ON_SESSION_END); diff --git a/src/proc/mobject/session/session.cpp b/src/proc/mobject/session/session.cpp index 58c795bd2..a28585c0a 100644 --- a/src/proc/mobject/session/session.cpp +++ b/src/proc/mobject/session/session.cpp @@ -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";