Skip to content
Open

Release #1390

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions classes/Visualizer/ActionScheduler/Store.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/**
* Action Scheduler store that tolerates a lost race when marking an action failed.
*
* `ActionScheduler_DBStore::mark_failure()` throws when its UPDATE changes no
* row. That happens when another process deleted the action, or already marked
* it failed: the queue cleaner and the queue runner can both reach the same
* action, and WP-Cron's queue run takes no lock. Nothing catches the exception,
* so the whole queue run ends with a fatal error (#1369, upstream
* woocommerce/action-scheduler#970).
*
* Registered through the `action_scheduler_store_class` filter in `index.php`.
*
* @category Visualizer
* @package ActionScheduler
*
* @since 4.0.9
*/
class Visualizer_ActionScheduler_Store extends ActionScheduler_DBStore {

/**
* Mark an action failed, and accept that another process got there first.
*
* A database error still throws, so real failures stay visible.
*
* @param int $action_id Action ID.
*
* @throws InvalidArgumentException When the UPDATE itself failed.
*
* @return void
*/
public function mark_failure( $action_id ) {
global $wpdb;

// Same UPDATE as the parent. Zero rows means the row was deleted or
// already failed; only `false` is a database error.
$updated = $wpdb->update(
$wpdb->actionscheduler_actions,
array( 'status' => self::STATUS_FAILED ),
array( 'action_id' => $action_id ),
array( '%s' ),
array( '%d' )
);
if ( false === $updated ) {
/* translators: %d is the action ID */
throw new InvalidArgumentException( sprintf( __( 'Unable to mark action %d as failed.', 'visualizer' ), $action_id ) );
}
}
}
20 changes: 10 additions & 10 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ function () {
require_once $action_scheduler_file;
}

// After Action Scheduler's own data controller, which sets the class at 100.
add_filter( 'action_scheduler_store_class', 'visualizer_action_scheduler_store_class', 200 );

add_filter( 'themeisle_sdk_products', 'visualizer_register_sdk', 10, 1 );
add_filter( 'pirate_parrot_log', 'visualizer_register_parrot', 10, 1 );
add_filter(
Expand Down Expand Up @@ -254,6 +257,20 @@ function visualizer_can_use_action_scheduler() {
return isset( $wpdb ) && is_callable( array( $wpdb, 'db_server_info' ) );
}

/**
* Use a store that survives a lost race when marking an action failed.
*
* Only replaces Action Scheduler's own database store. Another plugin's store
* and the legacy post store, which does not have the problem, are left alone.
*
* @param string $class_name Store class Action Scheduler resolved.
*
* @return string
*/
function visualizer_action_scheduler_store_class( $class_name ) {
return 'ActionScheduler_DBStore' === $class_name ? 'Visualizer_ActionScheduler_Store' : $class_name;
}

/**
* Registers with the SDK
*
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ parameters:
- %currentWorkingDirectory%/vendor/neitanod/forceutf8
- %currentWorkingDirectory%/vendor/openspout/openspout
- %currentWorkingDirectory%/vendor/codeinwp/themeisle-sdk
- %currentWorkingDirectory%/vendor/woocommerce/action-scheduler
excludePaths:
- classes/Visualizer/Gutenberg/build (?)
- classes/Visualizer/GutenChartBuilder/build (?)
Expand Down
8 changes: 8 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ function _manually_load_plugin() {
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
// Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';

// The framework snapshots hooks at the first test and restores that snapshot
// after every test. WP_Ajax_UnitTestCase removes these once per class, which
// only holds when an AJAX class runs first. Remove them here so no test file
// order makes AJAX tests call api.wordpress.org.
remove_action( 'admin_init', '_maybe_update_core' );
remove_action( 'admin_init', '_maybe_update_plugins' );
remove_action( 'admin_init', '_maybe_update_themes' );
activate_plugin( 'visualizer/index.php' );
global $current_user;
$current_user = new WP_User( 1 );
Expand Down
Loading
Loading