diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index bfc767d2..4a5b90fd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +39,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; @Lazy @Service @@ -188,6 +191,30 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { .flatMap(this.examDAO::byPK); } + @EventListener + public void notifyExamStarted(final ExamStartedEvent event) { + + log.info("ExamStartedEvent received, process applySEBClientRestriction..."); + + applySEBClientRestriction(event.exam) + .onError(error -> log.error( + "Failed to apply SEB restrictions for started exam: {}", + event.exam, + error)); + } + + @EventListener + public void notifyExamFinished(final ExamFinishedEvent event) { + + log.info("ExamFinishedEvent received, process releaseSEBClientRestriction..."); + + releaseSEBClientRestriction(event.exam) + .onError(error -> log.error( + "Failed to release SEB restrictions for finished exam: {}", + event.exam, + error)); + } + @Override public Result applySEBClientRestriction(final Exam exam) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamFinishedEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamFinishedEvent.java new file mode 100644 index 00000000..72ddaf49 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamFinishedEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.session; + +import org.springframework.context.ApplicationEvent; + +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; + +/** This event is fired just after an exam has been finished */ +public class ExamFinishedEvent extends ApplicationEvent { + + private static final long serialVersionUID = -1528880878532843063L; + + public final Exam exam; + + public ExamFinishedEvent(final Exam exam) { + super(exam); + this.exam = exam; + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index 24d0296b..24b85227 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -191,13 +191,6 @@ public interface ExamSessionService { * @return Result refer to the collection of connection tokens or to an error when happened. */ Result> getActiveConnectionTokens(Long examId); - /** Called to notify that the given exam has just been finished. - * This cleanup all exam session caches for the given exam and also cleanup session based stores on the persistent. - * - * @param exam the Exam that has just been finished - * @return Result refer to the finished exam or to an error when happened. */ - Result notifyExamFinished(final Exam exam); - /** Use this to check if the current cached running exam is up to date * and if not to flush the cache. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamStartedEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamStartedEvent.java new file mode 100644 index 00000000..ee6ac689 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamStartedEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.session; + +import org.springframework.context.ApplicationEvent; + +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; + +/** This event is fired just after an exam has been started */ +public class ExamStartedEvent extends ApplicationEvent { + + private static final long serialVersionUID = -6564345490588661010L; + + public final Exam exam; + + public ExamStartedEvent(final Exam exam) { + super(exam); + this.exam = exam; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index c5d335c2..642f0839 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -29,7 +29,6 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; @Service @@ -43,7 +42,6 @@ public class ExamSessionControlTask implements DisposableBean { private final ExamUpdateHandler examUpdateHandler; private final ExamProctoringRoomService examProcotringRoomService; private final WebserviceInfo webserviceInfo; - private final ExamSessionService examSessionService; private final Long examTimePrefix; private final Long examTimeSuffix; @@ -56,7 +54,6 @@ public class ExamSessionControlTask implements DisposableBean { final ExamUpdateHandler examUpdateHandler, final ExamProctoringRoomService examProcotringRoomService, final WebserviceInfo webserviceInfo, - final ExamSessionService examSessionService, @Value("${sebserver.webservice.api.exam.time-prefix:3600000}") final Long examTimePrefix, @Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix, @Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron, @@ -66,7 +63,6 @@ public class ExamSessionControlTask implements DisposableBean { this.sebClientConnectionService = sebClientConnectionService; this.examUpdateHandler = examUpdateHandler; this.webserviceInfo = webserviceInfo; - this.examSessionService = examSessionService; this.examTimePrefix = examTimePrefix; this.examTimeSuffix = examTimeSuffix; this.examTaskCron = examTaskCron; @@ -188,8 +184,6 @@ public class ExamSessionControlTask implements DisposableBean { .stream() .filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now)) .flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setFinished(exam, updateId))) - .flatMap(exam -> Result.skipOnError(this.examProcotringRoomService.disposeRoomsForExam(exam))) - .flatMap(exam -> Result.skipOnError(this.examSessionService.notifyExamFinished(exam))) .collect(Collectors.toMap(Exam::getId, Exam::getName)); if (!updated.isEmpty()) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index d761f068..e2f341a8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; @@ -47,6 +48,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; @Lazy @@ -402,20 +404,23 @@ public class ExamSessionServiceImpl implements ExamSessionService { .getActiveConnctionTokens(examId); } - @Override - public Result notifyExamFinished(final Exam exam) { - return Result.tryCatch(() -> { - if (!isExamRunning(exam.id)) { - this.flushCache(exam); + @EventListener + public void notifyExamFinished(final ExamFinishedEvent event) { + + log.info("ExamFinishedEvent received, process exam session cleanup..."); + + try { + if (!isExamRunning(event.exam.id)) { + this.flushCache(event.exam); if (this.distributedSetup) { this.clientConnectionDAO - .deleteClientIndicatorValues(exam) + .deleteClientIndicatorValues(event.exam) .getOrThrow(); } } - - return exam; - }); + } catch (final Exception e) { + log.error("Failed to cleanup on finished exam: {}", event.exam, e); + } } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index 3defad8d..c77244dc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -13,6 +13,7 @@ import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -24,6 +25,8 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent; @Lazy @Service @@ -33,17 +36,20 @@ class ExamUpdateHandler { private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class); private final ExamDAO examDAO; + private final ApplicationEventPublisher applicationEventPublisher; private final SEBRestrictionService sebRestrictionService; private final String updatePrefix; private final Long examTimeSuffix; public ExamUpdateHandler( final ExamDAO examDAO, + final ApplicationEventPublisher applicationEventPublisher, final SEBRestrictionService sebRestrictionService, final WebserviceInfo webserviceInfo, @Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix) { this.examDAO = examDAO; + this.applicationEventPublisher = applicationEventPublisher; this.sebRestrictionService = sebRestrictionService; this.updatePrefix = webserviceInfo.getLocalHostAddress() + "_" + webserviceInfo.getServerPort() + "_"; @@ -79,14 +85,21 @@ class ExamUpdateHandler { return this.examDAO .placeLock(exam.id, updateId) - .flatMap(e -> this.examDAO.updateState( - exam.id, - ExamStatus.RUNNING, - updateId)) - .flatMap(this.sebRestrictionService::applySEBClientRestriction) - .flatMap(e -> this.examDAO.releaseLock(e, updateId)) - .onError(error -> this.examDAO.forceUnlock(exam.id) - .onError(unlockError -> log.error("Failed to force unlock update look for exam: {}", exam.id))); + .flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.RUNNING, updateId)) + .map(e -> { + this.examDAO + .releaseLock(e, updateId) + .onError(error -> this.examDAO + .forceUnlock(exam.id) + .onError(unlockError -> log.error( + "Failed to force unlock update look for exam: {}", + exam.id))); + return e; + }) + .map(e -> { + this.applicationEventPublisher.publishEvent(new ExamStartedEvent(exam)); + return exam; + }); } Result setFinished(final Exam exam, final String updateId) { @@ -96,13 +109,21 @@ class ExamUpdateHandler { return this.examDAO .placeLock(exam.id, updateId) - .flatMap(e -> this.examDAO.updateState( - exam.id, - ExamStatus.FINISHED, - updateId)) - .flatMap(this.sebRestrictionService::releaseSEBClientRestriction) - .flatMap(e -> this.examDAO.releaseLock(e, updateId)) - .onError(error -> this.examDAO.forceUnlock(exam.id)); + .flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.FINISHED, updateId)) + .map(e -> { + this.examDAO + .releaseLock(e, updateId) + .onError(error -> this.examDAO + .forceUnlock(exam.id) + .onError(unlockError -> log.error( + "Failed to force unlock update look for exam: {}", + exam.id))); + return e; + }) + .map(e -> { + this.applicationEventPublisher.publishEvent(new ExamFinishedEvent(exam)); + return exam; + }); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java index a14265d2..2cb299f9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java @@ -40,6 +40,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; @@ -155,6 +156,15 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService }); } + @EventListener + public void notifyExamFinished(final ExamFinishedEvent event) { + + log.info("ExamFinishedEvent received, process disposeRoomsForExam..."); + + disposeRoomsForExam(event.exam) + .onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error)); + } + @Override public Result disposeRoomsForExam(final Exam exam) {