Skip to content

Commit f36565a

Browse files
committed
Bugfix release and updates
1 parent b768f11 commit f36565a

22 files changed

Lines changed: 2986 additions & 172 deletions

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ WPQueue::logs()->recent(100);
259259
#### Фаза 5: Документация и финализация
260260
- [x] **5.1** README.md с примерами
261261
- [x] **5.2** PHPDoc для всех публичных методов
262-
- [ ] **5.3** Интеграционные тесты
262+
- [x] **5.3** Интеграционные тесты (E2E)
263263
- [ ] **5.4** Финальный code review + pint
264264

265265
#### Фаза 6: Cron Monitor & System Status

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ WPQueue::logs()->clearOld(7); // Clear logs older than 7 days
234234

235235
Access via **WP Admin → WP Queue**
236236

237+
238+
237239
### Tabs
238240

239241
| Tab | Description |
@@ -343,11 +345,24 @@ add_action('wp_queue_schedule', fn($s) => $s->job(MyHourlyTask::class));
343345
## Testing
344346

345347
```bash
346-
composer test # Run tests
347-
composer test:coverage # With coverage
348-
composer lint # Code style
348+
# Unit-тесты (быстрые, изолированные)
349+
composer test:unit
350+
351+
# E2E тесты (с реальным WordPress)
352+
composer test:e2e
353+
354+
# Все тесты
355+
composer test
356+
357+
# С покрытием кода
358+
composer test:coverage
359+
360+
# Проверка стиля кода
361+
composer lint
349362
```
350363

364+
Подробнее: [tests/README.md](tests/README.md)
365+
351366
## License
352367

353368
GPL-2.0-or-later

assets/css/admin.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
opacity: 0;
206206
transform: translateX(100px);
207207
}
208+
208209
to {
209210
opacity: 1;
210211
transform: translateX(0);
@@ -292,13 +293,15 @@
292293

293294
/* Responsive table for cron */
294295
@media screen and (max-width: 1200px) {
296+
295297
.wp-queue-cron table th:nth-child(4),
296298
.wp-queue-cron table td:nth-child(4) {
297299
display: none;
298300
}
299301
}
300302

301303
@media screen and (max-width: 900px) {
304+
302305
.wp-queue-cron table th:nth-child(3),
303306
.wp-queue-cron table td:nth-child(3) {
304307
display: none;
@@ -343,4 +346,4 @@
343346
margin-top: 20px;
344347
padding-top: 15px;
345348
border-top: 1px solid #ddd;
346-
}
349+
}

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
},
4747
"scripts": {
4848
"test": "pest",
49+
"test:unit": "pest tests/Unit",
50+
"test:e2e": "pest tests/Feature --configuration=phpunit-e2e.xml",
4951
"test:coverage": "pest --coverage",
5052
"lint": "pint",
5153
"lint:check": "pint --test"

phpunit-e2e.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
bootstrap="tests/bootstrap-e2e.php"
5+
colors="true"
6+
cacheDirectory=".phpunit.cache"
7+
executionOrder="depends,defects"
8+
shortenArraysForExportThreshold="10"
9+
testdox="true">
10+
<testsuites>
11+
<testsuite name="E2E">
12+
<directory>tests/Feature</directory>
13+
</testsuite>
14+
</testsuites>
15+
<source>
16+
<include>
17+
<directory>src</directory>
18+
</include>
19+
</source>
20+
<php>
21+
<env name="WP_TESTS_DIR" value="/tmp/wordpress-tests-lib"/>
22+
<env name="WP_CORE_DIR" value="/tmp/wordpress"/>
23+
</php>
24+
</phpunit>

src/Admin/AdminPage.php

Lines changed: 297 additions & 90 deletions
Large diffs are not rendered by default.

src/Admin/RestApi.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public function getQueues(): WP_REST_Response
131131
$results = $wpdb->get_col(
132132
$wpdb->prepare(
133133
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
134-
'wp_queue_jobs_%'
134+
'wp_queue_jobs_%',
135135
),
136136
);
137137

src/CLI/QueueCommand.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function status(array $args, array $assocArgs): void
5656
$results = $wpdb->get_col(
5757
$wpdb->prepare(
5858
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
59-
'wp_queue_jobs_%'
59+
'wp_queue_jobs_%',
6060
),
6161
);
6262

@@ -127,7 +127,7 @@ public function work(array $args, array $assocArgs): void
127127
$processed = 0;
128128

129129
while ($processed < $limit) {
130-
if (! $worker->processNext($queue)) {
130+
if (! $worker->runNextJob($queue)) {
131131
break;
132132
}
133133
$processed++;
@@ -250,7 +250,7 @@ public function failed(array $args, array $assocArgs): void
250250
return;
251251
}
252252

253-
$items = array_map(fn ($log) => [
253+
$items = array_map(fn($log) => [
254254
'id' => $log['job_id'] ?? '-',
255255
'job' => $log['job_class'],
256256
'queue' => $log['queue'],
@@ -302,8 +302,8 @@ public function system(array $args, array $assocArgs): void
302302
['key' => 'PHP Version', 'value' => $report['php_version']],
303303
['key' => 'WordPress Version', 'value' => $report['wp_version']],
304304
['key' => 'Memory Limit', 'value' => $report['memory_limit_formatted']],
305-
['key' => 'Memory Usage', 'value' => $report['current_memory_formatted'].' ('.$report['memory_percent'].'%)'],
306-
['key' => 'Max Execution Time', 'value' => $report['max_execution_time'].'s'],
305+
['key' => 'Memory Usage', 'value' => $report['current_memory_formatted'] . ' (' . $report['memory_percent'] . '%)'],
306+
['key' => 'Max Execution Time', 'value' => $report['max_execution_time'] . 's'],
307307
['key' => 'WP-Cron Disabled', 'value' => $report['wp_cron_disabled'] ? 'Yes' : 'No'],
308308
['key' => 'Loopback Status', 'value' => $report['loopback']['status']],
309309
['key' => 'Timezone', 'value' => $report['timezone']],

src/Jobs/Job.php

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,27 +126,88 @@ protected function generateId(): string
126126
return bin2hex(random_bytes(16));
127127
}
128128

129+
/**
130+
* Serialize all properties including child class properties.
131+
* Uses Reflection to capture private/protected properties from child classes.
132+
*/
129133
public function __serialize(): array
130134
{
131-
return [
132-
'id' => $this->id,
133-
'queue' => $this->queue,
134-
'attempts' => $this->attempts,
135-
'maxAttempts' => $this->maxAttempts,
136-
'timeout' => $this->timeout,
137-
'delay' => $this->delay,
138-
'createdAt' => $this->createdAt,
139-
];
135+
$data = [];
136+
$reflection = new \ReflectionClass($this);
137+
138+
// Get all properties from this class and parent classes
139+
do {
140+
foreach ($reflection->getProperties() as $property) {
141+
$property->setAccessible(true);
142+
$name = $property->getName();
143+
144+
// Skip uninitialized typed properties
145+
if ($property->hasType() && !$property->isInitialized($this)) {
146+
continue;
147+
}
148+
149+
// Use class name prefix for private properties to avoid conflicts
150+
if ($property->isPrivate() && $property->getDeclaringClass()->getName() !== self::class) {
151+
$key = $property->getDeclaringClass()->getName() . '::' . $name;
152+
} else {
153+
$key = $name;
154+
}
155+
$data[$key] = $property->getValue($this);
156+
}
157+
} while ($reflection = $reflection->getParentClass());
158+
159+
return $data;
140160
}
141161

162+
/**
163+
* Unserialize all properties including child class properties.
164+
*/
142165
public function __unserialize(array $data): void
143166
{
144-
$this->id = $data['id'];
145-
$this->queue = $data['queue'];
146-
$this->attempts = $data['attempts'];
147-
$this->maxAttempts = $data['maxAttempts'];
148-
$this->timeout = $data['timeout'];
149-
$this->delay = $data['delay'];
150-
$this->createdAt = $data['createdAt'];
167+
$reflection = new \ReflectionClass($this);
168+
169+
// Build a map of all properties
170+
$properties = [];
171+
$ref = $reflection;
172+
do {
173+
foreach ($ref->getProperties() as $property) {
174+
$name = $property->getName();
175+
if ($property->isPrivate() && $property->getDeclaringClass()->getName() !== self::class) {
176+
$key = $property->getDeclaringClass()->getName() . '::' . $name;
177+
} else {
178+
$key = $name;
179+
}
180+
$properties[$key] = $property;
181+
}
182+
} while ($ref = $ref->getParentClass());
183+
184+
// Restore values from serialized data
185+
foreach ($data as $key => $value) {
186+
if (isset($properties[$key])) {
187+
$properties[$key]->setAccessible(true);
188+
$properties[$key]->setValue($this, $value);
189+
}
190+
}
191+
192+
// Initialize any remaining uninitialized typed properties with defaults
193+
foreach ($properties as $key => $property) {
194+
if ($property->hasType() && !$property->isInitialized($this)) {
195+
$type = $property->getType();
196+
if ($type instanceof \ReflectionNamedType && !$type->allowsNull()) {
197+
$default = match ($type->getName()) {
198+
'array' => [],
199+
'string' => '',
200+
'int' => 0,
201+
'float' => 0.0,
202+
'bool' => false,
203+
default => null,
204+
};
205+
if ($default !== null) {
206+
$property->setAccessible(true);
207+
$property->setValue($this, $default);
208+
}
209+
}
210+
}
211+
}
151212
}
152213
}

src/Queue/DatabaseQueue.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ public function pop(string $queue = 'default'): ?JobInterface
3737
}
3838

3939
$now = time();
40+
$staleThreshold = 300;
4041

4142
foreach ($jobs as $id => $data) {
42-
if ($data['available_at'] <= $now && $data['reserved_at'] === null) {
43-
// Reserve the job
43+
$isStale = $data['reserved_at'] !== null && ($now - $data['reserved_at']) > $staleThreshold;
44+
45+
if ($data['available_at'] <= $now && ($data['reserved_at'] === null || $isStale)) {
46+
if ($isStale) {
47+
error_log(sprintf(
48+
'WP Queue: Releasing stale job %s (reserved for %d seconds)',
49+
$id,
50+
$now - $data['reserved_at'],
51+
));
52+
}
53+
4454
$jobs[$id]['reserved_at'] = $now;
4555
$this->saveQueue($queue, $jobs);
4656

@@ -62,10 +72,22 @@ public function delete(string $jobId): bool
6272
if (isset($jobs[$jobId])) {
6373
unset($jobs[$jobId]);
6474
$this->saveQueue($queue, $jobs);
75+
76+
error_log(sprintf(
77+
'WP Queue: Deleted job %s from queue %s. Remaining: %d',
78+
$jobId,
79+
$queue,
80+
count($jobs)
81+
));
6582

6683
return true;
6784
}
6885
}
86+
87+
error_log(sprintf(
88+
'WP Queue: Failed to delete job %s - not found in any queue',
89+
$jobId
90+
));
6991

7092
return false;
7193
}
@@ -78,6 +100,8 @@ public function release(JobInterface $job, int $delay = 0): void
78100
if (isset($jobs[$job->getId()])) {
79101
$jobs[$job->getId()]['reserved_at'] = null;
80102
$jobs[$job->getId()]['available_at'] = time() + $delay;
103+
$jobs[$job->getId()]['payload'] = serialize($job);
104+
$jobs[$job->getId()]['attempts'] = $job->getAttempts();
81105
$this->saveQueue($queue, $jobs);
82106
}
83107
}

0 commit comments

Comments
 (0)