Records, history and saving¶
Two jobs that keep something after the engine stops: history in a database and configuration in the document's own file. Both are done by the engine. Run PRJEDU_records first: a tank whose alarms, events and value history go through these sinks into SQLite, and a kill that shows the journal read back.
A third way to keep something, with no database and no save: a value that comes back with its last state after a restart is 13_Durable_Documents.
Records¶
Every alarm, every event or audit line and every recorded value is a record: a
small JSON text with an id of its own. The engine carries it from the node that makes
it to the node that keeps it, holds it while that node or its database is away, and
writes the history row itself. A row is in the table once, and a record is let go
only after its row is committed. Scripts no longer build INSERTs for history.
| Field | Meaning |
|---|---|
uid |
53 bits, time-ordered, never repeated by a node; the table's key |
src |
the node that made it (tid) |
knd |
alm, evt, aud, val, row |
rol |
the role that made an evt, aud or row. The engine writes it; a script cannot give it |
tim |
milliseconds; for a value, the value's own time |
| the rest | the kind's fields: an alarm's doc rid atr valp valn stap stan fnc pri desc why, a value's doc rid atr txt typ pro val sta |
Where records go¶
<xdb id="XDB" tid="S01" rec_nid="1" rec_dir="%PRJ%/log/rec"
alm_adr="doc('HIS/S00')/id('ALM_H')"
evt_adr="doc('HIS/S00')/id('EVT_H')"
his_adr="doc('HIS/S00')/id('VAL_H')" ...>
Write the three addresses once on the root. A document without one uses its parent's,
and "" switches it off for that document.
| Attribute | Records | Made by |
|---|---|---|
alm_adr |
alarms, device alarms, the engine's health | ALM.*, ALM.devsta, the health bits |
evt_adr |
events and audit lines | HIS.rec, [HIS.evt] |
his_adr |
value history | [HIS.val] |
The sink¶
The document that keeps the history declares it. A table must already exist:
<con id="CN_SQL" xdb_fnc="[CON.sqlite,PRO.sql]" con_host="%PRJ%/log/h.db" con_mod="CLIENT" con_ses="0"
con_tmo="5" con_rct="2" con_keeptmo="0" con_idletmo="0" pro_role="MASTER" pro_tmo="10" pro_try="1"
pro="1" pro_typ="0xC412" pro_fnc="[]" sta="0x0303" sta_typ="0x1142" sta_fnc="[PRO.sta]"/>
<his id="ALM_H" xdb_fnc="[HIS.cfg]" sta="0x0303" sta_typ="0x142"
his_rec="alm" his_tbl="ALM" qry_con="CN_SQL"
his_col="id=uid,tim:loc,doc,rid,atr,fnc,stap,stan,why"
new="" new_typ="0x1401" new_fnc="[HIS.new,ATR.nup]"/>
<his id="VAL_H" xdb_fnc="[HIS.cfg]" sta="0x0303" sta_typ="0x142"
his_rec="val" his_tbl="VAL" qry_con="CN_SQL"
his_col="id=uid,tim:loc,doc,rid,val,sta"
new="" new_typ="0x1401" new_fnc="[HIS.new,ATR.nup]"/>
CON.sqlite needs xdb_lib="AyPRO_sqlite" on the root (05_Connections_and_Protocols).
| Attribute | Meaning |
|---|---|
his_rec |
the kinds it takes: alm, evt,aud, val. Absent = any |
his_tbl |
the table. Absent = a sink with no SQL |
his_col |
the columns, a white list: column[=field][:kind]. id=uid is the key. Nothing a record carries reaches SQL unless named here |
qry_con |
the CON.sql of the same document that writes the rows |
his_bat |
rows per statement (default 200) |
his_spl |
rows that may wait for the database (default 20000) |
his_ident |
1: the key is an IDENTITY column (SQL Server) |
new_typ |
0x1401 runs at once on the writer's thread; 0xE401 on the sink document's own thread, one record after another (for a hook that does SQL or mail) |
new_elm_new_* |
keep the open items as elements too, as ELM.new does (03_Scripting); the hook gives or changes the row with HIS.row(evt, {..}) |
A column kind: none = as the field is; :dtm = a millisecond field as a time value;
:loc = this machine's local time text with its offset; :txt = as text.
Once a second the connection writes the ready rows in multi-row statements, skipping a
key already there. Repeats need a unique key that holds the key column: on
PostgreSQL and SQLite it is the table's own unique key (PRIMARY KEY (id), or
(id, tim) on a TimescaleDB hypertable), and a table with none takes every repeat.
| When | What happens |
|---|---|
| The database refuses the statement | the rows wait (1, 2, 4 … 30 s), one ERROR line a minute with the driver's text |
| One row refused alone while others go in | that row is dropped, with one ERROR line |
| The waiting room is full | a record stays at its source and comes again |
| The sink's document is demoted or stops | its waiting rows go back to their sources |
A value's history¶
<var id="LEVEL" sta="0x0303" sta_typ="0x1142" val="0" val_typ="0x1308" val_rou="0.1"
val_fnc="[ATR.rou,ALM.onlvl,IF.chg,HIS.val]" val_alm_onlhi_alm="95"
his_per="600" his_dbd="0.5"/>
his_per is a period in seconds (600 by default), ending at multiples of itself. The
first change of a period is a record at once, and the last change is one pending record
per point, written when the period ends. A point therefore writes at most two rows a
period; a quiet one, one. With his_dbd a change closer than that to the value last
written makes none. Only a running point records.
The pending record is in memory: a crash, or a document stopping, loses it (it is
counted). HIS.valstat() answers {points, pending, now, end, dbd, lost, refused}.
From script¶
| Call | Does |
|---|---|
HIS.rec(evt, "evt" \| "aud", text \| {fields}) |
an event or audit record into evt_adr; answers its uid |
[..,HIS.evt] |
in a chain: an event "<desc>: 'old' -> 'new'" when the value changed |
HIS.row(evt, {fields} \| null) |
inside a sink's hook: the row takes these fields / no row |
HIS.uid(evt) |
inside a hook: the record's uid, which is the row's key |
HIS.put(evt, "doc('X')/id('Y')", {fields}) |
a history row outside a hook |
HIS.pending(uid) |
1 while that row waits: an UPDATE of your own row waits for it |
HIS.rec(evt, "aud", { txt: "set point changed" });
A record made by a low role is safe to send: where the sink's xdb_acl would refuse it,
an evt or aud is written as information only (it says what happened, and cannot start
a job), and a row is refused with one line.
Over several nodes¶
| Sink's document | The record |
|---|---|
| loaded here, not parked, running | is written here now |
| loaded here, not parked, still loading | waits in the journal until the document runs (engine 4.3.4.649) |
| loaded here, parked (spare, transit) | goes into this node's journal |
| not loaded here (a field node) | goes into the journal |
| no link carries it yet | waits in the journal; after 30 s with no link it is written directly, once |
A journalled record travels over every PRO.rep link whose pro_rep_doc names the
sink's document, and is sent again after rec_tmo seconds without an answer. No
connection has to be added. The sink remembers the last 4096 uids of each source, so a
record may arrive twice and be written once.
| Root attribute | Default | Meaning |
|---|---|---|
rec_nid |
from tid |
0..255, written into every uid. Write it on every node, different on each: two nodes with one number can make the same id |
rec_max |
20000 | records the journal holds. Full: the oldest val, then evt, aud, alm goes |
rec_tmo |
5 | seconds before a journalled record is sent again |
rec_dir |
a directory: the journal is on disk too | |
rec_seg |
4096 | KB per journal file |
rec_sync |
1000 | ms between disk commits |
With rec_dir a killed process loses nothing journalled, and a power cut at most
rec_sync ms. Without it, a node that dies loses what its journal held. On an engine
before 4.3.4.649 a record read back before the sink's document had made its <his> could
still be lost, with no line (PRJEDU_records says how often).
What it is not¶
No row updates or deletes, no query path: SQL.qry still reads a table back into
elements. At a start the engine does not reload the open items from SQL: a document's
script does. The pipeline is part of the engine on Linux too, but its test rigs have run
on Windows, against SQLite, SQL Server and PostgreSQL.
Reading the log¶
| Line | Meaning |
|---|---|
HIS(ALM_H): sink of 'alm' records into table ALM over CN_SQL, 9 column(s), key id, .. |
the sink was made |
HIS(<id>): no sink -- <why> |
the configuration is wrong; nothing is kept |
HIS(<id>): N row(s) wait -- table T refused them: <driver text> |
the database is away, or his_col names a column the table lacks |
HIS(<id>): rows taken N written N in N statement(s) \| waiting N \| dropped N \| spool full N |
every 10 s when it moved |
HIS.val: id('..') -- document D has no his_adr |
the point's document names no value sink |
REC: record ids carry node 1 (rec_nid), tid 'S01' |
at start; "derived from tid" means write rec_nid |
REC: journal N (oldest S s) \| journalled .. sent .. answered .. |
every 10 s when it moved |
per PRO.rep link: records out put= answered= \| in written= repeats= not ours= refused= |
that link's share |
Saving a document¶
A save edits the file the document was loaded from (engine 4.3.4.640 and later). The file is read, its start tags are walked beside the live elements, and only the values that differ are replaced. Comments, blank lines, line ends, the DOCTYPE, the attribute order and the spacing keep their bytes. The text goes to a temporary file and then replaces the original, so if anything is wrong nothing is written.
| Call | Meaning |
|---|---|
doc('X')/#save = 1, xdb.save() |
Whole: every attribute written in the file is brought up to date. Nothing is added |
doc('X')/#save = "//dev/bi/@evt_per", xdb.save("//dev/bi/@evt_per") |
Selector: only what it names |
xdb.save(["id('A')/@a", "id('B')/@c"]) |
several selectors, one write |
xdb.save(sel, path), xdb.save("%DB%/copy.xml") |
the edited text goes to another file |
A text is a selector when it starts with /, [ or id(. Both forms need role Engineer
or above; SetValue answers 0 when the save failed, and the log says why.
| Selector | Effect |
|---|---|
path/@atr |
that attribute, added behind the tag's last attribute if the tag lacks it |
path |
an element, added at its parent's end if the file lacks it |
id('X') with no live X |
X is taken out of the file with its lines |
So a device added at run time is saved by naming it after it is added, and a deleted one by naming it after it is deleted:
evt.SetValue("doc('PLT/S00')/#save", "id('" + id + "')");
An element it selects that is not in the file (a list row made at run time) refuses the whole save: all or nothing.
What a whole save writes¶
| Attribute | Whole save | Named |
|---|---|---|
| An ordinary attribute | written | written |
sta of thr, con, dev, bi, io, var, list |
live high byte + the file's low byte | the same |
sta of <xdb>, <doc> |
no | the same as above |
pro, val of an <io> |
no | no: it is the field's value |
pro of another element |
no | written |
val of another element |
written when it has no pro |
written |
A volatile type (0x8000) |
no | no |
An attribute with its own chain (evt, qry, opr, new, ref, his) |
no | written |
con_host empty in the file |
no | written |
*_fnc, xdb_cls, xdb_acl, id, *_typ, counters |
no | written |
Of sta, the high byte is the command and the role, so an operator's stop is kept. The
low byte stays as the file has it because the engine raises it at every start. While a
document is not running or is parked no sta is written.
Limits¶
- The source must be UTF-8.
- A document not read from a file (a mirror,
new XML(text)) is written whole. - A whole save writes a value a script changed at start, a
con_hostfor example. Use a selector when that matters. - A hand edit made while the engine runs is overwritten by a whole save with the old live value. A selector touches only what it names.
- Only this node's file is written: the other nodes of a cluster keep theirs.
- An element with no
idgoes by its place among its siblings, so if one is deleted the save is refused. Give it anid. - An engine before 4.3.4.640 ignores the value of
#saveand rewrites the whole file.
| Line | Cause |
|---|---|
EvtSaveDoc OK: ..\db_T.xml (1 value(s) edited) |
done |
.. is not in the file, nothing was written |
the selector chose a run-time element |
an element without id is no longer in the document |
give it an id |
the source file ... cannot be read |
the file was moved or removed |
line N: ... |
the file was edited by hand and is no longer well formed |