Skip to content

Commit 177e56f

Browse files
committed
Changes how we cancel pending events. We now use clear all and also hook in on cancel to fully remove them
1 parent ce31692 commit 177e56f

2 files changed

Lines changed: 176 additions & 10 deletions

File tree

src/Event/Process_Local_Post_Event.php

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,18 @@ public static function add_to_queue( int $post_id ): void {
6464
* @return void
6565
*/
6666
public static function add_to_queue_with_delay( int $post_id, int $delay = 1 * \HOUR_IN_SECONDS ): void {
67-
// If there is already a scheduled event with this post ID, cancel it.
68-
as_unschedule_action( self::HANDLE, array( 'post_id' => $post_id ) );
67+
// Ensure there are no duplicate events in the queue for this post ID.
68+
self::ensure_single_event( $post_id );
6969

7070
$time_to_run = time() + $delay;
7171
as_schedule_single_action(
7272
$time_to_run,
7373
self::HANDLE,
7474
array(
7575
'post_id' => $post_id,
76-
)
76+
),
77+
'',
78+
true
7779
);
7880
}
7981

@@ -123,5 +125,51 @@ public function __invoke( int $post_id ): void {
123125

124126
// Add the last updated meta key.
125127
update_post_meta( $post_id, Settings::OWN_LINK_LAST_PROCESSED, time() );
128+
129+
// Ensure there are no duplicate events in the queue for this post ID.
130+
self::ensure_single_event( $post_id );
131+
}
132+
133+
/**
134+
* Ensure that any duplicated events are removed from the queue, and that there is only ever one event per post ID in the queue.
135+
*
136+
* @since 1.3.5
137+
*
138+
* @param integer $post_id The post ID.
139+
*
140+
* @return void
141+
*/
142+
private static function ensure_single_event( int $post_id ): void {
143+
add_action( 'action_scheduler_canceled_action', array( self::class, 'handle_hard_delete' ) );
144+
145+
// Unschedule any existing events for this post ID.
146+
as_unschedule_all_actions( self::HANDLE, array( 'post_id' => $post_id ) );
147+
148+
// Remove the bulk cancel action to prevent interference with other events.
149+
remove_action( 'action_scheduler_canceled_action', array( self::class, 'handle_hard_delete' ) );
150+
}
151+
152+
/**
153+
* Handle hard delete of events.
154+
*
155+
* @param integer $action_id The action ID.
156+
*
157+
* @return void
158+
*/
159+
public static function handle_hard_delete( int $action_id ): void {
160+
// Bail if not set.
161+
if ( ! class_exists( 'ActionScheduler' ) ) {
162+
return;
163+
}
164+
165+
// Get the action.
166+
$store = \ActionScheduler::store();
167+
$action = $store->fetch_action( $action_id );
168+
169+
// If we have the right action.
170+
if ( self::HANDLE === $action->get_hook() ) {
171+
// Delete the action.
172+
$store->delete_action( $action_id );
173+
}
126174
}
127175
}

