The surgical half of rollback: one node, one config object, a set of them, without a maintenance
window and without touching anything the operator did not name. A whole-site rewind is a physical
restore and a different operation with different risks.
Three rules make it safe to run on a live site.
A snapshot is forced first. Before anything is written, the current state of every subject
about to change is flushed and committed, so the rollback itself has a rollback. A restore that
cannot take that snapshot refuses; there is no configuration to turn it off, because the one
moment an operator needs an undo is right after a restore they did not mean to run.
A degraded subject is skipped, not filled. Writing a partial reconstruction over a live value
produces a record that is neither the old one nor the new one. The plan lists them and the
operator can opt in per scope.
A subject that fails to write does not abort the rest. Each write is independent, so a
validation constraint on one node does not leave the other four hundred unrestored. The result
names every failure and the outcome is FAILED even when most of it worked.
Entities are saved through the entity API rather than written to their tables, so hooks, field
constraints and cache invalidation all run - and Strata's own capture hooks run too, which is
exactly right: a restore is a change to the site and belongs in the history like any other.
Rolling a user back to an old access timestamp would make an active account look dormant, which
is corruption rather than recovery, and it would erase the security trail of who signed in when.
The same reasoning excluded them from capture in the first place.
Set as whole data rather than key by key, because that is how it was captured and because
setting keys individually would leave any key added since the target commit in place - a
restored config object that is a merge of two points in time is not either of them.
Two shapes arrive here. A whole-table marker from the statement tap says a table changed and
carries no rows, because a statement names the rows it touched only in its WHERE clause. A row
captured by the reconciler does carry its contents, but writing it means an INSERT or UPDATE
whose correctness depends on constraints, sequences and triggers this has no view of.
Both are refused with the reason, and both are what a physical restore exists for.
The value a state or key-value replay reconstructed.
These stores hold arbitrary PHP values, so capture serializes them rather than encoding them as
JSON. A value that round-tripped through JSON would come back as an array and be written back as
the wrong type.
A subject the replay says did not exist at the target is NOT deleted here. Removing content
because it postdates a restore point is a destructive interpretation of a rollback, and it is
the one thing an operator would least expect from restoring a single node.
A field the entity no longer has is reported rather than forced: a field removed since the
commit was taken cannot be restored, and inventing a value for it would be worse than saying so.
Parameters
\Drupal\Core\Entity\FieldableEntityInterface
$entity
The entity to write into.
array
$fields
Field name keyed to its reconstructed value.
Returns
string|null
—
NULL on success, or why it did not work.
snapshot()
snapshot() : string|null
Seals the site's current state so the restore can be undone.
Returns
string|null
—
The snapshot commit id, or NULL when nothing could be sealed. An empty journal is not a
failure: with nothing pending, the head already IS the state the restore is about to change,
so that commit is the snapshot.