add aggr
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Model;
|
||||
|
||||
use App\Model\AggregateRefreshQueue;
|
||||
use Carbon\Carbon;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class AggregateRefreshQueueTest extends TestCase
|
||||
{
|
||||
protected function runInCoroutine(callable $callback): void
|
||||
{
|
||||
if (\Swoole\Coroutine::getCid() > 0) {
|
||||
$callback();
|
||||
return;
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
\Swoole\Coroutine\run(static function () use ($callback, &$exception): void {
|
||||
try {
|
||||
$callback();
|
||||
} catch (\Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
});
|
||||
if ($exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
private function uniqueRefreshDate(): string
|
||||
{
|
||||
// 与 ApiKeyTest 的 bin2hex(random_bytes(4)) 同源策略:用唯一性而非全表清理来隔离用例
|
||||
return sprintf('20%02d-%02d-%02d', random_int(50, 99), random_int(1, 12), random_int(1, 28));
|
||||
}
|
||||
|
||||
public function test_attributes_persist_correctly(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$unique_date = $this->uniqueRefreshDate();
|
||||
|
||||
try {
|
||||
AggregateRefreshQueue::query()->create([
|
||||
'refresh_date' => $unique_date,
|
||||
'aggregate_view' => 'orders_daily_by_created',
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$row = AggregateRefreshQueue::query()->where('refresh_date', $unique_date)->first();
|
||||
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame($unique_date, $row->refresh_date);
|
||||
$this->assertSame('orders_daily_by_created', $row->aggregate_view);
|
||||
$this->assertInstanceOf(Carbon::class, $row->created_at);
|
||||
} finally {
|
||||
AggregateRefreshQueue::query()->where('refresh_date', $unique_date)->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function test_insert_or_ignore_dedups_on_composite_pk(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$unique_date = $this->uniqueRefreshDate();
|
||||
|
||||
$payload = [
|
||||
'refresh_date' => $unique_date,
|
||||
'aggregate_view' => 'orders_daily_by_created',
|
||||
'created_at' => Carbon::now(),
|
||||
];
|
||||
|
||||
try {
|
||||
AggregateRefreshQueue::query()->insertOrIgnore($payload);
|
||||
AggregateRefreshQueue::query()->insertOrIgnore($payload);
|
||||
|
||||
$count = AggregateRefreshQueue::query()->where('refresh_date', $unique_date)->count();
|
||||
$this->assertSame(1, $count);
|
||||
} finally {
|
||||
AggregateRefreshQueue::query()->where('refresh_date', $unique_date)->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Platform;
|
||||
|
||||
use App\Model\AggregateRefreshQueue;
|
||||
use App\Platform\OrderConsumer;
|
||||
use Carbon\Carbon;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class OrderConsumerEnqueueTest extends TestCase
|
||||
{
|
||||
protected function runInCoroutine(callable $callback): void
|
||||
{
|
||||
if (\Swoole\Coroutine::getCid() > 0) {
|
||||
$callback();
|
||||
return;
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
\Swoole\Coroutine\run(static function () use ($callback, &$exception): void {
|
||||
try {
|
||||
$callback();
|
||||
} catch (\Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
});
|
||||
if ($exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 protected enqueueAffectedDates 方法
|
||||
*/
|
||||
protected function invokeEnqueue(array $payloads): void
|
||||
{
|
||||
$consumer = new OrderConsumer();
|
||||
$method = new ReflectionMethod($consumer, 'enqueueAffectedDates');
|
||||
$method->setAccessible(true);
|
||||
$method->invoke($consumer, $payloads);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定日期的队列条目(前置 + 后置双保险)
|
||||
*/
|
||||
protected function cleanupQueue(string $date): void
|
||||
{
|
||||
AggregateRefreshQueue::query()
|
||||
->where('refresh_date', $date)
|
||||
->where('aggregate_view', 'orders_daily_by_created')
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当日订单不应入队(在自动刷新策略覆盖窗口内)
|
||||
*/
|
||||
public function test_today_payload_does_not_enqueue(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$today = Carbon::now()->toDateString();
|
||||
$payloads = [['created_date' => Carbon::now()->format('Y-m-d H:i:sP')]];
|
||||
|
||||
$this->cleanupQueue($today);
|
||||
try {
|
||||
$this->invokeEnqueue($payloads);
|
||||
|
||||
$row = AggregateRefreshQueue::query()
|
||||
->where('refresh_date', $today)
|
||||
->where('aggregate_view', 'orders_daily_by_created')
|
||||
->first();
|
||||
$this->assertNull($row);
|
||||
} finally {
|
||||
$this->cleanupQueue($today);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 2 天前订单不应入队(仍在自动刷新策略 [now-3d, now-1h] 窗口内)
|
||||
*/
|
||||
public function test_two_days_ago_payload_does_not_enqueue(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$two_days_ago = Carbon::now()->subDays(2)->toDateString();
|
||||
$payloads = [['created_date' => Carbon::now()->subDays(2)->format('Y-m-d H:i:sP')]];
|
||||
|
||||
$this->cleanupQueue($two_days_ago);
|
||||
try {
|
||||
$this->invokeEnqueue($payloads);
|
||||
|
||||
$row = AggregateRefreshQueue::query()
|
||||
->where('refresh_date', $two_days_ago)
|
||||
->where('aggregate_view', 'orders_daily_by_created')
|
||||
->first();
|
||||
$this->assertNull($row);
|
||||
} finally {
|
||||
$this->cleanupQueue($two_days_ago);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 30 天前订单应入队 by_created(自动策略未覆盖,需补刷)
|
||||
*/
|
||||
public function test_old_payload_enqueues_by_created(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$old_date = Carbon::now()->subDays(30)->toDateString();
|
||||
$payloads = [['created_date' => Carbon::now()->subDays(30)->format('Y-m-d H:i:sP')]];
|
||||
|
||||
$this->cleanupQueue($old_date);
|
||||
try {
|
||||
$this->invokeEnqueue($payloads);
|
||||
|
||||
$row = AggregateRefreshQueue::query()
|
||||
->where('refresh_date', $old_date)
|
||||
->where('aggregate_view', 'orders_daily_by_created')
|
||||
->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame($old_date, $row->refresh_date);
|
||||
$this->assertSame('orders_daily_by_created', $row->aggregate_view);
|
||||
} finally {
|
||||
$this->cleanupQueue($old_date);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同日期重复入队由复合主键 + insertOrIgnore 防重
|
||||
*/
|
||||
public function test_dedup_via_insert_or_ignore(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$old_date = Carbon::now()->subDays(45)->toDateString();
|
||||
$payloads = [
|
||||
['created_date' => Carbon::now()->subDays(45)->format('Y-m-d H:i:sP')],
|
||||
['created_date' => Carbon::now()->subDays(45)->format('Y-m-d H:i:sP')],
|
||||
];
|
||||
|
||||
$this->cleanupQueue($old_date);
|
||||
try {
|
||||
// 单批次内同日期 → $unique_dates map 去重
|
||||
$this->invokeEnqueue($payloads);
|
||||
// 跨批次同日期 → 复合主键 insertOrIgnore 防重
|
||||
$this->invokeEnqueue($payloads);
|
||||
|
||||
$count = AggregateRefreshQueue::query()
|
||||
->where('refresh_date', $old_date)
|
||||
->where('aggregate_view', 'orders_daily_by_created')
|
||||
->count();
|
||||
$this->assertSame(1, $count);
|
||||
} finally {
|
||||
$this->cleanupQueue($old_date);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Service;
|
||||
|
||||
use App\Model\AggregateRefreshQueue;
|
||||
use App\Service\AggregateRefresherInterface;
|
||||
use App\Service\OrderAggregatesRefreshJob;
|
||||
use Carbon\Carbon;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 测试用 stub Refresher,记录每次 refresh 调用
|
||||
*/
|
||||
class RecordingRefresher implements AggregateRefresherInterface
|
||||
{
|
||||
/** @var array<int, array{view: string, date: string}> */
|
||||
public array $calls = [];
|
||||
|
||||
public function refresh(string $view, string $refresh_date): void
|
||||
{
|
||||
$this->calls[] = ['view' => $view, 'date' => $refresh_date];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用 stub Refresher,每次调用抛异常(模拟刷新失败)
|
||||
*/
|
||||
class ThrowingRefresher implements AggregateRefresherInterface
|
||||
{
|
||||
public function refresh(string $view, string $refresh_date): void
|
||||
{
|
||||
throw new RuntimeException('refresh failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class OrderAggregatesRefreshJobTest extends TestCase
|
||||
{
|
||||
protected function runInCoroutine(callable $callback): void
|
||||
{
|
||||
if (\Swoole\Coroutine::getCid() > 0) {
|
||||
$callback();
|
||||
return;
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
\Swoole\Coroutine\run(static function () use ($callback, &$exception): void {
|
||||
try {
|
||||
$callback();
|
||||
} catch (\Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
});
|
||||
if ($exception) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 沿用 P23.1 AggregateRefreshQueueTest 的随机未来日期隔离策略,
|
||||
* 确保不同测试用例间无冲突,也不会污染生产数据。
|
||||
*/
|
||||
private function uniqueRefreshDate(): string
|
||||
{
|
||||
return sprintf('20%02d-%02d-%02d', random_int(50, 99), random_int(1, 12), random_int(1, 28));
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列项 created_at < now()-1h 应被处理:调用 refresher 后从队列删除
|
||||
*/
|
||||
public function test_processes_old_items_and_clears_queue(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$date_a = $this->uniqueRefreshDate();
|
||||
$date_b = $this->uniqueRefreshDate();
|
||||
while ($date_b === $date_a) {
|
||||
$date_b = $this->uniqueRefreshDate();
|
||||
}
|
||||
|
||||
// 前置清理(防 random 撞历史残留)
|
||||
AggregateRefreshQueue::query()->whereIn('refresh_date', [$date_a, $date_b])->delete();
|
||||
|
||||
try {
|
||||
$two_hours_ago = Carbon::now()->subHours(2);
|
||||
AggregateRefreshQueue::query()->insertOrIgnore([
|
||||
['refresh_date' => $date_a, 'aggregate_view' => 'orders_daily_by_created', 'created_at' => $two_hours_ago],
|
||||
['refresh_date' => $date_b, 'aggregate_view' => 'orders_daily_by_created', 'created_at' => $two_hours_ago],
|
||||
]);
|
||||
|
||||
$refresher = new RecordingRefresher();
|
||||
$job = new OrderAggregatesRefreshJob($refresher);
|
||||
$result = $job->run();
|
||||
|
||||
$this->assertSame(2, $result['processed']);
|
||||
$this->assertSame(0, $result['failed']);
|
||||
$this->assertCount(2, $refresher->calls);
|
||||
|
||||
// 队列已清空(针对本次测试的两条)
|
||||
$remaining = AggregateRefreshQueue::query()
|
||||
->whereIn('refresh_date', [$date_a, $date_b])
|
||||
->count();
|
||||
$this->assertSame(0, $remaining);
|
||||
} finally {
|
||||
AggregateRefreshQueue::query()->whereIn('refresh_date', [$date_a, $date_b])->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列项 created_at >= now()-1h 应被跳过:refresher 无调用,队列保留
|
||||
*/
|
||||
public function test_recently_enqueued_items_are_skipped(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$date = $this->uniqueRefreshDate();
|
||||
AggregateRefreshQueue::query()->where('refresh_date', $date)->delete();
|
||||
|
||||
try {
|
||||
AggregateRefreshQueue::query()->insertOrIgnore([
|
||||
'refresh_date' => $date,
|
||||
'aggregate_view' => 'orders_daily_by_created',
|
||||
'created_at' => Carbon::now()->subMinutes(30), // < 1 hour
|
||||
]);
|
||||
|
||||
$refresher = new RecordingRefresher();
|
||||
$job = new OrderAggregatesRefreshJob($refresher);
|
||||
$result = $job->run();
|
||||
|
||||
$this->assertSame(0, $result['processed']);
|
||||
$this->assertSame(0, $result['failed']);
|
||||
$this->assertCount(0, $refresher->calls);
|
||||
|
||||
// 队列项保留
|
||||
$exists = AggregateRefreshQueue::query()->where('refresh_date', $date)->exists();
|
||||
$this->assertTrue($exists);
|
||||
} finally {
|
||||
AggregateRefreshQueue::query()->where('refresh_date', $date)->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* refresher 抛异常时:队列项保留(下次任务重试),失败计数 +1
|
||||
*/
|
||||
public function test_failure_keeps_queue_item(): void
|
||||
{
|
||||
$this->runInCoroutine(function (): void {
|
||||
$date = $this->uniqueRefreshDate();
|
||||
AggregateRefreshQueue::query()->where('refresh_date', $date)->delete();
|
||||
|
||||
try {
|
||||
AggregateRefreshQueue::query()->insertOrIgnore([
|
||||
'refresh_date' => $date,
|
||||
'aggregate_view' => 'orders_daily_by_created',
|
||||
'created_at' => Carbon::now()->subHours(2),
|
||||
]);
|
||||
|
||||
$job = new OrderAggregatesRefreshJob(new ThrowingRefresher());
|
||||
$result = $job->run();
|
||||
|
||||
$this->assertSame(0, $result['processed']);
|
||||
$this->assertSame(1, $result['failed']);
|
||||
|
||||
// 队列项保留以待重试
|
||||
$exists = AggregateRefreshQueue::query()->where('refresh_date', $date)->exists();
|
||||
$this->assertTrue($exists);
|
||||
} finally {
|
||||
AggregateRefreshQueue::query()->where('refresh_date', $date)->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user