Skip to content

feat: batch sync content using action scheduler#231

Draft
Kallyan01 wants to merge 20 commits into
mainfrom
feat/batch-sync-content
Draft

feat: batch sync content using action scheduler#231
Kallyan01 wants to merge 20 commits into
mainfrom
feat/batch-sync-content

Conversation

@Kallyan01

@Kallyan01 Kallyan01 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

What

Why

Related Issue(s):

How

AI Disclosure

Testing Instructions

Screenshots

Additional Info

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

@Kallyan01 Kallyan01 marked this pull request as ready for review June 2, 2026 11:40
Copilot AI review requested due to automatic review settings June 2, 2026 11:40

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 introduces an Action Scheduler–backed job system to move Algolia sync/re-index work off the request thread, enabling batched async processing with progress tracking and a richer admin UI.

Changes:

  • Add a new async job framework (AbstractJob, SyncJob, ReindexJob) plus a JobScheduler facade (Action Scheduler integration + job persistence).
  • Update re-index and post-change indexing flows to schedule jobs instead of performing inline Algolia operations, and expose REST endpoints for job monitoring/retry/cancel.
  • Extend the settings UI to show multi-site job progress, batch-level status, retry actions, and job history.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
uninstall.php Removes new scheduler/job state options & transients during uninstall.
phpstan/stubs/action-scheduler.php Adds PHPStan stubs for Action Scheduler APIs/classes.
phpstan.neon.dist Registers Action Scheduler stub scan file for static analysis.
onesearch.php Loads Action Scheduler early from vendor if not already available.
inc/Modules/Search/Watcher.php Schedules SyncJob on post transitions with inline fallback.
inc/Modules/Search/Index.php Persists “settings initialized” across requests and deprecates sync reindex method.
inc/Modules/Scheduler/JobScheduler.php Implements job lifecycle: schedule/execute/retry/cancel, storage, parent-child aggregation.
inc/Modules/Scheduler/Bootstrap.php Registers job types and initializes scheduler on plugins_loaded.
inc/Modules/Rest/Search_Controller.php Replaces reindex flow with ReindexJob orchestration + adds /re-index/status persistence.
inc/Modules/Rest/Job_Controller.php Adds REST API for job listing, history, status proxying, retry, and cancel.
inc/Modules/Jobs/SyncJob.php Implements batch post sync/delete to Algolia as a leaf job.
inc/Modules/Jobs/ReindexJob.php Implements parent job that chunks posts into SyncJob children and dispatches runners.
inc/Modules/Jobs/Registry.php Adds singleton job type registry.
inc/Modules/Jobs/AbstractJob.php Adds job base model: status/progress/retry/serialization/relationships.
inc/Main.php Registers Scheduler Bootstrap and Job REST controller.
composer.json Adds woocommerce/action-scheduler dependency.
composer.lock Locks Action Scheduler dependency and updates lock metadata.
assets/src/css/admin.scss Adds styles for job progress UI, badges, and history panel.
assets/src/components/SiteIndexableEntities.tsx Adds polling-based progress modal, retry UI, cancel UI, and history table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread uninstall.php Outdated
Comment thread inc/Modules/Search/Watcher.php Outdated
Comment on lines +547 to +551
for ( $attempt = 0; $attempt < $max_tries; ++$attempt ) {
if ( set_transient( $lock_key, 1, 15 ) ) {
break;
}
usleep( 100000 + $attempt * 50000 ); // 100ms base, +50ms per attempt.
Comment thread inc/Modules/Scheduler/JobScheduler.php Outdated
Comment on lines +190 to +192
public function schedule_recurring( AbstractJob $job, int $interval_seconds ): int {
$job->set_status( AbstractJob::STATUS_PENDING );
$this->persist_job( $job );
Comment on lines +723 to +725
role={ canExpand ? 'button' : undefined }
tabIndex={ canExpand ? 0 : undefined }
style={ { cursor: canExpand ? 'pointer' : 'default' } }
Comment on lines +31 to +39
public function register_routes(): void {
register_rest_route(
self::NAMESPACE,
'/jobs',
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_jobs' ],
'permission_callback' => [ $this, 'check_job_read_permissions' ],
Comment on lines +115 to +123
// Get active reindex state for UI persistence across page refreshes.
register_rest_route(
self::NAMESPACE,
'/re-index/status',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_reindex_status' ],
'permission_callback' => [ $this, 'check_api_permissions' ],
]
Comment thread inc/Modules/Scheduler/JobScheduler.php Outdated
Comment on lines +125 to +129
// Batch-add to active index every 50 children to reduce DB writes.
if ( count( $pending_active_ids ) >= 50 ) {
$scheduler->add_many_to_active_index( $pending_active_ids );
$pending_active_ids = [];
}

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot stopped reviewing on behalf of Kallyan01 due to an error June 4, 2026 18:52
… in CI

Strauss 0.27.0 validates the GitHub OAuth token format, but the
GITHUB_TOKEN provided by GitHub Actions uses the ghs_* format
(contains underscores) which fails Strauss's regex validation.

Fix: run `composer install --no-scripts` to skip post-install-cmd,
then run `composer run prefix-namespaces` in a separate step with
`COMPOSER_AUTH: '{}'` to prevent Strauss from reading the invalid token.

Applies to both reusable-phpcs.yml and reusable-phpstan.yml.

Co-authored-by: Kallyan01 <48629100+Kallyan01@users.noreply.github.com>

@justlevine justlevine left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Love what I'm seeing so far, but only had a little time to review. Here's the first round of feedback, I'll share the rest after the weekend.

Also, remember this is a public repo, please update the PR title and fill in the description (or turn it to draft to indicate it's not ready to review)

Comment thread uninstall.php
Comment on lines +86 to +96
// Delete all job status options (onesearch_job_status_*).
$job_status_options = $wpdb->get_col( // phpcs:ignore WordPressVIPMinimum.DirectDBQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
PLUGIN_PREFIX . 'job_status_%'
)
);

foreach ( $job_status_options as $option_name ) {
delete_option( $option_name );
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Q: If you're anyway doing a DB call to find all the options, why not just delete them directly versus all the separate delete_option() db calls?

Depending on your answer to the above, we should follow SRP and move either the entire thing out of delete_options() into it's own function, or just the get_job_status_option_names().

Comment thread uninstall.php
Comment on lines +124 to +128
if ( 0 === strpos( $transient_name, '_transient_timeout_' ) ) {
delete_transient( substr( $transient_name, 18 ) );
} elseif ( 0 === strpos( $transient_name, '_transient_' ) ) {
delete_transient( substr( $transient_name, 11 ) );
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

str_starts_with()

Comment thread uninstall.php
Comment on lines +113 to 130

// Delete all job status transients (onesearch_job_status_*).
$job_transients = $wpdb->get_col( // phpcs:ignore WordPressVIPMinimum.DirectDBQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
'_transient_' . PLUGIN_PREFIX . 'job_status_%',
'_transient_timeout_' . PLUGIN_PREFIX . 'job_status_%'
)
);

foreach ( $job_transients as $transient_name ) {
if ( 0 === strpos( $transient_name, '_transient_timeout_' ) ) {
delete_transient( substr( $transient_name, 18 ) );
} elseif ( 0 === strpos( $transient_name, '_transient_' ) ) {
delete_transient( substr( $transient_name, 11 ) );
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

#231 (comment)

Depending on your answer, SRP could put this in it's own getter or delete function, or even in the same SELECT option_name ... as above.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why are we stubbing these?

If PHPStan isn't picking up vendor/woocommerce/action-scheduler then point scanFiles to there. If we're overloading those for some reason, then make sure that the stubs here are accurate to the actual signatures (they're not).

Comment thread onesearch.php
Comment on lines 69 to +77

// Load Action Scheduler early, before plugins_loaded, so its own hooks register on time.
if ( ! function_exists( 'as_enqueue_async_action' ) ) {
$onesearch_as_path = ONESEARCH_DIR . 'vendor/woocommerce/action-scheduler/action-scheduler.php';
if ( file_exists( $onesearch_as_path ) ) {
require_once $onesearch_as_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's move this to Main::load_action_scheduler() and then call it right before Main::load()

Comment thread composer.json
"prefix-namespaces": [
"sh -c 'test -f ./tests/_output/strauss.phar || curl -o ./tests/_output/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/download/0.27.0/strauss.phar'",
"@php ./tests/_output/strauss.phar",
"COMPOSER_HOME=/tmp/strauss-composer-home php ./tests/_output/strauss.phar",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Revert this, and merge in the base branch which uses v0.27.3

Comment on lines +46 to 53
with:
composer-options: "--no-scripts"

- name: Run Composer post-install scripts
run: composer run prefix-namespaces
env:
COMPOSER_AUTH: '{}'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

NEVER change the CI as part of an unrelated code change.
Either the CI is broken and deserves its own PR to fix, or you need to doublecheck you assumptions about whether it needs changing at all.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's move this into Scheduler/Job_REST_Controller.php. It's a bad antipattern the rest of this repo isn't very colocated, but we at least don't need to make the situation worse.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All these new files (and classes) should be in Pascal_Snake_Case

@Kallyan01 Kallyan01 marked this pull request as draft June 12, 2026 17:30
@Kallyan01 Kallyan01 changed the title Feat/batch sync content feat: batch sync content using action scheduler Jun 12, 2026
@Kallyan01

Copy link
Copy Markdown
Collaborator Author

@justlevine, thanks for the review. I've made some significant architectural changes over the last few days that should make the job data handling more efficient and optimised. Specifically, I've moved the job data from the wp_options table to a custom table, which significantly reduces query complexity and should improve overall performance. It will also make future cleanup and maintenance much easier. I'll push the updated changes on Monday. Please wait until i push those changes to avoid reviewing deprecated code.

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