diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java index 50a20ce3..84ea54f3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/async/AsyncServiceSpringConfig.java @@ -78,7 +78,7 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer { @Bean public ThreadPoolTaskScheduler threadPoolTaskScheduler() { final ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.setPoolSize(5); + threadPoolTaskScheduler.setPoolSize(10); threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(false); threadPoolTaskScheduler.setThreadNamePrefix("SEB-Server-BgTask-"); return threadPoolTaskScheduler; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java index 1086b3ec..b6f9c046 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java @@ -23,6 +23,7 @@ import ch.ethz.seb.sebserver.SEBServerInit; import ch.ethz.seb.sebserver.SEBServerInitEvent; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.SEBClientPingServiceFactory; @Component @WebServiceProfile @@ -38,6 +39,7 @@ public class WebserviceInit implements ApplicationListener "); + SEBServerInit.INIT_LOGGER.info("----> Working with ping service: {}", + this.sebClientPingServiceFactory.getWorkingServiceType()); + SEBServerInit.INIT_LOGGER.info("----> "); SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address")); SEBServerInit.INIT_LOGGER.info("----> Server port: {}", this.environment.getProperty("server.port")); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index b52089ec..ffe7614e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -530,13 +530,15 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { return Collections.emptyList(); } - return this.clientConnectionRecordMapper + final List execute = this.clientConnectionRecordMapper .selectByExample() .where(ClientConnectionRecordDynamicSqlSupport.screenProctoringGroupId, isNull()) .and(ClientConnectionRecordDynamicSqlSupport.examId, isIn(examIds)) .and(ClientConnectionRecordDynamicSqlSupport.status, isEqualTo(ConnectionStatus.ACTIVE.name())) .build() .execute(); + + return execute; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientPingService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientPingService.java new file mode 100644 index 00000000..c908ca1b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientPingService.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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; + +public interface SEBClientPingService { + + static enum PingServiceType { + BLOCKING, + BATCH + } + + PingServiceType pingServiceType(); + + String notifyPing(String connectionToken, String instructionConfirm); + +} 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 35821f08..0a257429 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 @@ -533,7 +533,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { public Result updateExamCache(final Long examId) { // TODO check how often this is called in distributed environments - System.out.println("************** performance check: updateExamCache"); + //System.out.println("************** performance check: updateExamCache"); try { final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java index 81625a41..e97c9233 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBatchService.java @@ -11,41 +11,67 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ScheduledFuture; + +import javax.annotation.PreDestroy; 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.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; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingService; @Lazy @Component @WebServiceProfile -public class SEBClientPingBatchService { +public class SEBClientPingBatchService implements SEBClientPingService { private static final Logger log = LoggerFactory.getLogger(SEBClientPingBatchService.class); private final ExamSessionCacheService examSessionCacheService; private final SEBClientInstructionService sebClientInstructionService; + private final ThreadPoolTaskScheduler threadPoolTaskScheduler; + private final long schendulerInterval; private final Map pings = new ConcurrentHashMap<>(); private final Map instructions = new ConcurrentHashMap<>(); + private ScheduledFuture scheduleAtFixedRate = null; + public SEBClientPingBatchService( final ExamSessionCacheService examSessionCacheService, - final SEBClientInstructionService sebClientInstructionService) { + final SEBClientInstructionService sebClientInstructionService, + final ThreadPoolTaskScheduler threadPoolTaskScheduler, + @Value("${sebserver.webservice.api.exam.session.ping.batch.interval:500}") final long schendulerInterval) { this.examSessionCacheService = examSessionCacheService; this.sebClientInstructionService = sebClientInstructionService; + this.threadPoolTaskScheduler = threadPoolTaskScheduler; + this.schendulerInterval = schendulerInterval; } - @Scheduled(fixedDelayString = "${sebserver.webservice.api.exam.session.ping.batch.interval:500}") + void init() { + if (this.scheduleAtFixedRate == null) { + + log.info( + "Initialize SEBClientPingBatchService for schedule batch update at a rate of {} milliseconds", + this.schendulerInterval); + + this.scheduleAtFixedRate = this.threadPoolTaskScheduler.scheduleAtFixedRate( + () -> processPings(), + this.schendulerInterval); + } + } + + //@Scheduled(fixedDelayString = "${sebserver.webservice.api.exam.session.ping.batch.interval:500}") public void processPings() { if (this.pings.isEmpty()) { return; @@ -69,17 +95,34 @@ public class SEBClientPingBatchService { } } + @Override + public PingServiceType pingServiceType() { + return PingServiceType.BATCH; + } + + @Override public final String notifyPing( final String connectionToken, final String instructionConfirm) { + 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); } - return this.instructions.remove(connectionToken); +// System.out.println( +// "**************** notifyPing instructionConfirm: " + instructionConfirm + " pings: " + this.pings); + + return instruction; } private void processPing( @@ -110,4 +153,11 @@ public class SEBClientPingBatchService { } } + @PreDestroy + protected void shutdown() { + if (this.scheduleAtFixedRate != null) { + this.scheduleAtFixedRate.cancel(true); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBlockingService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBlockingService.java new file mode 100644 index 00000000..3d5c451a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingBlockingService.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 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.impl; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingService; + +@Lazy +@Component +@WebServiceProfile +public class SEBClientPingBlockingService implements SEBClientPingService { + + private static final Logger log = LoggerFactory.getLogger(SEBClientPingBlockingService.class); + + private final ExamSessionCacheService examSessionCacheService; + private final SEBClientInstructionService sebClientInstructionService; + + public SEBClientPingBlockingService( + final ExamSessionCacheService examSessionCacheService, + final SEBClientInstructionService sebClientInstructionService) { + + this.examSessionCacheService = examSessionCacheService; + this.sebClientInstructionService = sebClientInstructionService; + } + + @Override + public PingServiceType pingServiceType() { + return PingServiceType.BLOCKING; + } + + @Override + public String notifyPing(final String connectionToken, final String instructionConfirm) { + if (connectionToken == null) { + return null; + } + + final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService + .getClientConnection(connectionToken); + + if (activeClientConnection != null) { + activeClientConnection.notifyPing(Utils.getMillisecondsNow()); + } else { + log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken); + } + + if (instructionConfirm != StringUtils.EMPTY) { + this.sebClientInstructionService.confirmInstructionDone(connectionToken, instructionConfirm); + } + + return this.sebClientInstructionService.getInstructionJSON(connectionToken); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingServiceFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingServiceFactory.java new file mode 100644 index 00000000..ad300f3d --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientPingServiceFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 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.impl; + +import java.util.Collection; +import java.util.EnumMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingService; + +@Lazy +@Component +@WebServiceProfile +public class SEBClientPingServiceFactory { + + private static final Logger log = LoggerFactory.getLogger(SEBClientPingServiceFactory.class); + + private final EnumMap serviceMapping = + new EnumMap<>(SEBClientPingService.PingServiceType.class); + private final SEBClientPingService.PingServiceType workingServiceType; + + public SEBClientPingServiceFactory( + final Collection serviceBeans, + @Value("${sebserver.webservice.api.exam.session.ping.service.type:BLOCKING}") final String serviceType) { + + SEBClientPingService.PingServiceType serviceTypeToSet = SEBClientPingService.PingServiceType.BLOCKING; + try { + serviceTypeToSet = SEBClientPingService.PingServiceType.valueOf(serviceType); + } catch (final Exception e) { + serviceTypeToSet = SEBClientPingService.PingServiceType.BLOCKING; + } + this.workingServiceType = serviceTypeToSet; + + serviceBeans.stream().forEach(service -> this.serviceMapping.putIfAbsent(service.pingServiceType(), service)); + } + + public SEBClientPingService.PingServiceType getWorkingServiceType() { + return this.workingServiceType; + } + + public SEBClientPingService getSEBClientPingService() { + + log.info("Work with SEBClientPingService of type: {}", this.workingServiceType); + + switch (this.workingServiceType) { + case BATCH: { + final SEBClientPingService service = + this.serviceMapping.get(SEBClientPingService.PingServiceType.BATCH); + if (service != null) { + ((SEBClientPingBatchService) service).init(); + return service; + } else { + return this.serviceMapping.get(SEBClientPingService.PingServiceType.BLOCKING); + } + } + default: + return this.serviceMapping.get(SEBClientPingService.PingServiceType.BLOCKING); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java index 49ba71c4..9d668706 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java @@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientVersionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.SEBClientEventBatchService.EventData; @@ -48,7 +49,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { private final InternalClientConnectionDataFactory internalClientConnectionDataFactory; private final SecurityKeyService securityKeyService; private final SEBClientVersionService sebClientVersionService; - private final SEBClientPingBatchService sebClientPingService; + private final SEBClientPingService sebClientPingService; public SEBClientSessionServiceImpl( final ClientConnectionDAO clientConnectionDAO, @@ -59,7 +60,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { final InternalClientConnectionDataFactory internalClientConnectionDataFactory, final SecurityKeyService securityKeyService, final SEBClientVersionService sebClientVersionService, - final SEBClientPingBatchService sebClientPingService) { + final SEBClientPingServiceFactory sebClientPingServiceFactory) { this.clientConnectionDAO = clientConnectionDAO; this.examSessionService = examSessionService; @@ -69,7 +70,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { this.internalClientConnectionDataFactory = internalClientConnectionDataFactory; this.securityKeyService = securityKeyService; this.sebClientVersionService = sebClientVersionService; - this.sebClientPingService = sebClientPingService; + this.sebClientPingService = sebClientPingServiceFactory.getSEBClientPingService(); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java index fd7bfb27..cefeb63a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java @@ -319,14 +319,14 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { } catch (final Exception e) { log.error("Failed to apply screen proctoring session to SEB with connection: ", ccRecord, e); -// if (placeReservedInGroup != null) { -// // release reserved place in group -// this.screenProctoringGroupDAO.releasePlaceInCollectingGroup( -// ccRecord.getExamId(), -// placeReservedInGroup) -// .onError( -// error -> log.warn("Failed to release reserved place in group: {}", error.getMessage())); -// } + if (placeReservedInGroup != null) { + // release reserved place in group + this.screenProctoringGroupDAO.releasePlaceInCollectingGroup( + ccRecord.getExamId(), + placeReservedInGroup) + .onError( + error -> log.warn("Failed to release reserved place in group: {}", error.getMessage())); + } } } @@ -336,7 +336,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { if (!exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)) { log.warn("Can't verify collecting strategy for exam: {} use default group assignment.", exam.id); - return applyToDefaultGroup(ccRecord, exam); + return applyToDefaultGroup(ccRecord.getId(), ccRecord.getConnectionToken(), exam); } final CollectingStrategy strategy = CollectingStrategy.valueOf(exam.additionalAttributes @@ -350,20 +350,21 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { case EXAM: case FIX_SIZE: default: { - return applyToDefaultGroup(ccRecord, exam); + return applyToDefaultGroup(ccRecord.getId(), ccRecord.getConnectionToken(), exam); } } } private ScreenProctoringGroup applyToDefaultGroup( - final ClientConnectionRecord ccRecord, + final Long connectioId, + final String connectionToken, final Exam exam) { final ScreenProctoringGroup screenProctoringGroup = reservePlaceOnProctoringGroup(exam); this.clientConnectionDAO.assignToScreenProctoringGroup( - exam.id, - ccRecord.getConnectionToken(), + connectioId, + connectionToken, screenProctoringGroup.id) .getOrThrow();