Overview openPETRA architecture

From OpenPetra Wiki
Jump to navigation Jump to search

(An overview of this was orignially written down after a presentation by Christian at the Petra Team Meeting on 25/01/2006 by Timotheus. Article updated and greatly extended in July 2006 by Christian. Reviewed/updated in May 2008 and November 2008 by Christian. Timotheus removed the Petra 2.x specific bits in September 2010. Following that, Christian reviewed/updated the article for openPETRA in September 2010.)


N-Tier Architecture Overview

see diagram: openPETRA Server Architecture Diagram

PetraServer

  • Platform
    • can run on Windows and Linux (fully managed code)
      • runs as a Linux Service (basically a background console application) on Linux for network installations
      • runs as a invisible console application on Windows and (potentially Linux) for standalone installations
        • we don't make it a Windows Service because of Linux compatibility!
      • we could make it a WinForms application for Standalones, if we want
        • won't have any WinForms, but a Tray Icon
  • 1 to n Petra Clients connect to one PetraServer
  • PetraServer accesses the database
    • opens a database connection for the PetraServer itself on PetraServer startup (used eg. for user authentication)
    • opens a database connection for each Client session
  • creates an AppDomain for each Client session (see below: AppDomains)
    • reason for that: insulation!
      • program code executing separately for each Client session
      • prevent mixing/confusion/exchange/leaking of data between Client sessions
      • errors/exceptions in code only affect one AppDomain - namely the one of the Client session in which they occur!
        • it is therefore highly unlikely that an exception within a Client session can affect the stability of the PetraServer (to the best of our knowledge this has never happened in three years of production use of the PetraServer within OM)
  • maintains a list of running Client sessions
  • has got a Server Menu with several commands
    • listing details of connected/disconnected Client sessions
    • disconnecting a Client session
    • shutdown the PetraServer
    • plus other commands for debugging purposes
  • provides two different .NET remoting connections
    • one for Petra Clients
    • one for the PetraServerAdmin application
  • Logging (Screen, Logfile, ...)
    • setting in .NET Configuration file (XML) allows to set the level of logging/debugging
    • code that performs logging for debugging purposes needs to be enclosed with the conditional define DEBUGMODE (we don't want to ship that code in a production Petra installation)

AppDomains

  • A Client connects once to the PetraServer, from then on it gets his own AppDomain and all future communication happens between the Client and its allocated AppDomain
  • Intruders should not have a chance to run code unauthorised
    • only after a successful login of a Client...
      • ... code that can be called into from the Client side gets loaded (dynamic DLL loading)
      • ... remoted objects with unpredictable remoting URLs (cryto-lib generated) become available to that Client
        • this prevents the use of code that was loaded within an AppDomain by somebody that doesn't know the remoting URLs for that specific AppDomain (i.e. another Client could not access that code)
  • Benefits
    • AppDomains still run in the same Process, less cost involved than starting a separate Process (.EXE) for each Client connection
    • makes sure no memory is used outside of the AppDomain (leaking is prevented by the .NET Runtime)
    • Memory that was used by the Client is completely deallocated by the .NET Runtime when it's AppDomain is unloaded (no server-side memory leaks!)
    • Threads can pass the AppDomain barrier, if we allow it
  • Costs
    • not 'too fast' to start up an AppDomain and load DLLs into it (when a Client connects)
    • clearing up takes some time (when a Client disconnects)
    • 6 to 8 MB RAM required for each Client connection (on Linux with mono)
  • Important:
    • Threads: we need to make sure that all running Threads are aborted if an AppDomain is to be unloaded sucessfully - otherwise the AppDomain cannot be unloaded!

Petra Client

  • Platform
    • runs best on Windows
    • runs on other platforms as well. Layout of Forms isn't satisfactory yet, though.
      • Linux: with graphical desktop manager using mono's implementation of WinForms (therefore we need to stick to 'fully managed' code...)
      • MacOS X: using mono's implementation of WinForms (therefore we need to stick to 'fully managed' code...)
      • Browser-based (Operating System independent!): The PetraClient (or a part of it) could be implemented as a (rich) web application running in a browser
        • a basic reference implementation for a rich web application exists and is being improved on
  • a single Petra Client installation can connect to different PetraServers
    • command line switch allows using several .NET configuration files (XML), which contain different parameter settings that specify a connection to a specific PetraServer

Database System

  • Relational Database Management Systems (RDBMS)
    • With OpenPetra we have the choice of several OpenSouce RDBMS's, eg. PostgreSQL, MySQL, SQLite!!!
  • Platform
    • The RDBMS server needs to be able to run on Windows and Linux, any future RDBMS server needs to be able to do this as well (for network/standalone installations of Petra)
      • (SQLite is an exception to that as it doesn't have an RDMBS server - it is a file-based DB that is accessed directly through a native .NET driver which works on Windows and Linux)
  • DB access driver model
    • we support several RDBMS's through native .NET drivers, other RDBMS's can be connected through ODBC
      • native .NET drivers are ideal because they have less overhead and provide more functionality than ODBC
        • PostgreSQL, MySQL, SQLite are all accessed through native .NET drivers
        • the native .NET drivers are required to run on Windows and Linux (the ones for PostgreSQL, MySQL, SQLite do that)
      • ODBC DB access implementation works on Windows and Linux
  • DB Access Layer
    • PetraServer can deal with different ways of accessing data in DB's because all DB access goes through a DB Access Layer for RDBMS-agnostic DB access
    • only this DB Access Layer needs to be changed when changing RDBMS and DB access driver model or adding support for a new RDBMS!
  • Constraints
    • Referential Integrity
      • PostgreSQL: RDBMS automatically enforces referential integrity
    • Other constraints
      • PostgreSQL supports field-level constraints (eg. NOT NULL) which are enforced by the RDBMS
  • Transactions
    • can be used for Rollback of changes and for pessimistic locking
    • ADO.NET
      • nested transactions are not possible, since only one transaction can be running at any given time on one DB connection
      • this is a limitation of ADO.NET and not a limitation of the native .NET drivers or ODBC
  • Pessimistic / optimistic locking
    • .NET
      • optimistic locking is the standard. This is in effect no real locking: everybody can read any record; when writing, data could have changed in meantime
        • to prevent two people writing shortly after each other
          • a special field is added to every DB table ('s_modification_id_c'). Whenever a record gets updated through the DataStore, this field gets assigned a unique value (unique in the whole DB!). Whenever a record is read through our DataStore, the contents of this field are automatically read, when writing back to the DB the content of this field is automatically checked to be identical before saving the record. If the content of this field has changed, we know that someone else has modified the record between reading and writing. As result we prevent the saving of the record and inform the user about that by throwing a specific Exception that is processed by the GUI.
      • pessimistic locking is possible using a transaction: a transaction is opened with the desired isolation level and must be kept open as long as the data must be protected from overwriting

Server Management Application

  • Platform
    • can run on Windows and Linux (fully managed code)
  • provides a 'remote console' for the PetraServer
    • allows execution of all PetraServer menu commands on a remote console (e.g. list client sessions, disconnect client, shutdown server)
    • can be supplied with Command Line Arguments to run certain menu commands - this is used from within OM's SLS sysadm program (sysadm petra22 / sysadm petra23 command or menu).
  • can be run on a Windows machine while PetraServer is running on a Linux server (and vice versa!)
  • currently only available as a console application. A WinForms implementation could be made as well without too much effort since all functionality (except the text menu) is in DLL's, not in the Console EXE.

Business Object instantiation and data flow

see diagram: openPETRA Business Object instantiation and data flow diagram

Client Tier

  • a Petra screen does not contain logic to write or read the data from/to the DB, in fact it would not even know how to do that!
  • instead, a Petra screen makes a call to the PetraServer to retrieve data when its loaded. Although this only one call, it may well load data from several tables in the Petra DB through this call.
    • slow connections (ie. via analogue modem, ISDN, slow ADSL) require that the least possible number of calls to the PetraServer are made because of restricted network bandwidth and network latency. For this reason we will make only one 'all-including' PetraServer call when loading the screen wherever possible. Exception: see bullet Optional Delayed Data Loading below.
    • if we know (or expect) that such a call could take some time to come back to the Client, either
      • use multithreading in the Petra Client in such situations to not 'freeze' the UI. A call to the PetraServer will then be made in a new thread and this thread will either update the UI when the call is finished or give the main UI thread a signal that the call is finished.
      • use multithreading in the PetraServer and Petra Client to be able to return the request quickly to the Client and then make the Client poll the PetraServer for a result in a separate Thread to not 'freeze' the UI. This gives also the option of showing progress/progress bar and that the user can click a 'Cancel' or 'Stop' button to stop a long running operation. However, this generates quite some network traffic and a slighlty higher PetraServer utilisation because of the (probably) frequent polling. Examples screens that use that technique: Partner Find screen, Add Subscriptions screen.
  • such a Client call to the PetraServer is possible because the Client knows through Interfaces which Business Objects can be instantiated in which Namespace and what methods can be executed on them in the PetraServer. (In fact, the call goes first to an Instantiator Object [see below], but this is transparent to the Client.)
  • a Petra screen makes also only one call to the PetraServer when it saves changed (or new) data (using a Typed Dataset in most cases), although data that stems from (or goes into) several tables in the Petra DB might have been changed (added) in the screen.
  • Optional Delayed Data Loading
    • The PetraClient's 'Delayed Data Loading' functionality can be switched on for a certain user (either in the PetraClient.exe.config file or as a Command Line parameter) to reduce amount of data that is sent (and the time on slow connections) when opening a screen. The functionality is switched on automatically for 'Remote Client' installations (that aren't connected using a LAN, but a slower connection).
    • a Petra screen can choose to implement a functionality to load only limited information in the first call if 'Delayed Data Loading' is switched on
      • Example: Partner Edit screen
        • loads initially only the number of Addresses, Partner Subscriptions, ... to be displayed in the header of the Tab Page, but not the actual data)
        • only when the User changes to a Tab Page the Partner Edit screen loads the detail data that is to be displayed on the Tab page
        • Lists with details (Example: Partner Edit screen - Subscriptions): loads details for each list entry already when loading the list (when the user changes to the Tab page)


