Skip to content

proto: Drupal freeunit module (unverified) - #40

Draft
andypost wants to merge 15 commits into
freeunit/integrationfrom
proto/drupal-module
Draft

andypost wants to merge 15 commits into
freeunit/integrationfrom
proto/drupal-module

Conversation

@andypost

Copy link
Copy Markdown
Owner

Proposed changes

A prototype of the Drupal module described in docs/drupal/freeunit-module.md.

Features.

  • Static page cache for anonymous visitors: pages are served by the router, and entries are invalidated by cache tags.
  • Cron via FreeUnit schedules, with a control API client.
  • Restart after a deploy.
  • Drush commands.

What is verified:

  • all PHP files pass php -l;
  • the control API client, the schedule generator, the path mapper and the file writer ran against a live unitd;
  • the route snippet passes 12 behaviour checks.

Not verified: Drupal itself, because drupal.org and packagist are blocked in the build environment. The event subscriber, the invalidator, the hooks and the Drush commands have never run.

FreeUnit gaps found (F1–F7):

  • F1: traceparent should carry FreeUnit's span as parent. Now fixed in the integration branch.
  • F2: no X-Accel-Redirect-style offload.
  • F3: no read-only status access.
  • F4: no precompressed static variants.
  • F5: routes cannot express "header absent".
  • F7: no 103 Early Hints.

Follow-up: a Fable review of this module is in progress; its fixes will land on this branch.

Checklist

  • I have read CONTRIBUTING.md
  • If applicable, I have added tests
  • If applicable, I have updated documentation

🤖 Generated with Claude Code

https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA


Generated by Claude Code

See docs/drupal/freeunit-module.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
@andypost andypost mentioned this pull request Sep 24, 2026
7 tasks
…purge

Review of the prototype (REVIEW.md, round 1: 1 blocking, 6 major, 12
minor) and the fixes:

- the writer now requires byte-exact values for the headers the route
  reproduces (static_cache.headers), a public Cache-Control, and writes a
  page_cache HIT when its file is missing;
- hook_runtime_requirements + RequirementSeverity (11.2+, kept in 12),
  core_version_requirement ^11.2 || ^12, hook_uninstall purges;
- purge deletes the tree's contents instead of renaming the root;
- the route generator reads hosts, scheme and headers from config, adds
  an Authorization bypass, drops "types"; the file tree is keyed on the
  configured scheme on both sides;
- example: /index.php passes to the application instead of the docroot
  share; the check script covers Authorization, /index.php, traversal
  and the gzip variant's headers (all pass against a local unitd).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…osts guard

Round 2 of REVIEW.md (0 blocking, 0 major, 5 minor):

- drop the unused "freeunit:all" pseudo-tag row per file;
- let the writer drop Content-Length (the router computes its own);
- refuse to generate a route for an empty static_cache.hosts;
- document that the cached hosts must be served on one scheme.

Verified against a local unitd with a stub harness: mapper (traversal,
query, host and length refusals), atomic file writes and purge, the
generated route equals examples/unit-drupal-freeunit.json, the control
client PUTs it (parent creation, 404 -> NULL, errors -> exception), and
the applied route serves the files and honours every bypass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…rification

Round 3 of REVIEW.md (0 blocking, 0 major, 1 minor): keep one index row
per file so a page without cache tags still expires, and record the
verification results (php -l, PHPStan without Drupal symbols, the check
script and the stub harness against a local unitd).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
Unit tests (path mapper, header rules, route = example), kernel tests
(writer, tag index, invalidation, expiry, purge, uninstall, runtime
requirements, cron controller) and a BrowserTestBase test (anonymous page
to cache file with the exact route headers). phpcs and phpstan configs.

Caught and fixed:
- Drupal sends 'Content-Type: text/html; charset=utf-8' (HtmlRenderer),
  not UTF-8: with the old default no page was ever written.
- A header the route adds but the page lacked (Expires, Vary) did not
  stop the write; the header set must now match exactly.
- Uninstall: the table is dropped while the invalidator is still in the
  container, and saving core.extension invalidates tags, so the uninstall
  failed with 'no such table'. hook_uninstall now stops the invalidator.
- invalidateTags(): void; the CacheableResponseInterface check moved to
  where the type is used (phpstan).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…reeUnit

phpcs, phpstan, PHPUnit unit/kernel, functional tests served by a
FreeUnit built from this branch, the example route config on a live site,
check-static-cache.py, and hey metrics against page_cache and
nginx + php-fpm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…test classes on 12.x

CI caught: hook_cron expires against the request time, so a file
recorded with time()-1 on a slow runner is not expired yet (the code is
right, the test was not). On Drupal 12 phpstan-drupal does not register
Drupal\Tests, so the tests' base classes were unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…n unprivileged unitd

CI caught: an unprivileged unitd refuses any application "user", even
its own, so check-static-cache.py failed its PUT; 12.x dists of
drupal/core leave out core/tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…creates its node type

CI caught: committing by letting the Transaction go out of scope is
deprecated in 11.5 (Drupal 12 reports it); PHPUnit 12 fails mocks
without expectations; the standard profile of this core has no article
type, so the live-site node rendered a 500.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…time

Moves the metrics into .github/scripts/drupal-bench.py, which runs a set of
variants (same ini file on both stacks, revalidate_freq, static processes,
listen_threads, JIT), adds a trivial PHP script, a diag.php that reports
OPcache/JIT/realpath state per worker, and per-request server CPU from /proc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…JIT off

Adds a listener that passes straight to the app (no routes, no share
lookups), the front page next to /node/1, first-request timings after a
restart and after idle workers are reaped (spare 0 vs 1), a JIT-off variant
set at runtime on both stacks, and empties page_cache before the router
cache scenario so its MISS writes the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
…cs on the fair config

revalidate_freq=0 in the example's options.admin was the whole gap to
nginx + php-fpm (which ran with PHP's default 2): with the same ini on both
stacks they measure the same (runs 35975929248, 35977836593).  opcache.enable
in options.admin fails after startup and OPcache is on by default.  The
workflow's default metrics now run the fair variant only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
andypost pushed a commit that referenced this pull request Sep 24, 2026
- tools/factory, tools/fidx: the model-evaluation harness and the
  function index now live in andypost/unit (proto/factory).
- docs/adr/0005 (HTTP/2, HTTP/3) and docs/drupal/freeunit-module.md:
  designs for work that is not in this branch; kept with their
  prototypes in andypost/unit (#39, #40).
- docs/observability/usdt-plan.md, status-extensions.md: plans that
  are implemented; usdt.md and the OpenAPI spec describe what shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSMMShDBn9zs2gFfTrLqwA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants