PluginProbe
WANotifier for Forms and Actions / 3.1.1
WANotifier for Forms and Actions v3.1.1
3.1.1 3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 1.0.2 All 67 releases
notifier / libraries / action-scheduler / classes / ActionScheduler_QueueCleaner.php

ActionScheduler_QueueCleaner.php in WANotifier for Forms and Actions 3.1.1, at libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php

403 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class ActionScheduler_QueueCleaner
5 */
6 class ActionScheduler_QueueCleaner {
7 /**
8 * The cleaner action hook is scheduled to run daily to initiate cleanup.
9 *
10 * @var string
11 */
12 private const RUN_SCHEDULED_CLEANER_HOOK = 'action_scheduler_run_actions_cleanup_hook';
13
14 /**
15 * Hook used to keep deleting old actions in batches, with at most one continuation pending at a time.
16 *
17 * @var string
18 */
19 private const CONTINUE_SCHEDULED_CLEANER_HOOK = 'action_scheduler_continue_actions_cleanup_hook';
20
21 /**
22 * The batch size.
23 *
24 * @var int
25 */
26 protected $batch_size;
27
28 /**
29 * ActionScheduler_Store instance.
30 *
31 * @var ActionScheduler_Store
32 */
33 private $store = null;
34
35 /**
36 * 31 days in seconds.
37 *
38 * @var int
39 */
40 private $month_in_seconds = 2678400;
41
42 /**
43 * Default list of statuses purged by the cleaner process.
44 *
45 * @var string[]
46 */
47 private $default_statuses_to_purge = array(
48 ActionScheduler_Store::STATUS_COMPLETE,
49 ActionScheduler_Store::STATUS_CANCELED,
50 );
51
52 /**
53 * ActionScheduler_QueueCleaner constructor.
54 *
55 * @param ActionScheduler_Store|null $store The store instance.
56 * @param int $batch_size The batch size.
57 */
58 public function __construct( ?ActionScheduler_Store $store = null, $batch_size = 20 ) {
59 $this->store = $store ? $store : ActionScheduler_Store::instance();
60 $this->batch_size = $batch_size;
61 }
62
63 /**
64 * Registers action hooks to perform action deletions as a separate task.
65 *
66 * @since 4.0.0
67 * @internal
68 *
69 * @return void
70 */
71 public function register_cleaner_hooks() {
72 add_action( self::RUN_SCHEDULED_CLEANER_HOOK, array( $this, 'delete_old_actions' ) );
73 add_action( self::CONTINUE_SCHEDULED_CLEANER_HOOK, array( $this, 'delete_old_actions' ) );
74 add_action( 'action_scheduler_ensure_recurring_actions', array( $this, 'register_recurring_actions' ) );
75 }
76
77 /**
78 * Register the recurring action deletion task.
79 *
80 * @since 4.0.0
81 * @internal
82 *
83 * @return void
84 */
85 public function register_recurring_actions() {
86 if ( ! as_has_scheduled_action( self::RUN_SCHEDULED_CLEANER_HOOK ) ) {
87 $date = ActionScheduler_TimezoneHelper::set_local_timezone( new DateTime() )->modify( 'tomorrow 3am' );
88 as_schedule_recurring_action(
89 $date->getTimestamp(),
90 DAY_IN_SECONDS,
91 self::RUN_SCHEDULED_CLEANER_HOOK,
92 array(),
93 'ActionScheduler',
94 true,
95 0
96 );
97 }
98 }
99
100 /**
101 * Performs action deletions by aggregating configurations and coordinating clean_actions as needed.
102 *
103 * @since 4.0.0 by default, failed actions are removed after three months.
104 * @return array
105 */
106 public function delete_old_actions() {
107 /**
108 * Filter the minimum scheduled date age for action deletion.
109 *
110 * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted.
111 */
112 $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds );
113 $lifespan = is_numeric( $lifespan ) ? max( 0, (int) $lifespan ) : $this->month_in_seconds;
114
115 /**
116 * Set the retention period in seconds for actions with a failed status. If the action_scheduler_default_cleaner_statuses filter includes
117 * a failed status, this filter result will be ignored, and the retention period for failed actions will match that of other statuses.
118 *
119 * @param int $retention_period Retention period in seconds.
120 */
121 $lifespan_failed = apply_filters( 'action_scheduler_retention_period_for_failed', 3 * $this->month_in_seconds );
122 $lifespan_failed = is_numeric( $lifespan_failed ) ? max( 0, (int) $lifespan_failed ) : 3 * $this->month_in_seconds;
123 // We considered 12-month, 3-month, and 1-month options for failed action retention and selected a 3-month period
124 // to align with the quarterly accounting cycle. Store owners may adjust the retention period to achieve PCI DSS
125 // compliance or to align with a different accounting cycle, as needed.
126
127 try {
128 $cutoff_failed = as_get_datetime_object( $lifespan_failed . ' seconds ago' );
129 $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' );
130 } catch ( Exception $e ) {
131 _doing_it_wrong(
132 __METHOD__,
133 sprintf(
134 /* Translators: %s is the exception message. */
135 esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'action-scheduler' ),
136 esc_html( $e->getMessage() )
137 ),
138 '3.5.5'
139 );
140
141 return array();
142 }
143
144 /**
145 * Filter the statuses when cleaning the queue.
146 *
147 * @param string[] $default_statuses_to_purge Action statuses to clean.
148 */
149 $statuses_to_purge = apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge );
150 // Only an explicit empty array disables the purge; a non-array (e.g. a filter that forgot to return) falls back to the defaults.
151 if ( ! is_array( $statuses_to_purge ) ) {
152 $statuses_to_purge = $this->default_statuses_to_purge;
153 }
154
155 /**
156 * Filter whether failed actions are purged. Return false to disable failed action cleanup.
157 *
158 * @since 4.0.0
159 *
160 * @param bool $enabled Whether failed actions should be purged. Default true.
161 */
162 $clean_failed = (bool) apply_filters( 'action_scheduler_enable_failed_action_cleanup', true );
163
164 $deleted_failed_entries = array();
165 // Backward compatibility note: if store already purging the failed statuses, don't change the behaviour.
166 if ( $clean_failed && ! in_array( ActionScheduler_Store::STATUS_FAILED, $statuses_to_purge, true ) ) {
167 // Use a fixed default batch size to ensure that the cleanup of failed actions does not interfere with the regular cleanup.
168 $deleted_failed_entries = $this->clean_actions( array( ActionScheduler_Store::STATUS_FAILED ), $cutoff_failed, 20 );
169 }
170
171 $deleted_entries = array();
172 if ( ! empty( $statuses_to_purge ) ) {
173 $deleted_entries = $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() );
174 }
175
176 return array_merge( $deleted_failed_entries, $deleted_entries );
177 }
178
179 /**
180 * Delete selected actions based on status and date. The function's behavior depends on the context:
181 * - For scheduled cleanup actions, the function operates within execution budget constraints optimized for high-traffic stores.
182 * - Otherwise, it strictly follows the provided parameters without the scheduled cleanup optimizations.
183 *
184 * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete.
185 * @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago.
186 * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20.
187 * @param string $context Calling process context. Defaults to `old`.
188 *
189 * @return array Actions deleted.
190 */
191 public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) {
192 $batch_size = ! is_null( $batch_size ) ? $batch_size : $this->batch_size;
193 $cutoff = ! is_null( $cutoff_date ) ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' );
194 $lifespan = time() - $cutoff->getTimestamp();
195 $statuses_to_purge = empty( $statuses_to_purge ) ? $this->default_statuses_to_purge : $statuses_to_purge;
196
197 // When deletion is performed as a separate action, we can enforce a minimum batch size to achieve consistent deletion throughput.
198 // For inline cleanup during a queue run, the batch size should remain unchanged to avoid increasing the process footprint.
199 $is_scheduled_cleanup = doing_action( self::RUN_SCHEDULED_CLEANER_HOOK )
200 || doing_action( self::CONTINUE_SCHEDULED_CLEANER_HOOK );
201 // 250 balances replication safety, backlog clearance speed, and claim slot duration on high-volume stores.
202 $iteration_batch_size = $is_scheduled_cleanup ? max( 250, $batch_size ) : $batch_size;
203 $iteration_unused_budget = 0;
204 $continue_scheduled_cleanup = false;
205 if ( $is_scheduled_cleanup ) {
206 // Sort the statuses to optimize execution budget usage based on the typical status distribution.
207 usort(
208 $statuses_to_purge,
209 static function( $a, $b ) {
210 // Place the 'canceled' status first to help ensure that any unspent execution budget can be used for processing other statuses.
211 if ( ActionScheduler_Store::STATUS_CANCELED === $a ) {
212 return -1;
213 }
214 if ( ActionScheduler_Store::STATUS_CANCELED === $b ) {
215 return 1;
216 }
217
218 // Place the 'complete' status at the end to use any remaining execution budget for processing.
219 if ( ActionScheduler_Store::STATUS_COMPLETE === $a ) {
220 return 1;
221 }
222 if ( ActionScheduler_Store::STATUS_COMPLETE === $b ) {
223 return -1;
224 }
225
226 return 0;
227 }
228 );
229 }
230
231 $deleted_actions = array();
232 foreach ( $statuses_to_purge as $status ) {
233 $iteration_execution_budget = $iteration_batch_size + $iteration_unused_budget;
234 $actions_to_delete = $this->store->query_actions(
235 array(
236 'status' => $status,
237 'modified' => $cutoff,
238 'modified_compare' => '<=',
239 'per_page' => $iteration_execution_budget,
240 'orderby' => 'none',
241 )
242 );
243 $deleted_actions[] = $this->delete_actions( $actions_to_delete, $lifespan, $context );
244
245 $fetched_actions_count = count( $actions_to_delete );
246 $iteration_unused_budget = $is_scheduled_cleanup ? ( $iteration_execution_budget - $fetched_actions_count ) : 0;
247 $continue_scheduled_cleanup = $continue_scheduled_cleanup || ( $iteration_execution_budget === $fetched_actions_count );
248 }
249
250 // When called from the scheduled cleanup hook, unique=true prevents duplicates at the SQL level. When called
251 // from a continuation, that same flag would match the running entry, so check for a pending continuation first.
252 $called_from_run = doing_action( self::RUN_SCHEDULED_CLEANER_HOOK );
253 if ( $is_scheduled_cleanup && $continue_scheduled_cleanup && ( $called_from_run || ! $this->has_pending_continuation() ) ) {
254 as_schedule_single_action( time(), self::CONTINUE_SCHEDULED_CLEANER_HOOK, array(), 'ActionScheduler', $called_from_run, 0 );
255 }
256
257 return array_merge( array(), ...$deleted_actions );
258 }
259
260 /**
261 * Whether a continuation of the cleanup is already queued.
262 *
263 * @return bool
264 */
265 private function has_pending_continuation() {
266 $pending = as_get_scheduled_actions(
267 array(
268 'hook' => self::CONTINUE_SCHEDULED_CLEANER_HOOK,
269 'status' => ActionScheduler_Store::STATUS_PENDING,
270 'per_page' => 1,
271 ),
272 'ids'
273 );
274
275 return ! empty( $pending );
276 }
277
278 /**
279 * Delete actions.
280 *
281 * @param int[] $actions_to_delete List of action IDs to delete.
282 * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted.
283 * @param string $context Context of the delete request.
284 *
285 * @return int[] Deleted action IDs.
286 */
287 private function delete_actions( array $actions_to_delete, $lifespan, $context = 'old' ) {
288 $deleted_actions = array();
289 foreach ( $actions_to_delete as $action_id ) {
290 try {
291 $this->store->delete_action( $action_id );
292 $deleted_actions[] = $action_id;
293 } catch ( Exception $e ) {
294 /**
295 * Notify 3rd party code of exceptions when deleting a completed action older than the retention period
296 *
297 * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their
298 * actions.
299 *
300 * @param int $action_id The scheduled actions ID in the data store
301 * @param Exception $e The exception thrown when attempting to delete the action from the data store
302 * @param int $lifespan The retention period, in seconds, for old actions
303 * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch
304 * @since 2.0.0
305 */
306 do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) );
307 }
308 }
309 return $deleted_actions;
310 }
311
312 /**
313 * Unclaim pending actions that have not been run within a given time limit.
314 *
315 * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
316 * as a parameter is 10x the time limit used for queue processing.
317 *
318 * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
319 */
320 public function reset_timeouts( $time_limit = 300 ) {
321 $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
322
323 if ( $timeout < 0 ) {
324 return;
325 }
326
327 $cutoff = as_get_datetime_object( $timeout . ' seconds ago' );
328 $actions_to_reset = $this->store->query_actions(
329 array(
330 'status' => ActionScheduler_Store::STATUS_PENDING,
331 'modified' => $cutoff,
332 'modified_compare' => '<=',
333 'claimed' => true,
334 'per_page' => $this->get_batch_size(),
335 'orderby' => 'none',
336 )
337 );
338
339 foreach ( $actions_to_reset as $action_id ) {
340 $this->store->unclaim_action( $action_id );
341 do_action( 'action_scheduler_reset_action', $action_id );
342 }
343 }
344
345 /**
346 * Mark actions that have been running for more than a given time limit as failed, based on
347 * the assumption some uncatchable and unloggable fatal error occurred during processing.
348 *
349 * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
350 * as a parameter is 10x the time limit used for queue processing.
351 *
352 * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes).
353 */
354 public function mark_failures( $time_limit = 300 ) {
355 $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
356
357 if ( $timeout < 0 ) {
358 return;
359 }
360
361 $cutoff = as_get_datetime_object( $timeout . ' seconds ago' );
362 $actions_to_reset = $this->store->query_actions(
363 array(
364 'status' => ActionScheduler_Store::STATUS_RUNNING,
365 'modified' => $cutoff,
366 'modified_compare' => '<=',
367 'per_page' => $this->get_batch_size(),
368 'orderby' => 'none',
369 )
370 );
371
372 foreach ( $actions_to_reset as $action_id ) {
373 $this->store->mark_failure( $action_id );
374 do_action( 'action_scheduler_failed_action', $action_id, $timeout );
375 }
376 }
377
378 /**
379 * Do all of the cleaning actions.
380 *
381 * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
382 */
383 public function clean( $time_limit = 300 ) {
384 $this->delete_old_actions();
385 $this->reset_timeouts( $time_limit );
386 $this->mark_failures( $time_limit );
387 }
388
389 /**
390 * Get the batch size for cleaning the queue.
391 *
392 * @return int
393 */
394 protected function get_batch_size() {
395 /**
396 * Filter the batch size when cleaning the queue.
397 *
398 * @param int $batch_size The number of actions to clean in one batch.
399 */
400 return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) );
401 }
402 }
403