Skip to content

Commit 8fd3a40

Browse files
committed
Skip disabled cron registration in Cloudflare
1 parent 6d95c3b commit 8fd3a40

4 files changed

Lines changed: 45 additions & 6 deletions

File tree

packages/runtime-cloudflare/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ The bundled MDI source is pinned to immutable commit `1870fb41279e7eb5946e506c9c
1818

1919
The WordPress server corpus is a separate deployment artifact, never a Worker-module import. `generate:cloudflare-wordpress-runtime-corpus` uses bounded ZIP Range decoding against the pinned WordPress release, retains only `isWordPressRuntimeFile()` entries, and emits a deterministic ZIP under `artifacts/` plus the checked-in `assets/wordpress-runtime-artifact.json` provenance manifest. The immutable R2 key is derived from the corpus ZIP SHA-256. Cold boot fetches that exact object through `WORDPRESS_STATE_BUCKET`, validates the manifest and archive/file hashes and budgets, then streams it into PHP MEMFS. Static browser assets continue to use lazy exact archive ranges and Worker cache.
2020

21+
Canonical Cloudflare boots materialize a named mu-plugin that removes only core's `wp_cron` `init` callback when the runtime policy defines `DISABLE_WP_CRON` as true. Cron spawning is explicitly disabled by this runtime policy because the PHP-WASM/MDI shutdown callback path is not supported. Update and privacy scheduling callbacks remain registered; an explicit durable cron execution contract is separate future scope.
22+
2123
## MDI Init Diagnostics
2224

2325
Every `mdi-*` lifecycle diagnostic materializes the validated packaged canonical MDI seed and the separately managed R2 WordPress runtime artifact in a disposable PHP-WASM runtime. They never acquire a coordinator lease, read or write a canonical R2 revision, or promote state. The old three-option fixture was intentionally removed because it does not represent complete MDI WordPress state.
2426

2527
`?phase=canonical-wordpress` performs a full normal WordPress boot with that exact seed and artifact outside the coordinator, then returns WordPress version, memory, and non-sensitive table counts. Its widget-state count includes the `widget_*` options and `sidebars_widgets`. `?phase=canonical-bootstrap-setup` additionally runs the normal bootstrap setup with a probe-only internal password and fixed probe origin, flushes only the disposable runtime, and returns changed-path counts. Neither probe returns password, option, user, or content values.
2628

27-
The diagnostic-only `?phase=` probes `mdi-init-callbacks`, `mdi-init-exclude-scheduling`, `mdi-init-exclude-block-registration`, `mdi-init-exclude-theme-patterns-styles`, `mdi-init-exclude-widgets`, `mdi-init-exclude-rest-connectors-sitemaps`, and `mdi-init-exclude-initial-content-types` stop at `init`. The inventory probe returns sorted callback identifiers by priority. Each exclusion probe removes only its fixed callback group, runs `init` once, and returns before subsequent WordPress lifecycle work.
29+
The diagnostic-only `?phase=` probes `mdi-init-callbacks`, `mdi-init-exclude-scheduling`, `mdi-init-exclude-wp-cron`, `mdi-init-exclude-privacy-schedule`, `mdi-init-exclude-update-schedule`, `mdi-init-exclude-wp-cron-privacy-schedule`, `mdi-init-exclude-wp-cron-update-schedule`, `mdi-init-exclude-privacy-update-schedule`, `mdi-init-exclude-block-registration`, `mdi-init-exclude-theme-patterns-styles`, `mdi-init-exclude-widgets`, `mdi-init-exclude-rest-connectors-sitemaps`, and `mdi-init-exclude-initial-content-types` stop at `init`. The inventory probe returns sorted callback identifiers by priority. Each exclusion probe removes only its fixed callback group through WordPress's public `remove_action()` API, runs `init` once, and returns the exact removed names, completion, and memory measurements before subsequent WordPress lifecycle work.
2830

2931
The fixed widget probes split `wp_widgets_init` without changing its normal path: `mdi-widgets-callbacks` returns the sorted nested `widgets_init` inventory before `init`; `mdi-widgets-constructors` runs only the core default widget registrations and reports widget class count; `mdi-widgets-hooks` adds the nested action; `mdi-widgets-factory` runs only `WP_Widget_Factory::_register_widgets`; and `mdi-widgets-remaining-hooks` runs the remaining nested callbacks. The bounded direct-registration phases are `mdi-widgets-direct-basic-classic-first`, `mdi-widgets-direct-basic-classic-second`, `mdi-widgets-direct-media`, `mdi-widgets-direct-custom-html-block`, `mdi-widgets-direct-block`, and `mdi-widgets-direct-custom-html`. They remove every normal nested callback, then call `_register()` only on their fixed allowlisted factory objects and return attempted classes, count, completion, and memory. `mdi-widgets-option-reads` reports only fixed widget option names, grouped by `get_option()` presence and direct `$wpdb` SQLite-row presence; it returns no option values. `mdi-widgets-get-settings` calls only `get_settings()` on each default factory object, with `mdi-widgets-get-settings-first` and `mdi-widgets-get-settings-second` as fixed halves; each reports completed classes and array-shape dimensions only. `mdi-widgets-register-one` preloads the fixed `widget_text` option, calls that object's `get_settings()`, then calls only `_register_one()` for its fixed instance. Every phase uses fixed source identities and returns before later lifecycle work; none accepts callback names, removal rules, widget classes, or option names from request input.
3032

packages/runtime-cloudflare/src/worker.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,13 @@ async function discardRuntime(runtime: Runtime): Promise<void> {
265265
}
266266

267267
async function bootRuntime(bucket: R2Bucket, pointer: MarkdownPointer, origin: string, authConstants: Record<WordPressAuthConstant, string>): Promise<Runtime> {
268-
return { ...await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await readMarkdownRevision(bucket, pointer), new Uint8Array(markdownPrimaryBootstrapIndex), origin, authConstants, bucket), pointer }
268+
return { ...await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await readMarkdownRevision(bucket, pointer), new Uint8Array(markdownPrimaryBootstrapIndex), origin, authConstants, bucket, true), pointer }
269269
}
270270

271271
async function bootstrapCanonicalRuntime(env: Env, coordinator: DurableObjectStub, requestUrl: string, lease: Lease): Promise<Runtime> {
272272
if (!env.WORDPRESS_ADMIN_PASSWORD) throw new Error("WORDPRESS_ADMIN_PASSWORD is required to bootstrap a complete canonical WordPress revision.")
273273
const origin = new URL(requestUrl).origin
274-
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await packagedCanonicalMarkdownSeed(), new Uint8Array(markdownPrimaryBootstrapIndex), origin, await canonicalWordPressAuthConstants(env), env.WORDPRESS_STATE_BUCKET)
274+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await packagedCanonicalMarkdownSeed(), new Uint8Array(markdownPrimaryBootstrapIndex), origin, await canonicalWordPressAuthConstants(env), env.WORDPRESS_STATE_BUCKET, true)
275275
try {
276276
const passwordFile = "/tmp/wordpress-admin-password"
277277
runtime.php.writeFile(passwordFile, new TextEncoder().encode(env.WORDPRESS_ADMIN_PASSWORD))
@@ -568,7 +568,7 @@ async function runBootProbe(phase: string, bucket: R2Bucket): Promise<Response>
568568

569569
if (phase === "canonical-wordpress" || phase === "canonical-bootstrap-setup") {
570570
const canonicalSeed = await packagedCanonicalMarkdownSeed()
571-
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, canonicalSeed, new Uint8Array(markdownPrimaryBootstrapIndex), "https://canonical-probe.invalid", {}, bucket)
571+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, canonicalSeed, new Uint8Array(markdownPrimaryBootstrapIndex), "https://canonical-probe.invalid", {}, bucket, true)
572572
try {
573573
if (phase === "canonical-bootstrap-setup") {
574574
const passwordFile = "/tmp/wp-codebox-canonical-bootstrap-probe-password"
@@ -588,6 +588,9 @@ echo json_encode([
588588
'optionCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->options}"),
589589
'widgetOptionCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->options} WHERE option_name LIKE 'widget\\_%'"),
590590
'widgetStateOptionCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->options} WHERE option_name LIKE 'widget\\_%' OR option_name = 'sidebars_widgets'"),
591+
'wpCronInitAttached' => false !== has_action('init', 'wp_cron'),
592+
'updateScheduleInitAttached' => false !== has_action('init', 'wp_schedule_update_checks'),
593+
'privacyScheduleInitAttached' => false !== has_action('init', 'wp_schedule_delete_old_privacy_export_files'),
591594
'memoryBytes' => memory_get_usage(true),
592595
'peakMemoryBytes' => memory_get_peak_usage(true),
593596
]);` })).text.trim()
@@ -1000,6 +1003,7 @@ async function bootWordPressRuntime(
10001003
siteUrl = SITE_URL,
10011004
authConstants: Partial<Record<WordPressAuthConstant, string>> = {},
10021005
runtimeBucket?: R2Bucket,
1006+
canonicalCronAdapter = false,
10031007
): Promise<{ php: PHP; requestHandler: PHPRequestHandler; wordpressVersion: string }> {
10041008
const requestHandler = await bootWordPressAndRequestHandler({
10051009
createPhpRuntime,
@@ -1031,6 +1035,7 @@ async function bootWordPressRuntime(
10311035
materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownFiles)
10321036
if (markdownIndexSeed) php.writeFile(MARKDOWN_RESOLVED_INDEX_PATH, markdownIndexSeed)
10331037
}
1038+
if (canonicalCronAdapter) materializeCanonicalCronAdapter(php)
10341039
} : undefined,
10351040
beforeDatabaseSetup: databaseSeed ? (php: PHP) => {
10361041
php.mkdir("/wordpress/wp-content/database")
@@ -1058,6 +1063,20 @@ function materializeRuntimeFiles(php: PHP, root: string, files: RuntimeFile[]):
10581063
}
10591064
}
10601065

