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_QueueRunner.php

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

286 lines 11.0 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_QueueRunner
5 */
6 class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner {
7 const WP_CRON_HOOK = 'action_scheduler_run_queue';
8
9 const WP_CRON_SCHEDULE = 'every_minute';
10
11 /**
12 * ActionScheduler_AsyncRequest_QueueRunner instance.
13 *
14 * @var ActionScheduler_AsyncRequest_QueueRunner
15 */
16 protected $async_request;
17
18 /**
19 * ActionScheduler_QueueRunner instance.
20 *
21 * @var ActionScheduler_QueueRunner
22 */
23 private static $runner = null;
24
25 /**
26 * Whether the cleaner instance is a non-default one.
27 *
28 * @var bool
29 */
30 private $is_custom_cleaner;
31
32 /**
33 * Number of processed actions.
34 *
35 * @var int
36 */
37 private $processed_actions_count = 0;
38
39 /**
40 * Get instance.
41 *
42 * @return ActionScheduler_QueueRunner
43 * @codeCoverageIgnore
44 */
45 public static function instance() {
46 if ( empty( self::$runner ) ) {
47 $class = apply_filters( 'action_scheduler_queue_runner_class', 'ActionScheduler_QueueRunner' );
48 self::$runner = new $class();
49 }
50
51 return self::$runner;
52 }
53
54 /**
55 * ActionScheduler_QueueRunner constructor.
56 *
57 * @param ActionScheduler_Store|null $store Store object.
58 * @param ActionScheduler_FatalErrorMonitor|null $monitor Monitor object.
59 * @param ActionScheduler_QueueCleaner|null $cleaner Cleaner object.
60 * @param ActionScheduler_AsyncRequest_QueueRunner|null $async_request Async request runner object.
61 */
62 public function __construct( ?ActionScheduler_Store $store = null, ?ActionScheduler_FatalErrorMonitor $monitor = null, ?ActionScheduler_QueueCleaner $cleaner = null, ?ActionScheduler_AsyncRequest_QueueRunner $async_request = null ) {
63 parent::__construct( $store, $monitor, $cleaner );
64
65 if ( is_null( $async_request ) ) {
66 $async_request = new ActionScheduler_AsyncRequest_QueueRunner( $this->store );
67 }
68
69 $this->async_request = $async_request;
70 $this->is_custom_cleaner = get_class( $this->cleaner ) !== ActionScheduler_QueueCleaner::class;
71 }
72
73 /**
74 * Initialize.
75 *
76 * @codeCoverageIgnore
77 */
78 public function init() {
79
80 add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.CronSchedulesInterval
81
82 // Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param.
83 $next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK );
84 if ( $next_timestamp ) {
85 wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK );
86 }
87
88 $cron_context = array( 'WP Cron' );
89
90 if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) {
91 $schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE );
92 wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context );
93 }
94
95 add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) );
96 $this->hook_dispatch_async_request();
97
98 // Backward compatibility: If the action cleaner is standard, cleaning will be performed as an action to improve throughput
99 // and enable daily runs. If not, cleaning will occur explicitly before processing actions to ensure backward compatibility.
100 // The cleaner was initially designed as a QueueRunner dependency, which is why the hooks are registered here.
101 if ( ! $this->is_custom_cleaner ) {
102 $this->cleaner->register_cleaner_hooks();
103 }
104 }
105
106 /**
107 * Hook check for dispatching an async request.
108 */
109 public function hook_dispatch_async_request() {
110 add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) );
111 }
112
113 /**
114 * Unhook check for dispatching an async request.
115 */
116 public function unhook_dispatch_async_request() {
117 remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) );
118 }
119
120 /**
121 * Check if we should dispatch an async request to process actions.
122 *
123 * This method is attached to 'shutdown', so is called frequently. To avoid slowing down
124 * the site, it mitigates the work performed in each request by:
125 * 1. checking if it's in the admin context and then
126 * 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default)
127 * 3. haven't exceeded the number of allowed batches.
128 *
129 * The order of these checks is important, because they run from a check on a value:
130 * 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant
131 * 2. in memory - transients use autoloaded options by default
132 * 3. from a database query - has_maximum_concurrent_batches() run the query
133 * $this->store->get_claim_count() to find the current number of claims in the DB.
134 *
135 * If all of these conditions are met, then we request an async runner check whether it
136 * should dispatch a request to process pending actions.
137 */
138 public function maybe_dispatch_async_request() {
139 // Only start an async queue at most once every 60 seconds.
140 if (
141 is_admin()
142 && ! ActionScheduler::lock()->is_locked( 'async-request-runner' )
143 && ActionScheduler::lock()->set( 'async-request-runner' )
144 ) {
145 $this->async_request->maybe_dispatch();
146 }
147 }
148
149 /**
150 * Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue'
151 *
152 * The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0
153 * that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context
154 * passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK,
155 * should set a context as the first parameter. For an example of this, refer to the code seen in
156 *
157 * @see ActionScheduler_AsyncRequest_QueueRunner::handle()
158 *
159 * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
160 * Generally, this should be capitalised and not localised as it's a proper noun.
161 * @return int The number of actions processed.
162 */
163 public function run( $context = 'WP Cron' ) {
164 ActionScheduler_Compatibility::raise_memory_limit();
165 ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() );
166
167 do_action( 'action_scheduler_before_process_queue' );
168
169 $cleanup_time_limit = 10 * $this->get_time_limit();
170 // Backward compatibility: If the action cleaner is standard, cleaning will be performed as an action to improve throughput
171 // and enable daily runs. If not, cleaning will occur explicitly before processing actions to ensure backward compatibility.
172 if ( $this->is_custom_cleaner ) {
173 // Execute complete cleanup cycle, as in this logical branch deletion IS NOT executed via a separate action.
174 $this->cleaner->clean( $cleanup_time_limit );
175 } else {
176 // Execute partial cleanup cycle, as in this logical branch deletion IS executed via a separate action.
177 $this->cleaner->reset_timeouts( $cleanup_time_limit );
178 $this->cleaner->mark_failures( $cleanup_time_limit );
179 }
180
181 $this->processed_actions_count = 0;
182 if ( false === $this->has_maximum_concurrent_batches() ) {
183 $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 );
184 // Note: gc_collect_cycles() was considered here and in do_batch/clear_caches, but rejected:
185 // upside is speculative, GC sweep cost scales with object count and can cause pauses.
186 do {
187 $processed_actions_in_batch = $this->do_batch( $batch_size, $context );
188 $this->processed_actions_count += $processed_actions_in_batch;
189 } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $this->processed_actions_count ) ); // keep going until we run out of actions, time, or memory.
190 }
191
192 do_action( 'action_scheduler_after_process_queue' );
193 return $this->processed_actions_count;
194 }
195
196 /**
197 * Process a batch of actions pending in the queue.
198 *
199 * Actions are processed by claiming a set of pending actions then processing each one until either the batch
200 * size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded().
201 *
202 * @param int $size The maximum number of actions to process in the batch.
203 * @param string $context Optional identifier for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron'
204 * Generally, this should be capitalised and not localised as it's a proper noun.
205 * @return int The number of actions processed.
206 */
207 protected function do_batch( $size = 100, $context = '' ) {
208 $claim = $this->store->stake_claim( $size );
209 $this->monitor->attach( $claim );
210 $processed_actions = 0;
211
212 $claim_id = $claim->get_id();
213 foreach ( $claim->get_actions() as $action_id ) {
214 // Bail if we lost the claim.
215 if ( $claim_id !== $this->store->get_claim_id( $action_id ) ) {
216 break;
217 }
218
219 $this->process_action( $action_id, $context );
220 $processed_actions++;
221
222 if ( $this->batch_limits_exceeded( $processed_actions + $this->processed_actions_count ) ) {
223 break;
224 }
225 }
226 $this->store->release_claim( $claim );
227 $this->monitor->detach();
228 $this->clear_caches();
229 return $processed_actions;
230 }
231
232 /**
233 * Flush the cache if possible (intended for use after a batch of actions has been processed).
234 *
235 * This is useful because running large batches can eat up memory and because invalid data can accrue in the
236 * runtime cache, which may lead to unexpected results.
237 */
238 protected function clear_caches() {
239 /*
240 * Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object
241 * cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available.
242 *
243 * However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if
244 * it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it.
245 */
246 $flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' );
247 $flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' );
248
249 if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) {
250 wp_cache_flush_runtime();
251 } elseif (
252 ! wp_using_ext_object_cache()
253 /**
254 * When an external object cache is in use, and when wp_cache_flush_runtime() is not available, then
255 * normally the cache will not be flushed after processing a batch of actions (to avoid a performance
256 * penalty for other processes).
257 *
258 * This filter makes it possible to override this behavior and always flush the cache, even if an external
259 * object cache is in use.
260 *
261 * @since 1.0
262 *
263 * @param bool $flush_cache If the cache should be flushed.
264 */
265 || apply_filters( 'action_scheduler_queue_runner_flush_cache', false )
266 ) {
267 wp_cache_flush();
268 }
269 }
270
271 /**
272 * Add schedule to WP cron.
273 *
274 * @param array<string, array<string, int|string>> $schedules Schedules.
275 * @return array<string, array<string, int|string>>
276 */
277 public function add_wp_cron_schedule( $schedules ) {
278 $schedules['every_minute'] = array(
279 'interval' => 60, // in seconds.
280 'display' => __( 'Every minute', 'action-scheduler' ),
281 );
282
283 return $schedules;
284 }
285 }
286