Repository navigation
fix: let WP-CLI continue a rollback started from the dashboard - #1173
Merged
Merged
Conversation
"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>
Collaborator
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>
pirate-bot
requested changes
Oct 11, 2026
Collaborator
There was a problem hiding this comment.
🟡 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…printedTests: 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…printedORPHAN 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…printedORPHAN 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
…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>
pirate-bot
approved these changes
Oct 11, 2026
Collaborator
There was a problem hiding this comment.
🟢 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…printedOK (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…printedTests: 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue: #1161
"Transfer back from Optimole" in the dashboard turns
offload_mediaoff and starts the rollback.wp optimole media rollback_imagesrefused to run wheneveroffload_mediawas 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:rollbackis allowed whenis_offload_enabled(), i.e.offload_mediais on or a rollback is in progress. This is the check the Abilities APImove_mediaalready uses for the same action.offloadstill needsoffload_mediaon.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 (MySQLGET_LOCK, without waiting) while they move images:shutdownaction, for a fatal error on a persistent database connection;send_reads_to_masters()), so every run asks the same server;Note
The command is still refused when
offload_mediais 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:can_transfer_images()with offload on, a rollback in progress, and both off, for both actions;tests/wp-cli-stub.php, whereWP_CLI::error()throws):rollback_imagesrestores the images withoffload_mediaoff and a rollback in progress; it is not stopped by the offload limit flag;move_to_optimolestill stops when the account is at its limit;rollback_imagesis refused when nothing was offloaded;GET_LOCKquery fails; it is asked aftersend_reads_to_masters()where the database class has it, and its release is registered onshutdown.Live run
Local site
optimolewp.test(not connected to Optimole),offload_media=disabled,rollback_status=enabled:developmentrollback_imagesError: You need to have the offload_media option enabled …rollback_imagesError: Another image transfer is running. Wait for it to finish, then run this command again.rollback_imagesrollback_status=disabled,rollback_imagesmove_to_optimoleThe 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
developmentonce its gate is open. On a connected site the SDK is initialized.QA
On a connected site with offloaded images, add
define( 'DISABLE_WP_CRON', true );towp-config.php(a stuck cron), then choose Transfer back from Optimole inWP Admin → Optimole → Settings → Image Storage. The progress stays at 0%.Run
wp optimole media rollback_imageson 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.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
-1copies inwp-content/uploads.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)tests/test-media.phprun alonephpcs,phpstan(build-only include errors excluded locally; CI builds assets first)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.13afcb55): changes requested, the command could overlap the running background transfer (fixed with the advisory lock). Round 2 (0e438a7d): approved.0e438a7dNot proven
🤖 Generated with Claude Code