Skip to content

feat: implement async job handling - #365

Merged
justlevine merged 6 commits into
mainfrom
feat/action-scheduler-jobs
Oct 29, 2026
Merged

justlevine merged 6 commits into
mainfrom
feat/action-scheduler-jobs

Conversation

@justlevine

@justlevine justlevine commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

Moves OneSearch's indexing out of the request and into background jobs on Action Scheduler, and shows each site's indexing status on the governing site.

  • Re-index runs a job for each site. The governing site's own posts are indexed in batches of 100, and each brand site is asked to run its own job.
  • The old records stay searchable until a full run is done, and then the ones it didn't save again are deleted.
  • Clicking Re-index while a run is going starts it over.
  • Saving, trashing or deleting a post queues a job at the end of the request, instead of syncing in the save.
  • Each post's result is kept in post meta, either when it was indexed or why it couldn't be. Only errors about the post itself go there, like a post too big to index. A connection or service error fails the site's job with the reason instead.
  • The Indices and Search screen shows how much of each site is in search, the posts that couldn't be indexed and why, and each site's indexing progress or failure. It checks again every 10 seconds while a job is going.
  • Removing a brand site, or switching a governing site to a brand site, deletes the jobs about the sites it no longer has.
  • Uninstall deletes the waiting jobs, the job records and each post's index status. Actions that already ran stay, with their logs.

Why

A re-index ran inside the REST request. The governing site indexed itself and then each brand site one after another, so anything past a small network timed out (around 45 seconds per brand site in QA). It deleted the site's records first, so search was missing that site's posts until it finished, or for good if it timed out. It also said "Re-indexing scheduled successfully." when nothing was scheduled.

Post syncs ran in the save request, so a slow or failing Algolia call slowed down saving the post, and the error only went to the log. Nothing showed which posts were in search or why one wasn't.

Related Issue(s):

How

  • inc/Jobs/ is a small jobs layer on Action Scheduler (bundled through Composer), with no OneSearch names so the other One plugins can reuse it. Abstract_Job keeps a record of each job's state in its own option and fails it when it throws or when Action Scheduler cancels, deletes or times out its action. Parent_Job_Trait splits a job's work across child jobs.
  • Index_Site_Job runs for a site, either all its posts or a list of them, and splits them across Index_Posts_Job children. Index_Status keeps each post's result.
  • Indexer_Controller replaces Reindex_Controller at /onesearch/v1/indexer. GET returns each site's summary and jobs, and POST starts a re-index. Records get an indexed_at, which the stale cleanup uses.

AI Disclosure

Claude Code to redraft the jobs layer, the indexing jobs and their tests, review them in rounds, and QA them on a local wp-env network against a test Algolia app.

Testing Instructions

Note

Jobs run on WP-Cron, and a Re-index starts the runner right away. To run waiting jobs by hand, use npm run wp-env run cli -- wp action-scheduler run --group=onesearch, or Tools > Scheduled Actions.

  1. Connect a governing site and a brand site, add the Algolia credentials, pick the post types to index and click Save Changes. That starts a re-index.
  2. On the governing site's Indices and Search screen, each site shows "Indexing will start soon.", then its progress, then how many of its items are in search. Search keeps returning the old results while it runs.
  3. Click Re-index while a run is going. The modal says indexing is already running, and re-indexing starts it over.
  4. Edit a post, then trash it. Its records update in Algolia once its onesearch_job_index_site action runs.
  5. Add add_filter( 'onesearch_algolia_record_size_limit', fn() => 50 ); and re-index. The posts show in the site's failed list as too big to index. Remove the filter after.
  6. Set a wrong Algolia write key and re-index. The site shows as failed with the reason, and its posts don't get the error.
  7. Change the brand site's API key so the governing site can't reach it, and reload. The brand site shows the connection error.
  8. Remove the brand site. Its jobs no longer show.

Screenshots

Before After
Clicking Re-index image image
Indexing in progress image (no running state, just the end of the click) image
Re-index while a run is going None (the button is disabled while its request runs) image
Completed image image
Posts too big to index image (they drop out of search without a word) image
Wrong Algolia key image image
Brand site unreachable image image

Additional Info

  • Brand sites don't have a screen for this yet, so the governing site shows their status.
  • A followup PR will move the brand config purges and the record deletes of removed sites into jobs too

Checklist

  • I have read the Contribution Guidelines.
  • I have read the Development Guidelines.
  • I have added necessary tests to cover my changes.
  • I have updated the project documentation as needed.
  • My code has detailed inline documentation.
  • My code is tested to the best of my abilities.
  • My code passes all lints, tests, and checks.
Open WordPress Playground Preview

@codecov-commenter

codecov-commenter commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.75168% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.83%. Comparing base (7ae9441) to head (a091b67).

Files with missing lines Patch % Lines
inc/Modules/Search/Index_Status.php 76.27% 14 Missing ⚠️
inc/Modules/Search/Jobs/Index_Site_Job.php 85.00% 9 Missing ⚠️
uninstall.php 0.00% 9 Missing ⚠️
inc/Jobs/Job_Registry.php 80.00% 5 Missing ⚠️
inc/Modules/Search/Jobs/Jobs.php 0.00% 4 Missing ⚠️
inc/Jobs/Job_Records.php 90.90% 3 Missing ⚠️
assets/src/components/SiteIndexableEntities.tsx 91.30% 1 Missing and 1 partial ⚠️
inc/Jobs/Queue.php 90.90% 2 Missing ⚠️
inc/Modules/Search/Indexer.php 96.07% 2 Missing ⚠️
inc/Modules/Rest/Indexer_Controller.php 99.09% 1 Missing ⚠️
... and 3 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #365      +/-   ##
============================================
+ Coverage     87.07%   88.83%   +1.76%     
- Complexity      684      838     +154     
============================================
  Files            36       48      +12     
  Lines          2754     3270     +516     
  Branches        173      181       +8     
============================================
+ Hits           2398     2905     +507     
- Misses          297      308      +11     
+ Partials         59       57       -2     
Flag Coverage Δ
unit 89.70% <92.20%> (+1.61%) ⬆️
vitest 85.08% <97.43%> (+1.87%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
assets/src/admin/settings/page.tsx 77.27% <100.00%> (-0.34%) ⬇️
assets/src/components/AlgoliaSettings.tsx 86.36% <100.00%> (ø)
assets/src/components/IndexSummary.tsx 100.00% <100.00%> (ø)
assets/src/components/JobStatus.tsx 100.00% <100.00%> (ø)
assets/src/components/SiteSettings.tsx 94.54% <ø> (-0.29%) ⬇️
assets/src/js/use-index-status.ts 100.00% <100.00%> (ø)
inc/Algolia/Index.php 96.82% <100.00%> (-0.10%) ⬇️
inc/Jobs/Abstract_Job.php 100.00% <100.00%> (ø)
inc/Jobs/Parent_Job_Trait.php 100.00% <100.00%> (ø)
inc/Main.php 100.00% <ø> (ø)
... and 16 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

🟡 Changes recommended

Unresolved cleanup and concurrency defects can retain stale records, remove searchable content, or prevent restarted runs from indexing.

6 open issues
What changed in this PR

Moves OneSearch’s Algolia work from web requests into Action Scheduler jobs, with per-site progress and failure reporting.

Changes:

  • Adds batched re-indexing, queued post syncs, configuration purges, and site-record deletion.
  • Separates Algolia storage operations from job orchestration and introduces the /indexer API.
  • Adds progress displays, integration coverage, scheduler setup, and uninstall cleanup.
File Description
uninstall.php Cancels pending jobs and removes progress data.
tests/​phpunit/​TestCase.php Adds job execution and failure helpers.
tests/​phpunit/​Integration/​UninstallTest.php Tests job cancellation and progress cleanup.
tests/​phpunit/​Integration/​Modules/​Search/​WatcherTest.php Covers background post syncs and failures.
tests/​phpunit/​Integration/​Modules/​Search/​SettingsTest.php Tests queued purges and record deletion.
tests/​phpunit/​Integration/​Modules/​Search/​SearchIndexTest.php Covers extracted Algolia storage operations.
tests/​phpunit/​Integration/​Modules/​Search/​PostRecordTest.php Tests ID-based batch pagination.
tests/​phpunit/​Integration/​Modules/​Rest/​Remote_ClientTest.php Tests individual configuration purge responses.
tests/​phpunit/​Integration/​Modules/​Rest/​Reindex_Controller_GoverningSiteTest.php Removes superseded governing-site endpoint tests.
tests/​phpunit/​Integration/​Modules/​Rest/​Reindex_Controller_ConsumerSiteTest.php Removes superseded consumer-site endpoint tests.
tests/​phpunit/​Integration/​Modules/​Rest/​Indexer_Controller_GoverningSiteTest.php Tests governing-site scheduling and progress reception.
tests/​phpunit/​Integration/​Modules/​Rest/​Indexer_Controller_ConsumerSiteTest.php Tests brand-site scheduling, access, and failures.
tests/​phpunit/​Integration/​Modules/​Rest/​Brand_Site_Controller_GoverningSiteTest.php Runs queued cleanup after site removal.
tests/​phpunit/​Integration/​Jobs/​QueueTest.php Tests queue execution, cancellation, and failure reporting.
tests/​phpunit/​Integration/​Jobs/​ProgressTest.php Tests progress lifecycle and restart handling.
tests/​phpunit/​Integration/​Algolia/​IndexTest.php Removes obsolete delete-wait coverage.
tests/​phpunit/​bootstrap.php Initializes Action Scheduler for tests.
tests/​js/​SiteSettings.test.tsx Tests brand-site progress and failure displays.
tests/​js/​SiteIndexableEntities.test.tsx Tests polling, restart notices, and progress displays.
tests/​_data/​plugins/​localhost-helper.php Supports local scheduler and cron loopbacks.
phpstan.neon.dist Includes Action Scheduler definitions in analysis.
inc/​Modules/​Search/​Watcher.php Queues post synchronization jobs.
inc/​Modules/​Search/​Settings.php Queues configuration purges and removed-site cleanup.
inc/​Modules/​Search/​Search.php Uses the extracted search-index service.
inc/​Modules/​Search/​Search_Index.php Provides Algolia storage and stale-record cleanup.
inc/​Modules/​Search/​Post_Record.php Adds timestamps and ID-based batching.
inc/​Modules/​Search/​Indexer.php Orchestrates background indexing and site progress.
inc/​Modules/​Rest/​Remote_Client.php Adds remote scheduling and progress requests.
inc/​Modules/​Rest/​Reindex_Controller.php Removes the synchronous re-index endpoint.
inc/​Modules/​Rest/​Indexer_Controller.php Adds indexing start and progress endpoints.
inc/​Modules/​Rest/​Algolia_Credentials_Controller.php Uses the extracted credential validator.
inc/​Main.php Registers indexing jobs and the new controller.
inc/​Jobs/​Queue.php Wraps Action Scheduler operations.
inc/​Jobs/​Progress.php Stores and validates indexing progress.
inc/​Algolia/​Index.php Removes optional waiting from filtered deletion.
docs/​DEVELOPMENT.md Documents job architecture and execution commands.
composer.lock Locks the Action Scheduler dependency.
composer.json Adds and autoloads Action Scheduler.
assets/​src/​js/​use-index-progress.ts Loads, starts, and polls indexing progress.
assets/​src/​css/​admin.scss Styles progress and error displays.
assets/​src/​components/​SiteSettings.tsx Displays brand-site index status.
assets/​src/​components/​SiteIndexableEntities.tsx Integrates per-site progress and asynchronous starts.
assets/​src/​components/​index-progress.tsx Renders progress, failures, and accessible announcements.

Review effort: Balanced

Comment thread inc/Jobs/Progress.php Resolved
Comment thread inc/Modules/Search/Search_Index.php Resolved
Comment thread inc/Modules/Search/Search_Index.php Resolved
Comment thread assets/src/js/use-index-progress.ts Resolved
Comment thread assets/src/js/use-index-progress.ts Resolved
Comment thread inc/Jobs/Queue.php Resolved
Copilot AI balanced review requested due to automatic review settings October 21, 2026 23:41
@justlevine
justlevine force-pushed the feat/action-scheduler-jobs branch from 7b9afdb to 095f3f4 Compare October 21, 2026 23:41

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.

Comment thread inc/Modules/Search/Search_Index.php Resolved
Comment thread inc/Modules/Search/Watcher.php Resolved
up1512001

This comment was marked as duplicate.

@up1512001 up1512001 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is reviewed by Opus on behalf of master Utsav.

Also, the PR description mentions PUT /indexer, but that isn't in the code.

Comment thread inc/Modules/Search/Indexer.php Resolved
Comment thread inc/Modules/Search/Indexer.php Resolved
Comment thread inc/Modules/Rest/Indexer_Controller.php Resolved
Comment thread inc/Modules/Search/Search_Index.php Resolved
Comment thread inc/Jobs/Queue.php Resolved
Comment thread assets/src/js/use-index-progress.ts Resolved
@justlevine

Copy link
Copy Markdown
Collaborator Author

This is reviewed by Opus on behalf of master Utsav.

Wish I had seen this before I spent time trying to understand the inline feedback.... Is there any review from actual, human @up1512001 ? I too know how to ask 5.5 for a review.

Also, the PR description mentions PUT /indexer, but that isn't in the code.

Leftover from an early code review, PR description updated.

@up1512001

Copy link
Copy Markdown
Member

@justlevine will give real pass tonight

@justlevine
justlevine force-pushed the feat/action-scheduler-jobs branch from e74ec3c to 7439853 Compare October 26, 2026 18:39
Copilot AI balanced review requested due to automatic review settings October 26, 2026 18:39

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.

Comment thread inc/Jobs/Abstract_Job.php Resolved
Comment thread assets/src/components/JobStatus.tsx Resolved
Comment thread assets/src/js/use-index-status.ts Resolved
Comment thread inc/Jobs/Parent_Job_Trait.php Resolved
Comment thread inc/Modules/Rest/Indexer_Controller.php Resolved
Comment thread inc/Modules/Search/Index_Status.php Resolved
Comment thread inc/Modules/Search/Jobs/Jobs.php Resolved
Copilot AI balanced review requested due to automatic review settings October 26, 2026 19:20

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.

🔵 Needs a closer look

Job-record caching can skip queued updates, and premature completion reporting can hide cleanup failures.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Expose completed status only after stale-record cleanup succeeds

inc/​Jobs/​Abstract_Job.php:299

This write exposes completed before Index_Site_Job::on_complete() finishes its fallible stale-record cleanup. A status GET during cleanup can stop UI polling, leaving a subsequent failure undisplayed until reload. Keep the persisted status nonterminal until cleanup succeeds, guard against duplicate finalization, and preserve callbacks that delete their own records.

Medium severity Invalidate notoptions cache when clearing incremental job records

inc/​Jobs/​Job_Records.php:73

Clearing the individual option leaves WordPress's notoptions cache intact. If another request recreates an incremental job record after the runner deleted it, the runner can still read it as missing and skip the queued post update. Invalidate the negative cache too, and add a regression test for an externally recreated record after a cached miss.

9 resolved since last review

Review effort: Balanced

@justlevine
justlevine requested a review from up1512001 October 26, 2026 19:27
@justlevine
justlevine added this pull request to stack #367 October 26, 2026 20:54

@up1512001 up1512001 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@justlevine only one major change to split post ids while sync rest are just nits.

Comment thread assets/src/js/use-index-status.ts Resolved
Comment thread assets/src/js/use-index-status.ts Resolved
Comment thread inc/Modules/Search/Index_Status.php Resolved
Comment thread inc/Modules/Search/Watcher.php Resolved
justlevine and others added 5 commits October 28, 2026 15:57
Bundles Action Scheduler and adds a small jobs layer on top of it, with no
OneSearch names in it so the other One plugins can reuse it. Queue wraps
the Action Scheduler calls. Abstract_Job runs one job and keeps a record of
its state for each key it runs for, with the job's status, its one error
and the data it keeps. Job_Records keeps each record in its own option. A
job fails when it throws, and also when Action Scheduler cancels, deletes
or times out its action, or PHP dies under it. on_complete() and
on_failure() run when it finishes. Job_Registry builds the jobs of one
Action Scheduler group from their names and reports their statuses.

Nothing registers a job yet. Uninstall deletes the actions still waiting
to run and the job records, and keeps the actions that already ran along
with their logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-index and post edits now index in the background instead of inside the
request. POST /indexer replaces the old re-index route and runs an
Index_Site_Job for each site, the governing site included, then starts
Action Scheduler's runner so the admin watching doesn't wait for WP-Cron.
For this site Index_Site_Job splits the posts across Index_Posts_Job
children of 100 posts each. They save or delete their posts and report back
how many were done and how many failed. The old records stay searchable
until a full run is done, and then it deletes the ones it didn't save
again. For a brand site it asks the brand site to run its own
Index_Site_Job. Re-index while a run is going starts it over, and the old
run's waiting children stop. The parent side lives in Parent_Job_Trait in
the jobs layer, which finishes the parent once every child of its run has
finished.

Post edits run Index_Site_Job for this site with the posts that changed, at
the end of the request. That run has its own record, so a post edit never
replaces a re-index.

Index_Status keeps each post's result in post meta, when it was indexed or
why it couldn't be, and counts a summary of each site from it. Only errors
about the post itself go there, like a post too big to index. A connection
or service error fails the site's Index_Site_Job with the reason, and a
post edit that can't reach Algolia marks the site's own run failed, so the
next re-index tries again. GET /indexer returns every site's summary and
jobs, with each brand site's report fetched from it.

The settings screen shows each site's summary and jobs, and checks on them
while a job is going.

Removing a brand site, or switching a governing site to a brand site,
deletes the job records about those sites. A brand site that its governing
site removes forgets which posts were in search. Config purges and the
record deletes of removed sites still work as before. Uninstall also
deletes each post's index status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@justlevine
justlevine merged commit 5378c4c into main Oct 29, 2026
18 checks passed
@justlevine
justlevine deleted the feat/action-scheduler-jobs branch October 29, 2026 13:51
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.

4 participants