Shared between Server and Client: Interfaces

see diagram: openPETRA Screen, Interfaces, Instantiator and UIConnector Detail Diagram

  • Interfaces represent a 'contract' between Server Objects and the Client side
  • Interfaces are not Objects themselves
    • they get implemented by Server Objects
    • they are used by Client Objects to call methods on the server-side Objects (who implement the Interface). This is transparent for the PetraClient, ie. it uses the Interfaces like actual client-side Objects.
  • Interfaces tell the Client (and also the Compiler) which public Methods are callable on Server Objects - without the need to have a reference to the actual Server Object.
  • A reference to an Interface that is implemented by the specific Server Object is enough for the Client to execute the Server-side method that is specified in the Interface! The Client-to-Server call is transparently proxied by .NET remoting (the Client gets a 'Remoting Proxy' instead of a real server-side Object, and that 'Remoting Proxy' appears and works like a local Object to the PetraClient).
  • By using Interfaces, the executables (DLLs) that contain the server-side Objects don't need to be installed on Client PC's for the Client to work properly. Only the executables (DLLs) that contain the Interfaces are needed by the Client at runtime.
  • The Interfaces enable Code Completion and Code ToolTips in the IDE.


Two ways how Interfaces are used in Petra Client-Server communication

Interfaces implemented by Instantiators

These Interfaces create a virtual Petra Object Hierarchy. Through this they tell the PetraClient which (Business) Objects can be instantiated in which Namespace on the PetraServer.

  • A call from the PetraClient on one of the public methods of an Instantiator (that are included in those Interfaces) causes the server-side Instantiator Object to instantiate (create) a server-side Business Object for the calling PetraClient and return a 'Remoting Proxy' to the PetraClient that represents that exact server-side instance of the specific server-side Business Object to the PetraClient.
Interfaces implemented by (Business) Objects

These Interfaces tell the Client which Methods can be executed - once a (Business) Object is instantiated (=exists) on the Server side.

  • A call from the PetraClient on one of the public methods of an (Business) Object (that are included in those Interfaces) causes that method to be executed remotely (ie. that method executes within the PetraServer).

Server Tier

Instantiators

see diagram: openPETRA Screen, Interfaces, Instantiator and UIConnector Detail Diagram

  • Instantiators instantiate (create) UIConnector objects that are tailored for a specific calling Petra Screen on behalf of the Client
  • in many cases they return data to the Client (with a Typed DataSet in most cases) immediately after the UIConnector object is created.
    • this is done so that the Client doesn't need to make one call to create a UIConnector object and immediately after that another call to retrieve data, but only one call (less data transfer = faster on slow connections - also because of network latency!).
  • Instantiators are 'Factory' objects in object-oriented Client/Server programming terms.

UIConnectors (User Interface Connectors) = Business Objects of Petra.NET

see diagram: openPETRA Screen, Interfaces, Instantiator and UIConnector Detail Diagram

  • UIConnectors are Business Objects of Petra.NET that form the 'Business Objects Layer' of the PetraServer.
  • UIConnector objects are created by the PetraServer only when a request from one of the Petra Client screens reaches the PetraServer. This request comes in via .NET Remoting and goes to the Instantiator of that specific UIConnector, and not directly to the UIConnector (the UIConnector would not exist at that time).
    • Petra screens don't create other Objects in the PetraServer other than UIConnectors (that is, other Objects in the PetraServer are just used by the UIConnectors, but don't get accessed via .NET Remoting). UIConnector Objects are only instantiated on the Server side, never on the Client side!
    • Each screen of the Petra Client deals only with one Server-side Object (its UIConnector) on which it performs calls for reading and saving of data. The screen itself holds all data in a multi-table Typed DataSet whose changes can be easily sent to the Server.
  • Functionality
    • UIConnectors know how to read data
      • UIConnectors only retrieve needed tables (eg. either Person Details [p_person DB table] or Family Details [p_family DB table] in the Partner Edit screen) using the using 'Load...' methods of the DataAccess Objects, put all data into one (potentially big) object (a Typed DataSet in most cases) and return it through the Instantiator to the Petra screen - this is the most efficient way to transfer data to the Client. They can also call functions that work on the data (if needed) before returning data to the Petra screen.
    • UIConnectors know how to write data
      • a Petra screen sends only changed (or new) data to the UIConnector (using a Typed Dataset in most cases). The UIConnector in turn coordinates the saving of the data (eg. when a new Partner is created: first save data to PPartner table, and after that to PPerson [or PFamily, PChurch, ...] and PLocation and PPartnerLocation tables). Saving is done by calling SubmitChanges functions of each of the DataAccess objects in the correct order (deletion would be handled in reverse order). The SubmitChanges methods in the DataAccess layer update/add/delete the data for each DB table that is involved. Finally the UIConnector returns a result value to the Petra screen.
    • UIConnectors know what Aggregator Objects should be used (eg. for the PartnerEdit screen: loading from/saving to PLocation and PPartnerLocation tables is done using a 'PartnerAddresses' Aggregate Data Access Object).
    • UIConnectors can optionally create custom DataRelations between Typed DataTables or remove DataRelations, or set up custom Constraints or remove Constraints in the Typed DataSet that is sent to the Petra Client.
    • UIConnectors enforce business rules. They will do this usually by calling functions outside the UIConnector (to avoid duplication of business rules code), or by calling an internal function (if the business rule is too specific to be re-used).
    • UIConnectors may perform special validation of data (eg. across several DB tables) before saving data. They will do this usually by calling functions outside the UIConnector (to avoid duplication of validation code), or by calling an internal function (if the validation is too specific to be re-used)
  • UIConnectors are tailored for 'Rich UI experience' (.NET WinForms) Clients, other Petra Clients that might be developed (eg. a Web Client, [non-existing] external application using COM to access PetraServer) may have other Connectors (eg. WebConnectors, COMConnectors) to read/write data
    • this concept allows different data reading/writing strategies for differenct clients (eg. a Web Client will retrieve/save data for separate ASP.NET pages separately, whereas all this data might be presented in one WinForm and retrieved/saved in one go).
  • Shared UIConnectors for distinct parts of screens (Tabs, UserControls, etc.) can be created if they are re-used in several Petra screens (Example: the Office Specific Data (aka. 'Local Data') UIConnector [TOfficeSpecificDataLabelsUIConnector] is used in the Partner Edit screen (Partner Module) as well as in several other screens in the Personnel Module).

Data Access Layer

See Data Access Layer Diagram

Typed DataTables
  • Typed DataTables are automatically generated for each DB table in the Petra XML file (this file holds the complete definition of the Petra DB). They are organised in Petra Modules/Submodules according to associations in the Petra XML file.
  • what they are and what they do: TODO insert linked article form original


