Skip to content

Commit 5e3961a

Browse files
committed
fix: recover stale in-progress step actions
1 parent 1d6be74 commit 5e3961a

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

inc/Abilities/Job/RecoverStuckJobsAbility.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public function execute( array $input ): array {
210210
$job_id = (int) $job->job_id;
211211
$job_flow_id = (int) $job->flow_id;
212212

213-
if ( $this->hasActiveStepAction( $job_id ) ) {
213+
if ( $this->hasActiveStepAction( $job_id, $timeout_hours ) ) {
214214
++$skipped;
215215
$jobs[] = array(
216216
'job_id' => $job_id,
@@ -444,9 +444,10 @@ private function getTerminalBackedInProgressActions( ?int $flow_id ): array {
444444
* original job row is old.
445445
*
446446
* @param int $job_id Job ID.
447-
* @return bool True when a pending/in-progress step action exists.
447+
* @param int $timeout_hours Hours before in-progress actions are considered stale.
448+
* @return bool True when a pending or fresh in-progress step action exists.
448449
*/
449-
private function hasActiveStepAction( int $job_id ): bool {
450+
private function hasActiveStepAction( int $job_id, int $timeout_hours ): bool {
450451
global $wpdb;
451452

452453
if ( $job_id <= 0 ) {
@@ -459,7 +460,7 @@ private function hasActiveStepAction( int $job_id ): bool {
459460
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is generated from the WP prefix.
460461
$actions = $wpdb->get_results(
461462
$wpdb->prepare(
462-
"SELECT action_id, args
463+
"SELECT action_id, args, status, scheduled_date_gmt, last_attempt_gmt
463464
FROM {$actions_table}
464465
WHERE hook = %s
465466
AND status IN ( %s, %s )
@@ -472,9 +473,27 @@ private function hasActiveStepAction( int $job_id ): bool {
472473
);
473474
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
474475

476+
$timeout_seconds = max( 1, $timeout_hours ) * HOUR_IN_SECONDS;
477+
$now_gmt = strtotime( current_time( 'mysql', true ) );
478+
475479
foreach ( $actions as $action ) {
476480
if ( $job_id === $this->extractActionJobId( (string) ( $action->args ?? '' ) ) ) {
477-
return true;
481+
if ( 'pending' === (string) $action->status ) {
482+
return true;
483+
}
484+
485+
$last_attempt = (string) ( $action->last_attempt_gmt ?? '' );
486+
$scheduled = (string) ( $action->scheduled_date_gmt ?? '' );
487+
$reference = $last_attempt && '0000-00-00 00:00:00' !== $last_attempt ? $last_attempt : $scheduled;
488+
$started_at = $reference ? strtotime( $reference ) : false;
489+
490+
if ( false === $started_at || false === $now_gmt ) {
491+
return true;
492+
}
493+
494+
if ( ( $now_gmt - $started_at ) < $timeout_seconds ) {
495+
return true;
496+
}
478497
}
479498
}
480499

tests/recover-stuck-active-action-guard-smoke.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@ function assert_recover_stuck_guard_smoke( string $name, bool $condition, string
2828

2929
echo "Case 1: timed-out recovery skips jobs with active step actions\n";
3030
assert_recover_stuck_guard_smoke( 'active step guard method exists', str_contains( $source, 'private function hasActiveStepAction' ) );
31-
assert_recover_stuck_guard_smoke( 'timeout loop invokes active step guard before dry-run timeout', strpos( $timeout_loop, '$this->hasActiveStepAction( $job_id )' ) < strpos( $timeout_loop, 'if ( $dry_run )' ) );
31+
assert_recover_stuck_guard_smoke( 'timeout loop invokes active step guard before dry-run timeout', strpos( $timeout_loop, '$this->hasActiveStepAction( $job_id, $timeout_hours )' ) < strpos( $timeout_loop, 'if ( $dry_run )' ) );
3232
assert_recover_stuck_guard_smoke( 'guard records skipped status', str_contains( $source, "'status' => 'skipped'") && str_contains( $source, 'Pending or in-progress Action Scheduler step action exists' ) );
3333

3434
echo "Case 2: guard is limited to executable Data Machine step actions\n";
3535
assert_recover_stuck_guard_smoke( 'guard queries datamachine_execute_step', str_contains( $source, 'datamachine_execute_step' ) );
3636
assert_recover_stuck_guard_smoke( 'guard checks pending and in-progress actions', str_contains( $source, "'pending'") && str_contains( $source, "'in-progress'") );
3737
assert_recover_stuck_guard_smoke( 'guard confirms exact job id from action args', str_contains( $source, '$this->extractActionJobId' ) );
3838

39+
echo "Case 3: stale in-progress step actions do not block timeout recovery forever\n";
40+
assert_recover_stuck_guard_smoke( 'guard receives timeout window', str_contains( $source, 'private function hasActiveStepAction( int $job_id, int $timeout_hours )' ) );
41+
assert_recover_stuck_guard_smoke( 'guard reads action attempt timestamps', str_contains( $source, 'last_attempt_gmt' ) && str_contains( $source, 'scheduled_date_gmt' ) );
42+
assert_recover_stuck_guard_smoke( 'pending actions remain guarded unconditionally', str_contains( $source, 'if ( \'pending\' === (string) $action->status )' ) );
43+
assert_recover_stuck_guard_smoke( 'old in-progress actions can fall through', str_contains( $source, '( $now_gmt - $started_at ) < $timeout_seconds' ) );
44+
3945
echo "\nRecover-stuck active action guard smoke complete: {$total} assertions, {$failed} failures.\n";
4046
if ( $failed > 0 ) {
4147
exit( 1 );

0 commit comments

Comments
 (0)