PluginProbe
WANotifier for Forms and Actions / 3.2.0
WANotifier for Forms and Actions v3.2.0
3.2.0 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 All 68 releases
notifier / libraries / action-scheduler / classes / abstracts / ActionScheduler_Abstract_QueueRunner.php

ActionScheduler_Abstract_QueueRunner.php in WANotifier for Forms and Actions 3.2.0, at libraries/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php

432 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Abstract class with common Queue Cleaner functionality.
5 */
6 abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated {
7
8 /**
9 * ActionScheduler_QueueCleaner instance.
10 *
11 * @var ActionScheduler_QueueCleaner
12 */
13 protected $cleaner;
14
15 /**
16 * ActionScheduler_FatalErrorMonitor instance.
17 *
18 * @var ActionScheduler_FatalErrorMonitor
19 */
20 protected $monitor;
21
22 /**
23 * ActionScheduler_Store instance.
24 *
25 * @var ActionScheduler_Store
26 */
27 protected $store;
28
29 /**
30 * The created time.
31 *
32 * Represents when the queue runner was constructed and used when calculating how long a PHP request has been running.
33 * For this reason it should be as close as possible to the PHP request start time.
34 *
35 * @var int
36 */
37 private $created_time;
38
39 /**
40 * ActionScheduler_Abstract_QueueRunner constructor.
41 *
42 * @param ActionScheduler_Store|null $store Store object.
43 * @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object.
44 * @param ActionScheduler_QueueCleaner|null $cleaner Cleaner object.
45 */
46 public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null ) {
47
48 $this->created_time = microtime( true );
49
50 $this->store = $store ? $store : ActionScheduler_Store::instance();
51 $this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store );
52 $this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store );
53 }
54
55 /**
56 * Process an individual action.
57 *
58 * @param int $action_id The action ID to process.
59 * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
60 * Generally, this should be capitalised and not localised as it's a proper noun.
61 * @throws \Exception When error running action.
62 */
63 public function process_action( $action_id, $context = '' ) {
64 // Temporarily override the error handler while we process the current action.
65 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
66 set_error_handler(
67 /**
68 * Temporary error handler which can catch errors and convert them into exceptions. This facilitates more
69 * robust error handling across all supported PHP versions.
70 *
71 * @throws Exception
72 *
73 * @param int $type Error level expressed as an integer.
74 * @param string $message Error message.
75 */
76 function ( $type, $message ) {
77 throw new Exception( $message );
78 },
79 E_USER_ERROR | E_RECOVERABLE_ERROR
80 );
81
82 /*
83 * The nested try/catch structure is required because we potentially need to convert thrown errors into
84 * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same*
85 * structure).
86 */
87 $valid_action = true;
88 try {
89 try {
90 do_action( 'action_scheduler_before_execute', $action_id, $context );
91
92 if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) {
93 $valid_action = false;
94 do_action( 'action_scheduler_execution_ignored', $action_id, $context );
95 return;
96 }
97
98 do_action( 'action_scheduler_begin_execute', $action_id, $context );
99
100 $action = $this->fetch_complete_action( $action_id );
101 if ( null === $action ) {
102 $valid_action = false;
103 $this->cancel_corrupted_action( $action_id );
104 return;
105 }
106
107 $this->store->log_execution( $action_id );
108 $action->execute();
109
110 do_action( 'action_scheduler_after_execute', $action_id, $action, $context );
111
112 $this->store->mark_complete( $action_id );
113 } catch ( Throwable $e ) {
114 // Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for
115 // compatibility with ActionScheduler_Logger.
116 throw new Exception( $e->getMessage(), $e->getCode(), $e );
117 }
118 } catch ( Exception $e ) {
119 // This catch block exists for compatibility with PHP 5.6.
120 $this->handle_action_error( $action_id, $e, $context, $valid_action );
121 } finally {
122 restore_error_handler();
123 }
124
125 if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) {
126 $this->schedule_next_instance( $action, $action_id );
127 }
128 }
129
130 /**
131 * Fetches the action instance. On failure returns null instead of ActionScheduler_NullAction.
132 *
133 * @param int $action_id Action ID.
134 * @return ActionScheduler_Action|null
135 */
136 private function fetch_complete_action( int $action_id ) {
137 // Handle corrupted entries and potential DB read failures (corrupted arguments or schedule data; a known trigger is incomplete A-S migration).
138 $attempts_to_populate = 0;
139 do {
140 $action = $this->store->fetch_action( $action_id );
141 $failure = $action instanceof ActionScheduler_NullAction;
142 // Sleep 0.01 second before retrying.
143 usleep( 10000 * (int) $failure );
144 } while ( $failure && ++$attempts_to_populate < 2 );
145
146 return $action instanceof ActionScheduler_NullAction ? null : $action;
147 }
148
149 /**
150 * Cancels the corrupted actions and performs the necessary cleanup to address identified side effects.
151 *
152 * @param int $action_id Action ID.
153 * @return void
154 */
155 private function cancel_corrupted_action( int $action_id ) {
156 $this->store->cancel_action( $action_id );
157
158 // Corrupted entries typically contain an excessive number of logs, which we intend to remove immediately.
159 do_action( 'action_scheduler_canceled_corrupted_action', $action_id );
160
161 // Add a comment to clarify the missing logs and explain the reason for the cancellation.
162 ActionScheduler_Logger::instance()->log(
163 $action_id,
164 __( 'This action data appears to be corrupt. We are cancelling it to allow recurring actions to be re-created by extensions.', 'action-scheduler' )
165 );
166 }
167
168 /**
169 * Marks actions as either having failed execution or failed validation, as appropriate.
170 *
171 * @param int $action_id Action ID.
172 * @param Exception $e Exception instance.
173 * @param string $context Execution context.
174 * @param bool $valid_action If the action is valid.
175 *
176 * @return void
177 */
178 private function handle_action_error( $action_id, $e, $context, $valid_action ) {
179 if ( $valid_action ) {
180 $this->store->mark_failure( $action_id );
181 /**
182 * Runs when action execution fails.
183 *
184 * @param int $action_id Action ID.
185 * @param Exception $e Exception instance.
186 * @param string $context Execution context.
187 */
188 do_action( 'action_scheduler_failed_execution', $action_id, $e, $context );
189 } else {
190 /**
191 * Runs when action validation fails.
192 *
193 * @param int $action_id Action ID.
194 * @param Exception $e Exception instance.
195 * @param string $context Execution context.
196 */
197 do_action( 'action_scheduler_failed_validation', $action_id, $e, $context );
198 }
199 }
200
201 /**
202 * Schedule the next instance of the action if necessary.
203 *
204 * @param ActionScheduler_Action $action Action.
205 * @param int $action_id Action ID.
206 */
207 protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) {
208 // If a recurring action has been consistently failing, we may wish to stop rescheduling it.
209 if (
210 ActionScheduler_Store::STATUS_FAILED === $this->store->get_status( $action_id )
211 && $this->recurring_action_is_consistently_failing( $action, $action_id )
212 ) {
213 ActionScheduler_Logger::instance()->log(
214 $action_id,
215 __( 'This action appears to be consistently failing. A new instance will not be scheduled.', 'action-scheduler' )
216 );
217
218 return;
219 }
220
221 try {
222 ActionScheduler::factory()->repeat( $action );
223 } catch ( Exception $e ) {
224 do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action );
225 }
226 }
227
228 /**
229 * Determine if the specified recurring action has been consistently failing.
230 *
231 * @param ActionScheduler_Action $action The recurring action to be rescheduled.
232 * @param int $action_id The ID of the recurring action.
233 *
234 * @return bool
235 */
236 private function recurring_action_is_consistently_failing( ActionScheduler_Action $action, $action_id ) {
237 /**
238 * Controls the failure threshold for recurring actions.
239 *
240 * Before rescheduling a recurring action, we look at its status. If it failed, we then check if all of the most
241 * recent actions (upto the threshold set by this filter) sharing the same hook have also failed: if they have,
242 * that is considered consistent failure and a new instance of the action will not be scheduled.
243 *
244 * @param int $failure_threshold Number of actions of the same hook to examine for failure. Defaults to 5.
245 */
246 $consistent_failure_threshold = (int) apply_filters( 'action_scheduler_recurring_action_failure_threshold', 5 );
247
248 // This query should find the earliest *failing* action (for the hook we are interested in) within our threshold.
249 $query_args = array(
250 'hook' => $action->get_hook(),
251 'status' => ActionScheduler_Store::STATUS_FAILED,
252 'date' => date_create( 'now', timezone_open( 'UTC' ) )->format( 'Y-m-d H:i:s' ),
253 'date_compare' => '<',
254 'per_page' => 1,
255 'offset' => $consistent_failure_threshold - 1,
256 );
257
258 $first_failing_action_id = $this->store->query_actions( $query_args );
259
260 // If we didn't retrieve an action ID, then there haven't been enough failures for us to worry about.
261 if ( empty( $first_failing_action_id ) ) {
262 return false;
263 }
264
265 // Now let's fetch the first action (having the same hook) of *any status* within the same window.
266 unset( $query_args['status'] );
267 $first_action_id_with_the_same_hook = $this->store->query_actions( $query_args );
268
269 /**
270 * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a
271 * way to observe and optionally override that assessment.
272 *
273 * @param bool $is_consistently_failing If the action is considered to be consistently failing.
274 * @param ActionScheduler_Action $action The action being assessed.
275 */
276 return (bool) apply_filters(
277 'action_scheduler_recurring_action_is_consistently_failing',
278 $first_action_id_with_the_same_hook === $first_failing_action_id,
279 $action
280 );
281 }
282
283 /**
284 * Run the queue cleaner.
285 *
286 * @deprecated since 4.0.0, action deletion is now handled automatically via a dedicated scheduled task.
287 */
288 protected function run_cleanup() {
289 $this->cleaner->clean( 10 * $this->get_time_limit() );
290 }
291
292 /**
293 * Get the number of concurrent batches a runner allows.
294 *
295 * @return int
296 */
297 public function get_allowed_concurrent_batches() {
298 return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 );
299 }
300
301 /**
302 * Check if the number of allowed concurrent batches is met or exceeded.
303 *
304 * @return bool
305 */
306 public function has_maximum_concurrent_batches() {
307 return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches();
308 }
309
310 /**
311 * Get the maximum number of seconds a batch can run for.
312 *
313 * @return int The number of seconds.
314 */
315 protected function get_time_limit() {
316
317 $time_limit = 30;
318
319 // Apply deprecated filter from deprecated get_maximum_execution_time() method.
320 if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) {
321 _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' );
322 $time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit );
323 }
324
325 return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) );
326 }
327
328 /**
329 * Get the number of seconds the process has been running.
330 *
331 * @return int The number of seconds.
332 */
333 protected function get_execution_time() {
334 $execution_time = microtime( true ) - $this->created_time;
335
336 // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time.
337 if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) {
338 $resource_usages = getrusage();
339
340 if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) {
341 $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 );
342 }
343 }
344
345 return $execution_time;
346 }
347
348 /**
349 * Check if the host's max execution time is (likely) to be exceeded if processing more actions.
350 *
351 * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action.
352 * @return bool
353 */
354 protected function time_likely_to_be_exceeded( $processed_actions ) {
355 $execution_time = $this->get_execution_time();
356 $max_execution_time = $this->get_time_limit();
357
358 // Safety against division by zero errors.
359 if ( 0 === $processed_actions ) {
360 return $execution_time >= $max_execution_time;
361 }
362
363 $time_per_action = $execution_time / $processed_actions;
364 $estimated_time = $execution_time + ( $time_per_action * 3 );
365 $likely_to_be_exceeded = $estimated_time > $max_execution_time;
366
367 return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time );
368 }
369
370 /**
371 * Get memory limit
372 *
373 * Based on WP_Background_Process::get_memory_limit()
374 *
375 * @return int
376 */
377 protected function get_memory_limit() {
378 if ( function_exists( 'ini_get' ) ) {
379 $memory_limit = ini_get( 'memory_limit' );
380 } else {
381 $memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce.
382 }
383
384 if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) {
385 // Unlimited, set to 32GB.
386 $memory_limit = '32G';
387 }
388
389 return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit );
390 }
391
392 /**
393 * Memory exceeded
394 *
395 * Ensures the batch process never exceeds 90% of the maximum WordPress memory.
396 *
397 * Based on WP_Background_Process::memory_exceeded()
398 *
399 * @return bool
400 */
401 protected function memory_exceeded() {
402
403 $memory_limit = $this->get_memory_limit() * 0.90;
404 $current_memory = memory_get_usage( true );
405 $memory_exceeded = $current_memory >= $memory_limit;
406
407 return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this );
408 }
409
410 /**
411 * See if the batch limits have been exceeded, which is when memory usage is almost at
412 * the maximum limit, or the time to process more actions will exceed the max time limit.
413 *
414 * Based on WC_Background_Process::batch_limits_exceeded()
415 *
416 * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action.
417 * @return bool
418 */
419 protected function batch_limits_exceeded( $processed_actions ) {
420 return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions );
421 }
422
423 /**
424 * Process actions in the queue.
425 *
426 * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
427 * Generally, this should be capitalised and not localised as it's a proper noun.
428 * @return int The number of actions processed.
429 */
430 abstract public function run( $context = '' );
431 }
432