SSR health: alert mails on ok/down transitions + auto-failover to SPA after N consecutive misses (SSR_ALERT_EMAIL / SSR_FAILOVER_AFTER in var/vitec-ssr-purge.env)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-11-09 22:18:06 +01:00
1 parent b9736b9ac7
commit bc2e031b0c
3 files changed
+183 -2

No files matched your search

@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Evomedien\Vitec\Service;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Keeps the SSR routing flag file in sync with the site setting
@@ -91,6 +93,48 @@ final class SsrFlagService
return $desired;
}
/**
* Auto-failover (vitec:ssr-health): turns SSR delivery hard OFF.
*
* Removing only the flag file would not stick - the self-healing
* SyncSsrFlag middleware recreates it from the still-true site setting
* on the very next request. So the setting itself is flipped to false
* in config/sites/<identifier>/settings.yaml (the file the backend
* settings editor writes, flat dotted keys), caches are flushed so the
* compiled site settings forget the old value, and only then the flag
* is removed. Re-enabling is deliberately manual (backend switch or
* settings.yaml + cache flush).
*/
public function disableSsr(): void
{
$changed = false;
foreach ($this->siteFinder->getAllSites() as $site) {
$file = Environment::getConfigPath() . '/sites/' . $site->getIdentifier() . '/settings.yaml';
if (!is_file($file)) {
continue;
}
$yaml = (string)file_get_contents($file);
$new = preg_replace('/^(vitec\.ssr\.enabled\s*:).*$/m', '$1 false', $yaml, 1, $count);
if ($new !== null && $count > 0 && $new !== $yaml) {
if (file_put_contents($file, $new) === false) {
throw new \RuntimeException('Could not write ' . $file, 1762800001);
}
$changed = true;
}
}
if (!$changed && $this->isEnabledInSettings()) {
throw new \RuntimeException(
'vitec.ssr.enabled not found in any config/sites/*/settings.yaml - cannot disable it, '
. 'and the SyncSsrFlag middleware would recreate the flag from the setting',
1762800002
);
}
if ($changed) {
GeneralUtility::makeInstance(CacheManager::class)->flushCaches();
}
@unlink($this->getFlagPath());
}
/**
* Sync variant for the request hot path: never lets an IO hiccup
* (read-only FS, concurrent unlink ...) break request handling.
@@ -13,7 +13,8 @@ use TYPO3\CMS\Core\Http\RequestFactory;
* keeps a log of every attempt in tx_vitec_ssrpurgelog.
*
* Endpoint + secret live OUTSIDE the repository in var/vitec-ssr-purge.env
* (KEY=VALUE lines: SSR_PURGE_ENDPOINT, SSR_PURGE_SECRET, SSR_HEALTH_ENDPOINT)
* (KEY=VALUE lines: SSR_PURGE_ENDPOINT, SSR_PURGE_SECRET, SSR_HEALTH_ENDPOINT,
* SSR_ALERT_EMAIL, SSR_FAILOVER_AFTER)
* - same pattern as the routing flag file, nothing secret in git.
*/
final class SsrPurgeService
@@ -41,6 +42,25 @@ final class SsrPurgeService
return $this->getConfig()['SSR_HEALTH_ENDPOINT'] ?? '';
}
/**
* Comma-separated recipient list for vitec:ssr-health transition and
* failover alerts (SSR_ALERT_EMAIL); empty string = alerting off.
*/
public function getAlertEmail(): string
{
return trim($this->getConfig()['SSR_ALERT_EMAIL'] ?? '');
}
/**
* Consecutive DOWN samples after which vitec:ssr-health fails over to
* the SPA (SSR_FAILOVER_AFTER, default 2 = ~10 minutes at the 5-minute
* cron cadence). Never below 1.
*/
public function getFailoverAfter(): int
{
return max(1, (int)($this->getConfig()['SSR_FAILOVER_AFTER'] ?? 2));
}
/**
* Normalizes, dedupes and sends the paths; logs the outcome.
* Never throws - editors' saves must not fail on a purge problem.