\Drupal\strata\Hook CronCapture

Drives everything that has no request to run in.

Cron is what makes capture into a backup on a site nobody is watching. A busy site also flushes from the request that filled the window, but a quiet one may go hours between writes, and the age bound exists precisely so those few writes still reach the store.

Four things run here, in this order and for these reasons:

  1. Code capture, because a deploy is not an event Drupal dispatches. It runs FIRST so the operations it appends are sealed by the flush in the same run rather than waiting for the next.
  2. The reconciler, which detects tables that changed with nothing capturing them and appends the rows to close the gap - again before the flush, so those rows are sealed too.
  3. The flush, sealing whatever the window now holds.
  4. Keyspace discovery, which writes no operations and only describes what is there, so it runs last and cannot delay anything that does.
  5. Dictionary training, on its own long interval. It reads segments back and compresses the samples several times to score the candidates, which is far too slow to run every cron and pointless to run often: a dictionary describes the shape of what a realm writes, and that shape changes over weeks rather than minutes.
  6. Anomaly detection, which takes one reading of the store and scores it against the site's own earlier readings. It runs after the flush so the reading includes what this cron just sealed rather than describing the state before it.
  7. A restore drill, on its own interval and off by default. It replays a sample of subjects and compares them against what the site holds, which costs a replay per subject - the one stage here whose cost scales with the size of the sample rather than with what changed.
  8. Telemetry export, last, because it reports what every earlier stage did and an export that ran first would describe the previous run.

No stage can stop cron. A cron hook that throws stops every later hook in the queue, so a storage outage would take the site's search indexing and cache warming down with it. Each stage is caught separately, so one failing does not skip the others either.

The engine arrives rather than the eight things it builds, and that is load bearing. Every stage is assembled from configuration and assembling one can refuse: encryption is on by default with no key, so Engine::flusher() throws on a site nobody has configured yet. Drupal resolves a cron hook's service inside ModuleHandler::invokeAllWith(), which runs before the per-module try/catch in Cron::invokeCronHandlers() and before this class can check anything. Taking the stages as constructor arguments therefore skipped every module's cron and left the cron lock held for its full 900 seconds. Each stage resolves what it needs inside its own guard instead.

Summary

Methods
Properties
Constants
__construct
onCron
No public properties found
DICTIONARY_KEY
DICTIONARY_INTERVAL
BUDGET_CODE
DRILL_KEY
DRILL_INTERVAL
No protected methods found
No protected properties found
No protected constants found
detectAnomalies
assessBudget
repair
runDrill
setting
trainDictionaries
stage
failed
engine
scope
lease
logger
state
configFactory
dictionaryInterval
No private constants found

Constant

DICTIONARY_KEY

DICTIONARY_KEY = 'strata.dictionary.trained'

State key holding when the dictionary pass last ran.

DICTIONARY_INTERVAL

DICTIONARY_INTERVAL = 604800

Seconds between dictionary passes.

A week. Retraining more often costs a pass and gains nothing, because a new version only replaces the old one when it measures materially better.

BUDGET_CODE

BUDGET_CODE = 'budget.exceeded'

Finding code the budget stage raises and clears.

DRILL_KEY

DRILL_KEY = 'strata.drill.ran'

State key holding when the last restore drill ran.

DRILL_INTERVAL

DRILL_INTERVAL = 86400

Seconds between restore drills when nothing is configured.

Properties

$engine

$engine : \Drupal\strata\Engine

Type

Engine

$scope

$scope : \Drupal\strata\Capture\CaptureScope

Type

CaptureScope

$lease

$lease : \Drupal\strata\Flush\Lease

Type

Lease

$logger

$logger : \Psr\Log\LoggerInterface

Type

LoggerInterface

$state

$state : \Drupal\Core\State\StateInterface

Type

StateInterface

$configFactory

$configFactory : \Drupal\Core\Config\ConfigFactoryInterface

