How it works
Explanations on how some key components work or are structured.
Explanations on how some key components work or are structured.
Explanations on how some key components of hermes-server work or are structured.
hermes-server
commit_one
action if anycommit_all
action if anyupdateInterval
and restart from step 3.
if app has not been requested to stopIf any exception is raised in step 2.
, this step is restarted until it succeeds.
If any exception is raised in steps 3.
to 7.
, the cache is saved on disk, and the server restart from step 3.
.
Hermes-server can handle several data types, with link (foreign keys) between them, and to enforce integrity constraints.
Let’s use a typical Users / Groups / GroupsMember use case to illustrate this.
classDiagram direction BT GroupsMembers <-- Users GroupsMembers <-- Groups class Users{ user_id ... } class Groups{ group_id ... } class GroupsMembers{ user_id group_id integrity() _SELF.user_id in Users_pkeys and _SELF.group_id in Groups_pkeys }
In this scenario, entries in GroupsMembers
that have a user_id
that doesn’t exist in Users
, or a group_id
that doesn’t exist in Groups
will be silently ignored.
For more details, please see integrity_constraints in hermes-server configuration.
In a multi-source scenario, Hermes can aggregate entries providing from multiple sources as if they were providing from one, and optionally enforce aggregation constraints to ensure data consistency.
Let’s take a use case, with a university data set where Hermes should manage user accounts. Employees and students data are stored on two separate data sources. Hermes will be able to merge the two datasources in one virtual Users
, but must ensure that primary keys doesn’t collide.
Here we got two distinct data sources for a same data type.
classDiagram direction BT Users <|-- Users_employee Users <|-- Users_students class Users{ user_id login mail merge_constraints() s.user_id mustNotExist in e.user_id } class Users_students{ s.user_id s.login s.mail } class Users_employee{ e.user_id e.login e.mail }
In this scenario, entries in Users_students
that have a user_id
that exist in Users_employee
will be silently ignored.
But entries in Users_employee
that have a user_id
that exist in Users_students
will still be processed.
For more details, please see pkey_merge_constraint and merge_constraints in hermes-server configuration.
In a multi-source scenario, Hermes can recompose entries providing from multiple sources by merging their data, and optionally setting merge constraints to ensure data consistency.
Let’s take a use case, where Hermes should manage user accounts. Main data and wifi profile name are stored on two separate data sources. Hermes will be able to aggregate the two datasources in one virtual Users
, but must ensure that primary keys of second exists in first.
Here we got two distinct data sources for a same entry.
classDiagram direction BT Users <|-- Users_main Users <|-- Users_auxiliary class Users{ user_id login mail wifi_profile merge_constraints() a.user_id mustAlreadyExist in m.user_id } class Users_auxiliary{ a.user_id a.wifi_profile } class Users_main{ m.user_id m.login m.mail }
In this scenario, entries in Users_auxiliary
that have a user_id
that doesn’t exist in Users_main
will be silently ignored.
But entries in Users_main
that have a user_id
that doesn’t exist in Users_auxiliary
will be processed, and therefore the resulting Users
entry won’t have a wifi_profile
value.
For more details, please see pkey_merge_constraint and merge_constraints in hermes-server configuration.
An event always belongs to one of those categories:
base
: standard event, can be of type:
dataschema
: propagate the new dataschema to clients, after a server datamodel updateadded
: a new entry has been added to specified data type, with specified attributes and valuesremoved
: entry of specified pkey has been removed from specified data typemodified
: entry of specified pkey has been modified. Contains only added, modified, and removed attributes with their new valuesinitsync
: indicate that the event is a part of an initsync sequence, can be of type:
init-start
: beginning of an initsync sequence, also contains the current dataschemaadded
: a new entry has been added to specified data type, with specified attributes and values. As the server sends the contents of its cache to initialize clients, entries can only be addedinit-stop
: end of an initsync sequenceContains state of the server:
lastUpdate: datetime.datetime | None
Datetime of latest update.
errors: dict[str, dict[str, dict[str, Any]]]
Dictionary containing current errors, to be able to notify of any changes.
exception: str | None
String containing latest exception trace.
Contains the Dataschema, built upon the Datamodel. This cache file permit to server to process step 2.
from Workflow.
There’s one file per data type declared in Datamodel, containing the data cache of this data type, as a list of dict. Each dict from the list is an entry.
Explanations on how some key components of hermes-client work or are structured.
hermes-client
5.
7.
has never been reached):
errorQueue_retryInterval
has passed since the last attempt, retry to process events in error queuetrashbin_purgeInterval
has passed since the last attempt, retry to purge expired objects from trashbinonSave
when it exists in client plugin5.
if app hasn’t been requested to stopIf any exception is raised in step 6.1.1
, it will be considered as a fatal error, notified, and the client will stop.
If any exception is raised in steps 5.
to 6.
, it is notified, its event is added to error queue and the client restarts from step 7.
.
As the datamodel on server differs than that on client, the clients must convert remote events received on message bus to local events. If the resulting local event is empty (the data type or the attributes changed in remote event are not set on client datamodel), the event is ignored.
On client datamodel update, the client may generate local events that have no corresponding remote event, i.e. to update an attribute value computed with a Jinja template that just had been updated.
flowchart TB subgraph Hermes-client direction TB datamodelUpdate[["a datamodel update"]] remoteevent["Remote event"] localevent["Local event"] eventHandler(["Client plugin event handler"]) end datamodelUpdate-->|generate|localevent MessageBus-->|produce|remoteevent remoteevent-->|convert to|localevent localevent-->|pass to appropriate|eventHandler eventHandler-->|process|Target classDef external fill:#fafafa,stroke-dasharray: 5 5 class MessageBus,Target external
Sometimes, objects are linked together by foreign keys. When an error occurs on an object whose primary key refers to that of one or more other “parent” objects, it may be desirable to interrupt the processing of all or part of the events of these parent objects until this first event has been correctly processed. This can be done by adding the events of the parent objects to the error queue instead of trying to process them.
The first thing to do is to declare the foreign keys through hermes-server.datamodel.data-type-name.foreignkeys in hermes-server configuration. The server will do nothing with these foreign keys except propagate them to the clients.
Then, it is necessary to establish which policy to apply to the clients through hermes-client.foreignkeys_policy in each hermes-client configuration. There are three:
disabled
: No event, policy is disabled. Probably not relevant in most cases, but could perhaps be useful to someone one day.on_remove_event
: Only on removed events. Should be enough in most cases.on_every_event
: On every event types (added, modified, removed). To ensure perfect consistency no matter what.Sometimes, an event may be stored in error queue due to a data problem (e.g. a group name with a trailing dot will raise an error on Active Directory). If the trailing dot is then removed from the group name on datasource, the modified event will be stored on error queue, and won’t be processed until previous one is processed, which cannot happen without proceeding to a risky and undesirable operation: manually editing client cache file.
The autoremediation solves this type of problems by merging events of a same object in error queue. It is not enabled by default, as it may break the regular processing order of events.
Let’s take an example with a group created with an invalid name. As its name is invalid, its processing will fail, and the event will be stored in error queue like this:
flowchart TB subgraph errorqueue [Error queue] direction TB ev1 end ev1["`**event 1** *eventtype*: added *objType*: ADGroup *objpkey*: 42 *objattrs*: { grp_pkey: 42 name: 'InvalidName.' desc: 'Demo group' }`"] classDef leftalign text-align:left class ev1 leftalign
As the error has been notified, someone corrects the group name in the datasource. This change will conduce to an according modified event. This modified event will not be processed, but added to the error queue as its object already has an event in error queue.
flowchart TB subgraph errorqueuebis [With autoremediation] direction TB ev1bis end subgraph errorqueue [Without autoremediation] direction TB ev1 ev2 end ev1["`**event 1** *eventtype*: added *objType*: ADGroup *objpkey*: 42 *objattrs*: { grp_pkey: 42 name: 'InvalidName.' desc: 'Demo group' }`"] ev2["`**event 2** *eventtype*: modified *objType*: ADGroup *objpkey*: 42 *objattrs*: { modified: { name: 'ValidName' } }`"] ev1bis["`**event 1** *eventtype*: added *objType*: ADGroup *objpkey*: 42 *objattrs*: { grp_pkey: 42 name: 'ValidName' desc: 'Demo group' }`"] classDef leftalign text-align:left class ev1,ev2,ev1bis leftalign
Contains state of the client:
queueErrors: dict[str, str]
Dictionary containing all error messages of objects in error queue, to be able to notify of any changes.
datamodelWarnings: dict[str, dict[str, dict[str, Any]]]
Dictionary containing current datamodel warnings, for notifications.
exception: str | None
String containing latest exception trace.
initstartoffset: Any | None
Contains the offset of the first message of initSync sequence on message bus.
initstopoffset: Any | None
Contains the offset of the last message of initSync sequence on message bus.
nextoffset: Any | None
Contains the offset of the next message to process on message bus.
Cache of previous config, used to be able to build the previous datamodel and to render the Jinja templates with Attribute plugins.
Cache of latest Dataschema, received from hermes-server.
Cache of error queue.
One file per remote data type, containing all remote entries, as they had been successfully processed.
When error queue is empty, must have the same content than RemoteDataType_complete__.json
One file per remote data type, containing all remote entries, as they should be without error.
When error queue is empty, must have the same content than RemoteDataType.json
Only if trashbin is enabled. One file per remote data type, containing all remote entries that are in trashbin, as they had been successfully processed.
When error queue is empty, must have the same content than trashbin_RemoteDataType_complete__.json
Only if trashbin is enabled. One file per remote data type, containing all remote entries that are in trashbin, as they should be without error.
When error queue is empty, must have the same content than trashbin_RemoteDataType.json
One file per local data type, containing all local entries, as they had been successfully processed.
When error queue is empty, must have the same content than __LocalDataType_complete__.json
One file per local data type, containing all local entries, as they should be without error.
When error queue is empty, must have the same content than __LocalDataType.json
Only if trashbin is enabled. One file per local data type, containing all local entries that are in trashbin, as they had been successfully processed.
When error queue is empty, must have the same content than __trashbin_LocalDataType_complete__.json
Only if trashbin is enabled. One file per local data type, containing all local entries that are in trashbin, as they should be without error.
When error queue is empty, must have the same content than __trashbin_LocalDataType.json