Typed DataSets
  • Typed DataSets are automatically generated from special Typed DataSet XML files (not the Petra XML file). We maintain these XML files on our own and need to re-generate the Typed DataSets after each change (this is done using our make system).
  • Typed DataSets are the main means by which we transfer data between PetraServer and Petra Client (and vice versa)
  • Typed DataSets form a named set of 1..n Typed DataTables (either ones that already exist for Petra DB tables, or self-specified DataTables). A Typed DataSet is only one Object which contains many other Objects (ie. Typed DataTables).
  • we use our custom-developed 'ICT Typed DataSets' that extend the functionality of the .NET DataSets and .NET Typed DataSets
    • most exensions deal with things that Microsoft just didn't take care of
    • some extensions are shortcuts for powerful functionality which we would otherwise need to program over and over again
  • an arbitrary number of Typed DataSets can exist for each Petra module (and indeed for the whole Petra application)


Data Stores

'Data Store' is a high-level, conceptual term we use. Data Store objects as such don't exist.

A DataStore is made up of

  • DataAccess Objects
  • Aggregate Data Access Objects
  • Data Access Objects for dynamic building of SQL

A DataStore (or rather its Objects) exists for each of the Petra Modules/Submodules.

DataAccess Objects
  • One DataAccess Object exists for each DB table
    • automatically generated for each DB table in the Petra XML file (this file holds the complete definition of the Petra DB). They are organised in Petra Modules/Submodules according to associations in the Petra XML file.
    • uses the RDMBS-agnostic Database Access Object for the execution of SQL commands (DataAccess Objects are therefore not tied to a specific RDBMS!)
    • the DataAccess Objects' methods are all static (for easy use and speed reasons) and return and accept Typed DataTables
  • Functionality
    • perform serialization of the data contained in DB Tables, thereby relieving the programmer from writing SQL commands
      • reading (SELECT)
      • writing (INSERT/UPDATE)
      • deleting (DELETE)
      • additional: counting (SELECT COUNT(*))
    • manage optimistic locking of records
    • provide common exception model for DB errors, constraint violations and optimistic locking
    • planned functionality (for openPETRA)
      • will provide segmenting of data (record- and field level access security)
      • will provide verification of data (if needed)
  • Limitations
    • Data from exactly one DB Table can be retrieved using a given DataAccess object (no JOINs!)
    • When using any of the Load... methods, parameters for the WHERE clause can be submitted (using a Collection), but they are all AND-ed. OR-ing of the parameters is not possible. Other limitations to such parameters apply as well.
    • SubmitChanges methods work with DataRows, DataTables and DataSets.
DataCascading Objects
  • automatically generated for DB tables in the Petra XML file (this file holds the complete definition of the Petra DB). They are organised in Petra Modules/Submodules according to associations in the Petra XML file.
  • Functionality
    • these Objects perform 'cascading' operations on a number of DataTables that are referencing each other.
    • cascading is top-down, ie. Start DB Table -> all referenced DB Tables.
      • Example: deleting a p_location record with its DataCascading objects...
        • 1) deletes every record in DB Tables that references the specified p_location record in referencing DB Tables (eg. p_partner_location, m_extract, s_group_location).
        • 2) deletes the specified p_location record.
  • Limitations
    • currently only cascading Delete is implemented, cascading Updates not yet (but certainly doable).
