166 lines
5.7 KiB
PHP
166 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Modules\EcommerceFinance\Services\JianSunQueryCache;
|
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
|
use Tests\TestCase;
|
|
|
|
class JianSunQueryCacheTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config()->set('upstreams.snapshots.allow_inline_refresh', true);
|
|
|
|
Cache::setDefaultDriver('array');
|
|
Cache::flush();
|
|
Carbon::setTestNow('2026-07-22 10:00:00');
|
|
|
|
config()->set('upstreams.snapshots.cache_store', 'array');
|
|
config()->set('jiansun.snapshots.ttl_seconds', 60);
|
|
config()->set('jiansun.snapshots.summary_ttl_seconds', 60);
|
|
config()->set('jiansun.snapshots.options_ttl_seconds', 3600);
|
|
config()->set('jiansun.snapshots.stale_ttl_seconds', 3600);
|
|
config()->set('jiansun.snapshots.active_ttl_seconds', 3600);
|
|
config()->set('jiansun.snapshots.registry_limit', 300);
|
|
config()->set('jiansun.snapshots.refresh_limit', 2);
|
|
config()->set('jiansun.snapshots.miss_budget_per_window', 1000);
|
|
config()->set('jiansun.snapshots.budget_window_seconds', 60);
|
|
config()->set('jiansun.snapshots.max_concurrency', 2);
|
|
config()->set('jiansun.snapshots.slot_wait_ms', 0);
|
|
config()->set('jiansun.snapshots.lock_wait_seconds', 1);
|
|
config()->set('jiansun.snapshots.slow_query_ms', 5000);
|
|
config()->set('jiansun.enabled', true);
|
|
config()->set('jiansun.allowed_scopes', ['*']);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Carbon::setTestNow();
|
|
Cache::flush();
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_identical_queries_are_shared_without_user_count_limit(): void
|
|
{
|
|
$cache = app(JianSunQueryCache::class);
|
|
$calls = 0;
|
|
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$value = $cache->remember('goods.list', ['page' => 1], function () use (&$calls): array {
|
|
$calls++;
|
|
|
|
return ['data' => [['id' => 1]], 'total' => 1];
|
|
});
|
|
|
|
$this->assertSame(1, $value['total']);
|
|
}
|
|
|
|
$this->assertSame(1, $calls);
|
|
$this->assertNotEmpty(Cache::get('jiansun:snapshots:active:v2'));
|
|
}
|
|
|
|
public function test_expired_snapshot_returns_immediately_and_is_refreshed_later(): void
|
|
{
|
|
$cache = app(JianSunQueryCache::class);
|
|
$calls = 0;
|
|
|
|
$first = $cache->remember('goods.list', ['page' => 1], function () use (&$calls): array {
|
|
$calls++;
|
|
|
|
return ['data' => [['version' => 1]], 'total' => 1];
|
|
});
|
|
|
|
Carbon::setTestNow(now()->addSeconds(61));
|
|
|
|
$stale = $cache->remember('goods.list', ['page' => 1], function () use (&$calls): array {
|
|
$calls++;
|
|
|
|
return ['data' => [['version' => 999]], 'total' => 1];
|
|
});
|
|
|
|
$this->assertSame($first, $stale);
|
|
$this->assertSame(1, $calls, '页面读取过期快照时不应同步访问上游。');
|
|
$this->assertSame('stale', $cache->responseHeaders()['X-Caiwu-Data-Status']);
|
|
$this->assertTrue((bool) ($cache->activeEntries(1)[0]['urgent'] ?? false));
|
|
|
|
$status = $cache->refresh('goods.list', ['page' => 1], function () use (&$calls): array {
|
|
$calls++;
|
|
|
|
return ['data' => [['version' => 2]], 'total' => 1];
|
|
});
|
|
|
|
$fresh = $cache->remember('goods.list', ['page' => 1], fn (): array => []);
|
|
|
|
$this->assertSame('fresh', $status);
|
|
$this->assertSame(2, $fresh['data'][0]['version']);
|
|
$this->assertSame('fresh', $cache->responseHeaders()['X-Caiwu-Data-Status']);
|
|
$this->assertSame(2, $calls);
|
|
}
|
|
|
|
public function test_unrelated_cold_queries_do_not_share_one_global_query_lock(): void
|
|
{
|
|
$cache = app(JianSunQueryCache::class);
|
|
$parametersA = [
|
|
'page' => 1,
|
|
'__caiwu_snapshot_context' => [
|
|
'company_id' => 0,
|
|
'access_hash' => 'system',
|
|
],
|
|
];
|
|
ksort($parametersA);
|
|
$queryA = hash('sha256', 'goods.list|' . json_encode($parametersA, JSON_UNESCAPED_UNICODE));
|
|
$lockA = Cache::lock('jiansun:query-lock:' . $queryA, 30);
|
|
|
|
$this->assertTrue($lockA->get());
|
|
|
|
try {
|
|
$value = $cache->remember('goods.summary', [], fn (): array => ['total' => 7]);
|
|
$this->assertSame(7, $value['total']);
|
|
} finally {
|
|
$lockA->release();
|
|
}
|
|
}
|
|
|
|
public function test_upstream_slots_bound_parallel_cold_queries(): void
|
|
{
|
|
$cache = app(JianSunQueryCache::class);
|
|
$slot0 = Cache::lock('jiansun:upstream-slot:0', 30);
|
|
$slot1 = Cache::lock('jiansun:upstream-slot:1', 30);
|
|
|
|
$this->assertTrue($slot0->get());
|
|
$this->assertTrue($slot1->get());
|
|
|
|
try {
|
|
$this->expectException(ServiceUnavailableHttpException::class);
|
|
$cache->remember('goods.list', ['page' => 2], fn (): array => ['data' => [], 'total' => 0]);
|
|
} finally {
|
|
$slot0->release();
|
|
$slot1->release();
|
|
}
|
|
}
|
|
|
|
public function test_disallowed_cold_scope_never_calls_upstream(): void
|
|
{
|
|
config()->set('jiansun.allowed_scopes', ['invoice-deductions.*']);
|
|
$cache = app(JianSunQueryCache::class);
|
|
$calls = 0;
|
|
|
|
try {
|
|
$cache->remember('goods.list', [], function () use (&$calls): array {
|
|
$calls++;
|
|
|
|
return [];
|
|
});
|
|
$this->fail('未开放的数据范围不应进入冷查询。');
|
|
} catch (ServiceUnavailableHttpException) {
|
|
$this->assertSame(0, $calls);
|
|
}
|
|
}
|
|
}
|