Skip to content

wpcomsh: Optimize Atomic_Storage_Provider with per-request caching and reduced DB queries#48023

Open
bindlegirl wants to merge 2 commits intotrunkfrom
update/wpcomsh-apd-refactor
Open

wpcomsh: Optimize Atomic_Storage_Provider with per-request caching and reduced DB queries#48023
bindlegirl wants to merge 2 commits intotrunkfrom
update/wpcomsh-apd-refactor

Conversation

@bindlegirl
Copy link
Copy Markdown
Contributor

Proposed changes

Four performance optimizations for the Atomic_Storage_Provider to reduce per-request DB queries and redundant work:

  • Cache APD instance: Reuse a single Atomic_Persistent_Data instance across all get() calls instead of creating 4 per request (one per option). APD data is immutable within a request.
  • Cache email→user resolution: Add resolve_user_by_email() that caches the WP_User lookup. Both get_master_user_id() and get_user_tokens() use the same owner email, so this guarantees at most 1 DB lookup regardless of object-cache state.
  • Re-entrancy guard for token reads: Replace get_raw_option() (direct DB query) with Jetpack_Options::get_option() (reads from WP's autoloaded options cache) when loading existing tokens. A re-entrancy flag on should_handle('user_tokens') prevents the circular dependency. Wrapped in try/finally for safety. This is the same pattern used by CIAB/Garden.
  • Skip redundant write on conflict re-read: When validate_user_tokens() re-reads the latest DB state and finds no remaining conflicts (already resolved by a concurrent request), skip the write + delete + cache flush.

Impact summary:

Scenario Before After
Happy path (no conflicts) 2 DB queries + 4 APD reads 1 DB query + 1 APD read
Conflict path (already resolved) 5 DB queries + writes/deletes 2 DB queries + 0 writes
Conflict path (still present) 5 DB queries + writes/deletes 2 DB queries + writes/deletes

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

  • Existing AtomicStorageProviderTest tests cover all code paths (conflict resolution, orphaned token cleanup, matching tokens, invalid inputs). 5 new tests added for the caching and re-entrancy behaviors.
  • Run jp docker phpunit wpcomsh -- --filter=AtomicStorageProvider to execute the test suite.
  • On a WoA site, verify that connection status reads work correctly (blog_token, id, master_user, user_tokens all resolve from APD as expected).

@bindlegirl bindlegirl added the [Status] Needs Review This PR is ready for review. label Apr 9, 2026
@bindlegirl bindlegirl self-assigned this Apr 9, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

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 (WordPress.com Site Helper), and enable the update/wpcomsh-apd-refactor branch.

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
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

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!


Wpcomsh plugin:

  • Next scheduled release: Atomic deploys happen twice daily on weekdays (p9o2xV-2EN-p2)

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@bindlegirl bindlegirl requested a review from fgiannar April 9, 2026 11:55
@jp-launch-control
Copy link
Copy Markdown

Code Coverage Summary

No summary data is available for parent commit f5f8e6a, so cannot calculate coverage changes. 😴

If that commit is a feature branch rather than a trunk commit, this is expected. Otherwise, this should be updated once coverage for f5f8e6a is available.

Full summary · PHP report

Copy link
Copy Markdown
Contributor

@fgiannar fgiannar left a comment

Choose a reason for hiding this comment

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

Thanks for working on these improvements, Ana!

I'm testing on my WoA dev site with the Broken Token functionality so I can actually see the user tokens. Interestingly, while I do my user token on trunk, it not present on this branch. I do have a hypothesis, curious if it will fix my issue:

In resolve_user_by_email the cache negatively memoizes failed lookups, and any subsequent call with the same email gets the cached null without retrying.
Here's the exact path:

  private function resolve_user_by_email( $email ) {                                                                                                                                                                      
      if ( $email === $this->resolved_email ) {                                                                                                                                                                           
          return $this->resolved_user;          // ← can return cached NULL                                                                                                                                               
      }                                                                                                                                                                                                                   
                                                                                                                                                                                                                          
      $this->resolved_email = $email;            // ← set to a valid email                                                                                                                                                
      $this->resolved_user  = null;              // ← reset to null                                                                                                                                                     
                                                                                                                                                                                                                          
      if ( ! function_exists( 'get_user_by' ) || empty( $email ) || ! is_email( $email ) ) {
          return null;                           // ← bails, leaves resolved_user=null                                                                                                                                    
      }                                          //    but resolved_email=$email is now set                                                                                                                               
                                                                                                                                                                                                                          
      $user = get_user_by( 'email', $email );                                                                                                                                                                             
      if ( $user instanceof \WP_User ) {                                                                                                                                                                                  
          $this->resolved_user = $user;                                                                                                                                                                                 
      }                                          // ← if get_user_by returns false,
      return $this->resolved_user;               //    resolved_user stays null,                                                                                                                                          
  }                                              //    resolved_email is the real email                                                                                                                                   

So any of these will poison the cache:

  1. Pluggable not loaded yet on the first call: function_exists( 'get_user_by' ) is false → returns null → resolved_email is set to the real email → next call with the same email returns cached null even though
    pluggable is loaded now.
  2. get_user_by returns false on the first call (user not yet present in the local DB on a replicated site, transient miss, etc.) → caches null → second call gets the cached null even though the user is now
    resolvable.
  3. Order-of-operations within get(): provider->get('master_user') runs before provider->get('user_tokens'). If the master_user path hits the failure mode above, the user_tokens path sees the poisoned cache and
    get_user_tokens() returns false at if ( ! $user ) → you get empty tokens.

On trunk, neither call caches anything, so each invocation does its own fresh get_user_by() lookup and the second one succeeds even if the first one failed.
I believe we can fix this by not caching negative results. WP's own user-cache layer makes repeated get_user_by('email', …) calls cheap anyway, so the dedupe value of the per-instance cache is really only for the positive hit.

  private function resolve_user_by_email( $email ) {                                                                                                                                                                    
      // Only serve from cache when we have a positive resolution.                                                                                                                                                        
      if ( null !== $this->resolved_user && $email === $this->resolved_email ) {                                                                                                                                          
          return $this->resolved_user;                                                                                                                                                                                    
      }                                                                                                                                                                                                                   
                                                                                                                                                                                                                          
      $this->resolved_email = $email;
      $this->resolved_user  = null;                                                                                                                                                                                       
                                                                                                                                                                                                                        
      if ( ! function_exists( 'get_user_by' ) || empty( $email ) || ! is_email( $email ) ) {                                                                                                                              
          return null;
      }                                                                                                                                                                                                                   
                                                                                                                                                                                                                        
      $user = get_user_by( 'email', $email );
      if ( $user instanceof \WP_User ) {
          $this->resolved_user = $user;
      }                                                                                                                                                                                                                   
   
      return $this->resolved_user;                                                                                                                                                                                        
  }

It would be nice if we could also add a unit test for this case :)

/**
* Test that re-entrancy flag is reset even if get_option throws.
*/
public function test_reentrant_flag_reset_on_exception() {
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.

How do we trigger the exception here?

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.

2 participants