Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Database schema

What Ductus keeps in PostgreSQL, for transparency (and for DBAs tuning the server). This is not an API. Tables and columns can change in any release; read and change data through the REST API (/api/v1) and the ductus commands, which keep working across versions. Queries written against these tables may break on upgrade.

Changes only ever add (migrations in crates/ductus-store/migrations, see architecture.md invariant 4), so an older engine still runs on a newer schema: that's what makes rollback safe. A test checks that every table and column is described here.

Statuses (status columns): 0 pending, 1 sent, 2 filtered, 3 quarantined, 4 duplicate (messages only).

messages

Every message received, as received (the original is never changed). Partitioned by retention tier, then by time (migration 0010): messages_t21 holds the 3-week tier (21 days), and its partitions cover a period each, named by their first day (messages_t21_20261005): a day for the 1w tier, a week (from Monday, midnight UTC) for 3w and 7w, about 24 partitions in all. They're created 3 days ahead; a partition is dropped once its whole period is older than its tier's days (see retention).

The held area (migration 0014) is tier 0: messages_held, partitioned by month (messages_held_202609, bounds at midnight UTC). When a partition expires with messages someone still needs (pending, or quarantined for an interface that keeps them), those move here with their deliveries, and the partition drops. A month that's over is dropped once nothing in it is unfinished. The held tables are created when first needed.

