Skip to content

Status: seed jetpack_offline_mode to avoid per-request reads - #50961

Merged
darssen merged 7 commits into
trunkfrom
fix/jetpack-1539-connection-status-option-churn
Aug 4, 2026
Merged

Status: seed jetpack_offline_mode to avoid per-request reads#50961
darssen merged 7 commits into
trunkfrom
fix/jetpack-1539-connection-status-option-churn

Conversation

@darssen

@darssen darssen commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Fixes JETPACK-1994

Proposed changes

  • Status::is_offline_mode(): when the jetpack_offline_mode option is absent, seed it as an autoloaded false so it isn't re-queried on every request.
  • Distinguishes "never stored" from a stored false with a null sentinel default, and seeds via add_option (not update_option, which short-circuits when the new value equals the current default false and would leave the row unset).
  • Adjust the Status_Test unit 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 dedicated SELECT … FROM wp_options on every request it's read — a passed default doesn't help, because the query is what discovers the option is missing.

jetpack_offline_mode is read via Status::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 the jetpack_offline_mode filter, the JETPACK_DEV_DEBUG/WP_LOCAL_DEV constants, 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 bulk alloptions load, 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 same false; 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 — notoptions would persist across requests.) Confirm it's off:

wp eval 'var_dump( wp_using_ext_object_cache() );'   // must be false

2. Add a small logger so the option reads are visible on logged-out requests. Create wp-content/mu-plugins/1994-check.php:

<?php
add_filter( 'query', function ( $q ) {
    if ( stripos( $q, 'option_value' ) !== false && strpos( $q, 'jetpack_offline_mode' ) !== false ) {
        @file_put_contents( WP_CONTENT_DIR . '/jp1994.log',
            gmdate( 'c' ) . ' ' . trim( preg_replace( '/\s+/', ' ', $q ) ) . "\n",
            FILE_APPEND | LOCK_EX );
    }
    return $q;
}, 0 );

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:

https://your-site.jurassic.ninja/?cachebust=1
https://your-site.jurassic.ninja/?cachebust=2
https://your-site.jurassic.ninja/?cachebust=3

4. Check the log: cat wp-content/jp1994.log

  • Before 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:

    wp db query "SELECT option_name, autoload FROM wp_options WHERE option_name = 'jetpack_offline_mode'"
    

    (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 with JETPACK_DEV_DEBUG/WP_LOCAL_DEV, on a local site, when the jetpack_offline_mode filter returns true, or when the option is 1; false otherwise.

Unit tests: projects/packages/status (Status_Test).

darssen added 2 commits July 31, 2026 09:44
…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.
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the fix/jetpack-1539-connection-status-option-churn branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/jetpack-1539-connection-status-option-churn
bin/jetpack-downloader test jetpack-mu-wpcom-plugin fix/jetpack-1539-connection-status-option-churn

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

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:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jul 31, 2026
@jp-launch-control

jp-launch-control Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/packages/status/src/class-status.php 92/107 (85.98%) 0.69% 0 💚

Full summary · PHP report · JS report

- 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.
@darssen darssen removed the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jul 31, 2026
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).
@darssen darssen changed the title Connection/Status: stop re-reading offline_mode and tos_agreed every request Status: seed jetpack_offline_mode to avoid per-request reads Jul 31, 2026
@darssen darssen self-assigned this Jul 31, 2026
@darssen darssen added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Jul 31, 2026
@darssen
darssen marked this pull request as ready for review July 31, 2026 12:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_mode as an autoloaded false when it’s absent, avoiding repeated DB queries.
  • Update Status_Test to reflect the new “missing vs stored” behavior and seeding via add_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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 8b8bcf0

Comment on lines +57 to +61
// 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, implemented with f964df4

Comment thread projects/packages/status/tests/php/Status_Test.php
@darssen darssen removed the [Status] Needs Review This PR is ready for review. label Aug 3, 2026
darssen added 2 commits August 3, 2026 08:06
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.
darssen added a commit that referenced this pull request Aug 3, 2026
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).
@darssen darssen added the [Status] Needs Review This PR is ready for review. label Aug 3, 2026

@bindlegirl bindlegirl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@darssen darssen added [Status] In Progress and removed [Status] Needs Review This PR is ready for review. labels Aug 4, 2026
@darssen

darssen commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

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.

@darssen
darssen requested a review from bindlegirl August 4, 2026 10:32
@darssen darssen added the [Status] Needs Review This PR is ready for review. label Aug 4, 2026
@darssen
darssen requested a review from Copilot August 4, 2026 10:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@bindlegirl bindlegirl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

@darssen
darssen merged commit 973d775 into trunk Aug 4, 2026
122 checks passed
@darssen
darssen deleted the fix/jetpack-1539-connection-status-option-churn branch August 4, 2026 12:52
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label Aug 4, 2026
darssen added a commit that referenced this pull request Aug 7, 2026
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants