diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index aee5399..780e8fb 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -3,6 +3,7 @@ namespace App\Controller; use App\Entity\Article; +use App\Http\PhpExecutionTime; use App\Repository\ArticleRepository; use App\Service\ArticleBodyHtmlRenderer; use App\Enum\KindsEnum; @@ -35,10 +36,11 @@ class ArticleController extends AbstractController #[Route('/fragment/comments', name: 'article_comments_fragment', methods: ['GET'])] public function commentsFragment(Request $request, ArticleCommentThreadLoader $loader, LoggerInterface $logger): Response { - // {@see NostrClient::getArticleDiscussion} runs per-relay work in parallel CLI workers; allow headroom - // for all processes + Symfony (45s was too low and caused an uncatchable max-execution fatal → HTTP 500). - @set_time_limit(300); - @ini_set('max_execution_time', '300'); + // {@see NostrClient::getArticleDiscussion} uses parallel CLI workers; cap below multi-minute defaults + // so Apache event MPM scoreboard slots are not held unnecessarily (see {@see PhpExecutionTime}). + $t = PhpExecutionTime::NOSTR_BOUND_WEB_SEC; + @set_time_limit($t); + @ini_set('max_execution_time', (string) $t); $t0 = microtime(true); $coordinate = $request->query->getString('coordinate'); @@ -365,8 +367,9 @@ class ArticleController extends AbstractController ArticleBodyHtmlRenderer $articleBodyHtmlRenderer, NostrKeyHelper $nostrKeyHelper, ): Response { - set_time_limit(300); // 5 minutes - ini_set('max_execution_time', '300'); + $t = PhpExecutionTime::NOSTR_BOUND_WEB_SEC; + set_time_limit($t); + ini_set('max_execution_time', (string) $t); $html = $articleBodyHtmlRenderer->renderForArticle($article); @@ -601,8 +604,9 @@ class ArticleController extends AbstractController #[Route('/articles', name: 'articles')] public function latestArticles(Request $request, EntityManagerInterface $entityManager): Response { - set_time_limit(300); // 5 minutes - ini_set('max_execution_time', '300'); + $t = PhpExecutionTime::LIGHT_WEB_SEC; + set_time_limit($t); + ini_set('max_execution_time', (string) $t); $perPage = 25; $page = max(1, $request->query->getInt('page', 1)); diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php index 8e2a5f3..1785784 100644 --- a/src/Controller/AuthorController.php +++ b/src/Controller/AuthorController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller; +use App\Http\PhpExecutionTime; use App\Repository\ArticleRepository; use App\Repository\FeaturedAuthorRepository; use App\Service\CacheService; @@ -36,10 +37,9 @@ class AuthorController extends AbstractController ProfileIdentityLinksBuilder $profileIdentityLinks, NostrKeyHelper $nostrKeyHelper, ): Response { - // Profile pages chain several sequential Nostr REQ runs; match article pages so a slow relay - // set does not hit PHP’s default 30s max_execution_time during Twig render. - @set_time_limit(300); - @ini_set('max_execution_time', '300'); + // Profile pages chain several sequential Nostr REQ runs; cap wall time so Apache workers are not held for minutes. + @set_time_limit(PhpExecutionTime::NOSTR_BOUND_WEB_SEC); + @ini_set('max_execution_time', (string) PhpExecutionTime::NOSTR_BOUND_WEB_SEC); $pubkey = $nostrKeyHelper->convertToHex($npub); diff --git a/src/Http/PhpExecutionTime.php b/src/Http/PhpExecutionTime.php new file mode 100644 index 0000000..43bae04 --- /dev/null +++ b/src/Http/PhpExecutionTime.php @@ -0,0 +1,18 @@ +nostrClient->getArticleDiscussion($coordinate, $articleEventHexId); $partial = (bool) ($out['partial'] ?? false); - // Partial relay snapshots are intentionally short-lived so the next request can pick up late relays. - $item->expiresAfter($partial ? 15 : 86400); + // Partial: bounded TTL so late relays can still appear without re-fetching every few seconds. + $item->expiresAfter($partial ? self::PARTIAL_THREAD_CACHE_TTL_SEC : 86400); $this->logger->info('comments.loader.nostr_ok', [ 'nostr_elapsed_ms' => (int) round((microtime(true) - $tNostr) * 1000), 'thread' => \count($out['thread'] ?? []), diff --git a/src/Service/Nip05VerificationService.php b/src/Service/Nip05VerificationService.php index c38adb1..4e71651 100644 --- a/src/Service/Nip05VerificationService.php +++ b/src/Service/Nip05VerificationService.php @@ -16,7 +16,10 @@ final readonly class Nip05VerificationService { private const CACHE_PREFIX = 'nip05v1_'; - private const FETCH_TIMEOUT_SEC = 8; + private const FETCH_TIMEOUT_SEC = 5; + + /** Avoid sequential cold HTTP to many domains on one page tying up a worker. */ + private const MAX_COLD_VERIFICATIONS_PER_ENRICH = 5; public function __construct( private CacheItemPoolInterface $appCache, @@ -42,6 +45,7 @@ final readonly class Nip05VerificationService }, $rows); } $out = []; + $coldDone = 0; foreach ($rows as $r) { $label = (string) ($r['label'] ?? ''); $n = $this->normalizeNip05($label); @@ -56,12 +60,21 @@ final readonly class Nip05VerificationService $item = $this->appCache->getItem($k); if ($item->isHit() && \is_bool($item->get())) { $verified = (bool) $item->get(); + } elseif ($coldDone >= self::MAX_COLD_VERIFICATIONS_PER_ENRICH) { + $this->logger->info('nip05.verify_cold_skipped_budget', ['label' => $label]); + $verified = false; } else { + ++$coldDone; // Cold cache: verify now so the profile shows ✓ without a prior prewarm run. $verified = $this->verifyAndCache($h, $label); } } catch (InvalidArgumentException) { - $verified = $this->verifyAndCache($h, $label); + if ($coldDone >= self::MAX_COLD_VERIFICATIONS_PER_ENRICH) { + $verified = false; + } else { + ++$coldDone; + $verified = $this->verifyAndCache($h, $label); + } } $out[] = [...$r, 'verified' => $verified]; } diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index 0bfe45f..4574a85 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -38,9 +38,9 @@ class NostrClient { /** * Hard cap on unique relay URLs for article discussion. More relays do not help much (indexers duplicate) - * but blow up wall time when we fall back to sequential in-process {@see Request::send()}. + * but each URL can spawn a CLI worker ({@see NostrRelayFanoutTransport::sendParallelWorkers}) → Apache/CPU load. */ - private const MAX_DISCUSSION_RELAY_URLS = 10; + private const MAX_DISCUSSION_RELAY_URLS = 8; /** * Kind-9802 highlight ingest ({@see fetchHighlightEventsForArticle} / prewarm): main + article + profile