columntype
idbigintFrom a sequence: arrival order within an interface. With received_at, the primary key (a partitioned table's key must include the partition column).
received_attimestamptzWhen it was stored.
interfacetextThe interface's name.
statussmallintOverall status: pending until every destination is done; then quarantined if any destination quarantined it, else sent if any sent it, else filtered.
partition_keyintegerThe lane, from the ordering key (ductus_core::partition_of).
ordering_keytextThe ordering key's value (e.g. the MRN), if the interface has one.
control_idtextMSH-10 for HL7, when present.
payloadbyteaThe message as received.
attemptsintegerUsed before status per destination (v0.5); attempts are now counted per delivery. Kept for older engines; removed when the migrations are squashed before release.
last_errortextWhy it was quarantined or filtered (destination: reason; ... with several destinations).
updated_attimestamptzLast status change.
origin_idbigintRouted from another interface (source: internal): the id of the message there. NULL for messages received from outside. Not indexed.
tiersmallintThe retention tier the row is stored in, in days (7, 21, 49: 1w, 3w, 7w): its interface's retention when it was stored, until a retention change moves it. 49 for rows stored by engines that don't know tiers. 0: in the held area.

Indexes: primary key (id, received_at, tier) (a partitioned table's key must include its partition columns); messages_pending (interface, partition_key, id) WHERE status = 0; messages_by_interface (interface, id) (the message browser, newest first); messages_quarantined (interface, id) WHERE status = 3.

deliveries

One row per queued message and destination: each destination's work queue and its status. Inserted in the same statement as the message. Partitioned like messages (deliveries_t21_20261005, same tier and bounds).

columntype
message_idbigintmessages.id.
received_attimestamptzmessages.received_at (finds the partition).
destination_idintegerdestinations.id.
partition_keyintegerThe message's lane (copied, so the queue index needs no join).
statussmallintThis destination's status.
attemptsintegerFailed attempts so far.
last_errortextLast failure or the reason it was filtered/quarantined.
outputbyteaExactly what was sent, only for destinations with save_output: true.
updated_attimestamptzLast change.
tiersmallintAlways its message's tier (a retention change, or the move to the held area, moves both in one statement).

Indexes: primary key (message_id, received_at, destination_id, tier); deliveries_queue (destination_id, message_id, partition_key) WHERE status = 0, which is each destination's queue in arrival order (the lane is checked inside the index; migration 0011 replaced deliveries_pending, which had the lane second); deliveries_quarantined (destination_id, message_id) WHERE status = 3 (quarantine counts without reading the table).

destinations

Every destination the engine has been configured with. Deliveries refer to it by id (a small integer instead of repeating names in every row).

columntype
idintegerIdentity.
interfacetextThe interface's name.
nametextThe destination's name (default for a single destination:).

Indexes: primary key id; unique (interface, name).

dedupe_keys

Keys of recently seen messages, for interfaces with dedupe:. Claimed in the same transaction as the message insert. Old keys are deleted hourly.

columntype
interfacetextThe interface's name.
keytextThe message's key values, joined.
seen_attimestamptzLast time it was seen (a repeat outside the window renews it).

Indexes: primary key (interface, key); seen_at (for the hourly cleanup).

source_state

What sources remember between runs, always written in the same transaction as the messages it accounts for: file progress (file:<name>: size, time, byte offset, records, done) and schedules (schedule: anchor, previous run).

columntype
interfacetextThe interface's name.
keytextWhat it is (file:lab.csv, schedule).
valuejsonbThe state.
updated_attimestamptzLast change.

Index: primary key (interface, key).

api_tokens

REST API tokens (ductus token create, or POST /api/v1/tokens). Only a hash of each token is kept; the token itself is shown once. Revoked tokens stay, for the record.

columntype
idintegerIdentity.
nametextWho or what uses it (ci, monitoring). One live token per name.
rolesmallint0 viewer (reads), 1 operator (also drain and connection checks), 2 admin (also tokens).
hashbyteaSHA-256 of the token (tokens are 32 random bytes, so a slow password hash adds nothing).
created_attimestamptzWhen it was made.
last_used_attimestamptzLast use, to the minute (written at most once a minute per token).
revoked_attimestamptzWhen it was revoked; it stops working at once.
user_idintegerThe user it belongs to (users.id); none for a service token made by an admin. A user's token stops working when the user is disabled and never goes above the user's role or permissions.
permissionstext[]Extra permissions (payloads, audit); admins have all.

Indexes: primary key id; unique hash; api_tokens_live_name (name) WHERE revoked_at IS NULL (unique); api_tokens_user (user_id).

users

People who sign in (ductus user create, or POST /api/v1/users). Passwords are argon2id hashes; no password is ever stored or logged. Accounts are disabled, never deleted: the audit trail names them.

columntype
idintegerIdentity.
usernametextSign-in name; not case sensitive (unique on lower(username)).
display_nametextFor people reading the UI.
emailtextWhere alerts for this person go.
rolesmallint0 viewer, 1 operator, 2 admin (as tokens).
permissionstext[]payloads (may read message payloads), audit (may read the audit log). Admins have both.
sourcetextWhere the password is checked: local, or ldap (a directory; no hash kept).
password_hashtextargon2id, a self-describing string; null when the password lives elsewhere.
must_change_passwordbooleanSet when an admin made or reset the password: the next sign-in can do nothing but change it.
disabled_attimestamptzWhen it was disabled: sessions and tokens stop at once.
failed_loginsintegerWrong passwords counted within the lockout window.
last_failed_attimestamptzThe latest wrong password (starts a new window when the last one is over).
locked_untiltimestamptzPast the limit, the account is refused until then.
created_attimestamptz
last_sign_in_attimestamptz
directory_checked_attimestamptzldap accounts: when the directory was last asked about them (each sign-in, and every auth.ldap.refresh while they have a session or a token).
directory_refused_attimestamptzldap accounts: when the directory said no (in no Ductus group, disabled or deleted there). The account can't act (sessions ended, tokens stopped), like a disabled one, until a directory check says yes again; separate from disabled_at, which is an admin's decision here.

| directory_id | text | ldap accounts: the person's id in the directory, <attribute>:<value> (entryUUID:…, or objectGUID:hex:… for Active Directory's 16 bytes; auth.ldap.attributes.id). Set at the first sign-in or refresh that reads it (null before 0.6.7.1, and with id: none). A refresh looks the person up by it. |

Directory accounts (source = 'ldap') are made at their first sign-in; their role, permissions, display name and email are the directory's, rewritten at every sign-in and refresh (migration 0022 added the two directory_ columns). An account is the directory's person once its directory_id is known (0.6.7.1, migration 0025): when the directory renames their sign-in name, username takes the new one; when it gives a departed person's sign-in name to someone new, the earlier account is renamed <name>#<id> (its own id) and refused, so its tokens stay dead, and the new person gets an account of their own.

Indexes: primary key id; unique users_username (lower(username)); unique users_directory_id (directory_id) WHERE directory_id IS NOT NULL.

user_names

What an account was called before (0.6.7.1, migration 0025): every change of its username, display name or email (an admin's edit, the directory's at a sign-in or refresh, a name moved aside) keeps the old value here, in the same transaction as the change. Kept as long as the account. Empty old values aren't kept.

columntype
idbigintIdentity.
user_idintegerusers.id (deleted with it).
kindtextusername, email or display_name.
valuetextThe value it had.
untiltimestamptzWhen it changed.

Indexes: primary key id; user_names_value (lower(value)); user_names_user (user_id).

user_settings

A user's own choices, so they follow them to any machine. Today: the theme and the custom colors.

columntype
user_idintegerusers.id. Primary key.
themetextsystem, light, dark, contrast or custom.
colorsjsonbThe five custom colors and the values worked out from them, as the UI saves them.
updated_attimestamptz

api_sessions

Web UI sessions: a browser signs in with a username and password, or a token (POST /api/v1/session), and gets a cookie instead of keeping either. A session acts with its user's or token's role and ends when the user is disabled or the token revoked, when it's been idle too long (12 hours unless auth.session says otherwise), or at most 7 days after sign-in. Expired sessions are deleted when the next one is made.

columntype
idintegerIdentity.
hashbyteaSHA-256 of the cookie's value (32 random bytes).
token_idintegerThe token it signed in with (api_tokens.id); none for a user's session.
user_idintegerThe user it signed in as (users.id); none for a token's session. Exactly one of the two is set.
created_attimestamptzSign-in.
last_seen_attimestamptzLast use, to the minute (written at most once a minute).
expires_attimestamptzWhen it ends at the latest, whatever its use.

Indexes: primary key id; unique hash; api_sessions_expires (expires_at); api_sessions_user (user_id).

message_actions

What people did to messages through the API (POST /api/v1/messages/{id}/resend, /messages/resend, /messages/{id}/edit): one row per message and destination acted on, with the delivery as it was before, so nothing an action changes is lost. A resend puts the delivery back to pending in place (same message, same id); an edit queues a corrected copy (a new message) and marks the original's quarantined delivery to that destination filtered ("replaced by edited copy N"). Not dropped with the messages' partitions: it's the record of who did what (step 8's audit log builds on it; no purge yet).

