Skip to content

Commit

Permalink
SEBSERV-419 implementation and local testing
Browse files Browse the repository at this point in the history
  • Loading branch information
anhefti committed Jul 4, 2024
1 parent 16b2c8d commit fb8df62
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 318 deletions.
4 changes: 4 additions & 0 deletions src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ public enum ExamType {
ExamStatus.TEST_RUN.name(),
ExamStatus.RUNNING.name());

public static final List<String> RUNNING_STATE_NAMES = Arrays.asList(
ExamStatus.TEST_RUN.name(),
ExamStatus.RUNNING.name());

@JsonProperty(EXAM.ATTR_ID)
public final Long id;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ private void updateScreenProctoringAction(
final EntityKey entityKey = pageContext.getEntityKey();
final I18nSupport i18nSupport = this.pageService.getI18nSupport();

final TreeItem screeProcotringGroupAction = proctoringGUIService.getScreeProcotringGroupAction(group);
if (screeProcotringGroupAction != null) {
final TreeItem screeProctoringGroupAction = proctoringGUIService.getScreeProcotringGroupAction(group);
if (screeProctoringGroupAction != null) {
// update action
screeProcotringGroupAction.setText(i18nSupport.getText(new LocTextKey(
screeProctoringGroupAction.setText(i18nSupport.getText(new LocTextKey(
ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP.title.name,
group.name,
group.size)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;

import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;

import org.springframework.cache.annotation.CacheEvict;
Expand Down Expand Up @@ -124,6 +123,12 @@ Result<Collection<Exam>> getExamsForStatus(
* @return Result refer to a collection of exams or to an error if happened */
Result<Collection<Exam>> allThatNeedsStatusUpdate(long leadTime, long followupTime);

/** Quickly checks if an Exam is running or not (Sate RUNNING or TEST_RUN)
*
* @param examId The identifier of the exam to check
* @return true if the exam is in a running state */
boolean isRunning(Long examId);

/** Get a collection of all currently running exam identifiers
*
* @return collection of all currently running exam identifiers */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public Result<Collection<Long>> allRunningExamIds() {
isEqualTo(BooleanUtils.toInteger(true)))
.and(
ExamRecordDynamicSqlSupport.status,
isEqualTo(ExamStatus.RUNNING.name()))
isIn(Exam.RUNNING_STATE_NAMES))
.and(
ExamRecordDynamicSqlSupport.updating,
isEqualTo(BooleanUtils.toInteger(false)))
Expand Down Expand Up @@ -392,6 +392,27 @@ public Result<Collection<Exam>> allThatNeedsStatusUpdate(final long leadTime, fi
.flatMap(this::toDomainModel);
}

@Override
@Transactional(readOnly = true)
public boolean isRunning(final Long examId) {
try {
final Long exists = this.examRecordMapper.countByExample()
.where(
id,
isEqualTo(examId))
.and(
status,
isIn(Exam.RUNNING_STATE_NAMES))
.build()
.execute();

return exists >= 1;
} catch (final Exception e) {
log.error("Failed to check if exam is running: {} error: {}", examId, e.getMessage());
return false;
}
}

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public Result<Long> placeLock(final Long examId, final String updateId) {
Expand Down Expand Up @@ -736,7 +757,7 @@ public Result<Collection<Long>> allIdsOfRunningWithScreenProctoringEnabled() {
isEqualToWhenPresent(BooleanUtils.toIntegerObject(true)))
.and(
ExamRecordDynamicSqlSupport.status,
isEqualTo(ExamStatus.RUNNING.name()))
isIn(Exam.RUNNING_STATE_NAMES))
.build()
.execute();
});
Expand All @@ -754,7 +775,7 @@ public Result<Collection<Long>> allIdsOfRunning(final Long institutionId) {
isEqualToWhenPresent(BooleanUtils.toIntegerObject(true)))
.and(
ExamRecordDynamicSqlSupport.status,
isEqualTo(ExamStatus.RUNNING.name()))
isIn(Exam.RUNNING_STATE_NAMES))
.and(
ExamRecordDynamicSqlSupport.lmsAvailable,
isEqualToWhenPresent(BooleanUtils.toIntegerObject(true)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ public ClientConnectionDataInternal getClientConnection(final String connectionT
}

final ClientConnection clientConnection = getClientConnectionByToken(connectionToken);
if (clientConnection == null) {
// TODO check running exam within cache instead of DB call
if (clientConnection == null || (clientConnection.examId != null && !examDAO.isRunning(clientConnection.examId))) {
return null;
} else {
return this.internalClientConnectionDataFactory.createClientConnectionData(clientConnection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public InternalClientConnectionDataFactory(

public ClientConnectionDataInternal createClientConnectionData(final ClientConnection clientConnection) {

ClientConnectionDataInternal result;
final ClientConnectionDataInternal result;
if (clientConnection.status == ConnectionStatus.CLOSED
|| clientConnection.status == ConnectionStatus.DISABLED) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,18 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;

import javax.annotation.PreDestroy;

import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import org.apache.commons.lang3.StringUtils;
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;

import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
Expand All @@ -44,17 +41,20 @@ public class SEBClientPingBatchService implements SEBClientPingService {

private final ExamSessionCacheService examSessionCacheService;
private final SEBClientInstructionService sebClientInstructionService;
private final ClientConnectionDAO clientConnectionDAO;

private final Set<String> pingKeys = new HashSet<>();
private final Map<String, String> pings = new ConcurrentHashMap<>();
private final Map<String, String> instructions = new ConcurrentHashMap<>();

public SEBClientPingBatchService(
final ExamSessionCacheService examSessionCacheService,
final SEBClientInstructionService sebClientInstructionService) {
final SEBClientInstructionService sebClientInstructionService,
final ClientConnectionDAO clientConnectionDAO) {

this.examSessionCacheService = examSessionCacheService;
this.sebClientInstructionService = sebClientInstructionService;
this.clientConnectionDAO = clientConnectionDAO;
}

@Scheduled(fixedDelayString = "${sebserver.webservice.api.exam.session.ping.batch.interval:500}")
Expand All @@ -71,7 +71,7 @@ public void processPings() {
try {
this.pingKeys.clear();
this.pingKeys.addAll(this.pings.keySet());
this.pingKeys.stream().forEach(cid -> processPing(
this.pingKeys.forEach(cid -> processPing(
cid,
this.pings.remove(cid),
Utils.getMillisecondsNow()));
Expand All @@ -94,20 +94,15 @@ public final String notifyPing(
final String instruction = this.instructions.remove(connectionToken);

if (instructionConfirm != null) {
System.out.println("************ put instructionConfirm: " + instructionConfirm + " instructions: "
+ this.instructions);

this.pings.put(connectionToken, instructionConfirm);
// // TODO is this a good idea or is there another better way to deal with instruction confirm synchronization?
if (instruction != null && instruction.contains("\"instruction-confirm\":\"" + instructionConfirm + "\"")) {
return null;
}
} else if (!this.pings.containsKey(connectionToken)) {
this.pings.put(connectionToken, StringUtils.EMPTY);
}

// System.out.println(
// "**************** notifyPing instructionConfirm: " + instructionConfirm + " pings: " + this.pings);

return instruction;
}

Expand All @@ -126,19 +121,13 @@ private void processPing(
if (connectionData != null) {
if (connectionData.clientConnection.status == ClientConnection.ConnectionStatus.DISABLED) {
// SEBSERV-440 send quit instruction to SEB
sebClientInstructionService.registerInstruction(
connectionData.clientConnection.examId,
ClientInstruction.InstructionType.SEB_QUIT,
Collections.emptyMap(),
connectionData.clientConnection.connectionToken,
false,
false
);
sendQuitInstruction(connectionToken, connectionData.clientConnection.examId);
}

connectionData.notifyPing(timestamp);
} else {
log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken);
log.warn("Failed to get ClientConnectionDataInternal probably due to finished Exam for: {}.", connectionToken);
sendQuitInstruction(connectionToken,null);
}

if (StringUtils.isNotBlank(instructionConfirm)) {
Expand All @@ -154,4 +143,38 @@ private void processPing(
this.instructions.put(connectionToken, instructionJSON);
}
}

private void sendQuitInstruction(final String connectionToken, final Long examId) {

Long _examId = examId;
if (examId == null) {
final Result<ClientConnection> clientConnectionResult = clientConnectionDAO
.byConnectionToken(connectionToken);

if (clientConnectionResult.hasError()) {
log.error(
"Failed to get examId for client connection token: {} error: {}",
connectionToken,
clientConnectionResult.getError().getMessage());
}

_examId = clientConnectionResult.get().examId;
}

if (_examId != null) {

log.info("Send automated quit instruction to SEB for connection token: {}", connectionToken);

// TODO add SEB event log that SEB Server has automatically send quit instruction to SEB

sebClientInstructionService.registerInstruction(
_examId,
ClientInstruction.InstructionType.SEB_QUIT,
Collections.emptyMap(),
connectionToken,
false,
false
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public Result<Collection<ClientConnection>> getActiveCollectingRoomConnections(

final Collection<String> currentlyInBreakoutRooms = this.remoteProctoringRoomDAO
.getConnectionsInBreakoutRooms(examId)
.getOrElse(() -> Collections.emptyList());
.getOrElse(Collections::emptyList);

if (currentlyInBreakoutRooms.isEmpty()) {
return this.clientConnectionDAO
Expand All @@ -132,52 +132,58 @@ public Result<Collection<ClientConnection>> getActiveCollectingRoomConnections(

@Override
public void updateProctoringCollectingRooms() {
try {

// Applying to collecting room
this.clientConnectionDAO
.getAllForProctoringUpdateActive()
.getOrThrow()
.stream()
.forEach(this::assignToCollectingRoom);

// Dispose from collecting room
this.clientConnectionDAO
.getAllForProctoringUpdateInactive()
.getOrThrow()
.stream()
.forEach(this::removeFromRoom);

} catch (final Exception e) {
log.error("Unexpected error while trying to update proctoring collecting rooms: ", e);
}
// NOTE: Since life proctoring is not supported anymore, we disable automated updates here

// try {
//
// // Applying to collecting room
// this.clientConnectionDAO
// .getAllForProctoringUpdateActive()
// .getOrThrow()
// .forEach(this::assignToCollectingRoom);
//
// // Dispose from collecting room
// this.clientConnectionDAO
// .getAllForProctoringUpdateInactive()
// .getOrThrow()
// .forEach(this::removeFromRoom);
//
// } catch (final Exception e) {
// log.error("Unexpected error while trying to update proctoring collecting rooms: ", e);
// }
}

@EventListener(ExamDeletionEvent.class)
public void notifyExamDeletionEvent(final ExamDeletionEvent event) {
event.ids.forEach(examId -> {
try {

this.examAdminService
.examForPK(examId)
.flatMap(this::disposeRoomsForExam)
.getOrThrow();
// NOTE: Since life proctoring is not supported anymore, we disable automated updates here

} catch (final Exception e) {
log.error("Failed to delete depending proctoring data for exam: {}", examId, e);
}
});
// event.ids.forEach(examId -> {
// try {
//
// this.examAdminService
// .examForPK(examId)
// .flatMap(this::disposeRoomsForExam)
// .getOrThrow();
//
// } catch (final Exception e) {
// log.error("Failed to delete depending proctoring data for exam: {}", examId, e);
// }
// });
}

@EventListener
public void notifyExamFinished(final ExamFinishedEvent event) {

if (log.isDebugEnabled()) {
log.debug("ExamFinishedEvent received, process disposeRoomsForExam...");
}
// NOTE: Since life proctoring is not supported anymore, we disable automated updates here

disposeRoomsForExam(event.exam)
.onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error));
// if (log.isDebugEnabled()) {
// log.debug("ExamFinishedEvent received, process disposeRoomsForExam...");
// }
//
// disposeRoomsForExam(event.exam)
// .onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error));
}

@Override
Expand Down
Loading

0 comments on commit fb8df62

Please sign in to comment.