tests/Event/Test_Process_Local_Post_Event.php

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,82 @@ public function test_add_local_post_to_queue(): void {
7575
* @return void
7676
*/
7777
public function test_add_local_post_to_queue_multiple_times(): void {
78-
$post_id = 1;
78+
$post_id = 999;
7979

8080
// Create the event.
8181
$event = new Process_Local_Post_Event();
8282

83-
$event::add_to_queue( $post_id );
84-
$event::add_to_queue( $post_id );
85-
$event::add_to_queue( $post_id );
86-
$event::add_to_queue( $post_id );
87-
$event::add_to_queue( $post_id );
83+
Process_Local_Post_Event::add_to_queue( $post_id );
84+
Process_Local_Post_Event::add_to_queue( $post_id );
85+
Process_Local_Post_Event::add_to_queue( $post_id );
86+
Process_Local_Post_Event::add_to_queue( $post_id );
87+
Process_Local_Post_Event::add_to_queue( $post_id );
8888

8989
// Check that the action has been added to the queue.
90-
$actions = $this->wpdb->get_results( "SELECT * FROM {$this->wpdb->prefix}actionscheduler_actions where status='pending'" );
90+
$actions = $this->wpdb->get_results( "SELECT * FROM {$this->wpdb->prefix}actionscheduler_actions" );
91+
$this->assertCount( 1, $actions );
92+
93+
// Check to ensure the hard delete handler is removed, to prevent interference with other events.
94+
$this->assertFalse( has_action( 'action_scheduler_canceled_action', array( Process_Local_Post_Event::class, 'handle_hard_delete' ) ) );
95+
}
96+
97+
/**
98+
* @testdox If there are many pending rows for the same post, scheduling should collapse them to one.
99+
*
100+
* @see https://github.com/a8cteam51/internet-archive-wayback-machine-link-fixer/issues/294
101+
* @return void
102+
*/
103+
public function test_add_local_post_to_queue_with_existing_duplicates(): void {
104+
$post_id = 42;
105+
106+
// Insert 10 duplicate pending scheduled actions for the same post to
107+
// simulate the DB flood described in the issue.
108+
$table = $this->wpdb->prefix . 'actionscheduler_actions';
109+
$now = time();
110+
for ( $i = 0; $i < 10; $i++ ) {
111+
$ts = $now + $i;
112+
113+
$schedule = \str_replace(
114+
'11111111',
115+
"$ts",
116+
'"O:30:"ActionScheduler_SimpleSchedule":2:{s:22:"\x00*\x00scheduled_timestamp";i:11111111;s:41:"\x00ActionScheduler_SimpleSchedule\x00timestamp";i:11111111;}"',
117+
);
118+
119+
$this->wpdb->insert(
120+
$table,
121+
array(
122+
'action_id' => time() + $i,
123+
'hook' => Process_Local_Post_Event::HANDLE,
124+
'status' => 'pending',
125+
'scheduled_date_gmt' => gmdate( 'Y-m-d H:i:s', $ts ),
126+
'scheduled_date_local' => gmdate( 'Y-m-d H:i:s', $ts ),
127+
'priority' => 10,
128+
'args' => wp_json_encode( array( 'post_id' => $post_id ) ),
129+
'schedule' => $schedule,
130+
'group_id' => 3,
131+
'attempts' => 0,
132+
'last_attempt_gmt' => '0000-00-00 00:00:00',
133+
'last_attempt_local' => '0000-00-00 00:00:00',
134+
'claim_id' => 0,
135+
'extended_args' => null,
136+
),
137+
array( '%s', '%s', '%s', '%s', '%d', '%s', '%d', '%d', '%s', '%s', '%d', '%s' )
138+
);
139+
}
140+
141+
// Call the method which should remove duplicates and add a single
142+
// scheduled action.
143+
Process_Local_Post_Event::add_to_queue_with_delay( $post_id, 0 );
144+
145+
// Check there is only one pending action for that post.
146+
$actions = $this->wpdb->get_results(
147+
$this->wpdb->prepare(
148+
"SELECT * FROM {$table} WHERE status = 'pending' AND hook = %s AND args = %s",
149+
Process_Local_Post_Event::HANDLE,
150+
wp_json_encode( array( 'post_id' => $post_id ) )
151+
)
152+
);
153+
91154
$this->assertCount( 1, $actions );
92155
}
93156

@@ -265,4 +328,59 @@ function ( string $url ) use ( &$processed ): string {
265328
$this->assertGreaterThan( -10, abs( time() - $meta ) );
266329
}
267330

331+
/**
332+
* @testdox When we run the event, any other pending events for the same post should be removed from the queue.
333+
*
334+
* @see https://github.com/a8cteam51/internet-archive-wayback-machine-link-fixer/issues/294
335+
*
336+
* @return void
337+
*/
338+
public function test_process_local_post_event_removes_duplicates_from_queue(): void {
339+
// Mock the snapshot client to return success.
340+
$this->create_service(
341+
true,
342+
function ( array $config ): array {
343+
$config['snapshot']
344+
->method( 'create_snapshot' )
345+
->willReturnCallback( fn( $url ) => $url );
346+
return $config;
347+
}
348+
);
349+
350+
// Create a post.
351+
$post_id = \wp_insert_post(
352+
array(
353+
'post_title' => 'Test Post',
354+
'post_content' => 'Test Content',
355+
'post_status' => 'publish',
356+
)
357+
);
358+
359+
// Add 10 events to the queue for the same post ID.
360+
$now = time();
361+
for ( $i = 0; $i < 10; $i++ ) {
362+
$ts = $now + $i;
363+
364+
as_schedule_single_action(
365+
$ts,
366+
Process_Local_Post_Event::HANDLE,
367+
array(
368+
'post_id' => $post_id,
369+
)
370+
);
371+
}
372+
373+
// Add the event to the queue.
374+
Process_Local_Post_Event::add_to_queue_with_delay( $post_id, 0 );
375+
376+
// Process the event.
377+
$event = new Process_Local_Post_Event();
378+
$event( $post_id );
379+
380+
// Check there is NO pending actions for that post.
381+
$actions = $actions = $this->wpdb->get_results( "SELECT * FROM {$this->wpdb->prefix}actionscheduler_actions where status='pending'" );
382+
$this->assertEmpty( $actions );
383+
384+
385+
}
268386
}

0 commit comments

Comments
 (0)