implemented event handling for start and finish exams

This commit is contained in:
anhefti 2022-03-23 13:42:18 +01:00
parent b6433c7c99
commit ebbbf56314
8 changed files with 140 additions and 37 deletions

View file

@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; 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.sebconfig.ExamConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
@Lazy @Lazy
@Service @Service
@ -188,6 +191,30 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
.flatMap(this.examDAO::byPK); .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 @Override
public Result<Exam> applySEBClientRestriction(final Exam exam) { public Result<Exam> applySEBClientRestriction(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

@ -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;
}
}

View file

@ -191,13 +191,6 @@ public interface ExamSessionService {
* @return Result refer to the collection of connection tokens or to an error when happened. */ * @return Result refer to the collection of connection tokens or to an error when happened. */
Result<Collection<String>> getActiveConnectionTokens(Long examId); Result<Collection<String>> 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<Exam> notifyExamFinished(final Exam exam);
/** Use this to check if the current cached running exam is up to date /** Use this to check if the current cached running exam is up to date
* and if not to flush the cache. * and if not to flush the cache.
* *

View file

@ -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;
}
}

View file

@ -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.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; 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.ExamProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
@Service @Service
@ -43,7 +42,6 @@ public class ExamSessionControlTask implements DisposableBean {
private final ExamUpdateHandler examUpdateHandler; private final ExamUpdateHandler examUpdateHandler;
private final ExamProctoringRoomService examProcotringRoomService; private final ExamProctoringRoomService examProcotringRoomService;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final ExamSessionService examSessionService;
private final Long examTimePrefix; private final Long examTimePrefix;
private final Long examTimeSuffix; private final Long examTimeSuffix;
@ -56,7 +54,6 @@ public class ExamSessionControlTask implements DisposableBean {
final ExamUpdateHandler examUpdateHandler, final ExamUpdateHandler examUpdateHandler,
final ExamProctoringRoomService examProcotringRoomService, final ExamProctoringRoomService examProcotringRoomService,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final ExamSessionService examSessionService,
@Value("${sebserver.webservice.api.exam.time-prefix:3600000}") final Long examTimePrefix, @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.time-suffix:3600000}") final Long examTimeSuffix,
@Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron, @Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron,
@ -66,7 +63,6 @@ public class ExamSessionControlTask implements DisposableBean {
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
this.examUpdateHandler = examUpdateHandler; this.examUpdateHandler = examUpdateHandler;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.examSessionService = examSessionService;
this.examTimePrefix = examTimePrefix; this.examTimePrefix = examTimePrefix;
this.examTimeSuffix = examTimeSuffix; this.examTimeSuffix = examTimeSuffix;
this.examTaskCron = examTaskCron; this.examTaskCron = examTaskCron;
@ -188,8 +184,6 @@ public class ExamSessionControlTask implements DisposableBean {
.stream() .stream()
.filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now)) .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.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)); .collect(Collectors.toMap(Exam::getId, Exam::getName));
if (!updated.isEmpty()) { if (!updated.isEmpty()) {

View file

@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; 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.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; 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.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy @Lazy
@ -402,20 +404,23 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.getActiveConnctionTokens(examId); .getActiveConnctionTokens(examId);
} }
@Override @EventListener
public Result<Exam> notifyExamFinished(final Exam exam) { public void notifyExamFinished(final ExamFinishedEvent event) {
return Result.tryCatch(() -> {
if (!isExamRunning(exam.id)) { log.info("ExamFinishedEvent received, process exam session cleanup...");
this.flushCache(exam);
try {
if (!isExamRunning(event.exam.id)) {
this.flushCache(event.exam);
if (this.distributedSetup) { if (this.distributedSetup) {
this.clientConnectionDAO this.clientConnectionDAO
.deleteClientIndicatorValues(exam) .deleteClientIndicatorValues(event.exam)
.getOrThrow(); .getOrThrow();
} }
} }
} catch (final Exception e) {
return exam; log.error("Failed to cleanup on finished exam: {}", event.exam, e);
}); }
} }
@Override @Override

View file

@ -13,6 +13,7 @@ import org.joda.time.DateTimeZone;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; 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.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; 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.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
@Lazy @Lazy
@Service @Service
@ -33,17 +36,20 @@ class ExamUpdateHandler {
private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class); private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class);
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final ApplicationEventPublisher applicationEventPublisher;
private final SEBRestrictionService sebRestrictionService; private final SEBRestrictionService sebRestrictionService;
private final String updatePrefix; private final String updatePrefix;
private final Long examTimeSuffix; private final Long examTimeSuffix;
public ExamUpdateHandler( public ExamUpdateHandler(
final ExamDAO examDAO, final ExamDAO examDAO,
final ApplicationEventPublisher applicationEventPublisher,
final SEBRestrictionService sebRestrictionService, final SEBRestrictionService sebRestrictionService,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix) { @Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix) {
this.examDAO = examDAO; this.examDAO = examDAO;
this.applicationEventPublisher = applicationEventPublisher;
this.sebRestrictionService = sebRestrictionService; this.sebRestrictionService = sebRestrictionService;
this.updatePrefix = webserviceInfo.getLocalHostAddress() this.updatePrefix = webserviceInfo.getLocalHostAddress()
+ "_" + webserviceInfo.getServerPort() + "_"; + "_" + webserviceInfo.getServerPort() + "_";
@ -79,14 +85,21 @@ class ExamUpdateHandler {
return this.examDAO return this.examDAO
.placeLock(exam.id, updateId) .placeLock(exam.id, updateId)
.flatMap(e -> this.examDAO.updateState( .flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.RUNNING, updateId))
exam.id, .map(e -> {
ExamStatus.RUNNING, this.examDAO
updateId)) .releaseLock(e, updateId)
.flatMap(this.sebRestrictionService::applySEBClientRestriction) .onError(error -> this.examDAO
.flatMap(e -> this.examDAO.releaseLock(e, updateId)) .forceUnlock(exam.id)
.onError(error -> this.examDAO.forceUnlock(exam.id) .onError(unlockError -> log.error(
.onError(unlockError -> log.error("Failed to force unlock update look for exam: {}", exam.id))); "Failed to force unlock update look for exam: {}",
exam.id)));
return e;
})
.map(e -> {
this.applicationEventPublisher.publishEvent(new ExamStartedEvent(exam));
return exam;
});
} }
Result<Exam> setFinished(final Exam exam, final String updateId) { Result<Exam> setFinished(final Exam exam, final String updateId) {
@ -96,13 +109,21 @@ class ExamUpdateHandler {
return this.examDAO return this.examDAO
.placeLock(exam.id, updateId) .placeLock(exam.id, updateId)
.flatMap(e -> this.examDAO.updateState( .flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.FINISHED, updateId))
exam.id, .map(e -> {
ExamStatus.FINISHED, this.examDAO
updateId)) .releaseLock(e, updateId)
.flatMap(this.sebRestrictionService::releaseSEBClientRestriction) .onError(error -> this.examDAO
.flatMap(e -> this.examDAO.releaseLock(e, updateId)) .forceUnlock(exam.id)
.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;
});
} }
} }

View file

@ -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.RemoteProctoringRoomDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent; 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.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.ExamProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; 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 @Override
public Result<Exam> disposeRoomsForExam(final Exam exam) { public Result<Exam> disposeRoomsForExam(final Exam exam) {