Type

ConfigFactoryInterface

$dictionaryInterval

$dictionaryInterval : int

Type

int

Methods

__construct()

__construct(\Drupal\strata\Engine  $engine, \Drupal\strata\Capture\CaptureScope  $scope, \Drupal\strata\Flush\Lease  $lease, \Psr\Log\LoggerInterface  $logger, \Drupal\Core\State\StateInterface  $state, \Drupal\Core\Config\ConfigFactoryInterface  $configFactory, int  $dictionaryInterval = \self::DICTIONARY_INTERVAL) : mixed

Constructs the cron capture.

Parameters

\Drupal\strata\Engine $engine

Builds each stage on demand. Deliberately not the stages themselves; see the class docblock.

\Drupal\strata\Capture\CaptureScope $scope

Decides whether capture is on at all.

\Drupal\strata\Flush\Lease $lease

Collected on each run so an abandoned lease cannot block a flush forever.

\Psr\Log\LoggerInterface $logger

Records a stage that could not run.

\Drupal\Core\State\StateInterface $state

Remembers when the dictionary pass and the last drill ran.

\Drupal\Core\Config\ConfigFactoryInterface $configFactory

Decides whether the anomaly and drill stages are on, and how often the drill runs.

int $dictionaryInterval

Seconds between dictionary passes.

Returns

mixed —

onCron()

onCron() : void

Runs every unattended stage.

Returns

void —

detectAnomalies()

detectAnomalies() : list<\Drupal\strata\Anomaly\Anomaly>|null

Takes a reading and scores it, if anomaly detection is on.

Returns

list<\Drupal\strata\Anomaly\Anomaly>|null —

What departed from the site's own history, or NULL when the stage is off.

assessBudget()

assessBudget() : \Drupal\strata\Budget\BudgetAssessment|null

Prices the month against the ceilings, if any are set.

Skipped outright when neither ceiling is configured, which is the shipped state: reading the stat table and the frame index every cron run to price a site against no ceiling is two queries for an answer that cannot change anything.

The reading is announced and recorded rather than enforced here. What a rung means is applied where the work happens - EscalationLadder::pausesRealm() in the capture scope and stopsEverything() on the flush path - and both read the finding this stage leaves behind.

Returns

\Drupal\strata\Budget\BudgetAssessment|null —

The reading, or NULL when no ceiling is set.

repair()

repair() : \Drupal\strata\Health\RepairReport|null

Runs the repair a finding's rung names, if automatic repair is on.

This is the only unattended caller of the ladder. Every rung it can take reconstructs derived state from data that still exists, so the worst case of running one wrongly is wasted work; quarantine and refuse remove a restore target and are refused here by name.

Returns

\Drupal\strata\Health\RepairReport|null —

What was repaired, or NULL when the stage is off.

runDrill()

runDrill() : \Drupal\strata\Drill\DrillReport|null

Runs a restore drill, if one is due.

Returns

\Drupal\strata\Drill\DrillReport|null —

What the drill proved, or NULL when it is off or not due.

setting()

setting(string  $key, mixed  $default) : mixed

One setting, or a default when nothing is configured.

Parameters

string $key

The setting key.

mixed $default

What to use when the setting is absent.

Returns

mixed —

The configured value.

trainDictionaries()

trainDictionaries() : array<string,array{stored: bool, reason: string, ratio: float, source: string}>|null

Trains the dictionaries, if the interval has elapsed.

Returns

array|null —

What each realm did, or NULL when the pass was not due.

stage()

stage(string  $what, callable  $stage) : void

Runs one stage, catching whatever it throws.

Parameters

string $what

What the stage was doing, for the log line.

callable $stage

The stage.

Returns

void —

failed()

failed(string  $what, \Throwable  $error) : void

Records a stage that failed.

Parameters

string $what

What it was doing.

\Throwable $error

What went wrong.

Returns

void —