PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.29
ووسلام – همگام سازی ووکامرس و باسلام v1.10.29
1.10.29 1.10.28 1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 1.10.22 1.10.21 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 All 62 releases
sync-basalam / JobManager.php

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

350 lines 13.7 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 defined('ABSPATH') || exit;
6
7 class JobManager
8 {
9 private $jobManagerTableName;
10
11 private const ALLOWED_COLUMNS = [
12 'id', 'job_type', 'status', 'payload',
13 'attempts', 'max_attempts', 'retry_after',
14 'started_at', 'created_at', 'failed_at', 'error_message',
15 ];
16
17 function __construct()
18 {
19 global $wpdb;
20 $this->jobManagerTableName = $wpdb->prefix . 'sync_basalam_job_manager';
21 }
22
23 public function createJob($jobType, $status = 'pending', $payload = null, $maxAttempts = 3)
24 {
25 global $wpdb;
26
27 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
28 $result = $wpdb->insert(
29 $this->jobManagerTableName,
30 array(
31 'job_type' => $jobType,
32 'status' => $status,
33 'payload' => $payload,
34 'attempts' => 0,
35 'max_attempts' => $maxAttempts,
36 'created_at' => time(),
37 )
38 );
39
40 if ($result !== false) {
41 do_action('sync_basalam_job_created', $jobType, $status, $payload);
42 }
43
44 return $result;
45 }
46
47 public function getNextEligibleJob(string $jobType): ?object
48 {
49 global $wpdb;
50
51 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
52 return $wpdb->get_row($wpdb->prepare(
53 "SELECT * FROM {$this->jobManagerTableName}
54 WHERE job_type = %s
55 AND status = 'pending'
56 AND (retry_after IS NULL OR retry_after <= %d)
57 ORDER BY id ASC
58 LIMIT 1",
59 $jobType,
60 time()
61 ));
62 }
63
64 public function hasAnyProcessingJob(): bool
65 {
66 return $this->getCountJobs(['status' => 'processing']) > 0;
67 }
68
69 public function getProductUpdateStatus(): array
70 {
71 $quickJob = $this->getJob(['job_type' => 'sync_basalam_bulk_update_products', 'status' => 'processing'])
72 ?: $this->getJob(['job_type' => 'sync_basalam_bulk_update_products', 'status' => 'pending']);
73 $fullJob = $this->getJob(['job_type' => 'sync_basalam_update_all_products', 'status' => 'processing'])
74 ?: $this->getJob(['job_type' => 'sync_basalam_update_all_products', 'status' => 'pending']);
75 $singleCount = $this->getCountJobs([
76 'job_type' => 'sync_basalam_update_single_product',
77 'status' => ['pending', 'processing'],
78 ]);
79
80 return [
81 'active' => (bool) ($quickJob || $fullJob || $singleCount > 0),
82 'type' => $quickJob ? 'quick' : (($fullJob || $singleCount > 0) ? 'full' : ''),
83 'status' => $quickJob ? $quickJob->status : ($fullJob ? $fullJob->status : ''),
84 'count' => $singleCount,
85 ];
86 }
87
88 public function hasPendingOrStaleProcessingJobs(int $staleProcessingTimeoutSeconds = 120): bool
89 {
90 global $wpdb;
91
92 $now = time();
93 $staleBefore = $now - $staleProcessingTimeoutSeconds;
94
95 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
96 $result = $wpdb->get_var($wpdb->prepare(
97 "SELECT 1 FROM {$this->jobManagerTableName}
98 WHERE (
99 status = 'pending'
100 AND (retry_after IS NULL OR retry_after <= %d)
101 ) OR (
102 status = 'processing'
103 AND started_at IS NOT NULL
104 AND started_at < %d
105 )
106 LIMIT 1",
107 $now,
108 $staleBefore
109 ));
110
111 return (string) $result === '1';
112 }
113
114 public function getStaleProcessingJob(int $timeoutSeconds = 120): ?object
115 {
116 global $wpdb;
117
118 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom queue table; identifier from the WordPress prefix, values prepared.
119 return $wpdb->get_row($wpdb->prepare(
120 "SELECT id, started_at FROM {$this->jobManagerTableName}
121 WHERE status = 'processing'
122 AND started_at IS NOT NULL
123 AND started_at < %d
124 ORDER BY started_at ASC, id ASC LIMIT 1",
125 time() - $timeoutSeconds
126 ));
127 }
128
129 public function getJob($where = array())
130 {
131 global $wpdb;
132
133 if (empty($where)) return null;
134
135 $conditions = [];
136 $values = [];
137
138 foreach ($where as $column => $value) {
139 if (!in_array($column, self::ALLOWED_COLUMNS, true)) {
140 throw new \InvalidArgumentException(esc_html("Invalid column: {$column}"));
141 }
142 $conditions[] = "{$column} = %s";
143 $values[] = $value;
144 }
145
146 $sql = "SELECT * FROM {$this->jobManagerTableName} WHERE " . implode(" AND ", $conditions) . " LIMIT 1";
147
148 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifiers from $wpdb->prefix and whitelisted column list, not user input; values are prepared.
149 return $wpdb->get_row($wpdb->prepare($sql, $values));
150 }
151
152 public function getCountJobs($where = array())
153 {
154 global $wpdb;
155
156 if (empty($where)) return 0;
157
158 $conditions = [];
159 $values = [];
160
161 foreach ($where as $column => $value) {
162 if (!in_array($column, self::ALLOWED_COLUMNS, true)) {
163 throw new \InvalidArgumentException(esc_html("Invalid column: {$column}"));
164 }
165 if (is_array($value)) {
166 $placeholders = array_fill(0, count($value), '%s');
167 $conditions[] = "{$column} IN (" . implode(',', $placeholders) . ")";
168 $values = array_merge($values, $value);
169 } else {
170 $conditions[] = "{$column} = %s";
171 $values[] = $value;
172 }
173 }
174
175 $sql = "SELECT COUNT(*) FROM {$this->jobManagerTableName} WHERE " . implode(" AND ", $conditions);
176
177 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifiers from $wpdb->prefix and whitelisted column list, not user input; values are prepared.
178 return (int) $wpdb->get_var($wpdb->prepare($sql, $values));
179 }
180
181 public function updateJob($jobData, $where = array())
182 {
183 global $wpdb;
184
185 if (empty($where) || empty($jobData)) return false;
186
187 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
188 return $wpdb->update($this->jobManagerTableName, $jobData, $where);
189 }
190
191 public function deleteJob($where = array())
192 {
193 global $wpdb;
194
195 if (empty($where)) return false;
196
197 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
198 return $wpdb->delete($this->jobManagerTableName, $where);
199 }
200
201 public function ConvertStaleProcessingJobs($timeoutSeconds = 120)
202 {
203 global $wpdb;
204
205 $timeoutTimestamp = time() - $timeoutSeconds;
206
207 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
208 $wpdb->query(
209 $wpdb->prepare(
210 "UPDATE {$this->jobManagerTableName}
211 SET status = CASE
212 WHEN attempts + 1 >= max_attempts THEN 'failed'
213 ELSE 'pending'
214 END,
215 attempts = attempts + 1,
216 started_at = NULL,
217 failed_at = CASE
218 WHEN attempts + 1 >= max_attempts THEN %d
219 ELSE failed_at
220 END
221 WHERE status = 'processing'
222 AND started_at IS NOT NULL
223 AND started_at < %d",
224 time(),
225 $timeoutTimestamp
226 )
227 );
228 }
229
230 public function hasProductJobInProgress(int $productId, string $jobType): bool
231 {
232 global $wpdb;
233
234 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
235 $jobs = $wpdb->get_results($wpdb->prepare(
236 "SELECT payload FROM {$this->jobManagerTableName}
237 WHERE job_type = %s
238 AND (status = %s OR status = %s)",
239 $jobType,
240 'pending',
241 'processing'
242 ));
243
244 if (empty($jobs)) {
245 return false;
246 }
247
248 foreach ($jobs as $job) {
249 $payload = json_decode($job->payload, true);
250 $jobProductId = $payload['product_id'] ?? $payload;
251
252 if (intval($jobProductId) === intval($productId)) {
253 return true;
254 }
255 }
256
257 return false;
258 }
259
260 public function retryJob(int $jobId, ?string $errorMessage = null): bool
261 {
262 global $wpdb;
263
264 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom plugin table; identifier from $wpdb->prefix, not user input.
265 $job = $wpdb->get_row($wpdb->prepare(
266 "SELECT * FROM {$this->jobManagerTableName} WHERE id = %d",
267 $jobId
268 ));
269
270 if (!$job) return false;
271
272 $newAttempts = intval($job->attempts) + 1;
273
274 $errorMessages = [];
275 if (!empty($job->error_message)) {
276 $decoded = json_decode($job->error_message, true);
277 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) $errorMessages = $decoded;
278 }
279
280 if ($errorMessage) $errorMessages[$newAttempts] = $errorMessage;
281
282 $encodedErrors = json_encode($errorMessages, JSON_UNESCAPED_UNICODE);
283
284 if ($newAttempts >= intval($job->max_attempts)) {
285 $this->updateJob(
286 [
287 'status' => 'failed',
288 'error_message' => $encodedErrors,
289 'failed_at' => time(),
290 'started_at' => 0,
291 'attempts' => $newAttempts,
292 ],
293 ['id' => $jobId]
294 );
295 return false;
296 }
297
298 // Progressive exponential backoff: 30s, 60s, 120s, 240s, ...
299 $delaySeconds = 30 * (int) pow(2, $newAttempts - 1);
300 $retryAfter = time() + $delaySeconds;
301
302 // Atomic DELETE + INSERT inside a transaction so a crash can't lose the job.
303 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control for atomic job requeue; no object cache applicable.
304 $wpdb->query('START TRANSACTION');
305 try {
306 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
307 $wpdb->delete($this->jobManagerTableName, ['id' => $jobId]);
308
309 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; no object cache for these operational queries.
310 $wpdb->insert(
311 $this->jobManagerTableName,
312 [
313 'job_type' => $job->job_type,
314 'status' => 'pending',
315 'payload' => $job->payload,
316 'attempts' => $newAttempts,
317 'max_attempts' => $job->max_attempts,
318 'error_message' => $encodedErrors,
319 'created_at' => $job->created_at,
320 'retry_after' => $retryAfter,
321 'started_at' => 0,
322 ]
323 );
324
325 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control for atomic job requeue; no object cache applicable.
326 $wpdb->query('COMMIT');
327 } catch (\Exception $e) {
328 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transaction control for atomic job requeue; no object cache applicable.
329 $wpdb->query('ROLLBACK');
330 throw $e;
331 }
332
333 return true;
334 }
335
336 public function failJob(int $jobId, ?string $errorMessage = null): bool
337 {
338 return $this->updateJob(
339 [
340 'status' => 'failed',
341 'error_message' => $errorMessage,
342 'failed_at' => time(),
343 'started_at' => 0,
344 ],
345 ['id' => $jobId]
346 );
347 }
348
349 }
350