Usuwanie nagrania i webinaru zatrzymuje zadanie u dostawcy
Deploy PROD / deploy (push) Successful in 19s

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 08:11:25 +02:00
parent f35bfe1b8c
commit 4203ca036e
2 changed files with 72 additions and 1 deletions
@@ -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) {
@@ -31,7 +31,12 @@ public class WebinarService {
static final Set<RegistrationStatus> 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);
}