PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.14
ووسلام – همگام سازی ووکامرس و باسلام v1.10.14
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
sync-basalam / JobsRunner.php

JobsRunner.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.14, at JobsRunner.php

229 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam;
4
5 use SyncBasalam\Admin\Settings;
6 use SyncBasalam\Services\Api\CircuitBreaker;
7
8 defined('ABSPATH') || exit;
9
10 class JobsRunner
11 {
12 private const ASYNC_ACTION = 'sync_basalam_run_jobs_async';
13 private const ASYNC_DISPATCH_LOCK_TRANSIENT = 'sync_basalam_jobs_runner_async_dispatch_lock';
14 private const ASYNC_DISPATCH_LOCK_SECONDS = 1;
15 private const ASYNC_TIME_LIMIT_SECONDS = 20;
16 private const GLOBAL_RUNNER_LAST_RUN_OPTION = 'sync_basalam_jobs_runner_last_run';
17 private const STALE_PROCESSING_TIMEOUT_SECONDS = 120;
18
19 private $jobExecutor;
20 private $jobManager;
21 private $discountScheduler;
22 private $CheckHttpBlockService;
23
24 public function __construct(
25 $jobManager,
26 $jobExecutor,
27 $discountScheduler,
28 $CheckHttpBlockService
29 ) {
30 add_action('wp_ajax_' . self::ASYNC_ACTION, [$this, 'handleAsyncRequest']);
31 add_action('wp_ajax_nopriv_' . self::ASYNC_ACTION, [$this, 'handleAsyncRequest']);
32 // Keep database work out of shutdown, where another callback may have
33 // left the shared mysqli connection with an unread result set.
34 add_action('init', [$this, 'maybeDispatchAsyncRequest'], 10, 0);
35 add_action('sync_basalam_job_created', [$this, 'maybeDispatchAsyncRequest'], 10, 0);
36
37 $this->jobManager = $jobManager;
38 $this->jobExecutor = $jobExecutor;
39 $this->discountScheduler = $discountScheduler;
40 $this->CheckHttpBlockService = $CheckHttpBlockService;
41 }
42
43 public function maybeDispatchAsyncRequest(): void
44 {
45 // Retain a guard for third-party callers that may still invoke this
46 // method during shutdown even though no shutdown hook is registered.
47 if (function_exists('did_action') && did_action('shutdown')) return;
48 if ($this->isCurrentAsyncRequest()) return;
49 if ($this->CheckHttpBlockService->SyncBasalamHttpBlock()) return;
50 if (get_transient(self::ASYNC_DISPATCH_LOCK_TRANSIENT)) return;
51
52 if (!$this->jobManager->hasPendingOrStaleProcessingJobs(self::STALE_PROCESSING_TIMEOUT_SECONDS)) {
53 return;
54 }
55
56 set_transient(
57 self::ASYNC_DISPATCH_LOCK_TRANSIENT,
58 1,
59 self::ASYNC_DISPATCH_LOCK_SECONDS
60 );
61
62 $this->dispatchAsyncRequest();
63 }
64
65 public function handleAsyncRequest(): void
66 {
67 if (!check_ajax_referer(self::ASYNC_ACTION, 'nonce', false)) {
68 wp_send_json_error(['message' => 'Invalid async jobs runner nonce.'], 403);
69 }
70
71 if (function_exists('ignore_user_abort')) {
72 ignore_user_abort(true);
73 }
74
75 if (function_exists('session_write_close')) {
76 session_write_close();
77 }
78
79 $processed = $this->runAsyncBatch();
80
81 wp_send_json_success(['processed' => $processed]);
82 }
83
84 public function checkAndRunJobs(): bool
85 {
86 if ($this->CheckHttpBlockService->SyncBasalamHttpBlock()) return false;
87 if (!$this->jobExecutor->acquireGlobalJobsLock(0)) return false;
88
89 try {
90 return $this->runEligibleJobs();
91 } finally {
92 $this->jobExecutor->releaseGlobalJobsLock();
93 }
94 }
95
96 private function runAsyncBatch(): int
97 {
98 $processed = 0;
99 $deadline = microtime(true) + (float) apply_filters(
100 'sync_basalam_jobs_runner_async_time_limit',
101 self::ASYNC_TIME_LIMIT_SECONDS
102 );
103
104 while (microtime(true) < $deadline) {
105 if (!$this->jobManager->hasPendingOrStaleProcessingJobs(self::STALE_PROCESSING_TIMEOUT_SECONDS)) {
106 break;
107 }
108
109 $ranJob = $this->checkAndRunJobs();
110
111 if ($ranJob) {
112 $processed++;
113 }
114
115 $delay = $this->secondsUntilNextAllowedRun();
116 if ($delay <= 0.0) {
117 if (!$ranJob) break;
118 continue;
119 }
120
121 if ((microtime(true) + $delay) >= $deadline) {
122 break;
123 }
124
125 usleep((int) ($delay * 1000000));
126 }
127
128 return $processed;
129 }
130
131 private function runEligibleJobs(): bool
132 {
133 $this->jobManager->ConvertStaleProcessingJobs(self::STALE_PROCESSING_TIMEOUT_SECONDS);
134 $this->discountScheduler->process();
135
136 $circuitBreaker = new CircuitBreaker();
137 if ($circuitBreaker->getState() === CircuitBreaker::STATE_OPEN) {
138 return false;
139 }
140
141 if ($this->jobManager->hasAnyProcessingJob()) {
142 return false;
143 }
144
145 $lastRun = floatval(get_option(self::GLOBAL_RUNNER_LAST_RUN_OPTION, 0));
146 $now = microtime(true);
147
148 if (($now - $lastRun) < $this->getRunThresholdSeconds()) {
149 return false;
150 }
151
152 $sortedJobTypes = $this->jobExecutor->getSortedJobTypes();
153
154 foreach ($sortedJobTypes as $jobType => $jobExecutor) {
155 if (!$this->jobExecutor->canRun($jobType)) {
156 continue;
157 }
158
159 $job = $this->jobManager->getNextEligibleJob($jobType);
160 $processingJob = $this->jobManager->getJob(['job_type' => $jobType, 'status' => 'processing']);
161
162 if (!$job || $processingJob) {
163 continue;
164 }
165
166 update_option(self::GLOBAL_RUNNER_LAST_RUN_OPTION, microtime(true), false);
167
168 $this->jobManager->updateJob(
169 ['status' => 'processing', 'started_at' => time()],
170 ['id' => $job->id]
171 );
172
173 $this->executeJob($job);
174 return true;
175 }
176
177 return false;
178 }
179
180 private function executeJob(object $job): void
181 {
182 $jobType = $job->job_type;
183 $this->jobExecutor->execute($jobType, $job);
184 }
185
186 private function dispatchAsyncRequest(): void
187 {
188 $url = add_query_arg('action', self::ASYNC_ACTION, admin_url('admin-ajax.php'));
189
190 wp_remote_post(esc_url_raw($url), [
191 'timeout' => 0.01,
192 'blocking' => false,
193 'body' => [
194 'action' => self::ASYNC_ACTION,
195 'nonce' => wp_create_nonce(self::ASYNC_ACTION),
196 ],
197 'cookies' => $_COOKIE,
198 'sslverify' => apply_filters('https_local_ssl_verify', false),
199 'headers' => [
200 'X-WP-Async-Request' => self::ASYNC_ACTION,
201 ],
202 ]);
203 }
204
205 private function isCurrentAsyncRequest(): bool
206 {
207 if (!wp_doing_ajax()) return false;
208
209 $action = isset($_REQUEST['action']) ? sanitize_key(wp_unslash($_REQUEST['action'])) : '';
210
211 return $action === self::ASYNC_ACTION;
212 }
213
214 private function secondsUntilNextAllowedRun(): float
215 {
216 $lastRun = floatval(get_option(self::GLOBAL_RUNNER_LAST_RUN_OPTION, 0));
217 $elapsed = microtime(true) - $lastRun;
218
219 return max(0.0, $this->getRunThresholdSeconds() - $elapsed);
220 }
221
222 private function getRunThresholdSeconds(): float
223 {
224 $tasksPerMinute = max(1, intval(Settings::getEffectiveTasksPerMinute()));
225
226 return 60.0 / $tasksPerMinute;
227 }
228 }
229