06fc4680f4
CachingPageFetcher — декоратор над загрузчиком карточек (SelfRenderClient/ XfetchClient): кэширует в общем Cache (Redis/file) по ключу URL на N дней ТОЛЬКО карточки (/firm/ 2ГИС, /maps/org/ Яндекс). Поиск (/search/) и ленты — всегда мимо кэша (свежее добывание новых фирм). Пустое не кэшируем — разовый сбой не отравит кэш. Кэш ОБЩИЙ для шага 1 (поиск) и шага 2 (изучение+разворот) и повторных прогонов: ключ по URL → любые экземпляры делят один кэш. Вшит в AutopodborServiceProvider через wrapCache(); 2ГИС-база RoutingBatchFetcher и self-render card reader обёрнуты, яндекс-Playwright/ленты — нет. Флаг AUTOPODBOR_RENDER_CACHE (ВЫКЛ по умолчанию) + render_cache_ttl_days (5). TDD: 8 тестов CachingPageFetcher + 4 теста проводки — зелёные. Живой замер ДСК: тёплый прогон — 26 карточек из кэша, 0 обращений к 2ГИС/Яндексу, источников 11=11. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
5.7 KiB
PHP
150 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\Fetch\BatchPageFetcher;
|
|
use App\Services\Autopodbor\Agent\Fetch\CachingPageFetcher;
|
|
use App\Services\Autopodbor\Agent\Fetch\PageFetcher;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
// CachingPageFetcher — декоратор над реальным BatchPageFetcher. Кэширует в Redis по URL на N дней
|
|
// ТОЛЬКО карточки (/firm/ 2ГИС, /maps/org/ Яндекс). Поиск (/search/) и прочее — всегда мимо кэша.
|
|
// Пустой ответ НЕ кэшируется (разовый сбой не отравит кэш). Кэш общий для шага 1 и шага 2 (ключ по URL).
|
|
|
|
// Инструментированный inner-фетчер: считает вызовы по URL и отдаёт заготовленные ответы.
|
|
final class SpyBatchFetcher implements BatchPageFetcher
|
|
{
|
|
/** @var array<string,int> url => сколько раз спросили */
|
|
public array $htmlCalls = [];
|
|
|
|
/** @var array<string,int> url => сколько раз спросили в пачке */
|
|
public array $batchCalls = [];
|
|
|
|
/** @param array<string,string> $responses url => ответ */
|
|
public function __construct(private array $responses = []) {}
|
|
|
|
public function html(string $url): string
|
|
{
|
|
$this->htmlCalls[$url] = ($this->htmlCalls[$url] ?? 0) + 1;
|
|
|
|
return $this->responses[$url] ?? '';
|
|
}
|
|
|
|
public function htmlBatch(array $urls): array
|
|
{
|
|
$out = [];
|
|
foreach ($urls as $url) {
|
|
$this->batchCalls[$url] = ($this->batchCalls[$url] ?? 0) + 1;
|
|
$out[$url] = $this->responses[$url] ?? '';
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
}
|
|
|
|
beforeEach(function () {
|
|
config()->set('cache.default', 'array');
|
|
Cache::store('array')->flush();
|
|
});
|
|
|
|
function caching(SpyBatchFetcher $inner, int $ttlDays = 5): CachingPageFetcher
|
|
{
|
|
return new CachingPageFetcher($inner, ttlDays: $ttlDays);
|
|
}
|
|
|
|
it('реализует оба интерфейса', function () {
|
|
$c = caching(new SpyBatchFetcher);
|
|
|
|
expect($c)->toBeInstanceOf(PageFetcher::class)
|
|
->and($c)->toBeInstanceOf(BatchPageFetcher::class);
|
|
});
|
|
|
|
it('карточка 2ГИС: второй вызов — из кэша (inner дёрнут один раз)', function () {
|
|
$url = 'https://2gis.ru/krasnoyarsk/firm/70000001045067758';
|
|
$inner = new SpyBatchFetcher([$url => '{"state":1}']);
|
|
$c = caching($inner);
|
|
|
|
expect($c->html($url))->toBe('{"state":1}')
|
|
->and($c->html($url))->toBe('{"state":1}');
|
|
|
|
expect($inner->htmlCalls[$url])->toBe(1);
|
|
});
|
|
|
|
it('карточка Яндекс /maps/org/ тоже кэшируется', function () {
|
|
$url = 'https://yandex.ru/maps/org/metropolis/1234567890';
|
|
$inner = new SpyBatchFetcher([$url => '<html>ya</html>']);
|
|
$c = caching($inner);
|
|
|
|
expect($c->html($url))->toBe('<html>ya</html>')
|
|
->and($c->html($url))->toBe('<html>ya</html>');
|
|
|
|
expect($inner->htmlCalls[$url])->toBe(1);
|
|
});
|
|
|
|
it('поиск /search/ — всегда мимо кэша (inner каждый раз)', function () {
|
|
$url = 'https://2gis.ru/krasnoyarsk/search/ломбард';
|
|
$inner = new SpyBatchFetcher([$url => 'SEARCH']);
|
|
$c = caching($inner);
|
|
|
|
expect($c->html($url))->toBe('SEARCH')
|
|
->and($c->html($url))->toBe('SEARCH');
|
|
|
|
expect($inner->htmlCalls[$url])->toBe(2);
|
|
});
|
|
|
|
it('пустой ответ карточки НЕ кэшируется — inner дёрнут снова', function () {
|
|
$url = 'https://2gis.ru/krasnoyarsk/firm/999';
|
|
$inner = new SpyBatchFetcher([]); // всегда пусто
|
|
$c = caching($inner);
|
|
|
|
expect($c->html($url))->toBe('')
|
|
->and($c->html($url))->toBe('');
|
|
|
|
expect($inner->htmlCalls[$url])->toBe(2);
|
|
});
|
|
|
|
it('htmlBatch: часть из кэша, часть добирается', function () {
|
|
$cached = 'https://2gis.ru/krasnoyarsk/firm/111';
|
|
$fresh = 'https://2gis.ru/krasnoyarsk/firm/222';
|
|
$inner = new SpyBatchFetcher([$cached => 'A', $fresh => 'B']);
|
|
$c = caching($inner);
|
|
|
|
// Первый заход — прогреть кэш только для $cached.
|
|
expect($c->html($cached))->toBe('A');
|
|
expect($inner->htmlCalls[$cached])->toBe(1);
|
|
|
|
// Пачка: $cached уже в кэше (inner НЕ спрашивают), $fresh добирается.
|
|
$out = $c->htmlBatch([$cached, $fresh]);
|
|
|
|
expect($out)->toBe([$cached => 'A', $fresh => 'B']);
|
|
expect($inner->batchCalls[$cached] ?? 0)->toBe(0)
|
|
->and($inner->batchCalls[$fresh])->toBe(1);
|
|
});
|
|
|
|
it('htmlBatch: /search/ всегда добирается, карточка после первого раза — из кэша', function () {
|
|
$search = 'https://2gis.ru/krasnoyarsk/search/шины';
|
|
$card = 'https://2gis.ru/krasnoyarsk/firm/333';
|
|
$inner = new SpyBatchFetcher([$search => 'S', $card => 'C']);
|
|
$c = caching($inner);
|
|
|
|
$c->htmlBatch([$search, $card]); // прогрев: card в кэш, search — нет
|
|
$out = $c->htmlBatch([$search, $card]);
|
|
|
|
// Порядок в карте url→контент не гарантирован — сверяем по ключам.
|
|
expect($out[$search])->toBe('S')->and($out[$card])->toBe('C');
|
|
// search добирался оба раза, card — только в первый.
|
|
expect($inner->batchCalls[$search])->toBe(2)
|
|
->and($inner->batchCalls[$card])->toBe(1);
|
|
});
|
|
|
|
it('htmlBatch: пустой ответ карточки не кэшируется', function () {
|
|
$card = 'https://2gis.ru/krasnoyarsk/firm/444';
|
|
$inner = new SpyBatchFetcher([]); // пусто
|
|
$c = caching($inner);
|
|
|
|
$c->htmlBatch([$card]);
|
|
$c->htmlBatch([$card]);
|
|
|
|
expect($inner->batchCalls[$card])->toBe(2);
|
|
});
|