Optional_OptionalalarmOptionalalarmOptionalauthToday's authenticated spend, or undefined when nothing has been charged in this lifetime.
In memory only, and read back from ctx.storage on each charge rather than trusted across
one: an eviction between two authenticated renders must not reset the day's budget, and the
durable record is the only thing that survives it.
OptionalbumpOptionalbumpsOptionalconsecutiveconsecutive failing batches, for the capped backoff; reset by any batch that progressed
OptionaldoDurable Object invocations since the last flush, counted in memory.
Same shape as rowsSinceFlush and for the same reason: persisting per invocation would cost
a row per invocation, so the meter would inflate the OTHER meter it sits beside. Folded into a
per-UTC-day total on the alarm, where one row is already being written.
what the last heap restore attempt did, or null when it was never attempted
Where a chunked restore has got to, or null when there is nothing in flight.
In memory only, which is the safety argument. The cursor indexes into a LIVE heap, so
its correct lifetime is exactly that heap's lifetime. Persisting it to ctx.storage would let
a cursor survive an eviction that destroyed the heap it describes, and the next firing would
resume at chunk 7 of a heap that is back to zeros -- a restore that reports complete over a
heap that is 6 chunks of nothing. Dying with the isolate makes a resumed-onto-nothing restore
unrepresentable; the cost is repeating at most one object's worth of memcpy after an eviction,
which is idempotent because every chunk writes fixed bytes at fixed offsets.
OptionalhttpOptionallastOptionallastOptionallastOptionallastwall time of the last fill that ALSO booted; kept for diagnostics, never used as an estimate
OptionallastOptionallastOptionallastwhat the last supervised alarm found, for /__health
OptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallastOptionallogsOptionalmailsTrend rings for the three signals a slope is claimed about.
Held here rather than inside src/ops/supervisor.ts, which is not allowed to own state: a
tripwire keeping its own history would keep a poisoned observation alive across the very
recycle the layer exists to survive. In-memory on purpose -- an eviction should forget the
trend rather than resume a stale one.
Optionalmigrateper-path render sizes, previous renders only; the baseline renderSizeAnomaly compares against
per-path serve counts, in memory only; the R2 page mirror publishes the busiest first
OptionalpagesOptionalphpOptionalpinnedStrong references to every vrzno handle value this interpreter has handed PHP.
In memory only: it exists to stop the handle table's WeakRefs dying under
a heap that still holds their integers, and it is meaningless once that heap is gone. See
pinHandles().
OptionalrenderOptionalrowsOptionalrowswrites accumulated since the last alarm folded them into the daily total
Optionalrowsaccumulated across the object's life; undefined until the first statement
OptionalserveOptionalsqlstatement counter for PW_SQL_TRACE, so the tail can be read as a sequence
OptionalstorageOptionalsuppressOptionaltxnOptionaltxnOptionaltxnOptionalwindowOptionalwindowOptionalwindowOptionalwindowsOptionalwriteper-table rows-written tally; only allocated when /__writes turns it on
Alarm handler. Touches the database so the isolate stays resident and the page cache stays populated, then re-arms.
Deliberately cheap: a keep-warm that renders a page would burn CPU on every tick for no user.
Arms the fill alarm without disturbing one that is already sooner.
Extracted because bumpGeneration() is synchronous and cannot await
getAlarm(); overwriting an existing alarm here would push a pending fill
chain out. Fire-and-forget is safe: the worst case is an alarm at +1 ms that
finds nothing to do.
false requeues without waking the chain, leaving the caller to arm it.
A long-running write needs that: armFillAlarm() schedules at +1 ms and does not await
the write, so a fill starts while the caller still has seconds of work in the same event.
Flips one byte of a stored chunk and leaves its digest alone, so the next restore must refuse.
This exists to make the refusal EXECUTABLE IN PRODUCTION rather than only in a test. The
distinction is not pedantic here: LAZY_MOUNT was covered by tests and unreachable in the
deployed worker for its entire life, and the failure this guards against -- a chunk that lands
with the wrong bytes -- produces a heap of the right length that renders something subtly wrong
with no error at all. The only convincing evidence that the guard works is watching a deployed
object refuse a chunk it was actually asked to apply.
Writes the DIGEST-BEARING side of the pair untouched: corrupting both would restore cleanly and prove nothing.
Optionalwhere: stringtoday's DO invocations, without flushing
today's rows written, without flushing -- for a read that must not write
Fetches everything PHP deferred, in JS, where awaiting is legal.
Runs between PHP invocations rather than inside one. Bounded per call because a queue full of slow hosts would otherwise occupy the object indefinitely.
The fetch cache and the deferred queue; durable, so a drain survives eviction.
KEYED BY METHOD+URL+BODY, NOT BY URL. Both tables were url TEXT PRIMARY KEY, which is a live
correctness bug rather than a POST-only gap: two deferred fetches to the same endpoint are one
row, so the second overwrites the first and a caller can be handed a response fetched for
somebody else. For a captcha verification that is one visitor receiving another's verdict.
The key is the exact tuple, LENGTH-PREFIXED, and never a hash. A non-cryptographic hash is
forgeable and this key decides which cached response a request reads; a cryptographic one
cannot be used because crypto.subtle.digest is async and the key is derived inside the
synchronous call PHP makes. See deferredKey.
Builds the interpreter, installs the bridge, mounts the tree. Once.
Lazy rather than in the constructor so boot cost is attributable to a route and measurable on its own. /request-boot already proved a static build can be instantiated inside a request handler (bootMs 36) -- no runtime codegen, so workerd permits it.
What the next render on this instance is expected to cost, in ms.
The budget has to be a prediction, because a render cannot be interrupted
once it starts. php._run() is one synchronous call into wasm: while it
executes nothing else in that thread runs, so no setTimeout, no
AbortSignal and no Promise.race can preempt it. Measured -- a Worker
racing a 1 ms timer against a stub.fetch() that rendered for 119 ms lost
the race, because the timer could not fire until the wasm call returned.
So the decision is taken before the render starts, from what this instance has already observed. The last render is the best predictor available: it was produced by the same kernel state the next one will meet.
Pessimistic before any evidence exists. A Durable Object
hibernates after roughly 10 s of inactivity and discards its in-memory state,
which includes this.php, the mounted tree and the booted kernel -- so a cold
instance is the common case in production, not the exception, and it must not
gamble a visitor's request on a 3,754 ms boot.
Every Drupal statement, plus the automatic invalidation trigger.
Suppressed during /__migrate: replaying the packed site inserts the packed cachetags rows, which is setup rather than a content change.
Coalesced, because one content save invalidates many tags and each tag is its
own merge('cachetags'). Once a bump has happened there is nothing left to
invalidate, so further writes are ignored until a page is cached again --
fillOne() clears the flag. Without this a node save would bump the generation
dozens of times and re-run DELETE on an already-empty table for each.
Optionalparams: SqlBindingsAtomic replay of a buffered Drupal transaction.
Drupal's Connection::beginTransaction()/commit()/rollBack() is a BEGIN-COMMIT api; ctx.storage.transactionSync() is callback-scoped. They do not compose, and issuing BEGIN as SQL throws outright ("please use the state.storage.transaction() ... APIs instead"). So the PHP driver withholds writes and hands the whole list here to be replayed inside one transactionSync.
commit: false is the speculative path: replay, run one read so the caller
can see its own uncommitted write, then abort. Throwing from inside the
callback is what makes transactionSync roll back, so the throw is deliberate
and its message is not an error.
TWO LANES, and which one answers is the throughput story.
The storage lane serves a cached page out of cfw_page with pure
ctx.storage.sql and never enters the gate, so a HIT is answered WHILE a
render is in flight instead of queueing behind it. That matters most for the
thing it was built for: a sliced render holds the PHP lane for its whole sliced
lifetime, and without this split every HIT in that window would wait, putting
the single-object ceiling back through the side door.
Three conditions make it safe, and each one is a real constraint rather than a formality:
ensureServeTables() issues CREATE TABLE IF NOT EXISTS,
and DDL while the PHP lane holds an open transaction replay dirties
sqlite_master and turns every later read in that transaction into a
speculative replay -- the O(W x R) cost that once wedged the local runtime
hard enough to take unrelated sites down with it. So the fast lane
declines until some gated request has created the tables.lane=gate forces the gated path, which is what makes the split testable: the
same race with the fast lane disabled must show the HIT waiting.
Folds this firing's Durable Object invocations into a per-UTC-day total.
Counted because the DO-request quota is one of the two daily ceilings the whole architecture is scored against, and the limits page reported "nothing measures this yet" for it -- an unmeasured meter beside a measured one reads as the healthy one.
IT COUNTS WHAT REACHED THIS OBJECT, which is the honest scope: a request answered by the edge cache never enters the isolate, so this is the DO meter and explicitly not the Worker-request meter. Conflating them would report a confident wrong number for the serving ceiling.
Folds the writes accumulated since the last flush into a per-UTC-day total.
Flushed on the alarm, never per write, and that is a correctness requirement rather than an optimisation: persisting the counter costs a row write, so a per-write flush would DOUBLE the number it is measuring. Once per firing it is one row against a batch of them, and that row is itself counted, so the meter includes its own cost.
Keyed by UTC date because the limit is daily and the object is not. A Durable Object is evicted whenever Cloudflare likes, so an in-memory lifetime counter reports a fraction of the day and reads as healthy; the date key means an eviction loses at most the writes since the last alarm rather than the whole day.
the running total for today, after folding in whatever had accumulated.
The site generation: one integer that every edge cache key carries.
Drupal's cache tags cannot purge a URL-keyed edge cache -- tag purge is an Enterprise feature -- so invalidation is done by making every previously cached URL unreachable instead of by deleting anything. Appending this to the key means one integer write invalidates the whole site, everywhere, for free.
Routing, deliberately OUTSIDE the gate.
A subclass has to be able to fall through to these routes from inside its own gated handler. Calling a gated fetch() from within gate.run() self-deadlocks: the inner run() awaits the release of the outer link, which only resolves once the outer callback returns. ctx.blockConcurrencyWhile() forbids nesting outright for the same reason. So fetch() gates once and every route body lives here.
The emscripten heap, as bytes.
Neither member is typed by php-wasm, and which one exists depends on the build: HEAPU8 is
the view emscripten maintains, and wasmMemory is the fallback for a build that does not
export it. Returns null when neither is present, because a snapshot of the wrong object
would be a plausible-looking heap of the wrong bytes.
Wall-clock budget a MISS may spend rendering before it gives up on the visitor and hands the path to the alarm chain.
2 s covers the whole measured first-render range -- 195 ms on minimal, 909 ms on standard, 1,636 ms observed here on a loaded machine -- and excludes the 3,754 ms cold boot, which cannot be waited out. It bounds the visitor's patience, not a billed resource: wall time is not charged against the CPU budget (4 ms of Worker CPU against 827 ms of wall, measured).
budget on the query string overrides so the fallback is testable, and 0
disables inline rendering entirely, which restores the always-202 shape.
Paid defaults to 10 s rather than 2 s, which only matters once an interpreter exists: a cold
object refuses on !this.php before this number is ever consulted. See bootInline.
Installs the synchronous SQL entry point on the PHP Module so vrzno can reach it, wrapped in the codec.
The codec matters here specifically: node IDs, file sizes and timestamps all cross this boundary, PHP is 32-bit, and a SQLite INTEGER can exceed 2^31. Handing back a raw JS number silently wraps it -- that is the bug class the codec closes, and a database driver is where it would bite hardest.
Both entry points run inside withMask(): they are JS frames under the PHP
stack, and a slice interrupt that fires there cannot suspend (see src/mask.js).
The decode/encode codec calls sit inside the same window, so the codec needs no
mask of its own.
The host half of drupal/drupflare/.
Every one of these is synchronous, which is the design constraint rather than a
shortcut. Host::call() in PHP does $reply = $invoke($json) and
reads the result immediately; PHP cannot await, so a host function that returned
a Promise would hand PHP an object it can only stringify. So the capabilities
that need the network are split in two:
cfwQueueFetch records the request and returns at once (no suspension).cfwHttpCacheGet / cfwFetch answer from what a previous drain already
fetched (no suspension).fetch() happens in drainHttpQueue(), in JS, between PHP runs.That is exactly the cached -> deferred -> sync layering CfwDeferredHttp documents, with the sync tier absent until a JSPI build exists. A miss is reported as a miss rather than faked.
The rolling median body size for one path, over PREVIOUS renders only.
Reading cfw_page instead would compare the render against the row it just wrote -- a ratio
of exactly 1.00 every time, so renderSizeAnomaly could not fire at all. The 90,038-byte
admin page served to an anonymous visitor is the incident that check exists for, and it is
detectable only against the path's own history.
In memory and capped, like pageHits: an eviction should forget the baseline rather than
compare a fresh render against a stale one, and history in SQL would cost a row per render on
the meter that binds.
The /migrate body for the JS engine.
Shaped to answer both plans from one route. all=1 (or a paid plan) replays every
chunk in this invocation, which is what a 30 s CPU budget wants. The default replays
chunksPerInvocation() and arms an alarm to carry the rest, which is what a 10 ms
budget requires -- so a free-plan deploy self-migrates over ~15 alarm firings without
an operator poking the route 15 times.
the migration cursor for diagnostics, or null on a site that never started one
The cursor when a migration is started but unfinished, else null.
Null for a site that never started one, because that is every deploy predating the chunked engine and those must keep serving. "Never started" and "half done" are genuinely different states and conflating them would take the whole existing fleet offline.
Advances the migration by one invocation's worth of chunks, or returns null when there is nothing to do.
Returns null in two distinct cases that must not be conflated: no migration has ever been started (so this object is not mid-flight and an alarm should get on with filling), and the migration is finished. A site with no manifest at all also returns null rather than throwing, because that is the shape of every pre-existing deploy and an alarm that throws stops re-arming.
The JS-side migrator, lazily built.
Lazy because the manifest is an asset fetch and most invocations never migrate; cached on the instance because a warm object re-reading it per chunk would pay 15 pointless subrequests against the 50-per-invocation cap.
The R2 bucket to offload files to, or null when there is none.
NULL IS A SUPPORTED STATE, not a misconfiguration, and that is the whole reason this is a
method rather than a direct this.env.FILES read at the call site. The Durable Object is
where the file durably lives (src/db/file-store.ts stores bytes in DO SQL precisely so an
eviction cannot lose them); R2 is an offload that buys serving-ceiling headroom by answering
requests without a Worker invocation. A site with no bucket bound is fully functional and
simply pays a Worker request per file, which is the free-tier default.
Typed structurally so the drain is drivable over a stand-in -- the cases worth testing are a put that throws and a binding that is absent, and neither needs a real bucket.
Whether this site has never been provisioned, so a page request has nothing to render from.
NO CURSOR AT ALL is a different state from a half-finished one, and it was the state a fresh
deploy sat in forever: migrateStepIfPending() returns null without a cursor, so the alarm
chain never started, and /serve answered warming on every request for the life of the
object. Provisioning happened only if somebody called /migrate by hand -- which is a
DIAGNOSTIC route, so on the canonical config there was no way to do it at all.
records one render's size AFTER the median for it was taken, so a render never sets its own baseline
Date.now() is not available at DO global scope in some contexts and the value must also survive the 32-bit PHP boundary, so it is read here and encoded by the codec on the way out rather than passed raw.
What the object can see about itself at the end of an alarm.
Every field is a scalar the caller already had or a ring capped at 8, so this cannot become a
full-table scan on a path that runs every firing. countOrNull() is the one exception and it
is why semaphoreRows is omitted on an unmigrated site rather than reported as 0: a table
that does not exist is not an invariant that holds.
The pack generation this object migrated from, or null.
A snapshot is only valid for the pack it was booted against: a new pack means a different tree, so a heap restored across that boundary holds interned paths into files that moved. The migrate cursor already records the generation, so there is no second source of truth.
One stored page, as an HTTP response.
x-cfw-generation is on every serve response: it is how the
Worker learns the current generation without spending a Durable Object
request to ask for it.
Keeps every vrzno handle alive for the interpreter's whole life.
MEASURED, and it is why capture alone was not enough. Module.targets.byInteger is php-wasm's
WeakerMap -- a Map of WeakRefs with a FinalizationRegistry -- and its iterator DELETES
any entry whose referent has been collected. So a handle PHP acquired during the kernel boot
and did not call again was gone from the table by snapshot time, while the integer was still
sitting in the heap. On the edge that read as misses: [2] from /heap?op=trace: the restored
heap asked for handle 2 and the table held only handle 1.
A snapshot cannot fix that after the fact, so the pin goes in before any PHP runs. The strong set is per-interpreter and per-object, and a booted kernel mints a handful of handles, so this trades a few retained JS objects for a handle table that still describes the heap.
add is defined non-writable on the index, which is why the whole object is swapped rather
than the method patched; the glue re-reads Module.targets at every call site.
Loads the CI-rendered pages straight into the serving table.
Default on for free, off for paid, overridable both ways. Pre-filling changes what a
MISS means -- a prefilled path is a HIT on its first ever request -- so the switch is
explicit per plan rather than implied. See prefillDefault(). Rendered on native PHP, where
a warm page costs 5.45 ms against 46 ms of edge cpuTime, by
scripts/drupal/prefill-cache.php. An absent prefill.json is normal: a site that skipped
the CI step just starts cold.
Extracted from the /__migrate route, which was a production bug rather than untidiness.
Living in the route handler meant only a request-driven migration ever prefilled. A migration
that completes on the ALARM chain is the default -- migrationSelfDrives() arms it from the
first call -- and is the only path a deployed site takes, so a real deploy finished migrating
with cfw_page empty and answered 503 on its front page until somebody happened to request a
render. Both callers now share this.
the ?prefill= override: '1' forces on, '0' forces off, null defers to the plan
Whether a page request has asked this site to provision itself.
Durable, because the request that asks and the alarm that acts are different invocations and
an eviction sits between them. One row in cfw_meta, the table the serve path already writes.
Writes this site's row into the fleet inventory, when there is anything worth writing.
Silent when no D1 binding exists, which is the free-tier default and not an error: a single site does not need an inventory, and the whole point of the predicate is that the steady state costs one row per site per day.
Records that a visitor wants this site, and wakes the alarm chain to build it.
Applies the next slice of an in-flight restore. Driven by alarm(), never by a request.
The heap is read back out of the LIVE binary each firing rather than held in a field, because
the cursor is only meaningful for the heap that is actually mounted right now -- if the isolate
were replaced between firings, this.php would be null and there would be nothing to resume
onto. That case cannot arise silently: the cursor is in-memory, so it died with the isolate,
and the next boot starts a fresh restore at chunk 0.
runs a PHP fragment and returns everything it wrote
runs a fragment and parses the JSON object it printed
Keeps the interpreter warm.
Boot is per-DO-lifetime, not per-request: 3,754 ms of CPU on the edge, of which roughly 1 s of the residual has no identified lever. So the strategy cannot be "make boot fast", it has to be "boot rarely". An alarm is the only way a DO wakes itself, so it is the cold-start strategy rather than an optimisation.
The interval is a floor, not a guarantee: Cloudflare may still evict, and alarms are best-effort. It reduces cold starts, it does not remove them.
the meta table as a SecretStore, so the mint needs no Durable Object to test against
A cached page, or null to say "not mine".
Synchronous by construction: an async here would introduce the await that
condition 2 forbids. The serve counter is incremented only on the answering
path so a request is still counted exactly once whichever lane takes it.
Whether this alarm firing should spend rows on garbage collection.
Two gates, and both are about the shared meter rather than caution. Page fills and GC both spend rows written (100k/day), so GC never runs while pages are waiting -- a visitor's MISS outranks reclaiming disk. And it is interval-gated because the measured steady state is 0 rows written: running it on every firing would spend statements to discover there is nothing to do.
Writes this object's booted heap into its own SQLite.
Post-boot, pre-render: a rendered heap is a request-contaminated heap, and the uid-1 cache-poisoning bug is what that contamination looks like when it goes wrong.
The fd table travels WITH the bytes. The open descriptor table is the load-bearing part of a restore -- not inode alignment, which was tested and falsified -- so a snapshot whose descriptors were reconstructed from some other instance is not a snapshot.
So does the vrzno handle table: the heap stores Module.targets ids as bare integers, so a
render through a restored heap died with TypeError: target is not a function on a deployed
worker. See HandleRecord.
Counts a failure against the queue head when the fill THREW rather than reported.
The three-strikes rule lives inside fillOne(), which means it only runs when the render
comes back with an error to record. A JS-level throw -- an uncatchable one out of the wasm
import, say -- skips it entirely, and the row it should have struck is the row the alarm
re-arm reads to decide whether to fire again in 1 ms. That combination is a spin, and it was
measured as one.
the attempts now recorded, or null when the queue was empty
Runs the host tripwires and moves the repair ladder.
ON THE ALARM AND NOWHERE ELSE. Two reasons, and neither is style: recordFinding() is a row
write and rows written is the meter that binds the regeneration ceiling, so a per-request
tripwire pass would spend the budget it exists to watch; and a waiting visitor outranks
bookkeeping, which is the same rule that puts GC and cron after the fills.
The state is persisted only when it CHANGES. A healthy site writes zero rows here, so the whole layer is free until something is actually wrong.
Restores a stored heap into this instance, or explains why it refused.
Called AFTER the mount and after the bridge and capabilities are installed, because those populate the JS side that the heap's vrzno handles index into. Restoring before them would leave the interpreter holding indices into an array that does not exist yet.
The fd table is asserted BEFORE the memcpy and the refusal is loud. Dropping
/dev/urandom's descriptor alone throws RandomException; dropping the three sqlite
descriptors gives a locking-protocol error after an 80-120 second stall, which on the edge
is a hung request rather than an error. A refusal costs one boot; proceeding costs a hang.
Whether a database-update run is in progress and owes the alarm chain work.
One indexed read of one row, so an ordinary alarm on a site that has never run updb
pays essentially nothing for this check.
Advances a database-update run by one beat.
updbStep() owns no transport, no alarm and no env, exactly like cronStep(), so the
dependency bag is assembled here. It refuses on a cold interpreter rather than booting
one, because boot is 4,019 ms of indivisible CPU and a beat that starts cold cannot fit
any per-invocation budget.
One site: the interpreter AND the database, in the same isolate.
This is the arrangement the whole project has been pointing at and had never executed. ctx.storage.sql is synchronous only from inside the Durable Object, and PHP's PDO is blocking, so the driver can only work if PHP runs HERE. Until this class existed the PHP half was tested against a PDO stand-in and the JS half against synthetic statements, and the two had never met.