Usuwanie nagrania i webinaru zatrzymuje zadanie u dostawcy
Deploy PROD / deploy (push) Successful in 19s
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:
@@ -62,6 +62,7 @@ public class RecordingService {
|
|||||||
private final FileStorageService storage;
|
private final FileStorageService storage;
|
||||||
private final SettingsService settings;
|
private final SettingsService settings;
|
||||||
private final VideoService videoService;
|
private final VideoService videoService;
|
||||||
|
private final com.softspm.webinar.video.LiveKitEgressService egressService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Referencja na samego siebie, przez proxy Springa.
|
* Referencja na samego siebie, przez proxy Springa.
|
||||||
@@ -80,12 +81,14 @@ public class RecordingService {
|
|||||||
FileStorageService storage,
|
FileStorageService storage,
|
||||||
SettingsService settings,
|
SettingsService settings,
|
||||||
VideoService videoService,
|
VideoService videoService,
|
||||||
|
com.softspm.webinar.video.LiveKitEgressService egressService,
|
||||||
@org.springframework.context.annotation.Lazy RecordingService self) {
|
@org.springframework.context.annotation.Lazy RecordingService self) {
|
||||||
this.recordingRepository = recordingRepository;
|
this.recordingRepository = recordingRepository;
|
||||||
this.webinarRepository = webinarRepository;
|
this.webinarRepository = webinarRepository;
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.videoService = videoService;
|
this.videoService = videoService;
|
||||||
|
this.egressService = egressService;
|
||||||
this.self = self;
|
this.self = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +368,34 @@ public class RecordingService {
|
|||||||
public void delete(Long recordingId) {
|
public void delete(Long recordingId) {
|
||||||
Recording recording = require(recordingId);
|
Recording recording = require(recordingId);
|
||||||
String objectKey = recording.getObjectKey();
|
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);
|
recordingRepository.delete(recording);
|
||||||
|
|
||||||
if (objectKey != null) {
|
if (objectKey != null) {
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ public class WebinarService {
|
|||||||
static final Set<RegistrationStatus> ACTIVE_STATUSES =
|
static final Set<RegistrationStatus> ACTIVE_STATUSES =
|
||||||
Set.of(RegistrationStatus.PENDING, RegistrationStatus.CONFIRMED);
|
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.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 WebinarRepository webinarRepository;
|
||||||
private final RegistrationRepository registrationRepository;
|
private final RegistrationRepository registrationRepository;
|
||||||
private final InteractionRepository interactionRepository;
|
private final InteractionRepository interactionRepository;
|
||||||
@@ -47,7 +52,9 @@ public class WebinarService {
|
|||||||
SlugGenerator slugGenerator,
|
SlugGenerator slugGenerator,
|
||||||
SettingsService settings,
|
SettingsService settings,
|
||||||
ReminderRuleService reminderRuleService,
|
ReminderRuleService reminderRuleService,
|
||||||
HtmlSanitizer htmlSanitizer) {
|
HtmlSanitizer htmlSanitizer,
|
||||||
|
com.softspm.webinar.repository.RecordingRepository recordingRepository,
|
||||||
|
com.softspm.webinar.video.LiveKitEgressService egressService) {
|
||||||
this.providerFactory = providerFactory;
|
this.providerFactory = providerFactory;
|
||||||
this.webinarRepository = webinarRepository;
|
this.webinarRepository = webinarRepository;
|
||||||
this.registrationRepository = registrationRepository;
|
this.registrationRepository = registrationRepository;
|
||||||
@@ -55,6 +62,8 @@ public class WebinarService {
|
|||||||
this.slugGenerator = slugGenerator;
|
this.slugGenerator = slugGenerator;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.reminderRuleService = reminderRuleService;
|
this.reminderRuleService = reminderRuleService;
|
||||||
|
this.recordingRepository = recordingRepository;
|
||||||
|
this.egressService = egressService;
|
||||||
this.htmlSanitizer = htmlSanitizer;
|
this.htmlSanitizer = htmlSanitizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +126,37 @@ public class WebinarService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
Webinar webinar = require(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);
|
webinarRepository.delete(webinar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user