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.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<Exam> applySEBClientRestriction(final Exam exam) {
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. */
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
* 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.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()) {

View file

@ -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<Exam> 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

View file

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

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.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<Exam> disposeRoomsForExam(final Exam exam) {