Aggregate Data Access Objects
  • need to be manually written
  • work on several DB tables at once instead of dealing with them separately (e.g. PartnerAddresses aggregator: knows how to read, write, create, delete Addresses (involves both p_location and p_partner_location DB tables)
  • may be used by several UIConnectors
  • will usually work with a custom Typed DataSet that holds the involved tables.
Dynamic Data Access Objects
  • need to be manually written
  • used wherever we need to build SQL commands on-the-fly (eg. for Find Screens)
    • can work on any number of DB tables
    • can use nested SELECTs
    • can used the whole array of ANSI SQL-92 commands
  • can use eg. the 'TPagedDataSet' object that currently resides in Ict.Petra.Server.MCommon to allow returning of large amounts of datarows in 'pages'.
  • may encapsulate business rules (e.g. PartnerAddresses aggregator enforces loads of them)


Database Access Object (DBAccess)
  • designed to have RDBMS abstraction abilities (RDBMS-agnostic data access) in the PetraServer
  • contains functions that open and close the connection to the Database
    • designed to support connections to different kinds of databases through native .NET drivers or ODBC (both on Windows and Linux)
    • Connection Pooling is used if a RDBMS and its .NET driver support that (PostgreSQL does and we use it)
  • allows execution of SQL statements
    • SQL statements that return data (various options)
    • SQL statements that return only one value (eg. for 'SELECT COUNT(*) FROM ...' queries)
    • SQL statements that don't return data
    • some basic 'processing' of SQL statements is done to make them execute on different RDBMS
    • ANSI SQL-92 commands must be used that are understood by all RDBMS systems that should be supported - no 'translation' of the SQL commands is done!
  • supports batch execution of a number of SQL statements that don't return data (currently not in use)
  • supports creation of DB Transactions
  • raises custom exceptions in case of DB errors
  • provides extensive and configurable logging functionality

Garbage Collected Runtime Environment

  • Garbage Collection (GC) removes unused Objects and Variables from the computer's memory (RAM).
    • this relieves the developer from the need to actively manage memory and to release Objects properly, thereby reducing the risk of memory leaks
  • GC works automatically in the background.
  • GC in .NET is very similar to GC in Java.
  • Pitfalls
    • don't take it for granted that Destructors of Objects are run!
      • they might never execute if the application is shut down
      • even if Destructors are run, it cannot be predicted when this happens
      • if you need some form of cleanup or releasing of resources, do this explicitly in code before you set an Object to nil or stop using it
    • Developers have no control when memory is freed exactly
      • Indirect control
        • by assigning 'Variable = null' the developer marks an Object as being eligble for GC.
        • GC can be forced if needed. This makes sense when the developer knows that he just released a huge number of objects and that freeing memory immediately would be beneficial.
  • GC guarantees that non-referenced Objects will be freed at some point - at the latest when an application exits.


Effective use of Data Types with GC

  • Array with 250 elements => one Object; ArrayList with 250 elements: 250 objects - all of which need to be Garbage Collected!
  • small objects may fit into the 2nd level cache of the processor - which makes access to them much faster!
  • mark Objects for Garbage Collection as soon as you don't need them any longer
    • 'MyObject = null' makes an Object eligble for GC (tells the GC that it can collect [=destroy] the Object)
    • any Object that is defined locally in a procedure/function gets marked for GC automatically as soon as the procedure/function is exited in any way


'Big' business objects, with lots of functionality vs. 'small' business objects with less functionality

  • 'big' business objects will have a longer lifetime since many calls will be made to them over the time
    • benefit: less business objects need to be created, more powerful and stateful business objects
    • disadvantage: business objects will stay longer in memory and won't be Garbage Collected that soon after releasing them (.NETs GC is optimised for short-lived objects)
  • 'small' business objects will have a shorter lifetime
    • benefit: will keep less information in memory for a shorter time, will be Garbage Collected immediately/soon after releasing them (.NETs GC is optimised for short-lived objects)
    • disadvantages: more business objects will need to be created (but .NET is optimised for creation of small objects), business objects are not that powerful and stateless
  • we initially decided that we go with the 'small' business object approach; however, we found out that in doing that we cannot reduce the amount of data that is sent over the network as effective as we can with the 'big', stateful business objects. So we ended up with the 'big' business approach - at least for the UIConnectors for the Petra Client.


GC in Client-Server Scenario

The PetraServer needs to know whether a PetraClient still needs a Business Object that is instantiated in the PetraServer.

  • Client needs to hold a reference to the Server Object to prevent it from being discarded and Garbage Collected (using TEnsureKeepAlive.Register)
  • Client needs to tell the PetraServer when it doesn't need the Server Object any more so that it can be Garbage Collected (using TEnsureKeepAlive.UnRegister).
  • When a Client disconnects, the PetraServer closes the database connection for the Client and the Client's AppDomain is unloaded (including all Objects that were ever instantiated in it) by the PetraServer.
  • The Client sends a 'Keep Alive' signal to the PetraServer at regular intervals; if the Client crashes, the PetraServer closes the database connection for the Client and tears down the AppDomain after a configurable time, thereby releasing all the memory it used (including all Objects that were ever instantiated in it)
  • 'Keep Alive' signal is sent on a separate thread in the Client .EXE (realised in the TPollClientTasks class)


References

Articles:

Diagrams: