Skip to content

fix: let WP-CLI continue a rollback started from the dashboard - #1173

Merged
pirate-bot merged 6 commits into
developmentfrom
bugfix/1161
Oct 12, 2026
Merged

pirate-bot merged 6 commits into
developmentfrom
bugfix/1161

Conversation

@selul

@selul selul commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Related issue: #1161

"Transfer back from Optimole" in the dashboard turns offload_media off and starts the rollback. wp optimole media rollback_images refused to run whenever offload_media was off, so it could not continue a rollback that WP-Cron could not finish (the customer case: 11,000+ images on a host with restricted cron). It now runs while a rollback is in progress, unless the background transfer is actually moving images at that moment.

What changed

  • Optml_Settings::can_transfer_images( $action ) (new) — the gate for a bulk transfer:

    • rollback is allowed when is_offload_enabled(), i.e. offload_media is on or a rollback is in progress. This is the check the Abilities API move_media already uses for the same action.
    • offload still needs offload_media on.
  • Optml_Cli_Media::update_images_template() — uses that gate (same error text when refused). It also stopped a rollback whenever the "offload limit reached" flag was set; the limit only concerns uploads, so that check now applies to the offload action only.

  • One run moves images at a time (acquire_moving_images_lock() / release_moving_images_lock(), new) — with the gate open, the command could move the same images as the dashboard's background chain at the same time and restore them twice. Both now take a database advisory lock (MySQL GET_LOCK, without waiting) while they move images:

    • a background step takes it for its batch; when it cannot, the command is running, and the step does nothing;
    • the command takes it for its whole run; when it cannot, a step is moving images at that moment, and it stops with "Another image transfer is running. Wait for it to finish, then run this command again." (plain English, like the other WP-CLI messages of the plugin, so the fix adds no string to translate);
    • taking it is atomic, and the database frees it when the request ends, also after a crash. It is also released on the shutdown action, for a fatal error on a persistent database connection;
    • with several database servers (HyperDB, LudicrousDB) reads are sent to the primary first (send_reads_to_masters()), so every run asks the same server;
    • where the database has no advisory locks, the lock is granted and transfers work as before.

Note

The command is still refused when offload_media is off and no rollback was started.

Why not the existing transfer lock (the transient): the background chain takes it when it is scheduled, and it is scheduled again on every request while a transfer is in progress, also where cron never runs it. On a site with a stuck cron that lock is always held, so it cannot tell whether images are being moved right now. It is unchanged; the new lock only covers "moving images at this moment".

A step that is skipped does not continue the chain. The chain is scheduled again once its transfer lock expires (10 minutes), finds nothing left to move after the command, and closes the transfer.

Tests

  • tests/test-media.php:
    • a data provider for can_transfer_images() with offload on, a rollback in progress, and both off, for both actions;
    • the real WP-CLI command, run through a small WP-CLI stand-in (tests/wp-cli-stub.php, where WP_CLI::error() throws): rollback_images restores the images with offload_media off and a rollback in progress; it is not stopped by the offload limit flag; move_to_optimole still stops when the account is at its limit; rollback_images is refused when nothing was offloaded;
    • the lock, held from a second database connection: the command is refused and moves no image, then runs once it is free; a background step moves nothing and schedules nothing while it is held, then runs; the command runs while a background transfer is only scheduled and leaves that transfer lock alone; the lock is free again after each exit of the command (done, offload limit, nothing to move); it is granted when the GET_LOCK query fails; it is asked after send_reads_to_masters() where the database class has it, and its release is registered on shutdown.
  • Mutations: the settings gate (old strict gate for rollback, rollback always allowed, offload relaxed) and the command (old gate at the call site, limit flag stopping a rollback again, limit never stopping an offload) each fail a test. So do ten mutations of the lock handling (the command or the step not taking the lock, the step keeping it, no fallback without advisory locks, the lock always granted, the command keeping it at each of its three exits, reads not sent to the primary, and no release on shutdown).

Live run

Local site optimolewp.test (not connected to Optimole), offload_media=disabled, rollback_status=enabled:

State development This branch
Nobody is moving images (a rollback is only scheduled, as with a stuck cron), rollback_images Error: You need to have the offload_media option enabled … starts rolling back
A second process holds the "moving images" lock, rollback_images refused (same error) Error: Another image transfer is running. Wait for it to finish, then run this command again.
That process ended without releasing the lock, rollback_images refused (same error) starts rolling back (the database freed the lock)
rollback_status=disabled, rollback_images refused refused
move_to_optimole refused refused

The test site is not connected, so the rollback itself then stopped at the first image with the SDK's "Please initialize the Optimole SDK first", the same as development once its gate is open. On a connected site the SDK is initialized.

QA

  1. On a connected site with offloaded images, add define( 'DISABLE_WP_CRON', true ); to wp-config.php (a stuck cron), then choose Transfer back from Optimole in WP Admin → Optimole → Settings → Image Storage. The progress stays at 0%.

  2. Run wp optimole media rollback_images on the server.

    Expect: the command shows a progress bar and finishes with "All images have been uploaded to your media library". The images are back in wp-content/uploads.

  3. Remove the constant, start an offload, and start Transfer back from Optimole again. While the progress moves, run the command a few times.

    Expect: either "Another image transfer is running. Wait for it to finish, then run this command again." (a background step is moving images at that moment), or the command runs and finishes the transfer (it started between two steps; the background steps then do nothing). In both cases every image is restored once, with no -1 copies in wp-content/uploads.

  4. With Image Storage off and no transfer running, run the command again.

    Expect: "You need to have the offload_media option enabled in order to use this command".

Verification (head 0e438a7d)

Check Result
PHPUnit, full suite, PHP 8.4 (WP 7.1.3) PASS: 381 tests (baseline 364 + 17 new)
tests/test-media.php run alone PASS: 40 tests
phpcs, phpstan (build-only include errors excluded locally; CI builds assets first) PASS
Codex review (gpt-6-astra, high) Round 1 (9e3bc007): the command itself and the limit exemption were not exercised (now covered by running the real command). Round 2 (13afcb55): no findings. Round 3 (e723e58d): a first lock fix, taking over the transient lock, was not atomic (replaced by the advisory lock). Round 4 (99891d97): with HyperDB the lock query was not pinned to one server, and a fatal error could leave the lock on a persistent connection (both fixed). Round 5 (afd49c20): no findings. Round 6 (0e438a7d): no findings.
pirate-bot Round 1 (13afcb55): changes requested, the command could overlap the running background transfer (fixed with the advisory lock). Round 2 (0e438a7d): approved.
CI on 0e438a7d PASS: PHPCS, PHPStan, PHPUnit on PHP 8.0–8.3, ESLint and Jest, e2e, build, translation

Not proven

  • A full CLI rollback on a connected site with offloaded images.
  • A real HyperDB or LudicrousDB setup: the test uses a stand-in database class and checks the order of the calls.
  • Several writable primaries that do not share advisory locks (multi-primary clusters): two runs on different primaries can still both get the lock.

🤖 Generated with Claude Code

"Transfer back from Optimole" turns offload_media off when it starts the
rollback, and `wp optimole media rollback_images` refused to run whenever
offload_media was off. A rollback may now run while one is in progress
(is_offload_enabled(), as the Abilities API already does); moving images
to Optimole still needs offload_media on. The new
Optml_Settings::can_transfer_images() holds that rule so it can be tested.

The command also stopped a rollback when the offload limit flag was set;
the limit only concerns uploads, so it now stops the offload only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pirate-bot

pirate-bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Plugin build for 0e438a7 is ready 🛎️!

The tests only covered Optml_Settings::can_transfer_images(), so the
command could go back to its old checks unnoticed. A small WP-CLI stand-in
(tests/wp-cli-stub.php) lets the real command run: it continues a rollback
started from the dashboard, is not stopped by the offload limit flag, still
stops an offload when the account is at its limit, and is refused when
nothing was offloaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@selul
selul requested a review from pirate-bot October 11, 2026 13:03

@pirate-bot pirate-bot 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.

🟡 Changes requested

The CLI rollback can now overlap the background rollback and restore the same image twice, so make the two runners exclusive.

Validation details
  • Files reviewed: 4/4 changed files.
  • Test_Media:: passed at HEAD on PHP 8.3 and WP 7.1.3. On the base tree, 8 new tests failed. This A/B run has no run record.
  • Two new tests pass on the base tree. They fail after a mutation of the changed lines: cd /workspace/checks/C6/mut && vendor/bin/phpunit --do-not-cache-result --filter 'Test_Media::test_cli_(offload_stops_at… printed Tests: 2, Assertions: 2, Failures: 2. (run R2)
  • A rollback without overlap gave cd /workspace/checks/F1/tree && NESTED=0 vendor/bin/phpunit --do-not-cache-result --filter 'Test_Overlap::test_overlap_r… printed ORPHAN FILES (not in any attachment metadata): 4 (run R5)
  • A second rollback inside the first one's download gave cd /workspace/checks/F1/tree && NESTED=1 vendor/bin/phpunit --do-not-cache-result --filter 'Test_Overlap::test_overlap_r… printed ORPHAN FILES (not in any attachment metadata): 10 (run R6)
  • Security: the changed gate is reachable only from WP-CLI.
Untested areas
  • The overlap ran in one process through an HTTP hook, not as two processes.
  • A full CLI rollback on a connected site was not run.

🤖 Automated review · run code-review-agent_6acb891a919485.96016820.


🤖 Review agent — review posted ✅ on 13afcb55 · changes requested · 1 finding · 29 min

Run code-review-agent_6acb891a919485.96016820 · trail

Comment thread inc/cli/cli_media.php
selul and others added 4 commits October 11, 2026 18:00
…sfer

With the gate open, `wp optimole media rollback_images` could move the
same images as the dashboard's background chain at the same time, and
restore them twice.

The background chain takes the transfer lock when it is scheduled, and
maybe_reschedule() takes it again on every request while a transfer is
in progress, also where cron never runs the chain. So the command cannot
simply wait for the lock: it would never run on a site with a stuck cron.
The lock now records when a step ran (on renew). The command is refused
while a background step ran within the lock's lifetime, and otherwise
takes the lock over: a background step that fires later cannot renew it
and stops, and nothing is rescheduled while the command holds it. The
command renews the lock per batch and releases it on every exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The takeover of the transfer lock was a read-then-write: a background
step could renew the lock between the two, and both runs then moved the
same images.

The transfer lock (a transient) goes back to what it was: it says which
chain owns a transfer, and is taken when the chain is scheduled. A second
lock now says who is moving images at this moment: a MySQL advisory lock
(GET_LOCK), which is atomic and freed by the database when the request
ends, also after a crash. A background step takes it for its batch and
does nothing when it cannot; the WP-CLI command takes it for its whole
run and stops with a message when it cannot. Where the database has no
advisory locks, the lock is granted and transfers work as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tdown

With several database servers (HyperDB, LudicrousDB) the lock query could go
to a different server in each run, so two runs could both get the lock. Reads
are now sent to the primary before the lock is asked.

A fatal error skips the cleanup of the run. On a persistent connection the
lock would then outlive the request, so it is also released on shutdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The message of the refused command is plain English, like the other WP-CLI
messages of the plugin, so the fix adds no string to translate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@selul
selul requested a review from pirate-bot October 11, 2026 17:22

@pirate-bot pirate-bot 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.

🟢 Approved

The PR passes because the advisory lock now keeps the command and the background steps from moving images at the same time.

Validation details
  • Files reviewed: 5/5 changed files.
  • PHP 8.3 with MariaDB gave cd /repo; set -o pipefail; timeout 300 vendor/bin/phpunit --do-not-cache-result --filter 'Test_Media::' 2>&1 | tee /work… printed OK (40 tests, 170 assertions) (run R4).
  • Both lock checks were replaced with if ( false ). The lock tests then failed: cd /workspace/checks/AB; rm -rf mut; cp -a /repo mut; cd mut; python3 - <<'EOF' import re p='inc/cli/cli_media.php'; s=o… printed Tests: 2, Assertions: 5, Failures: 2. (run R1)
  • Security: the new queries use prepare() with no request input. The gate is reachable only from WP-CLI.
Untested areas
  • HyperDB and LudicrousDB were not run.
  • Database drivers without advisory locks were not run.
  • A full rollback on a connected site was not run.
  • The single-image move takes no lock. That path predates this PR.

🤖 Automated review · run code-review-agent_6acbc5be1029f9.63091240.


🤖 Review agent — review posted ✅ on 0e438a7d · approved · 0 findings · 20 min

Run code-review-agent_6acbc5be1029f9.63091240 · trail

@pirate-bot
pirate-bot merged commit da97ddc into development Oct 12, 2026
16 checks passed
@pirate-bot
pirate-bot deleted the bugfix/1161 branch October 12, 2026 10:01
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.

2 participants