From 4203ca036edd027e9ada47fc8fe0efbe08428c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Zatorski?= Date: Sat, 5 Sep 2026 08:11:25 +0200 Subject: [PATCH] Usuwanie nagrania i webinaru zatrzymuje zadanie u dostawcy Ekran Zasoby pozwalal POSPRZATAC osierocone nagrania, ale nie zapobiegal ich powstawaniu. Dwie sciezki tworzyly je dalej: - `RecordingService.delete` kasowal wpis, zostawiajac zadanie dzialajace u dostawcy w nieskonczonosc, - `WebinarService.delete` kasowal nagrania KASKADOWO, przez co trwajace zadanie stawalo sie niewidoczne dla aplikacji: scheduler domykajacy nagrania szuka po naszych wpisach, a tych juz nie bylo. Ta druga sciezka wywolala realny incydent: webinar usunieto w trakcie nagrywania, a przegladarka po stronie Egress krecila sie kilkanascie godzin i zjadala jedna piata procesora na serwerze dzielonym z dwiema innymi aplikacjami. Zaden ekran tego nie pokazywal. Obie zatrzymuja teraz zadanie PRZED skasowaniem wpisu. BLAD ZATRZYMANIA NIE BLOKUJE USUNIECIA Najczestsza przyczyna to zadanie, ktore samo sie juz zakonczylo, a administrator ma prawo posprzatac wpis niezaleznie od tego, co odpowie dostawca. Slad idzie do logu, a zadanie mimo wszystko dzialajace pokaze ekran Zasoby jako osierocone, razem z przyciskiem zatrzymania. Zabezpieczenie dziala wiec z dwoch stron: zapobiega i naprawia. Sprawdzone, ze nie powstaje cykl zaleznosci: `LiveKitEgressService` zalezy wylacznie od repozytoriow, ustawien i fabryki dostawcow, wiec nie siega z powrotem ani do `RecordingService`, ani do `WebinarService`. Co-Authored-By: Claude Opus 5 --- .../webinar/media/RecordingService.java | 31 ++++++++++++++ .../webinar/webinar/WebinarService.java | 42 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/softspm/webinar/media/RecordingService.java b/backend/src/main/java/com/softspm/webinar/media/RecordingService.java index 23458bf..fd5f254 100644 --- a/backend/src/main/java/com/softspm/webinar/media/RecordingService.java +++ b/backend/src/main/java/com/softspm/webinar/media/RecordingService.java @@ -62,6 +62,7 @@ public class RecordingService { private final FileStorageService storage; private final SettingsService settings; private final VideoService videoService; + private final com.softspm.webinar.video.LiveKitEgressService egressService; /** * Referencja na samego siebie, przez proxy Springa. @@ -80,12 +81,14 @@ public class RecordingService { FileStorageService storage, SettingsService settings, VideoService videoService, + com.softspm.webinar.video.LiveKitEgressService egressService, @org.springframework.context.annotation.Lazy RecordingService self) { this.recordingRepository = recordingRepository; this.webinarRepository = webinarRepository; this.storage = storage; this.settings = settings; this.videoService = videoService; + this.egressService = egressService; this.self = self; } @@ -365,6 +368,34 @@ public class RecordingService { public void delete(Long recordingId) { Recording recording = require(recordingId); String objectKey = recording.getObjectKey(); + + /* + * Nagranie WCIAZ TRWAJACE trzeba najpierw zatrzymac u dostawcy. + * + * Skasowanie samego wpisu zostawialo zadanie dzialajace w nieskonczonosc: + * Egress nagrywa dalej, a scheduler domykajacy nagrania szuka po naszych + * wpisach, wiec czegos, czego w bazie juz nie ma, nigdy nie zobaczy. + * Zdarzylo sie to realnie: przegladarka po stronie Egress krecila sie + * kilkanascie godzin i zjadala jedna piata procesora, a zaden ekran + * w aplikacji tego nie pokazywal. + * + * Blad zatrzymania NIE BLOKUJE usuniecia: najczestsza przyczyna to + * zadanie, ktore samo sie juz zakonczylo, a administrator ma prawo + * posprzatac wpis niezaleznie od tego, co odpowie dostawca. Slad idzie + * do logu, a nagranie wciaz widoczne u dostawcy zobaczy ekran Zasoby + * jako osierocone. + */ + if (recording.getStatus() == RecordingStatus.DOWNLOADING + && recording.getProviderRecordingId() != null + && !recording.getProviderRecordingId().isBlank()) { + try { + egressService.stopRecording(recording.getProviderRecordingId()); + } catch (Exception e) { + log.warn("Nie udalo sie zatrzymac nagrania {} przy usuwaniu: {}", + recording.getProviderRecordingId(), e.getMessage()); + } + } + recordingRepository.delete(recording); if (objectKey != null) { diff --git a/backend/src/main/java/com/softspm/webinar/webinar/WebinarService.java b/backend/src/main/java/com/softspm/webinar/webinar/WebinarService.java index 9a811f3..b54fdb9 100644 --- a/backend/src/main/java/com/softspm/webinar/webinar/WebinarService.java +++ b/backend/src/main/java/com/softspm/webinar/webinar/WebinarService.java @@ -31,7 +31,12 @@ public class WebinarService { static final Set ACTIVE_STATUSES = Set.of(RegistrationStatus.PENDING, RegistrationStatus.CONFIRMED); + private static final org.slf4j.Logger log = + org.slf4j.LoggerFactory.getLogger(WebinarService.class); + private final com.softspm.webinar.video.VideoProviderFactory providerFactory; + private final com.softspm.webinar.repository.RecordingRepository recordingRepository; + private final com.softspm.webinar.video.LiveKitEgressService egressService; private final WebinarRepository webinarRepository; private final RegistrationRepository registrationRepository; private final InteractionRepository interactionRepository; @@ -47,7 +52,9 @@ public class WebinarService { SlugGenerator slugGenerator, SettingsService settings, ReminderRuleService reminderRuleService, - HtmlSanitizer htmlSanitizer) { + HtmlSanitizer htmlSanitizer, + com.softspm.webinar.repository.RecordingRepository recordingRepository, + com.softspm.webinar.video.LiveKitEgressService egressService) { this.providerFactory = providerFactory; this.webinarRepository = webinarRepository; this.registrationRepository = registrationRepository; @@ -55,6 +62,8 @@ public class WebinarService { this.slugGenerator = slugGenerator; this.settings = settings; this.reminderRuleService = reminderRuleService; + this.recordingRepository = recordingRepository; + this.egressService = egressService; this.htmlSanitizer = htmlSanitizer; } @@ -117,6 +126,37 @@ public class WebinarService { @Transactional public void delete(Long id) { Webinar webinar = require(id); + + /* + * Nagrania WCIAZ TRWAJACE trzeba zatrzymac u dostawcy PRZED kasowaniem. + * + * Wpisy znikaja tu kaskadowo, wiec zadanie nagrywajace zostawaloby + * dzialajace i **niewidoczne dla aplikacji**: scheduler domykajacy + * nagrania szuka po naszych wpisach, a tych juz nie ma. Zdarzylo sie + * to realnie: webinar usunieto w trakcie nagrywania, a przegladarka + * po stronie Egress krecila sie kilkanascie godzin, zjadajac jedna + * piata procesora serwera dzielonego z innymi aplikacjami. + * + * Blad zatrzymania nie blokuje usuniecia: administrator ma prawo + * skasowac webinar niezaleznie od tego, co odpowie dostawca. Slad idzie + * do logu, a zadanie wciaz dzialajace pokaze ekran Zasoby jako + * osierocone, razem z przyciskiem zatrzymania. + */ + for (var recording : recordingRepository.findByWebinarIdAndStatusOrderByCreatedAtDesc( + webinar.getId(), com.softspm.webinar.domain.RecordingStatus.DOWNLOADING)) { + String egressId = recording.getProviderRecordingId(); + if (egressId == null || egressId.isBlank()) { + continue; + } + try { + egressService.stopRecording(egressId); + log.info("Zatrzymano nagranie {} przed usunieciem webinaru {}", egressId, id); + } catch (Exception e) { + log.warn("Nie udalo sie zatrzymac nagrania {} przed usunieciem webinaru {}: {}", + egressId, id, e.getMessage()); + } + } + webinarRepository.delete(webinar); }