Status: seed jetpack_offline_mode to avoid per-request reads - #50961
Conversation
…request On sites without a persistent object cache (e.g. ma.tt), the unset, non-autoloaded options `jetpack_offline_mode` and `jetpack_tos_agreed` are re-queried on every request. Seed each as an autoloaded default when it is absent so subsequent reads are served from the bulk `alloptions` load instead of a dedicated SELECT. Neither option is synced, so seeding has no Sync side effects. Behaviour is unchanged: an unset option already resolved to the same default. Part of JETPACK-1539.
The new WorDBless test revealed that `Jetpack_Options::update_option( 'tos_agreed', false )` short-circuits when the new value equals the current default (`false`), so the row was never actually created. Seed via `add_option` instead, matching the existing `get_option_and_ensure_autoload` pattern. - Terms_Of_Service_Seeding_Test: real options-table coverage for the absent → seed (autoloaded) and stored → no-reseed paths. - Status_Test: explicit "already stored → not re-seeded" case, plus an add_option stub for the generic cached-result test.
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
Code Coverage SummaryCoverage changed in 1 file.
|
- Guard `ReflectionMethod::setAccessible()` behind `PHP_VERSION_ID < 80100`: it is a no-op since 8.1 and deprecated in 8.5, and the suite fails the build on test-triggered deprecations. Matches the existing repo convention. - PHPCS: add the `@covers` annotation to match `#[CoversClass]`, drop the useless `setUp()` override, and correct the `@see` namespace.
The tos_agreed seed raises a separate tracking-gate question, so it's split into #50965. This PR now covers only jetpack_offline_mode (Status package).
There was a problem hiding this comment.
Pull request overview
This PR optimizes Jetpack’s Status::is_offline_mode() path by persisting a default jetpack_offline_mode option as an autoloaded row the first time it’s observed missing, reducing repeated per-request option-table reads on sites without a persistent object cache.
Changes:
- Seed
jetpack_offline_modeas an autoloadedfalsewhen it’s absent, avoiding repeated DB queries. - Update
Status_Testto reflect the new “missing vs stored” behavior and seeding viaadd_option. - Add a changelog entry documenting the performance-related fix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| projects/packages/status/src/class-status.php | Adds missing-option detection and one-time seeding via add_option() to prevent per-request option reads. |
| projects/packages/status/tests/php/Status_Test.php | Adjusts unit tests for the new seeding/missing-option behavior. |
| projects/packages/status/changelog/fix-jetpack-1539-option-read-churn | Adds a patch-level changelog entry describing the change. |
| Significance: patch | ||
| Type: fixed | ||
|
|
||
| Offline mode: avoid a per-request database query for the jetpack_offline_mode option on sites without a persistent object cache by seeding it as an autoloaded default. |
| // Use null as the default so we can tell an unset option apart from a stored `false`. | ||
| $option = get_option( 'jetpack_offline_mode', null ); | ||
|
|
||
| if ( null === $option ) { | ||
| // This escape-hatch option is never written by Jetpack, so on most sites it is |
Capitalize the verb after the component prefix and drop the internal option name, keeping the entry user-facing.
null as the get_option default is ambiguous with a stored value that resolves to null (e.g. via a pre_option/option filter), which would re-seed on every request. Use a stdClass sentinel checked by identity so only a genuinely absent option is seeded. Update the mocks accordingly. Per review feedback on the PR.
null as the get_option default is ambiguous with a stored value that resolves to null, which would re-seed on every request. Use a stdClass sentinel checked by identity so only a genuinely absent option is seeded. Per review feedback on the sibling PR (#50961).
bindlegirl
left a comment
There was a problem hiding this comment.
Thanks for working on this @darssen I agree with the approach.
I do have some questions.
1. Should the seed be skipped when there's a persistent object cache?
The optimization only helps sites without one. But the seed runs unconditionally, so sites with persistent cache (WoW) also get a write and a permanent autoloaded row.
Gating it would confine the change to the sites that benefit:
if ( $sentinel === $option && ! wp_using_ext_object_cache() ) {
add_option( 'jetpack_offline_mode', false, '', true );
}I can see the argument for keeping behaviour uniform rather than branching on the cache backend, so I'm not insisting.
2. The failure case is now worse than the status quo
If add_option() doesn't create the row (e.g. a write error) every subsequent request pays the original SELECT plus an INSERT attempt.
I think it's worth considering restricting the seed to contexts where a write is expected anyway (admin, cron, WP-CLI) and the front-end path stays read-only. What do you think?
3. The sentinel is truthy (non-blocking, but cheap to harden)
new \stdClass() is truthy, so anything that reaches (bool) $option as an object silently puts the site into offline mode. The identity check handles the normal path correctly, but note that default_option_jetpack_offline_mode filters now receive the sentinel as $default_value, and $passed_default flips from false to true because get_option() is called with two arguments. A filter returning a modified or cloned value lands in the else branch and evaluates as true.
One-liner that closes it:
$offline_mode = is_scalar( $option ) ? (bool) $option : false;Separately and much more theoretically: add_option() skips its own existence guard when notoptions already has the key — which our get_option() call just put there — so it goes straight to INSERT ... ON DUPLICATE KEY UPDATE. A concurrent write in that window would be clobbered. Given this option is only ever set by hand, I'd note it and move on.
- Only seed the option where the optimization helps (no persistent object cache) and in write-appropriate contexts (admin, cron, WP-CLI), so anonymous front-end requests stay read-only. - Cast to bool only for scalar values, so a non-scalar returned by a default_option filter can't silently enable offline mode.
|
Many thanks for the review @bindlegirl! They are not really blockers, since 2 is the only one a bit more severe, but still, if the site is not able to add options, it's not really healthy. In any case I addressed all three with 9585330. |
null as the get_option default is ambiguous with a stored value that resolves to null, which would re-seed on every request. Use a stdClass sentinel checked by identity so only a genuinely absent option is seeded. Per review feedback on the sibling PR (#50961).
Fixes JETPACK-1994
Proposed changes
Status::is_offline_mode(): when thejetpack_offline_modeoption is absent, seed it as an autoloadedfalseso it isn't re-queried on every request.falsewith anullsentinel default, and seeds viaadd_option(notupdate_option, which short-circuits when the new value equals the current defaultfalseand would leave the row unset).Status_Testunit tests accordingly.Why are these changes being made
On a site without a persistent object cache, WP's per-request option caches (
alloptions/notoptions) are discarded each request, so any non-autoloaded option costs a dedicatedSELECT … FROM wp_optionson every request it's read — a passed default doesn't help, because the query is what discovers the option is missing.jetpack_offline_modeis read viaStatus::is_offline_mode()on essentially every request (it runs during Jetpack's bootstrap), and it's never written by Jetpack — it's a manual escape hatch (offline mode is normally enabled via thejetpack_offline_modefilter, theJETPACK_DEV_DEBUG/WP_LOCAL_DEVconstants, or local-site detection, all checked before the option). So on most sites the option is absent and re-read every request. Seeding an autoloaded default the first time it's found missing moves the value into the single bulkalloptionsload, eliminating the per-request read. The option is not synced, so seeding produces no Sync traffic, and behaviour is unchanged (an unset option already resolved to the samefalse; a stored value still wins and is never overwritten).(A row that exists but is non-autoloaded would still churn — we only seed when the option is absent and don't flip autoload on existing rows — but that doesn't arise for this option in the sites we investigated.)
Related product discussion/links
Does this pull request change what data or activity we track or use?
No. Seeding writes the existing default value (
false) to the local options table; it is not synced to WordPress.com.Testing instructions
1. Create a test site with no persistent object cache. On jurassic.ninja, open the options and uncheck "Drop-in Cache Plugins", then create the site. (Without this, JN installs an object cache that hides the issue —
notoptionswould persist across requests.) Confirm it's off:2. Add a small logger so the option reads are visible on logged-out requests. Create
wp-content/mu-plugins/1994-check.php:3. Load the front end logged out, using a different query string each time so the edge page cache doesn't serve a cached page (each unique URL is a cache miss that runs PHP). For example, open these in a private/incognito window:
4. Check the log:
cat wp-content/jp1994.logBefore this PR: one
SELECT … 'jetpack_offline_mode'per request.After this PR: it appears only on the first request (the one-time seed), then stops. Confirm the row now exists autoloaded:
(should show
autoload = on). Re-test with fresh?cachebust=values, since the ones above are now cached.5. Behaviour is unchanged:
Status::is_offline_mode()still returns true withJETPACK_DEV_DEBUG/WP_LOCAL_DEV, on a local site, when thejetpack_offline_modefilter returns true, or when the option is1; false otherwise.Unit tests:
projects/packages/status(Status_Test).