Zadanie ubite razem z kontenerem znika z ekranu Zasoby po osmiu godzinach
Deploy PROD / deploy (push) Successful in 19s

Egress zapisuje stan zadan w Redisie, a status koncowy ustawia sam proces
obslugujacy nagranie. Gdy proces zostanie ubity razem z kontenerem (restart,
wyczerpanie pamieci), status nigdy sie nie zmienia: zadanie figuruje jako
EGRESS_ACTIVE **na zawsze**.

`StopEgress` konczy sie wtedy sukcesem i NIC NIE ZMIENIA, bo nie ma juz czego
zatrzymywac. Sprawdzone na zywo: wpis wisial po restarcie Egress, panel
pokazywal go jako osierocone zadanie, a klikniecie "Zatrzymaj" logowalo
"Zatrzymano Egress" i zostawialo wszystko po staremu. Zuzycie procesora bylo
przy tym zerowe, czyli zadanie faktycznie nie istnialo.

Ekran mowil wiec nieprawde i nie dawalo sie tego odkrecic z poziomu aplikacji:
wpis usunieto recznie z Redisa.

Liczy sie teraz takze WIEK zadania, nie tylko jego status. Osiem godzin: dluzej
niz najdluzszy sensowny webinar razem z montazem, a krocej niz doba, zeby wpis
po nocnym restarcie nie straszyl rano.

`startedAt` jest w nanosekundach od epoki. Proto podaje sam `int64` bez
jednostki, ale webhooki LiveKit niosa tam wartosci 19-cyfrowe, a `duration`
w `FileInfo` jest liczone tak samo. Zero oznacza zadanie, ktore nie zdazylo
wystartowac: traktujemy je jak biezace, zeby swiezo zlecone nagranie nie
znikalo z ekranu, zanim ruszy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 08:19:12 +02:00
parent 4203ca036e
commit 0292b03d5e
@@ -54,6 +54,15 @@ public class LiveKitEgressService {
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
/**
* Po jakim czasie zadanie u dostawcy uznajemy za martwe, niezaleznie od
* jego statusu.
*
* Osiem godzin: dluzej niz najdluzszy sensowny webinar razem z montazem,
* a krocej niz doba, zeby wpis po nocnym restarcie nie straszyl rano.
*/
private static final java.time.Duration STALE_EGRESS_AFTER = java.time.Duration.ofHours(8);
private final SettingsService settings; private final SettingsService settings;
private final WebinarRepository webinarRepository; private final WebinarRepository webinarRepository;
private final RecordingRepository recordingRepository; private final RecordingRepository recordingRepository;
@@ -350,11 +359,43 @@ public class LiveKitEgressService {
if (!response.isSuccessful() || response.body() == null) { if (!response.isSuccessful() || response.body() == null) {
throw new ValidationException("Serwer LiveKit odpowiedzial: " + describe(response.code())); throw new ValidationException("Serwer LiveKit odpowiedzial: " + describe(response.code()));
} }
/*
* ZADANIE UBITE ZOSTAJE W REJESTRZE JAKO AKTYWNE
*
* Egress zapisuje stan w Redisie, a status koncowy ustawia sam
* proces obslugujacy nagranie. Gdy proces zostanie ubity razem
* z kontenerem (restart, wyczerpanie pamieci), status nigdy sie
* nie zmienia: zadanie figuruje jako EGRESS_ACTIVE na zawsze.
* `StopEgress` konczy sie wtedy sukcesem i NIC NIE ZMIENIA, bo
* nie ma juz czego zatrzymywac.
*
* Sprawdzone na zywo: wpis wisial po restarcie Egress, a panel
* pokazywal go jako osierocone zadanie mimo zerowego zuzycia
* procesora i mimo udanego zatrzymania.
*
* Dlatego liczy sie takze WIEK. Zadanie starsze niz okno sensownego
* nagrania jest martwe niezaleznie od tego, co twierdzi status:
* najdluzszy webinar trwa kilka godzin, a montaz pliku minuty.
*/
long staleBefore = System.currentTimeMillis() - STALE_EGRESS_AFTER.toMillis();
return response.body().stream() return response.body().stream()
.filter(info -> switch (info.getStatus()) { .filter(info -> switch (info.getStatus()) {
case EGRESS_STARTING, EGRESS_ACTIVE, EGRESS_ENDING -> true; case EGRESS_STARTING, EGRESS_ACTIVE, EGRESS_ENDING -> true;
default -> false; default -> false;
}) })
/*
* `startedAt` jest w NANOSEKUNDACH od epoki: proto podaje
* sam `int64` bez jednostki, ale webhooki LiveKit niosa
* tam wartosci 19-cyfrowe, a `duration` w `FileInfo` jest
* liczone tak samo. Stad dzielenie przez milion.
*
* Zero oznacza zadanie, ktore nie zdazylo wystartowac:
* traktujemy je jak biezace, zeby swiezo zlecone nagranie
* nie znikalo z ekranu, zanim ruszy.
*/
.filter(info -> info.getStartedAt() == 0L
|| info.getStartedAt() / 1_000_000L > staleBefore)
.map(LivekitEgress.EgressInfo::getEgressId) .map(LivekitEgress.EgressInfo::getEgressId)
.toList(); .toList();
} catch (ValidationException e) { } catch (ValidationException e) {