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
← All changes | libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php +232 -63 2.7.93.1.1 View file →
@@ -3,13 +3,34 @@
3 3 /**
4 4 * Class ActionScheduler_QueueCleaner
5 5 */
6 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';
7 13
8 - /** @var int */
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 + */
9 26 protected $batch_size;
10 27
11 - /** @var ActionScheduler_Store */
28 + /**
29 + * ActionScheduler_Store instance.
30 + *
31 + * @var ActionScheduler_Store
32 + */
12 33 private $store = null;
13 34
14 35 /**
15 36 * 31 days in seconds.
@@ -18,29 +39,69 @@
18 39 */
19 40 private $month_in_seconds = 2678400;
20 41
21 42 /**
22 - * @var string[] Default list of statuses purged by the cleaner process.
43 + * Default list of statuses purged by the cleaner process.
44 + *
45 + * @var string[]
23 46 */
24 - private $default_statuses_to_purge = [
47 + private $default_statuses_to_purge = array(
25 48 ActionScheduler_Store::STATUS_COMPLETE,
26 49 ActionScheduler_Store::STATUS_CANCELED,
27 - ];
50 + );
28 51
29 52 /**
30 53 * ActionScheduler_QueueCleaner constructor.
31 54 *
32 - * @param ActionScheduler_Store $store The store instance.
33 - * @param int $batch_size The batch size.
55 + * @param ActionScheduler_Store|null $store The store instance.
56 + * @param int $batch_size The batch size.
34 57 */
35 - public function __construct( ActionScheduler_Store $store = null, $batch_size = 20 ) {
36 - $this->store = $store ? $store : ActionScheduler_Store::instance();
58 + public function __construct( ?ActionScheduler_Store $store = null, $batch_size = 20 ) {
59 + $this->store = $store ? $store : ActionScheduler_Store::instance();
37 60 $this->batch_size = $batch_size;
38 61 }
39 62
40 63 /**
41 - * Default queue cleaner process used by queue runner.
64 + * Registers action hooks to perform action deletions as a separate task.
42 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.
43 104 * @return array
44 105 */
45 106 public function delete_old_actions() {
46 107 /**
@@ -48,11 +109,25 @@
48 109 *
49 110 * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted.
50 111 */
51 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;
52 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 +
53 127 try {
54 - $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' );
128 + $cutoff_failed = as_get_datetime_object( $lifespan_failed . ' seconds ago' );
129 + $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' );
55 130 } catch ( Exception $e ) {
56 131 _doing_it_wrong(
57 132 __METHOD__,
58 133 sprintf(
@@ -65,64 +140,153 @@
65 140
66 141 return array();
67 142 }
68 143
69 -
70 144 /**
71 145 * Filter the statuses when cleaning the queue.
72 146 *
73 147 * @param string[] $default_statuses_to_purge Action statuses to clean.
74 148 */
75 - $statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge );
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 + }
76 154
77 - return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() );
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 );
78 177 }
79 178
80 179 /**
81 - * Delete selected actions limited by status and date.
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.
82 183 *
83 184 * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete.
84 - * @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago.
85 - * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20.
86 - * @param string $context Calling process context. Defaults to `old`.
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 + *
87 189 * @return array Actions deleted.
88 190 */
89 191 public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) {
90 - $batch_size = $batch_size !== null ? $batch_size : $this->batch_size;
91 - $cutoff = $cutoff_date !== null ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' );
92 - $lifespan = time() - $cutoff->getTimestamp();
93 - if ( empty( $statuses_to_purge ) ) {
94 - $statuses_to_purge = $this->default_statuses_to_purge;
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 + );
95 229 }
96 230
97 - $deleted_actions = [];
231 + $deleted_actions = array();
98 232 foreach ( $statuses_to_purge as $status ) {
99 - $actions_to_delete = $this->store->query_actions( array(
100 - 'status' => $status,
101 - 'modified' => $cutoff,
102 - 'modified_compare' => '<=',
103 - 'per_page' => $batch_size,
104 - 'orderby' => 'none',
105 - ) );
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 );
106 244
107 - $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) );
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 );
108 248 }
109 249
110 - return $deleted_actions;
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 );
111 258 }
112 259
113 260 /**
114 - * @param int[] $actions_to_delete List of action IDs to delete.
115 - * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted.
116 - * @param string $context Context of the delete request.
117 - * @return array Deleted action IDs.
261 + * Whether a continuation of the cleanup is already queued.
262 + *
263 + * @return bool
118 264 */
119 - private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) {
120 - $deleted_actions = [];
121 - if ( $lifespan === null ) {
122 - $lifespan = $this->month_in_seconds;
123 - }
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 + );
124 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();
125 289 foreach ( $actions_to_delete as $action_id ) {
126 290 try {
127 291 $this->store->delete_action( $action_id );
128 292 $deleted_actions[] = $action_id;
@@ -137,9 +301,8 @@
137 301 * @param Exception $e The exception thrown when attempting to delete the action from the data store
138 302 * @param int $lifespan The retention period, in seconds, for old actions
139 303 * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch
140 304 * @since 2.0.0
141 - *
142 305 */
143 306 do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) );
144 307 }
145 308 }
@@ -155,21 +318,25 @@
155 318 * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
156 319 */
157 320 public function reset_timeouts( $time_limit = 300 ) {
158 321 $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
322 +
159 323 if ( $timeout < 0 ) {
160 324 return;
161 325 }
162 - $cutoff = as_get_datetime_object($timeout.' seconds ago');
163 - $actions_to_reset = $this->store->query_actions( array(
164 - 'status' => ActionScheduler_Store::STATUS_PENDING,
165 - 'modified' => $cutoff,
166 - 'modified_compare' => '<=',
167 - 'claimed' => true,
168 - 'per_page' => $this->get_batch_size(),
169 - 'orderby' => 'none',
170 - ) );
171 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 +
172 339 foreach ( $actions_to_reset as $action_id ) {
173 340 $this->store->unclaim_action( $action_id );
174 341 do_action( 'action_scheduler_reset_action', $action_id );
175 342 }
@@ -176,9 +343,9 @@
176 343 }
177 344
178 345 /**
179 346 * Mark actions that have been running for more than a given time limit as failed, based on
180 - * the assumption some uncatachable and unloggable fatal error occurred during processing.
347 + * the assumption some uncatchable and unloggable fatal error occurred during processing.
181 348 *
182 349 * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
183 350 * as a parameter is 10x the time limit used for queue processing.
184 351 *
@@ -185,20 +352,24 @@
185 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).
186 353 */
187 354 public function mark_failures( $time_limit = 300 ) {
188 355 $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
356 +
189 357 if ( $timeout < 0 ) {
190 358 return;
191 359 }
192 - $cutoff = as_get_datetime_object($timeout.' seconds ago');
193 - $actions_to_reset = $this->store->query_actions( array(
194 - 'status' => ActionScheduler_Store::STATUS_RUNNING,
195 - 'modified' => $cutoff,
196 - 'modified_compare' => '<=',
197 - 'per_page' => $this->get_batch_size(),
198 - 'orderby' => 'none',
199 - ) );
200 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 +
201 372 foreach ( $actions_to_reset as $action_id ) {
202 373 $this->store->mark_failure( $action_id );
203 374 do_action( 'action_scheduler_failed_action', $action_id, $timeout );
204 375 }
@@ -207,9 +378,8 @@
207 378 /**
208 379 * Do all of the cleaning actions.
209 380 *
210 381 * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
211 - * @author Jeremy Pry
212 382 */
213 383 public function clean( $time_limit = 300 ) {
214 384 $this->delete_old_actions();
215 385 $this->reset_timeouts( $time_limit );
@@ -218,9 +388,8 @@
218 388
219 389 /**
220 390 * Get the batch size for cleaning the queue.
221 391 *
222 - * @author Jeremy Pry
223 392 * @return int
224 393 */
225 394 protected function get_batch_size() {
226 395 /**