Sees every write the database performs, including the ones nothing else instruments.
Entity, config and state capture cover what goes through an API. This covers what does not: a raw
Connection::insert() into a custom table, an update hook's DDL, a contributed module writing
directly. Without it a site can be modified in ways the backup never hears about, and the backup
would not know it had a gap.
What it records is that a table changed, not which rows. A statement says which rows it
touched only in its WHERE clause, and evaluating that would mean running the query again. So one
operation per table per window, which keeps the tree bounded by the schema rather than by
traffic, and the reconciler turns a dirty table into the per-row detail a restore can actually
use.
Cost. The verb guard runs on every statement and reads one keyword. Enabling the events at
all costs more than the guard does: StatementExecutionEndEvent only fires when the Start event
is also enabled, and Start's constructor calls Connection::findCallerFromDebugBacktrace(),
measured at 0.78 us at stack depth 20 and 2.99 us at depth 100. Total capture overhead stays
under 10 us per mutation, about 0.6 ms on a 200-query request. strata:calibrate measures it on
the real host and the settings form shows that number next to the switch.
The connection's table prefix comes off before anything else looks at the name. The SQL a
statement event carries is already prefixed, and a prefix is a settings.php detail that can
differ between the site a backup came from and the site it goes back to. It also hides this
module's own tables from CaptureScope::coversTable(), which would make journaling a write a write
that gets journaled.
The gap this has, stated plainly. Statement events are per-connection and off by default, so
they are enabled on the first request event this subscriber sees. Queries that run before that -
bootstrap, routing, session - are not captured. That is what the reconciler's watermark is for: a
table that changed without a captured operation is drift, and drift is reported.
The journal arrives as a factory, and that is load bearing. Building a journal reads
strata.settings, and reading configuration queries the database, and a query dispatches the very
event this class listens for. Taking strata.journal as a constructor argument therefore made the
container resolve this subscriber while it was already resolving it, which is a circular reference
and takes the site down on any request that logs. So the factory is injected and journal()
resolves once on first use, by which point the container has finished with this object. The
re-entrancy flag guards the resolution itself against being entered twice; it is defence for a
future caller, because nothing on the statement path reaches journal() today.
Enough to recognise the query in a timeline. The full text is not stored: an unbounded column
fed by every write on a busy site is how an audit table becomes the largest thing in the
database, and the statement is not what a restore replays anyway.
MAX_TABLES
MAX_TABLES = 200
Most distinct tables to accumulate in one request before new ones are dropped.
A bound rather than a policy. A request touching more tables than this is a migration or an
update hook, and the reconciler covers what falls off the end.
Properties
$tapping
$tapping : bool
Whether events have been enabled on the connection.
Type
bool
$dirty
$dirty : array
What each table saw this request, keyed by table name.
Resolving one reads configuration, and reading configuration queries. Nothing on the statement
path asks for the journal today, so this is a guard against a future one rather than a live
mechanism.
The terminate event, unused beyond triggering this.
Returns
void
—
enable()
enable() : void
Enables statement events on the connection.
Public because a request is not the only thing that writes: cron, a drush command and a queue
worker all need to call this, and none of them dispatches a kernel request event.
Returns
void
—
disable()
disable() : void
Turns the tap off.
Used by a restore, which writes a great deal and whose writes are already described by the
commit it is restoring from, and by a calibration run measuring the cost of the tap itself.
This is the widest blast radius in the module, so it is the one path that must never throw.
It runs inside Connection::execute() on every write the site performs, and an exception here
does not fail a capture - it fails the query, and with it the request, on every write. Every
other capture source already holds to "a backup never takes a save down with it"; this one
reads configuration on its first call and folds a value object on every later one, and neither
is something to bet a site's availability on.
A failed classification is dropped rather than retried. The table it belonged to is then a
table that changed with no captured operation, which is exactly what the reconciler's watermark
reports as drift, so the gap is visible rather than silent.
Journals one operation per table touched, and clears the accumulator.
Deferred to the end of the request so a request writing four hundred rows to one table produces
one operation rather than four hundred, and so the journal write happens off the response's
critical path. Public so cron and a drush command can flush what they touched without a
terminate event.
The journal, or NULL while one is already being resolved, which means the caller is the
configuration read that resolution itself triggered.
recordDisabled()
recordDisabled(\Throwable $error) : void
Puts the tap switching itself off in front of an operator.
Table capture stops for the rest of the request when this happens, and until 1.0.3 the only
trace was a log line - so a site could stop capturing writes and its own health dashboard
would still read clean.
Recorded at WARN rather than ERROR on purpose. Severity picks the repair rung, and everything
ERROR and above is automatic; there is no unattended pass that fixes a statement tap, so an
ERROR here would spend a bucket-wide reindex on a problem a reindex cannot touch.