Skip to content

Commit a900ae5

Browse files
committed
Use MDI constrained runtime for R2 revisions
1 parent 97ba1f7 commit a900ae5

5 files changed

Lines changed: 104 additions & 5 deletions

File tree

packages/runtime-cloudflare/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
This additive integration is the first acceptance gate for [wp-codebox#1838](https://github.com/Automattic/wp-codebox/issues/1838). It compiles the current PHP 8.5 Asyncify Wasm asset for workerd, boots WordPress through Playground with the current SQLite integration release, executes PHP, and returns a Codebox runtime-command-result envelope containing the stable `wp-codebox/cloudflare-runtime-health/v1` health payload.
44

5-
The Worker owns Cloudflare transport and isolate lifecycle. Full WordPress initialization requires the paid-plan five-minute CPU allowance, declared as `limits.cpu_ms: 300000`; Cloudflare's fixed 128 MB isolate memory limit remains unchanged. This gate uses one Playground PHP instance per isolate; Durable Objects will serialize site mutation in a later gate. The response identifies the generic `wordpress-playground` backend and `wordpress` environment; callers do not receive Cloudflare binding or limit details. Runtime snapshots, restore, R2, serialization, and cold restoration are deferred to later gates.
5+
The Worker owns Cloudflare transport, PHP-WASM execution, and the caller-owned disposable SQLite cache. [MDI PR #126](https://github.com/Automattic/markdown-database-integration/pull/126), pinned at `94b9f875ffb8402d5e8eb726893a12324e20f45c`, supplies the constrained public primary runtime: normal MDI driver SQL writes are explicitly flushed to deterministic relative Markdown/JSON paths. R2 stores immutable canonical revisions and the current pointer; the Durable Object owns only the persisted lease, base-revision validation, and CAS pointer promotion. SQLite is never uploaded. This preserves cold reconstruction from canonical R2 files and concurrent mutation serialization without expanding MDI's storage-only boundary.
6+
7+
Existing evidence covers full WordPress initialization and canonical R2 revision behavior. This update changes the source relationship from ad hoc writes to MDI's public constrained runtime, adds source-level bundle and mutation guards, and supports local packaging verification. It does not claim a new remote deployment.
68

79
## Verification
810

Binary file not shown.

packages/runtime-cloudflare/src/worker.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import wordpressInstallSeed from "../assets/wordpress-install-seed.sqlite"
1313
const PHP_VERSION = "8.5.8"
1414
const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip"
1515
const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip"
16-
const MARKDOWN_DATABASE_INTEGRATION_REVISION = "af451895a9429895e3ad4d9b4e073bfd88873745"
16+
const MARKDOWN_DATABASE_INTEGRATION_REVISION = "94b9f875ffb8402d5e8eb726893a12324e20f45c"
1717
const SITE_URL = "https://wp-codebox-runtime.invalid"
1818
const DATABASE_PATH = "/wordpress/wp-content/database/.ht.sqlite"
1919
const MARKDOWN_ROOT = "/wordpress/wp-content/markdown"
@@ -23,6 +23,43 @@ const R2_MARKDOWN_POINTER_KEY = "sites/default/markdown/current.json"
2323
const R2_MARKDOWN_REVISION_PREFIX = "sites/default/markdown/revisions"
2424
const R2_MARKDOWN_OBJECT_PREFIX = "sites/default/markdown/objects"
2525
const MUTATION_LEASE_VERSION = "mdi-canonical-v1"
26+
const SERIALIZED_MARKDOWN_MUTATION_CODE = `<?php
27+
define('SHORTINIT', true);
28+
require '/wordpress/wp-load.php';
29+
require_once '/wordpress/wp-content/plugins/markdown-database-integration/inc/class-wp-markdown-primary-storage-runtime.php';
30+
if (!isset($GLOBALS['@pdo']) || !($GLOBALS['@pdo'] instanceof PDO)) {
31+
throw new Exception('MDI disposable index connection is unavailable.');
32+
}
33+
$prefix = $wpdb->prefix;
34+
$connection = new WP_SQLite_Connection(['pdo' => $GLOBALS['@pdo'], 'path' => FQDB]);
35+
$runtime = WP_Markdown_Primary_Storage_Runtime::bootstrap(
36+
['content_root' => MARKDOWN_DB_CONTENT_DIR, 'state_root' => MARKDOWN_DB_STATE_DIR],
37+
$connection,
38+
defined('DB_NAME') && '' !== DB_NAME ? DB_NAME : 'database_name_here',
39+
null,
40+
true,
41+
array_filter(array_map('trim', explode(',', MARKDOWN_DB_EXCLUDED_TYPES))),
42+
$prefix
43+
);
44+
$driver = $runtime->get_driver();
45+
$option_rows = $driver->query("SELECT option_id, option_value FROM \`{\$prefix}options\` WHERE option_name = 'wp_codebox_mdi_revision'");
46+
$current = empty($option_rows) ? 0 : (int) $option_rows[0]->option_value;
47+
$previous_rows = 0 === $current ? [] : $driver->query("SELECT ID FROM \`{\$prefix}posts\` WHERE post_name = 'cloudflare-r2-proof-$current'");
48+
$value = $current + 1;
49+
$post_id_rows = $driver->query("SELECT COALESCE(MAX(ID), 0) + 1 AS post_id FROM \`{\$prefix}posts\`");
50+
$post_id = (int) $post_id_rows[0]->post_id;
51+
$now = gmdate('Y-m-d H:i:s');
52+
$slug = 'cloudflare-r2-proof-' . $value;
53+
$title = 'Cloudflare R2 Proof ' . $value;
54+
$content = 'Persisted by MDI primary mode in R2 revision ' . $value . '.';
55+
$driver->query("INSERT INTO \`{\$prefix}posts\` (ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count) VALUES ($post_id, 0, '$now', '$now', '$content', '$title', '', 'publish', 'closed', 'closed', '', '$slug', '', '', '$now', '$now', '', 0, '', 0, 'post', '', 0)");
56+
if (empty($option_rows)) {
57+
$driver->query("INSERT INTO \`{\$prefix}options\` (option_name, option_value, autoload) VALUES ('wp_codebox_mdi_revision', '$value', 'off')");
58+
} else {
59+
$driver->query("UPDATE \`{\$prefix}options\` SET option_value = '$value', autoload = 'off' WHERE option_name = 'wp_codebox_mdi_revision'");
60+
}
61+
$changes = $runtime->flush();
62+
echo json_encode(['revisionValue' => $value, 'previousPostFound' => !empty($previous_rows), 'postId' => $post_id, 'wordpressVersion' => $wp_version, 'canonicalChanges' => $changes]);`
2663
let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined
2764

2865
interface Env {
@@ -182,12 +219,13 @@ async function runSerializedMarkdownMutation(env: Env, coordinator: DurableObjec
182219
new Uint8Array(markdownPrimaryBootstrapIndex),
183220
)
184221
let canonicalFiles: RuntimeFile[]
185-
let mutation: { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string }
222+
let mutation: { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string; canonicalChanges: MarkdownChanges }
186223
try {
187224
const mutationOutput = (await runtime.php.run({
188-
code: "<?php define('SHORTINIT', true); require '/wordpress/wp-load.php'; $current = (int) $wpdb->get_var($wpdb->prepare(\"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s\", 'wp_codebox_mdi_revision')); $previous_found = 0 === $current || null !== $wpdb->get_var($wpdb->prepare(\"SELECT ID FROM {$wpdb->posts} WHERE post_name = %s\", 'cloudflare-r2-proof-' . $current)); $value = $current + 1; $post_id = (int) $wpdb->get_var(\"SELECT COALESCE(MAX(ID), 0) + 1 FROM {$wpdb->posts}\"); $now = gmdate('Y-m-d H:i:s'); $post = (object) ['ID' => $post_id, 'post_author' => 0, 'post_date' => $now, 'post_date_gmt' => $now, 'post_content' => 'Persisted by MDI primary mode in R2 revision ' . $value . '.', 'post_title' => 'Cloudflare R2 Proof ' . $value, 'post_excerpt' => '', 'post_status' => 'publish', 'comment_status' => 'closed', 'ping_status' => 'closed', 'post_password' => '', 'post_name' => 'cloudflare-r2-proof-' . $value, 'to_ping' => '', 'pinged' => '', 'post_modified' => $now, 'post_modified_gmt' => $now, 'post_content_filtered' => '', 'post_parent' => 0, 'guid' => '', 'menu_order' => 0, 'post_type' => 'post', 'post_mime_type' => '', 'comment_count' => 0]; $excluded = array_filter(array_map('trim', explode(',', MARKDOWN_DB_EXCLUDED_TYPES))); $storage = new WP_Markdown_Storage(MARKDOWN_DB_CONTENT_DIR, $excluded); $path = $storage->write_post($post); if (false === $path) { throw new Exception('Canonical Markdown post write failed.'); } $options_dir = MARKDOWN_DB_STATE_DIR . '/_options'; if (!is_dir($options_dir)) { mkdir($options_dir, 0755, true); } $option = ['option_id' => 1000, 'option_name' => 'wp_codebox_mdi_revision', 'option_value' => (string) $value, 'autoload' => 'off']; file_put_contents($options_dir . '/wp_codebox_mdi_revision.json', json_encode($option, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); echo json_encode(['revisionValue' => $value, 'previousPostFound' => $previous_found, 'postId' => $post_id, 'wordpressVersion' => $wp_version]);",
225+
code: SERIALIZED_MARKDOWN_MUTATION_CODE,
189226
})).text.trim()
190227
mutation = JSON.parse(mutationOutput) as typeof mutation
228+
validateMarkdownChanges(mutation.canonicalChanges)
191229
canonicalFiles = collectRuntimeFiles(runtime.php, MARKDOWN_ROOT)
192230
} finally {
193231
runtime.php.exit()
@@ -217,6 +255,27 @@ async function runSerializedMarkdownMutation(env: Env, coordinator: DurableObjec
217255
}
218256
}
219257

258+
interface MarkdownChanges {
259+
created: string[]
260+
changed: string[]
261+
deleted: string[]
262+
}
263+
264+
function validateMarkdownChanges(changes: MarkdownChanges): void {
265+
for (const group of [changes.created, changes.changed, changes.deleted]) {
266+
if (!Array.isArray(group) || group.some((path) => !isCanonicalRelativePath(path))) {
267+
throw new Error("MDI flush returned an invalid canonical path.")
268+
}
269+
if (group.some((path, index) => index > 0 && group[index - 1] >= path)) {
270+
throw new Error("MDI flush returned non-deterministic canonical paths.")
271+
}
272+
}
273+
}
274+
275+
function isCanonicalRelativePath(path: string): boolean {
276+
return path.length > 0 && !path.startsWith("/") && !path.split("/").includes("..")
277+
}
278+
220279
async function acquireMutationLease(coordinator: DurableObjectStub): Promise<AcquiredLease> {
221280
for (let attempt = 0; attempt < 300; attempt++) {
222281
const response = await coordinator.fetch(new Request("https://coordinator/begin", { method: "POST" }))

scripts/build-cloudflare-mdi-runtime-bundle.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { writeFile } from "node:fs/promises"
22
import { decodeZip, encodeZip } from "@php-wasm/stream-compression"
33

4-
const revision = "af451895a9429895e3ad4d9b4e073bfd88873745"
4+
const revision = "94b9f875ffb8402d5e8eb726893a12324e20f45c"
55
const archiveUrl = `https://codeload.github.com/Automattic/markdown-database-integration/zip/${revision}`
66
const output = new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url)
77
const response = await fetch(archiveUrl)
@@ -13,6 +13,7 @@ const runtimePaths = new Set([
1313
"inc/class-wp-markdown-driver.php",
1414
"inc/class-wp-markdown-frontmatter-profiles.php",
1515
"inc/class-wp-markdown-loader.php",
16+
"inc/class-wp-markdown-primary-storage-runtime.php",
1617
"inc/class-wp-markdown-search.php",
1718
"inc/class-wp-markdown-storage.php",
1819
"inc/class-wp-markdown-write-engine.php",
@@ -26,6 +27,7 @@ for await (const entry of decodeZip(response.body)) {
2627
}
2728
if (runtimeFiles.length !== runtimePaths.size) throw new Error(`Expected ${runtimePaths.size} MDI runtime files, received ${runtimeFiles.length}.`)
2829

30+
runtimeFiles.sort((left, right) => left.name.localeCompare(right.name))
2931
const archive = await new Response(encodeZip(runtimeFiles)).arrayBuffer()
3032
await writeFile(output, new Uint8Array(archive))
3133
console.log(`Bundled ${runtimeFiles.length} MDI runtime files from ${revision} (${archive.byteLength} bytes).`)

tests/cloudflare-runtime.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from "node:assert/strict"
22
import { readFile } from "node:fs/promises"
33
import test from "node:test"
4+
import { decodeZip } from "@php-wasm/stream-compression"
45
import { RUNTIME_COMMAND_RESULT_SCHEMA } from "../packages/runtime-core/src/runtime-contracts.js"
56
import { CLOUDFLARE_RUNTIME_HEALTH_MARKER, CLOUDFLARE_RUNTIME_HEALTH_SCHEMA, cloudflareRuntimeHealthResponse } from "../packages/runtime-cloudflare/src/health-envelope.js"
67

@@ -49,3 +50,38 @@ test("Cloudflare runtime packages the disposable WordPress install seed", async
4950
assert.deepEqual(config.durable_objects?.bindings, [{ name: "WORDPRESS_STATE", class_name: "WordPressStateCoordinator" }])
5051
assert.ok(config.migrations?.some((migration) => migration.new_sqlite_classes?.includes("WordPressStateCoordinator")))
5152
})
53+
54+
test("Cloudflare runtime pins and bundles the public constrained MDI runtime", async () => {
55+
const revision = "94b9f875ffb8402d5e8eb726893a12324e20f45c"
56+
const generator = await readFile(new URL("../scripts/build-cloudflare-mdi-runtime-bundle.mjs", import.meta.url), "utf8")
57+
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
58+
const runtime = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url))
59+
const names: string[] = []
60+
for await (const entry of decodeZip(new Blob([runtime]).stream())) names.push(entry.name)
61+
62+
assert.match(generator, new RegExp(`const revision = "${revision}"`))
63+
assert.match(worker, new RegExp(`MARKDOWN_DATABASE_INTEGRATION_REVISION = "${revision}"`))
64+
assert.deepEqual(names.sort(), [
65+
"db.php",
66+
"inc/class-wp-markdown-db.php",
67+
"inc/class-wp-markdown-driver.php",
68+
"inc/class-wp-markdown-frontmatter-profiles.php",
69+
"inc/class-wp-markdown-loader.php",
70+
"inc/class-wp-markdown-primary-storage-runtime.php",
71+
"inc/class-wp-markdown-search.php",
72+
"inc/class-wp-markdown-storage.php",
73+
"inc/class-wp-markdown-write-engine.php",
74+
])
75+
})
76+
77+
test("serialized Cloudflare mutations use the public MDI runtime and its flush paths", async () => {
78+
const source = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
79+
const mutation = source.slice(source.indexOf("const SERIALIZED_MARKDOWN_MUTATION_CODE"), source.indexOf("let bootPromise"))
80+
81+
assert.match(mutation, /WP_Markdown_Primary_Storage_Runtime::bootstrap/)
82+
assert.match(mutation, /new WP_SQLite_Connection\(\['pdo' => \$GLOBALS\['@pdo'\], 'path' => FQDB\]\)/)
83+
assert.match(mutation, /\$runtime->get_driver\(\)/)
84+
assert.match(mutation, /\$runtime->flush\(\)/)
85+
assert.doesNotMatch(mutation, /\$wpdb->dbh|\{\$\{prefix\}|WP_Markdown_Storage|write_post|file_put_contents|wp_codebox_mdi_revision\.json/)
86+
assert.match(source, /validateMarkdownChanges\(mutation\.canonicalChanges\)/)
87+
})

0 commit comments

Comments
 (0)