1066+
function materializeCanonicalCronAdapter(php: PHP): void {
1067+
const path = "/wordpress/wp-content/mu-plugins/wp-codebox-canonical-cron-policy.php"
1068+
php.mkdir(path.slice(0, path.lastIndexOf("/")))
1069+
php.writeFile(path, new TextEncoder().encode(`<?php
1070+
/**
1071+
* Plugin Name: WP Codebox Canonical Cron Policy
1072+
*/
1073+
1074+
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
1075+
remove_action( 'init', 'wp_cron' );
1076+
}
1077+
`))
1078+
}
1079+
10611080
function collectRuntimeFiles(php: PHP, root: string): RuntimeFile[] {
10621081
const files: RuntimeFile[] = []
10631082
const visit = (directory: string): void => {

scripts/cloudflare-local-gate.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,14 @@ async function assertCanonicalProbes() {
172172
const wordpress = await (await request(`${origin}/?phase=canonical-wordpress`)).json()
173173
const setup = await (await request(`${origin}/?phase=canonical-bootstrap-setup`)).json()
174174
const counts = ["canonicalSeedFiles", "postCount", "pageCount", "userCount", "optionCount", "widgetOptionCount", "widgetStateOptionCount", "memoryBytes", "peakMemoryBytes"]
175-
if (wordpress.completed !== true || typeof wordpress.evidence?.wordpressVersion !== "string" || counts.some((key) => !Number.isInteger(wordpress.evidence?.[key])) || wordpress.evidence.widgetStateOptionCount !== 19) {
175+
if (wordpress.completed !== true || typeof wordpress.evidence?.wordpressVersion !== "string" || counts.some((key) => !Number.isInteger(wordpress.evidence?.[key])) || wordpress.evidence.widgetStateOptionCount !== 19 || wordpress.evidence.wpCronInitAttached !== false || wordpress.evidence.updateScheduleInitAttached !== true || wordpress.evidence.privacyScheduleInitAttached !== true) {
176176
throw new Error(`Canonical WordPress probe did not boot the complete seed: ${JSON.stringify(wordpress)}`)
177177
}
178178
const changedCounts = ["createdPathCount", "changedPathCount", "deletedPathCount"]
179179
if (setup.completed !== true || typeof setup.evidence?.wordpressVersion !== "string" || changedCounts.some((key) => !Number.isInteger(setup.evidence?.[key])) || setup.evidence.changedPathCount < 1) {
180180
throw new Error(`Canonical bootstrap setup probe did not produce ephemeral changes: ${JSON.stringify(setup)}`)
181181
}
182-
console.log(`Canonical probes: WordPress ${wordpress.evidence.wordpressVersion}, ${wordpress.evidence.widgetOptionCount} widget_* options plus sidebars_widgets, ${setup.evidence.changedPathCount} changed ephemeral paths.`)
182+
console.log(`Canonical probes: WordPress ${wordpress.evidence.wordpressVersion}, cron init disabled while update/privacy scheduling remain registered, ${wordpress.evidence.widgetOptionCount} widget_* options plus sidebars_widgets, ${setup.evidence.changedPathCount} changed ephemeral paths.`)
183183
}
184184

185185
async function assertMdiDiagnostics() {

tests/cloudflare-runtime.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,24 @@ test("Cloudflare MDI lifecycle diagnostics use the complete packaged seed withou
313313
assert.match(worker, /function canonicalChangedPathCounts/)
314314
})
315315

316+
test("Cloudflare canonical runtime materializes a conditional cron policy without removing other scheduling callbacks", async () => {
317+
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
318+
const adapter = worker.match(/function materializeCanonicalCronAdapter\(php: PHP\): void \{[\s\S]*?\n\}/)?.[0]
319+
320+
assert.ok(adapter, "The canonical cron policy materializer is present.")
321+
assert.match(adapter, /wp-content\/mu-plugins\/wp-codebox-canonical-cron-policy\.php/)
322+
assert.match(adapter, /defined\( 'DISABLE_WP_CRON' \) && DISABLE_WP_CRON/)
323+
assert.match(adapter, /remove_action\( 'init', 'wp_cron' \)/)
324+
assert.doesNotMatch(adapter, /wp_schedule_update_checks|wp_schedule_delete_old_privacy_export_files|\$GLOBALS\['wp_filter'\]/)
325+
assert.match(worker, /runtimeBucket\?: R2Bucket,\n canonicalCronAdapter = false,/)
326+
assert.match(worker, /authConstants, bucket, true\), pointer/)
327+
assert.match(worker, /env\.WORDPRESS_STATE_BUCKET, true\)/)
328+
assert.match(worker, /canonical-probe\.invalid", \{\}, bucket, true\)/)
329+
assert.match(worker, /'wpCronInitAttached' => false !== has_action\('init', 'wp_cron'\)/)
330+
assert.match(worker, /'updateScheduleInitAttached' => false !== has_action\('init', 'wp_schedule_update_checks'\)/)
331+
assert.match(worker, /'privacyScheduleInitAttached' => false !== has_action\('init', 'wp_schedule_delete_old_privacy_export_files'\)/)
332+
})
333+
316334
test("Cloudflare keeps PHP-WASM in the entry Worker and uses the Durable Object only for leases", async () => {
317335
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
318336
const coordinator = await readFile(new URL("../packages/runtime-cloudflare/src/state-coordinator.ts", import.meta.url), "utf8")

0 commit comments

Comments
 (0)