columntype
idbigintIdentity.
attimestamptzWhen.
actiontextresend, edit or dismiss (a quarantined delivery closed without sending: it became filtered, "dismissed by NAME: reason").
token_idintegerThe API token used (api_tokens.id).
user_idintegerThe person who acted (users.id), when it was one (their token's owner counts).
actortextThe user's or token's name at the time (names can be reused).
reasontextWhy, if given.
message_idbigintThe message acted on (for an edit: the original).
received_attimestamptzIts received_at (with message_id, finds it in its partition while it's kept).
interfacetextIts interface.
destination_idintegerThe destination (destinations.id).
previous_statussmallintThe delivery's status before (as deliveries.status); none if the message had no delivery for that destination.
previous_attemptsintegerIts attempts before (a resend starts them over).
previous_errortextIts last_error before.
previous_outputbyteaWhat was sent before, for destinations with save_output: true (sending again replaces it). A copy of a payload, so it is set to NULL once the action is older than the interface's message retention (the row stays).
copy_idbigintedit: the copy's message id.
audit_idbigintThe audit_log row of the request that made this action (one request, e.g. a bulk resend, makes one audit row and one action per delivery). No foreign key: both are trimmed after audit.keep.

Indexes: primary key id; message_actions_message (message_id); message_actions_copy (copy_id) WHERE copy_id IS NOT NULL; message_actions_audit (audit_id) WHERE audit_id IS NOT NULL; message_actions_outputs (at) WHERE previous_output IS NOT NULL (finds the copies to clear). Rows older than audit.keep (default 6 years) are deleted hourly in batches of 5,000.

audit_log

Who did what, to what, from where, and how it ended. Written in the transaction of the change itself where there is one (a resend, a dismissal, an account change), and before the action where there isn't (a pause, a drain; the action is refused if the row can't be written). Payload reads are written before the payload is returned. Not tied to users or tokens by a foreign key: it must say who acted long after the account is gone. Rows older than audit.keep are deleted hourly in batches of 5,000.

columntype
idbigintIdentity.
attimestamptzWhen.
actor_kindtextuser, token, system (the engine, or someone at the server's command line) or anonymous (a sign-in as a name nobody has; what they typed isn't kept, it may be a password).
actor_idintegerusers.id or api_tokens.id, by actor_kind.
user_idintegerThe person behind it: the user, or the owner of a user's token. What GET /me/activity reads.
actor_nametextThe user's or token's name at the time (a snapshot).
viatextsession (the web UI), token, cli, engine; none for rows made from message_actions when this table was added.
ipinetThe address the connection came from (a proxy's address behind one).
actiontextsession.sign_in, session.failed, session.sign_out, password.change, password.reset, user.create (also a directory account's first sign-in, by directory), user.update, user.disable, user.unlock, user.directory_change (the directory changed an account's role, permissions, name or email, or let it back in), user.directory_refused (the directory said no to an account: its sessions ended), token.create, token.revoke, message.payload.read, message.release (a resend that put back only quarantined deliveries, whether or not it named their destinations), message.replay (one that sent again a delivery that had finished as sent or filtered), message.edit, message.dismiss, destination.pause, destination.resume, engine.drain, engine.resume, retention.change, retention.confirm, doctor.run, audit.export, alert.ack, alert.silence, alert.unsilence, alert.test (a test notification to a channel), engine.start, engine.stop, engine.handover (this engine took over from another: systemctl reload, an upgrade or a deploy; detail.from and detail.to with version, pid and instance, detail.sending_paused_ms), engine.refused (a start refused: another live engine runs some of its interfaces, or its lanes would strand waiting messages, or a handover would lower lanes; detail.reason is owned, lanes or handover_lanes), interface.stopped, interface.partitions_raised (an interface started with more lanes while messages waited: a key's new messages may pass its older ones until those are sent).
subjecttextWhat it was done to: a username, a token name, interface/destination.
interfacetextThe interface it concerns.
message_idbigintThe message, when the request was about exactly one.
outcometextok, denied or failed.
detailjsonbMore, by action: the reason, the messages and destinations of a bulk request, the version and previous run for engine.start. Never a password, token or payload.

Indexes: primary key id; audit_log_at (at); audit_log_actor (actor_kind, actor_id, at); audit_log_user (user_id, at); audit_log_action (action, at); audit_log_interface (interface, at); audit_log_message (message_id).

retention

Each interface's retention as the engine applied it, and the change in progress. Changes run one at a time per interface: a different value configured meanwhile waits in waiting, and so does a shorter one (it deletes messages) until someone confirms it (ductus retention confirm).

columntype
interfacetextThe interface's name.
dayssmallintThe tier its new messages go to (the target of a running change).
changing_fromsmallintSet while a change runs: the tier it's changing from. The interface's rows in other tiers are being moved into days, or deleted when they're past its window, in small batches.
rows_leftbigintRows still to move or delete (counted when the change started, then counted down).
waitingsmallintA configured value not applied yet.
confirmedsmallintA shorter waiting value someone confirmed.
changed_attimestamptzWhen the current value, or the running change, started.
updated_attimestamptzLast change.
purge_quarantinedbooleanquarantined: purge: its quarantined messages go with their expired partition instead of moving to the held area (false, the default: kept until resolved).

Index: primary key interface.

engines

Every run of an engine on this database (migration 0023). While it runs, an engine holds a session advisory lock with lock_key on a connection of its own; that is what "alive" means here (pg_locks), so a crash or power loss needs no timeout to wait out (the server frees the lock with the session; the lock connection asks it to notice a vanished client within ~25 s). A refused start leaves no row. Rows of engines that started more than audit.keep ago, own nothing and aren't alive are deleted by the hourly audit job.

columntype
instancetextThe run's name, as audit_log engine.start/engine.stop carry it in detail.instance.
lock_keybigintIts advisory lock (positive, below 2^62: pg_locks shows it as classid × 2^32 + objid).
hosttextThe server it runs on.
pidintegerIts process id there.
versiontextThe engine's version.
environmenttext--env.
started_attimestamptzWhen it started.
stopped_attimestamptzSet by a clean stop. NULL and not alive: it crashed.

Indexes: primary key instance; unique lock_key.

interface_owners

Which engine runs each interface's workers, file pickup and schedule: one engine per interface across every engine on the database. An engine claims all of its (queued) interfaces at start, before changing anything for them, all or none; a row whose engine isn't alive any more is taken over. Respond-mode HTTP interfaces aren't claimed (no queue). A clean stop deletes its rows.

columntype
interfacetextThe interface's name.
instancetextengines.instance (deleted with it).
sincetimestamptzWhen it was claimed.

Indexes: primary key interface; interface_owners_instance (instance).

interface_applied

The lanes each interface last ran with: ordering.partitions, or 1 without ordering.key. At start, an interface with fewer lanes than recorded (or none recorded) has its waiting deliveries checked: any in lanes it no longer has refuse the start (no worker would read them). More lanes with messages waiting only warn (interface.partitions_raised).

columntype
interfacetextThe interface's name.
lanesintegerIts lanes.
updated_attimestamptzWhen it last changed.

Index: primary key interface.

interface_activity

What monitoring must remember across a restart or a failover: when each interface last received something. Written every few seconds by one task (only the times that changed; a time never moves back).

columntype
interfacetextThe interface's name.
watched_sincetimestamptzWhen an engine first ran the interface: active_by deadlines before it don't count.
last_receivedtimestamptzWhen a message (or a duplicate) last arrived.
updated_attimestamptzLast change.

Index: primary key interface.

destination_activity

Each destination's last send and failure (written like interface_activity), and its pause (written at once).

columntype
destination_idintegerdestinations.id.
last_senttimestamptzWhen a message was last delivered (or routed).
last_errortextThe last failure: a retry's error or a quarantine's reason.
last_error_attimestamptzWhen.
paused_attimestamptzSet while the destination is paused on purpose (ductus pause): nothing is sent or retried.
paused_bytextThe API token that paused it.
pause_reasontextWhy, once someone says (often added after the pause).
reason_bytextThe token that gave the reason.
reason_attimestamptzWhen.
baseline_inflowreal[]What's normal, per hour of the week (168 values, Monday 00:00 local first; NULL for hours not watched yet): messages arriving per minute. Learned at the end of each hour the engine watched for 15 minutes or more (migration 0013).
baseline_send_msreal[]The same for a successful send's mean time, in ms.
updated_attimestamptzLast change.

Index: primary key destination_id.

docs_snapshot

The project's documentation as the company wrote it, for SQL reports and BI tools ("which interfaces have no signed BAA": SELECT name FROM docs_snapshot WHERE kind = 'interface' AND docs->'custom'->>'baa_signed' = 'no'). One row per interface and per resources.yaml entry. The YAML in Git is the source of truth: every engine start makes the table match the project (changed rows are rewritten, entries the project no longer has are removed), so it is as current as the last start. Versioned history comes with versioned releases (v0.6).

columntype
kindtextinterface or resource.
nametextThe interface's name, or the resource as api empi, database patient_db, sftp server lab.
docsjsonbAn interface: {"description", "owner", "custom": {...}, "destinations": {"<name>": {"custom": {...}}}}; a resource: {"custom": {...}}. custom uses the company's field names from docs-schema.yaml; absent values are absent.
updated_attimestamptzWhen the row last changed.

Indexes: primary key (kind, name); docs_snapshot_docs (GIN on docs).

alert_state

One row per thing that can alert: each interface (iface:<interface>), each destination (dest:<interface>/<destination>) and each certificate the engine connects to (cert:<resource>:<host>:<port>) or presents (cert:<name>:<engine's host>:0), made when an engine with alerts: in ductus.yaml first runs it (so it can be silenced before anything fires). Changed by compare-and-set on rev: two engines on one database judge the same state and only one records each change.

columntype
idintegerIdentity; the API's /alerts/{id}.
keytextiface:<interface>, dest:<interface>/<destination> or cert:<resource>:<host>:<port> (unique).
interfacetextThe interface; for a certificate, its resource (as in certificate_status).
destinationtextThe destination; none for the interface's own alert; host:port for a certificate.
statetextok, or what is wrong: error, stalled, behind (a destination), stopped, silent, missed (an interface), certificate (expires within alerts.certificates.warn_days, or has expired).
severitytextwarning or critical (from the first rule naming the state), while open.
sincetimestamptzBad since (when it fired).
ok_sincetimestamptzFine again since then, not yet for alerts.settle: bad again before that reopens the same alert.
last_notified_attimestamptzWhen people were last told, or will be once quiet hours end; reminders count from it.
notify_countintegerNotifications sent since it fired.
summarytextThe health view's words, as of the last check.
ack_bytextWho acknowledged it (no reminders until it escalates or recovers).
ack_attimestamptzWhen.
silenced_untiltimestamptzNothing is sent about it until then.
silenced_bytextWho silenced it.
silence_reasontextWhy.
revbigintEach change adds one (compare-and-set).
updated_attimestamptzLast change.
stageintegerA certificate alert's step: the warn day (30, 14, 7 by default) people were last told about, 0 once expired (migration 0021). None for other alerts.

Indexes: primary key id; unique key.

alert_events

What happened to each alert: fired, escalated, reminded, recovered (the engine) and acknowledged, silenced, unsilenced (people, also in audit_log). Rows older than alerts.keep (default a year) are deleted hourly with their notifications.

columntype
idbigintIdentity.
attimestamptzWhen.
alert_idintegeralert_state.id (no foreign key).
keytextThe alert's key.
kindtextSee above.
statetextThe state then; for recovered, what it recovered from.
severitytextThe severity then.
sincetimestamptzWhen the alert turned bad, for the words of a notification sent later.
summarytextThe health view's words then.
by_nametextWho acknowledged or silenced it.
detailjsonbFor a silence: until and reason.

Indexes: primary key id; alert_events_alert (alert_id, id); alert_events_at (at).

alert_deliveries

The outbox: one row per notification (an event to a channel), stored with the event before anything is sent. Senders claim due rows with FOR UPDATE SKIP LOCKED and a two-minute lease, so two engines send each once; a crash between sending and marking it sent sends it again after the lease (at least once).

columntype
idbigintIdentity.
event_idbigintalert_events.id (deleted with it).
channeltextA channel's name; <name>/email and <name>/webhook for a channel with both; email:<address> for an owner.
statustextpending, sent, failed (given up after 10 tries, about six hours), cancelled (it recovered before quiet hours ended).
attemptsintegerTries so far.
last_errortextWhy the last try failed.
next_try_attimestamptzDue then (after quiet hours, or a back-off of 30 s doubling to an hour); while claimed, the end of the lease.
sent_attimestamptzWhen it went out.

Indexes: primary key id; alert_deliveries_due (next_try_at) WHERE status = 'pending'; alert_deliveries_event (event_id).

certificate_status

The certificate each TLS server the engine connects to presented at its last check (migration 0021): HTTPS APIs and destinations, the alerts' mail server, webhooks and heartbeat. Written by the engine's scheduled checks (doctor.schedule, every 6 hours by default) and by checks someone asks for (POST /api/v1/doctor). Read even when this server doesn't trust it (a private CA). A check that couldn't read a certificate sets error and checked_at and keeps the dates from the check before.

Our own certificates are rows too (migration 0024, side = 'ours'): each certificates: pair the engine presents, to senders on a TLS listener or as a client certificate. They are read from what the engine loaded (no network), written at its start and again only when a renewal is picked up or a renewed pair can't be used.

columntype
sidetextremote (a server the engine connects to) or ours (a pair it presents; migration 0024). Older engines' rows are remote.
resourcetextempi (resources.yaml), alerts.smtp, alerts.<channel> (a webhook), alerts.heartbeat, or <interface>/<destination> for a destination with a URL of its own; ours: the certificates: entry's name.
hosttextThe server's name as configured; ours: the engine's server (its host name).
portintegerIts port; ours: 0 (one pair may be served on several ports).
subjecttextThe server certificate's common name (or organisation).
issuertextWho issued it.
fingerprinttextSHA-256 of the server certificate, hex: changes when it's renewed or replaced.
not_beforetimestamptzValid from.
not_aftertimestamptzThe server certificate's expiry.
chain_min_not_aftertimestamptzThe earliest expiry in the chain the server presents: what alerts count (an intermediate that ends breaks clients as surely).
chain_min_subjecttextWhose that is, when it isn't the server certificate's.
trustedbooleanWhether this server trusts it (the system's trusted certificates); none for ours (whether senders do isn't known here).
errortextWhy the last check failed (couldn't connect, not trusted, expired); none when it passed. Ours: why renewed files can't be used (the pair loaded before is still presented).
checked_attimestamptzThe last check; ours: when the engine last read the pair (its start, or a renewal).
read_attimestamptzWhen the certificate in the row was read: the last check, unless that one couldn't read it (then error says why and the dates are older).

Index: primary key (resource, host, port).

doctor_runs

Each connection check the engine ran on its schedule or was asked for, with its whole report (migration 0021). The last 30 are kept.

columntype
idbigintIdentity.
attimestamptzWhen it ran.
modetextcertificates (DNS, TCP, TLS handshake: nothing logs in) or full (ductus doctor's checks, logins included).
started_bytextschedule, or who asked (a user or token name).
secondsdouble precisionHow long it took.
checksintegerConnections checked.
failedintegerOf those, failed.
reportjsonbThe report, as POST /api/v1/doctor returns it.

Index: primary key id.

Functions

  • ductus_tiers(): the tiers in days, {7,21,49}. ductus_tier_unit(tier): the period one partition of a tier covers (day for 7, else week).
  • ductus_create_tier_partition(table, tier, day): the partition of messages or deliveries holding that tier's rows of that day, created if missing.
  • ductus_create_held_partition(table, day): the held area's partition of messages or deliveries for that day's month (and the held table itself), created if missing.
  • ductus_ensure_tier_partitions(table, tier, days_ahead), ductus_ensure_delivery_partitions(tier): one tier's missing partitions; the engine runs them hourly, one table and tier per transaction, with a short lock timeout (see architecture.md, Storage).
  • ductus_ensure_message_partitions(days_ahead), ductus_ensure_delivery_partitions(), ductus_ensure_partitions(days_ahead): every tier at once, shortest tier first: what older engines call. Removed when the migrations are squashed.