봇 요청 스레드의 동기 생성을 제거하고 디스크 스트리밍 서빙으로 전환했다. 신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale) 또는 503+Retry-After 로 응답해, 대용량에서 봇 요청이 OOM/타임아웃을 유발하던 경로를 끊었다. - 잡: ShouldBeUnique(고정 uniqueId)+uniqueFor+retryUntil, timeout 300→1800, backoff - 컨트롤러: 인덱스 stale/503 분기 + 자식 라우트(.xml/.xml.gz 동일 액션) - 스케줄 withoutOverlapping, 관리자 재생성 큐 이관 - 설정 4키(분할 기준·압축·stale 서빙·수집기 상한) + 고급탭 Sitemap 캐시 짝 - SitemapManager 응답 메시지 다국어화 설정 저장 테스트가 실제 설정 파일(사이트명/언어 포함)을 덮어쓰던 사전결함을 TestCase 전역 격리로 차단하고, 두 탭이 같은 다국어 키를 공유해 한쪽에 틀린 한도를 안내하던 사전결함을 키 분리로 해소했다.
96 lines
3.1 KiB
PHP
96 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Public;
|
|
|
|
use App\Contracts\Extension\CacheInterface;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Jobs\GenerateSitemapJob;
|
|
use App\Seo\SitemapFileStore;
|
|
use App\Seo\SitemapManager;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
/**
|
|
* Sitemap XML 컨트롤러
|
|
*
|
|
* 비공개 디스크에 커밋된 sitemap 인덱스/자식 파일을 스트리밍으로 서빙합니다.
|
|
* 봇 요청 스레드에서 sitemap 을 생성하지 않으며(대용량에서 OOM/타임아웃 원인),
|
|
* 신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale)를 그대로 내보냅니다.
|
|
*/
|
|
class SitemapController extends Controller
|
|
{
|
|
/**
|
|
* 세트가 없어 503 을 반환할 때 봇에 안내할 재시도 간격 (초)
|
|
*/
|
|
private const RETRY_AFTER_SECONDS = 120;
|
|
|
|
/**
|
|
* sitemapindex 를 반환합니다.
|
|
*
|
|
* 커밋된 세트가 있으면 스트리밍하고, 신선도(메타 캐시)가 만료되었으면
|
|
* 생성 잡을 예약합니다. 세트가 전혀 없으면 503 + Retry-After 를 반환합니다.
|
|
*
|
|
* @param SitemapFileStore $store sitemap 파일 저장소 (읽기측)
|
|
* @param CacheInterface $cache 코어 캐시 드라이버
|
|
* @return StreamedResponse sitemapindex 스트림
|
|
*/
|
|
public function index(SitemapFileStore $store, CacheInterface $cache): StreamedResponse
|
|
{
|
|
$this->assertSitemapEnabled();
|
|
|
|
$isFresh = $cache->get(SitemapManager::META_CACHE_KEY) !== null;
|
|
|
|
if ($isFresh) {
|
|
$response = $store->indexResponse();
|
|
if ($response !== null) {
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
// 신선도 만료 또는 세트 부재 — 생성은 큐에 맡긴다 (ShouldBeUnique 로 중복 디스패치 방지).
|
|
GenerateSitemapJob::dispatch();
|
|
|
|
$serveStale = (bool) g7_core_settings('seo.sitemap_serve_stale_on_miss', true);
|
|
|
|
if ($serveStale) {
|
|
$response = $store->indexResponse();
|
|
if ($response !== null) {
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
abort(503, __('seo.sitemap_not_ready'), ['Retry-After' => (string) self::RETRY_AFTER_SECONDS]);
|
|
}
|
|
|
|
/**
|
|
* 자식 sitemap 을 반환합니다.
|
|
*
|
|
* gzip 여부는 manifest 가 결정하므로 `.xml` / `.xml.gz` 요청을 같은 액션에서 처리합니다.
|
|
*
|
|
* @param int $n 자식 번호 (1부터)
|
|
* @param SitemapFileStore $store sitemap 파일 저장소 (읽기측)
|
|
* @return StreamedResponse 자식 sitemap 스트림
|
|
*/
|
|
public function child(int $n, SitemapFileStore $store): StreamedResponse
|
|
{
|
|
$this->assertSitemapEnabled();
|
|
|
|
$response = $store->childResponse($n);
|
|
|
|
if ($response === null) {
|
|
abort(404);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Sitemap 기능이 비활성화되어 있으면 404 로 중단합니다.
|
|
*/
|
|
private function assertSitemapEnabled(): void
|
|
{
|
|
if (! (bool) g7_core_settings('seo.sitemap_enabled', true)) {
|
|
abort(404);
|
|
}
|
|
}
|
|
}
|