One pass at a time¶
Two concurrent passes against one host observe the same state, build overlapping plans and apply them against each other. This page specifies the mechanism that prevents it, which the concepts section asserts without saying how.
Implementation status
The lock is implemented as described, including --wait and the scheduled pass that skips its
tick rather than queueing behind whatever holds it.
An operator racing the scheduled pass¶
An agent scheduling its own passes can avoid overlapping them, since one process controls when each starts. That is not the dangerous case.
The dangerous case is an operator running datum reconcile during an incident while the
scheduled pass is mid-apply. Those are two separate processes, both running as root,
both holding a plan built from an observation the other one is invalidating. An agent that serialises
only its own passes does nothing about this at all.
00:07:00 scheduled pass begins, observes File[nginx-config] as mode 0644
00:07:04 operator runs datum reconcile, observes the same
00:07:05 scheduled pass writes the file and restarts nginx
00:07:06 manual pass writes the file again and restarts nginx again
The outcome there is survivable and the general shape is not. Two providers acting on one target with interleaved observations can leave a file with content from one pass and a mode from another. Both passes report success, so a later pass does not reliably correct it.
An exclusive lock, taken by everything that reconciles¶
Every process that applies changes takes an exclusive advisory lock on one file before it observes, and holds it until verification is finished.
The lock is taken before observation and not before apply, because the observation is what the plan depends on. Taking it at apply time would allow two passes to observe concurrently and then apply in sequence, each acting on a snapshot the other had invalidated.
The lock is released after verification rather than after apply, since verification reads the host back and a concurrent writer between the two would cause a spurious verification failure.
| Command | Takes the lock |
|---|---|
datum reconcile |
Yes, for the whole pass |
datum observe, datum diff, datum plan |
No |
datum render, datum explain, datum validate, datum affected |
No, they never read the host |
datum status |
No, it reads a report |
The read-only commands do not take it at all. An operator investigating a host during an incident
should never be blocked by a reconciliation in progress, and a plan read while a pass is running is
stale in the same way any plan is stale the moment it is printed. datum plan is therefore
available at any time, including while a pass is running.
The second caller fails rather than waits¶
A process that cannot take the lock reports who holds it and exits.
$ datum reconcile
error: a pass is already running on this host
holder pid 4182, pass 01JQ8Z3M0000000000000000, started 00:07:00
waited 0s
exit 3
Failing immediately rather than blocking is the right default for an interactive command. An
operator who runs datum reconcile and sees nothing for four minutes has no way to tell a slow pass
from a hung one, whereas an error naming the holder answers the question and leaves the choice of
what to do next with the person making it.
--wait blocks until the lock is available, for scripts where queueing is what is wanted, and the
scheduled pass never waits. A scheduled pass that cannot take the lock skips its tick and records that
it did, for the same reason
an overrunning pass is not queued.
A new exit code¶
| Code | Meaning |
|---|---|
3 |
Could not acquire the pass lock. Another pass is running. |
A distinct code, not the generic 1, means a script can tell "somebody else is reconciling" from
"reconciliation failed", and those call for opposite responses. The first should be retried and the
second should not, and a caller that cannot distinguish them has to either retry real failures or
give up on transient contention. The full set is in the command
reference.
Why an advisory file lock¶
An exclusive flock on a file in the state directory needs no daemon, no shared memory and no cleanup
path, and the kernel releases it when the holding process dies for any reason.
That last property is what rules out the alternatives. A PID file written at the start of a pass has to be removed at the end, so a process killed mid-pass leaves a stale file, and every implementation then grows a heuristic for deciding whether the recorded process is still alive. A lock held by a file descriptor is released when the process exits, so there is no stale lock to clear.
The lock file lives under state, which is
already root-owned at mode 0700 and
already refused as a target for Datum's own resources,
so no new path needs protecting.
Important limitation
The lock coordinates Datum with Datum and nothing else. A package installed by hand, a configuration management tool run alongside Datum, or an administrator editing a managed file during a pass are all invisible to it, and the design has no way to exclude them. Such a change is detected afterwards, since verification reads the host back and the next pass observes what was left behind. Repeated correction of the same resource is the signal that something else is managing it, which is a question the design has not yet answered.