From def31a46690e8555b396f27e458813b8d9057bc0 Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 15 Nov 2021 14:17:04 +0100 Subject: [PATCH 1/6] added better error notification for timeouts, and work with longer request timout limits --- .../seb/sebserver/ClientHttpRequestFactoryService.java | 2 +- .../seb/sebserver/gui/service/page/PageContext.java | 4 ++++ .../gui/service/remote/webservice/api/RestCall.java | 10 ++++++++++ src/main/resources/config/application-gui.properties | 5 +++++ src/main/resources/messages.properties | 1 + 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java b/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java index 567b1152..06b16245 100644 --- a/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java +++ b/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java @@ -76,7 +76,7 @@ public class ClientHttpRequestFactoryService { final ClientCredentialService clientCredentialService, @Value("${sebserver.http.client.connect-timeout:15000}") final int connectTimeout, @Value("${sebserver.http.client.connection-request-timeout:20000}") final int connectionRequestTimeout, - @Value("${sebserver.http.client.read-timeout:10000}") final int readTimeout) { + @Value("${sebserver.http.client.read-timeout:60000}") final int readTimeout) { this.environment = environment; this.clientCredentialService = clientCredentialService; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java index 406a61c6..1c9a24a2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java @@ -281,6 +281,10 @@ public interface PageContext { * * @param error the original error */ default void notifyUnexpectedError(final Exception error) { + if (error instanceof PageMessageException) { + publishInfo(((PageMessageException) error).getMessageKey()); + return; + } notifyError(UNEXPECTED_ERROR_KEY, error); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java index 3fc41d7e..790963fa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java @@ -28,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -46,11 +47,15 @@ import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.PageSortOrder; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageMessageException; public abstract class RestCall { private static final Logger log = LoggerFactory.getLogger(RestCall.class); + public static final LocTextKey REQUEST_TIMEOUT_MESSAGE = new LocTextKey("sebserver.overall.message.requesttimeout"); + public enum CallType { UNDEFINED, GET_SINGLE, @@ -161,6 +166,11 @@ public abstract class RestCall { } return Result.ofError(restCallError); + } catch (final ResourceAccessException rae) { + if (rae.getMessage().contains("Read timed out")) { + return Result.ofError(new PageMessageException(REQUEST_TIMEOUT_MESSAGE)); + } + return Result.ofError(rae); } catch (final Exception e) { final RestCallError restCallError = new RestCallError("Unexpected error while rest call", e); restCallError.errors.add(APIMessage.ErrorMessage.UNEXPECTED.of( diff --git a/src/main/resources/config/application-gui.properties b/src/main/resources/config/application-gui.properties index 6b20a69e..2fe17115 100644 --- a/src/main/resources/config/application-gui.properties +++ b/src/main/resources/config/application-gui.properties @@ -23,6 +23,11 @@ sebserver.gui.http.webservice.port=8080 sebserver.gui.http.webservice.contextPath=${server.servlet.context-path} sebserver.gui.entrypoint=/gui + +sebserver.http.client.connect-timeout=60000 +sebserver.http.client.connection-request-timeout=100000 +sebserver.http.client.read-timeout=200000 + sebserver.gui.webservice.apipath=${sebserver.webservice.api.admin.endpoint} # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page sebserver.gui.webservice.poll-interval=2000 diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 9a11c799..c0af9223 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -9,6 +9,7 @@ sebserver.overall.help=Documentation sebserver.overall.help.link=https://seb-server.readthedocs.io/en/latest/index.html sebserver.overall.message.leave.without.save=You have unsaved changes!
Are you sure you want to leave the page? The changes will be lost. +sebserver.overall.message.requesttimeout=There was a request timeout. If this is a search please try to narrow down your search by using the filter above and try again. sebserver.overall.upload=Please select a file sebserver.overall.upload.unsupported.file=This file type is not supported. Supported files are: {0} sebserver.overall.action.modify.cancel=Cancel From 072930abb20376677b8bb1f04663ec4341df0153 Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 16 Nov 2021 10:04:18 +0100 Subject: [PATCH 2/6] Add modal window when clicking "Release SEB Lock" #3 --- src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java | 3 +++ src/main/resources/messages.properties | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index 030a3ebe..55de20bc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -104,6 +104,8 @@ public class ExamForm implements TemplateComposer { new LocTextKey("sebserver.exam.form.quizurl"); private static final LocTextKey FORM_LMSSETUP_TEXT_KEY = new LocTextKey("sebserver.exam.form.lmssetup"); + private final static LocTextKey ACTION_MESSAGE_SEB_RESTRICTION_RELEASE = + new LocTextKey("sebserver.exam.action.sebrestriction.release.confirm"); private final static LocTextKey CONSISTENCY_MESSAGE_TITLE = new LocTextKey("sebserver.exam.consistency.title"); @@ -407,6 +409,7 @@ public class ExamForm implements TemplateComposer { && BooleanUtils.isFalse(isRestricted)) .newAction(ActionDefinition.EXAM_DISABLE_SEB_RESTRICTION) + .withConfirm(() -> ACTION_MESSAGE_SEB_RESTRICTION_RELEASE) .withEntityKey(entityKey) .withExec(action -> this.examSEBRestrictionSettings.setSEBRestriction(action, false, this.restService)) .publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index c0af9223..ad9c9669 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -457,6 +457,7 @@ sebserver.exam.action.sebrestriction.enable=Apply SEB Lock sebserver.exam.action.sebrestriction.disable=Release SEB Lock sebserver.exam.action.sebrestriction.details=SEB Restriction Details sebserver.exam.action.createClientToStartExam=Export Exam Connection Configuration +sebserver.exam.action.sebrestriction.release.confirm=You are about to release the SEB restriction lock for this exam on the LMS.
Are you sure you want to release the SEB restriction? sebserver.exam.info.pleaseSelect=At first please select an Exam from the list From fb0fc2aec00e4764e20722873b8840ac9d70176d Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 16 Nov 2021 10:11:29 +0100 Subject: [PATCH 3/6] cache and connection improvements --- .../exam/impl/SEBClientEventAdminServiceImpl.java | 2 +- .../session/impl/SEBClientConnectionServiceImpl.java | 9 +++++---- src/main/resources/config/ehcache.xml | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java index 3c395bdd..be4219f0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java @@ -225,7 +225,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic private final String sort; private int pageNumber = 1; - private final int pageSize = 100; + private final int pageSize = 1000; private Collection nextRecords; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 0e476e64..804f560b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -280,10 +280,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic return clientConnection; } else { // It seems that this is a request from an other device then the original - log.error("ClientConnection integrity violation: client connection mismatch: {}", - clientConnection); - throw new IllegalArgumentException( - "ClientConnection integrity violation: client connection mismatch"); + log.warn( + "SEB retired to establish an already established client connection with another IP address. Client adress: {} : {}", + clientConnection.clientAddress, + clientAddress); + return clientConnection; } } else if (!clientConnection.status.clientActiveStatus) { log.error("ClientConnection integrity violation: client connection is not in expected state: {}", diff --git a/src/main/resources/config/ehcache.xml b/src/main/resources/config/ehcache.xml index 6161391b..328874b7 100644 --- a/src/main/resources/config/ehcache.xml +++ b/src/main/resources/config/ehcache.xml @@ -86,7 +86,7 @@ java.lang.String ch.ethz.seb.sebserver.gbl.model.exam.QuizData - 1 + 5 10000 From 66475b84d9fb11c308fe03963a33f0d636fda54f Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 16 Nov 2021 16:09:03 +0100 Subject: [PATCH 4/6] confirm message for town-hall action --- .../gui/content/MonitoringRunningExam.java | 14 +++++++++++++- src/main/resources/messages.properties | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index f194f983..4e101f29 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -82,6 +82,10 @@ public class MonitoringRunningExam implements TemplateComposer { new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected.confirm"); private static final LocTextKey CONFIRM_QUIT_ALL = new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm"); + private static final LocTextKey CONFIRM_OPEN_TOWNHALL = + new LocTextKey("sebserver.monitoring.exam.connection.action.openTownhall.confirm"); + private static final LocTextKey CONFIRM_CLOSE_TOWNHALL = + new LocTextKey("sebserver.monitoring.exam.connection.action.closeTownhall.confirm"); private static final LocTextKey CONFIRM_DISABLE_SELECTED = new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm"); @@ -276,7 +280,15 @@ public class MonitoringRunningExam implements TemplateComposer { actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) .withEntityKey(entityKey) - .withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService, + .withConfirm(action -> { + if (!this.monitoringProctoringService.isTownhallRoomActive(action.getEntityKey().modelId)) { + return CONFIRM_OPEN_TOWNHALL; + } else { + return CONFIRM_CLOSE_TOWNHALL; + } + }) + .withExec(action -> this.monitoringProctoringService.toggleTownhallRoom( + proctoringGUIService, action)) .noEventPropagation() .publish(); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index ad9c9669..c9b7071c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1718,6 +1718,8 @@ sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring +sebserver.monitoring.exam.connection.action.openTownhall.confirm=You are about to open the town-hall room and force all SEB clients to join the town-hall room.
Are you sure to open the town-hall? +sebserver.monitoring.exam.connection.action.closeTownhall.confirm=You are about to close the town-hall room and force all SEB clients to join it's proctoring room.
Are you sure to close the town-hall? sebserver.monitoring.exam.connection.notificationlist.actions= sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification From 3e35a7745bc9af255170aa8fa7e38c9951f77a73 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 18 Nov 2021 08:21:03 +0100 Subject: [PATCH 5/6] fixes and fine-tuning for distributed setup --- .../sebserver/webservice/WebserviceInit.java | 7 ++++++ .../dao/impl/WebserviceInfoDAOImpl.java | 2 +- .../session/impl/ExamSessionServiceImpl.java | 22 ++++++++++++------- .../impl/SEBClientConnectionServiceImpl.java | 1 + .../impl/indicator/DistributedPingCache.java | 14 ++++++++++-- .../weblayer/api/ExamAPI_V1_Controller.java | 10 ++++----- .../config/application-dev-gui.properties | 2 +- .../config/application-dev-ws.properties | 6 +++-- .../config/application-dev.properties | 16 +++++++++----- .../config/application-gui.properties | 6 ++--- .../config/application-ws.properties | 1 + src/main/resources/config/ehcache.xml | 3 +-- .../integration/api/admin/WebserviceTest.java | 4 ++-- .../resources/application-test.properties | 3 ++- 14 files changed, 64 insertions(+), 33 deletions(-) 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 860f8358..91322c60 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInit.java @@ -86,8 +86,15 @@ public class WebserviceInit implements ApplicationListener"); SEBServerInit.INIT_LOGGER.info("----> *** Info:"); + SEBServerInit.INIT_LOGGER.info("----> JDBC connection pool max size: {}", + this.environment.getProperty("spring.datasource.hikari.maximumPoolSize")); + if (this.webserviceInfo.isDistributed()) { SEBServerInit.INIT_LOGGER.info("----> Distributed Setup: {}", this.webserviceInfo.getWebserviceUUID()); + SEBServerInit.INIT_LOGGER.info("------> Ping update time: {}", + this.environment.getProperty("sebserver.webservice.distributed.pingUpdate")); + SEBServerInit.INIT_LOGGER.info("------> Connection update time: {}", + this.environment.getProperty("sebserver.webservice.distributed.connectionUpdate")); } try { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/WebserviceInfoDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/WebserviceInfoDAOImpl.java index 7c03a347..cc452b89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/WebserviceInfoDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/WebserviceInfoDAOImpl.java @@ -40,7 +40,7 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO { public WebserviceInfoDAOImpl( final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper, @Value("${sebserver.webservice.forceMaster:false}") final boolean forceMaster, - @Value("${sebserver.webservice.master.delay.threshold:10000}") final long masterDelayTimeThreshold) { + @Value("${sebserver.webservice.master.delay.threshold:30000}") final long masterDelayTimeThreshold) { this.webserviceServerInfoRecordMapper = webserviceServerInfoRecordMapper; this.masterDelayTimeThreshold = masterDelayTimeThreshold; 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 ad376ef0..eb2d0ad1 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 @@ -61,6 +61,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { private final CacheManager cacheManager; private final SEBRestrictionService sebRestrictionService; private final boolean distributedSetup; + private final long distributedConnectionUpdate; private long lastConnectionTokenCacheUpdate = 0; @@ -72,7 +73,8 @@ public class ExamSessionServiceImpl implements ExamSessionService { final IndicatorDAO indicatorDAO, final CacheManager cacheManager, final SEBRestrictionService sebRestrictionService, - @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) { + @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup, + @Value("${sebserver.webservice.distributed.connectionUpdate:2000}") final long distributedConnectionUpdate) { this.examSessionCacheService = examSessionCacheService; this.examDAO = examDAO; @@ -82,6 +84,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { this.indicatorDAO = indicatorDAO; this.sebRestrictionService = sebRestrictionService; this.distributedSetup = distributedSetup; + this.distributedConnectionUpdate = distributedConnectionUpdate; } @Override @@ -114,9 +117,11 @@ public class ExamSessionServiceImpl implements ExamSessionService { return Result.tryCatch(() -> { final Collection result = new ArrayList<>(); - final Exam exam = this.examDAO - .byPK(examId) - .getOrThrow(); + final Exam exam = (this.isExamRunning(examId)) + ? this.examSessionCacheService.getRunningExam(examId) + : this.examDAO + .byPK(examId) + .getOrThrow(); // check lms connection if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION) { @@ -193,7 +198,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { } @Override - public synchronized Result getRunningExam(final Long examId) { + public /* synchronized */ Result getRunningExam(final Long examId) { if (log.isTraceEnabled()) { log.trace("Running exam request for exam {}", examId); } @@ -361,6 +366,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { @Override public Result updateExamCache(final Long examId) { + final Exam exam = this.examSessionCacheService.getRunningExam(examId); if (exam == null) { return Result.ofEmpty(); @@ -398,10 +404,10 @@ public class ExamSessionServiceImpl implements ExamSessionService { // at least every second. This allows caching over multiple monitoring requests but // ensure an update every second for new incoming connections private void updateClientConnections(final Long examId) { - try { + final long currentTimeMillis = System.currentTimeMillis(); if (this.distributedSetup && - System.currentTimeMillis() - this.lastConnectionTokenCacheUpdate > Constants.SECOND_IN_MILLIS) { + currentTimeMillis - this.lastConnectionTokenCacheUpdate > this.distributedConnectionUpdate) { // go trough all client connection and update the ones that not up to date this.clientConnectionDAO.evictConnectionTokenCache(examId); @@ -420,7 +426,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { .stream() .forEach(this.examSessionCacheService::evictClientConnection); - this.lastConnectionTokenCacheUpdate = System.currentTimeMillis(); + this.lastConnectionTokenCacheUpdate = currentTimeMillis; } } catch (final Exception e) { log.error("Unexpected error while trying to update client connections: ", e); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 804f560b..905a9953 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -555,6 +555,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic .getOrThrow() .stream() .flatMap(examId -> distributed + // TODO fetch only the connection tokens form active connections here ? this.clientConnectionDAO .getConnectionTokensNoCache(examId) .getOrThrow() diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java index 5c027577..3384fc89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java @@ -30,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; @@ -45,9 +46,11 @@ public class DistributedPingCache implements DisposableBean { private final ClientEventLastPingMapper clientEventLastPingMapper; private final ClientEventRecordMapper clientEventRecordMapper; - private ScheduledFuture taskRef; + private final long pingUpdateTolerance; + private ScheduledFuture taskRef; private final Map pingCache = new ConcurrentHashMap<>(); + private long lastUpdate = 0L; public DistributedPingCache( final ClientEventLastPingMapper clientEventLastPingMapper, @@ -58,6 +61,7 @@ public class DistributedPingCache implements DisposableBean { this.clientEventLastPingMapper = clientEventLastPingMapper; this.clientEventRecordMapper = clientEventRecordMapper; + this.pingUpdateTolerance = pingUpdate * 2 / 3; if (webserviceInfo.isDistributed()) { try { this.taskRef = taskScheduler.scheduleAtFixedRate(this::updateCache, pingUpdate); @@ -166,7 +170,6 @@ public class DistributedPingCache implements DisposableBean { try { Long ping = this.pingCache.get(pingRecordId); if (ping == null) { - if (log.isDebugEnabled()) { log.debug("*** Get and cache ping time: {}", pingRecordId); } @@ -187,6 +190,12 @@ public class DistributedPingCache implements DisposableBean { @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) public void updateCache() { + final long millisecondsNow = Utils.getMillisecondsNow(); + if (millisecondsNow - this.lastUpdate < this.pingUpdateTolerance) { + log.warn("Skip ping update schedule because the last one was less then 2 seconds ago"); + return; + } + if (this.pingCache.isEmpty()) { return; } @@ -210,6 +219,7 @@ public class DistributedPingCache implements DisposableBean { if (mapping != null) { this.pingCache.clear(); this.pingCache.putAll(mapping); + this.lastUpdate = millisecondsNow; } } catch (final Exception e) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index d3c7d614..5aed1f6c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -270,20 +270,20 @@ public class ExamAPI_V1_Controller { final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER); final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM); - if (log.isTraceEnabled()) { - log.trace("****************** SEB client connection: {} ip: {}", - connectionToken, - getClientAddress(request)); + if (connectionToken == null) { + log.warn("Missing connection token on ping. Ignore the request"); + return; } if (instructionConfirm != null) { this.sebClientConnectionService.confirmInstructionDone(connectionToken, instructionConfirm); } + final Long clientTime = timeStampString != null ? Long.parseLong(timeStampString) : 0L; final String instruction = this.sebClientConnectionService .notifyPing( connectionToken, - Long.parseLong(timeStampString), + clientTime, pingNumString != null ? Integer.parseInt(pingNumString) : -1); if (instruction == null) { diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties index 3e830c82..8069b264 100644 --- a/src/main/resources/config/application-dev-gui.properties +++ b/src/main/resources/config/application-dev-gui.properties @@ -8,7 +8,7 @@ sebserver.gui.webservice.address=localhost sebserver.gui.webservice.port=8080 sebserver.gui.webservice.apipath=/admin-api/v1 # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page -#sebserver.gui.webservice.poll-interval=1000 +sebserver.gui.webservice.poll-interval=1000 sebserver.gui.theme=css/sebserver.css sebserver.gui.list.page.size=15 diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index 5b2e5488..cef30a70 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -13,12 +13,14 @@ spring.datasource.hikari.initializationFailTimeout=30000 spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.maxLifetime=1800000 -spring.datasource.hikari.maximumPoolSize=5 +spring.datasource.hikari.maximumPoolSize=10 +spring.datasource.hikari.leakDetectionThreshold=2000 sebserver.http.client.connect-timeout=15000 sebserver.http.client.connection-request-timeout=10000 sebserver.http.client.read-timeout=20000 - +sebserver.webservice.distributed.pingUpdate=1000 +sebserver.webservice.distributed.connectionUpdate=1000 sebserver.webservice.clean-db-on-startup=false # webservice configuration diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index ae66eec3..ed384d92 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -10,12 +10,16 @@ server.tomcat.uri-encoding=UTF-8 logging.level.ch=INFO logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO logging.level.org.springframework.cache=INFO -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=TRACE -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=TRACE +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG +#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG +#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper=DEBUG #logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE +logging.level.com.zaxxer.hikari=DEBUG -sebserver.http.client.connect-timeout=150000 -sebserver.http.client.connection-request-timeout=100000 -sebserver.http.client.read-timeout=200000 +sebserver.http.client.connect-timeout=15000 +sebserver.http.client.connection-request-timeout=10000 +sebserver.http.client.read-timeout=60000 diff --git a/src/main/resources/config/application-gui.properties b/src/main/resources/config/application-gui.properties index 2fe17115..dd575a20 100644 --- a/src/main/resources/config/application-gui.properties +++ b/src/main/resources/config/application-gui.properties @@ -24,9 +24,9 @@ sebserver.gui.http.webservice.contextPath=${server.servlet.context-path} sebserver.gui.entrypoint=/gui -sebserver.http.client.connect-timeout=60000 -sebserver.http.client.connection-request-timeout=100000 -sebserver.http.client.read-timeout=200000 +sebserver.http.client.connect-timeout=15000 +sebserver.http.client.connection-request-timeout=10000 +sebserver.http.client.read-timeout=60000 sebserver.gui.webservice.apipath=${sebserver.webservice.api.admin.endpoint} # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index 5c3fc468..4325f14a 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -32,6 +32,7 @@ spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.maxLifetime=1800000 spring.datasource.hikari.maximumPoolSize=100 +spring.datasource.hikari.leakDetectionThreshold=10000 ### webservice security spring.datasource.password=${sebserver.mariadb.password} diff --git a/src/main/resources/config/ehcache.xml b/src/main/resources/config/ehcache.xml index 328874b7..ecc1b6f6 100644 --- a/src/main/resources/config/ehcache.xml +++ b/src/main/resources/config/ehcache.xml @@ -86,12 +86,11 @@ java.lang.String ch.ethz.seb.sebserver.gbl.model.exam.QuizData - 5 + 10 10000 - \ No newline at end of file diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/WebserviceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/WebserviceTest.java index 3770935c..573be2e1 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/WebserviceTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/WebserviceTest.java @@ -67,7 +67,7 @@ public class WebserviceTest extends AdministrationAPIIntegrationTester { assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2)); try { - Thread.sleep(5000); + Thread.sleep(500); } catch (final InterruptedException e) { } @@ -75,7 +75,7 @@ public class WebserviceTest extends AdministrationAPIIntegrationTester { assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2)); try { - Thread.sleep(6000); + Thread.sleep(600); } catch (final InterruptedException e) { } diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 4f74dda7..46784e83 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -41,4 +41,5 @@ sebserver.webservice.api.redirect.unauthorized=none sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.moodle.api.token.request.paths -management.endpoints.web.base-path=/actuator \ No newline at end of file +management.endpoints.web.base-path=/actuator +sebserver.webservice.master.delay.threshold=1000 \ No newline at end of file From a88e308ba960f7f34f99fc7478a0559a43f945ad Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 24 Nov 2021 09:59:43 +0100 Subject: [PATCH 6/6] SEBSERV-194 better distributed ping handling (dedicated ThreadPool) --- .../ClientHttpRequestFactoryService.java | 2 +- .../gbl/async/AsyncServiceSpringConfig.java | 31 ++++++ .../seb/sebserver/webservice/CacheConfig.java | 1 - .../batis/ClientEventExtensionMapper.java | 2 +- .../session/SEBClientConnectionService.java | 3 +- .../session/impl/ExamSessionCacheService.java | 2 +- .../session/impl/ExamSessionServiceImpl.java | 3 +- .../impl/SEBClientConnectionServiceImpl.java | 35 +++--- .../indicator/AbstractClientIndicator.java | 11 +- .../impl/indicator/AbstractLogIndicator.java | 14 ++- .../AbstractLogLevelCountIndicator.java | 7 ++ .../indicator/AbstractLogNumberIndicator.java | 9 ++ .../impl/indicator/AbstractPingIndicator.java | 102 +++++++++++++----- .../impl/indicator/DistributedPingCache.java | 74 ++++++++----- .../PingIntervalClientIndicator.java | 23 ++-- .../weblayer/api/ClientEventController.java | 20 ++-- .../weblayer/api/ExamAPI_V1_Controller.java | 35 +++--- .../config/application-dev-ws.properties | 5 +- .../config/application-dev.properties | 15 ++- .../config/application-ws.properties | 7 +- .../PingIntervalClientIndicatorTest.java | 11 +- 21 files changed, 273 insertions(+), 139 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java b/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java index 567b1152..5272843e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java +++ b/src/main/java/ch/ethz/seb/sebserver/ClientHttpRequestFactoryService.java @@ -76,7 +76,7 @@ public class ClientHttpRequestFactoryService { final ClientCredentialService clientCredentialService, @Value("${sebserver.http.client.connect-timeout:15000}") final int connectTimeout, @Value("${sebserver.http.client.connection-request-timeout:20000}") final int connectionRequestTimeout, - @Value("${sebserver.http.client.read-timeout:10000}") final int readTimeout) { + @Value("${sebserver.http.client.read-timeout:20000}") final int readTimeout) { this.environment = environment; this.clientCredentialService = clientCredentialService; 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 23b8de59..dd7385cd 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 @@ -17,6 +17,7 @@ import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @Configuration @EnableAsync @@ -25,6 +26,7 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer { public static final String EXECUTOR_BEAN_NAME = "AsyncServiceExecutorBean"; + /** This ThreadPool is used for internal long running background tasks */ @Bean(name = EXECUTOR_BEAN_NAME) public Executor threadPoolTaskExecutor() { final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); @@ -39,6 +41,9 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer { public static final String EXAM_API_EXECUTOR_BEAN_NAME = "ExamAPIAsyncServiceExecutorBean"; + /** This ThreadPool is used for SEB client connection establishment and + * should be able to handle incoming bursts of SEB client connection requests (handshake) + * when up to 1000 - 2000 clients connect at nearly the same time (start of an exam) */ @Bean(name = EXAM_API_EXECUTOR_BEAN_NAME) public Executor examAPIThreadPoolTaskExecutor() { final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); @@ -51,6 +56,32 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer { return executor; } + public static final String EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME = "examAPIPingThreadPoolTaskExecutor"; + + /** This ThreadPool is used for ping handling in a distributed setup and shall reject + * incoming ping requests as fast as possible if there is to much load on the DB. + * We prefer to loose a shared ping update and respond to the client in time over a client request timeout */ + @Bean(name = EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) + public Executor examAPIPingThreadPoolTaskExecutor() { + final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(0); + executor.setThreadNamePrefix("SEBPingService-"); + executor.initialize(); + executor.setWaitForTasksToCompleteOnShutdown(false); + return executor; + } + + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + final ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(5); + threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(false); + threadPoolTaskScheduler.setThreadNamePrefix("SEB-Server-BgTask-"); + return threadPoolTaskScheduler; + } + @Override public Executor getAsyncExecutor() { return threadPoolTaskExecutor(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java index c93ba58a..20923778 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java @@ -46,7 +46,6 @@ public class CacheConfig extends JCacheConfigurerSupport { final CachingProvider cachingProvider = Caching.getCachingProvider(); final javax.cache.CacheManager cacheManager = cachingProvider.getCacheManager(new URI(this.jCacheConfig), this.getClass().getClassLoader()); - System.out.println("cacheManager:" + cacheManager); final CompositeCacheManager composite = new CompositeCacheManager(); composite.setCacheManagers(Arrays.asList( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtensionMapper.java b/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtensionMapper.java index 655f2791..ce538fea 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtensionMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/datalayer/batis/ClientEventExtensionMapper.java @@ -80,7 +80,7 @@ public interface ClientEventExtensionMapper { .from(ClientEventRecordDynamicSqlSupport.clientEventRecord) - .leftJoin(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord) + .join(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord) .on( ClientEventRecordDynamicSqlSupport.clientEventRecord.clientConnectionId, equalTo(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord.id)); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientConnectionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientConnectionService.java index 790923fc..df8771b9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientConnectionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientConnectionService.java @@ -137,8 +137,9 @@ public interface SEBClientConnectionService { * @param connectionToken the connection token * @param timestamp the ping time-stamp * @param pingNumber the ping number + * @param instructionConfirm instruction confirm sent by the SEB client or null * @return SEB instruction if available */ - String notifyPing(String connectionToken, long timestamp, int pingNumber); + String notifyPing(String connectionToken, long timestamp, int pingNumber, String instructionConfirm); /** Notify a SEB client event for live indication and storing to database. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index c218208f..c93483da 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -69,7 +69,7 @@ public class ExamSessionCacheService { cacheNames = CACHE_NAME_RUNNING_EXAM, key = "#examId", unless = "#result == null") - public Exam getRunningExam(final Long examId) { + public synchronized Exam getRunningExam(final Long examId) { if (log.isDebugEnabled()) { log.debug("Verify running exam for id: {}", examId); 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 67a62448..c42270f8 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 @@ -193,7 +193,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { } @Override - public Result getRunningExam(final Long examId) { + public synchronized Result getRunningExam(final Long examId) { if (log.isTraceEnabled()) { log.trace("Running exam request for exam {}", examId); } @@ -212,6 +212,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { return Result.of(exam); } else { if (exam != null) { + log.info("Exam {} is not running anymore. Flush caches", exam); flushCache(exam); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 679da882..49dba8bf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -34,7 +34,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; @@ -70,14 +69,13 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic private final SEBClientConfigDAO sebClientConfigDAO; private final SEBClientInstructionService sebInstructionService; private final SEBClientNotificationService sebClientNotificationService; - private final WebserviceInfo webserviceInfo; private final ExamAdminService examAdminService; private final DistributedPingCache distributedPingCache; + private final boolean isDistributedSetup; protected SEBClientConnectionServiceImpl( final ExamSessionService examSessionService, final EventHandlingStrategyFactory eventHandlingStrategyFactory, - final SEBClientConfigDAO sebClientConfigDAO, final SEBClientInstructionService sebInstructionService, final SEBClientNotificationService sebClientNotificationService, @@ -92,9 +90,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic this.sebClientConfigDAO = sebClientConfigDAO; this.sebInstructionService = sebInstructionService; this.sebClientNotificationService = sebClientNotificationService; - this.webserviceInfo = sebInstructionService.getWebserviceInfo(); this.examAdminService = examAdminService; this.distributedPingCache = distributedPingCache; + this.isDistributedSetup = sebInstructionService.getWebserviceInfo().isDistributed(); } @Override @@ -475,7 +473,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic } // delete stored ping if this is a distributed setup - if (this.webserviceInfo.isDistributed()) { + if (this.isDistributedSetup) { this.distributedPingCache .deletePingForConnection(updatedClientConnection.id); } @@ -529,7 +527,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic } // delete stored ping if this is a distributed setup - if (this.webserviceInfo.isDistributed()) { + if (this.isDistributedSetup) { this.distributedPingCache .deletePingForConnection(updatedClientConnection.id); } @@ -543,7 +541,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic public void updatePingEvents() { try { - final boolean distributed = this.webserviceInfo.isDistributed(); final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION); final long now = Utils.getMillisecondsNow(); final Consumer missingPingUpdate = missingPingUpdate(now); @@ -552,7 +549,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic .allRunningExamIds() .getOrThrow() .stream() - .flatMap(examId -> distributed + .flatMap(examId -> this.isDistributedSetup ? this.clientConnectionDAO .getConnectionTokensNoCache(examId) .getOrThrow() @@ -581,16 +578,26 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic public String notifyPing( final String connectionToken, final long timestamp, - final int pingNumber) { + final int pingNumber, + final String instructionConfirm) { + + processPing(connectionToken, timestamp, pingNumber); + + if (instructionConfirm != null) { + this.sebInstructionService.confirmInstructionDone(connectionToken, instructionConfirm); + } + + return this.sebInstructionService.getInstructionJSON(connectionToken); + } + + private void processPing(final String connectionToken, final long timestamp, final int pingNumber) { final ClientConnectionDataInternal activeClientConnection = - this.examSessionService.getConnectionDataInternal(connectionToken); + this.examSessionCacheService.getClientConnection(connectionToken); if (activeClientConnection != null) { activeClientConnection.notifyPing(timestamp, pingNumber); } - - return this.sebInstructionService.getInstructionJSON(connectionToken); } @Override @@ -732,7 +739,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic } private void checkExamIntegrity(final Long examId) { - if (this.webserviceInfo.isDistributed()) { + if (this.isDistributedSetup) { // if the cached Exam is not up to date anymore, we have to update the cache first final Result updateExamCache = this.examSessionService.updateExamCache(examId); if (updateExamCache.hasError()) { @@ -779,7 +786,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic if (clientEventRecord != null) { // store event and and flush cache this.eventHandlingStrategy.accept(clientEventRecord); - if (this.webserviceInfo.isDistributed()) { + if (this.isDistributedSetup) { // mark for update and flush the cache this.clientConnectionDAO.save(connection.clientConnection); this.examSessionCacheService.evictClientConnection( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractClientIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractClientIndicator.java index c5fa6bfb..eafc5e89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractClientIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractClientIndicator.java @@ -10,21 +10,16 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator; import org.joda.time.DateTimeUtils; -import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator; public abstract class AbstractClientIndicator implements ClientIndicator { - private static final long PERSISTENT_UPDATE_INTERVAL = Constants.SECOND_IN_MILLIS; - protected Long indicatorId; protected Long examId; protected Long connectionId; protected boolean cachingEnabled; protected boolean active = true; - protected long persistentUpdateInterval = PERSISTENT_UPDATE_INTERVAL; - protected long lastPersistentUpdate = 0; protected boolean valueInitializes = false; protected double currentValue = Double.NaN; @@ -72,15 +67,11 @@ public abstract class AbstractClientIndicator implements ClientIndicator { final long now = DateTimeUtils.currentTimeMillis(); if (!this.valueInitializes) { this.currentValue = computeValueAt(now); - this.lastPersistentUpdate = now; this.valueInitializes = true; } if (!this.cachingEnabled && this.active) { - if (now - this.lastPersistentUpdate > this.persistentUpdateInterval) { - this.currentValue = computeValueAt(now); - this.lastPersistentUpdate = now; - } + this.currentValue = computeValueAt(now); } return this.currentValue; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogIndicator.java index 743361e3..39edc2d5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogIndicator.java @@ -24,11 +24,16 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; public abstract class AbstractLogIndicator extends AbstractClientIndicator { + protected static final Long DISTRIBUTED_LOG_UPDATE_INTERVAL = 5 * Constants.SECOND_IN_MILLIS; + protected final Set observed; protected final List eventTypeIds; protected String[] tags; + protected long lastDistributedUpdate = 0L; + protected AbstractLogIndicator(final EventType... eventTypes) { + this.observed = Collections.unmodifiableSet(EnumSet.of(eventTypes[0], eventTypes)); this.eventTypeIds = Utils.immutableListOf(Arrays.stream(eventTypes) .map(et -> et.id) @@ -44,7 +49,6 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator { final boolean cachingEnabled) { super.init(indicatorDefinition, connectionId, active, cachingEnabled); - super.persistentUpdateInterval = 2 * Constants.SECOND_IN_MILLIS; if (indicatorDefinition == null || StringUtils.isBlank(indicatorDefinition.tags)) { this.tags = null; @@ -75,4 +79,12 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator { return this.observed; } + protected boolean loadFromPersistent(final long timestamp) { + if (!super.valueInitializes) { + return true; + } + + return timestamp - this.lastDistributedUpdate > DISTRIBUTED_LOG_UPDATE_INTERVAL; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogLevelCountIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogLevelCountIndicator.java index 8a7fa0fd..50cc95eb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogLevelCountIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogLevelCountIndicator.java @@ -57,6 +57,10 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato @Override public double computeValueAt(final long timestamp) { + if (!loadFromPersistent(timestamp)) { + return super.currentValue; + } + try { final Long errors = this.clientEventRecordMapper @@ -72,9 +76,12 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato .execute(); return errors.doubleValue(); + } catch (final Exception e) { log.error("Failed to get indicator count from persistent storage: ", e); return super.currentValue; + } finally { + super.lastDistributedUpdate = timestamp; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogNumberIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogNumberIndicator.java index bb9d13dc..5e9f381c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogNumberIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractLogNumberIndicator.java @@ -62,8 +62,15 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator { @Override public double computeValueAt(final long timestamp) { + + if (!loadFromPersistent(timestamp)) { + return super.currentValue; + } + try { + System.out.println("************** loadFromPersistent"); + final List execute = this.clientEventRecordMapper.selectByExample() .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId)) .and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds)) @@ -90,6 +97,8 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator { } catch (final Exception e) { log.error("Failed to get indicator number from persistent storage: {}", e.getMessage()); return this.currentValue; + } finally { + super.lastDistributedUpdate = timestamp; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractPingIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractPingIndicator.java index a67a7546..0005171b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractPingIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/AbstractPingIndicator.java @@ -11,34 +11,38 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator; import java.util.Collections; import java.util.EnumSet; import java.util.Set; +import java.util.concurrent.Executor; -import org.joda.time.DateTime; -import org.joda.time.DateTimeUtils; -import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; -import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; public abstract class AbstractPingIndicator extends AbstractClientIndicator { private static final Logger log = LoggerFactory.getLogger(AbstractPingIndicator.class); - private static final long INTERVAL_FOR_PERSISTENT_UPDATE = Constants.SECOND_IN_MILLIS; - private final Set EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class)); + private final Executor executor; protected final DistributedPingCache distributedPingCache; - private final long lastUpdate = 0; - protected Long pingRecord = null; - - protected AbstractPingIndicator(final DistributedPingCache distributedPingCache) { + //protected Long pingRecord = null; + protected PingUpdate pingUpdate = null; + protected AbstractPingIndicator( + final DistributedPingCache distributedPingCache, + @Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor executor) { super(); + this.executor = executor; this.distributedPingCache = distributedPingCache; } @@ -53,33 +57,31 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator { if (!this.cachingEnabled && this.active) { try { - this.pingRecord = this.distributedPingCache.initPingForConnection(this.connectionId); + createPingUpdate(); } catch (final Exception e) { - this.pingRecord = this.distributedPingCache.getPingRecordIdForConnectionId(connectionId); + createPingUpdate(); } } } public final void notifyPing(final long timestamp, final int pingNumber) { - final long now = DateTime.now(DateTimeZone.UTC).getMillis(); - super.currentValue = now; - super.lastPersistentUpdate = now; + super.currentValue = timestamp; if (!this.cachingEnabled) { - if (this.pingRecord == null) { + if (this.pingUpdate == null) { tryRecoverPingRecord(); - if (this.pingRecord == null) { + if (this.pingUpdate == null) { return; } } - // Update last ping time on persistent storage - final long millisecondsNow = DateTimeUtils.currentTimeMillis(); - if (millisecondsNow - this.lastUpdate > INTERVAL_FOR_PERSISTENT_UPDATE) { - synchronized (this) { - this.distributedPingCache.updatePing(this.pingRecord, millisecondsNow); - } + // Update last ping time on persistent storage asynchronously within a defines thread pool with no + // waiting queue to skip further ping updates if all update threads are busy + try { + this.executor.execute(this.pingUpdate); + } catch (final Exception e) { + //log.warn("Failed to schedule ping task: {}" + e.getMessage()); } } } @@ -91,15 +93,21 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator { } try { - this.pingRecord = this.distributedPingCache.getPingRecordIdForConnectionId(this.connectionId); - if (this.pingRecord == null) { - this.pingRecord = this.distributedPingCache.initPingForConnection(this.connectionId); + createPingUpdate(); + if (this.pingUpdate == null) { + createPingUpdate(); } } catch (final Exception e) { log.error("Failed to recover ping record for connection: {}", this.connectionId, e); } } + private void createPingUpdate() { + this.pingUpdate = new PingUpdate( + this.distributedPingCache.getClientEventLastPingMapper(), + this.distributedPingCache.initPingForConnection(this.connectionId)); + } + @Override public Set observedEvents() { return this.EMPTY_SET; @@ -107,4 +115,46 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator { public abstract ClientEventRecord updateLogEvent(final long now); + @Override + public double computeValueAt(final long timestamp) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void notifyValueChange(final ClientEvent event) { + // TODO Auto-generated method stub + + } + + @Override + public void notifyValueChange(final ClientEventRecord clientEventRecord) { + // TODO Auto-generated method stub + + } + + @Override + public IndicatorType getType() { + // TODO Auto-generated method stub + return null; + } + + static final class PingUpdate implements Runnable { + + private final ClientEventLastPingMapper clientEventLastPingMapper; + final Long pingRecord; + + public PingUpdate(final ClientEventLastPingMapper clientEventLastPingMapper, final Long pingRecord) { + this.clientEventLastPingMapper = clientEventLastPingMapper; + this.pingRecord = pingRecord; + } + + @Override + public void run() { + this.clientEventLastPingMapper + .updatePingTime(this.pingRecord, Utils.getMillisecondsNow()); + } + + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java index 5c027577..13c2269c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedPingCache.java @@ -12,11 +12,12 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; import static org.mybatis.dynamic.sql.SqlBuilder.isIn; import java.util.ArrayList; +import java.util.Collection; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.stream.Collectors; -import org.ehcache.impl.internal.concurrent.ConcurrentHashMap; import org.joda.time.DateTimeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,13 +26,14 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper.ClientEventLastPingRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; @@ -45,9 +47,11 @@ public class DistributedPingCache implements DisposableBean { private final ClientEventLastPingMapper clientEventLastPingMapper; private final ClientEventRecordMapper clientEventRecordMapper; - private ScheduledFuture taskRef; + private final long pingUpdateTolerance; + private ScheduledFuture taskRef; private final Map pingCache = new ConcurrentHashMap<>(); + private long lastUpdate = 0L; public DistributedPingCache( final ClientEventLastPingMapper clientEventLastPingMapper, @@ -58,9 +62,10 @@ public class DistributedPingCache implements DisposableBean { this.clientEventLastPingMapper = clientEventLastPingMapper; this.clientEventRecordMapper = clientEventRecordMapper; + this.pingUpdateTolerance = pingUpdate * 2 / 3; if (webserviceInfo.isDistributed()) { try { - this.taskRef = taskScheduler.scheduleAtFixedRate(this::updateCache, pingUpdate); + this.taskRef = taskScheduler.scheduleAtFixedRate(this::updatePings, pingUpdate); } catch (final Exception e) { log.error("Failed to initialize distributed ping cache update task"); this.taskRef = null; @@ -70,6 +75,10 @@ public class DistributedPingCache implements DisposableBean { } } + public ClientEventLastPingMapper getClientEventLastPingMapper() { + return this.clientEventLastPingMapper; + } + @Transactional public Long initPingForConnection(final Long connectionId) { try { @@ -129,17 +138,6 @@ public class DistributedPingCache implements DisposableBean { } } - public void updatePing(final Long pingRecordId, final Long pingTime) { - try { - - this.clientEventLastPingMapper - .updatePingTime(pingRecordId, pingTime); - - } catch (final Exception e) { - log.error("Failed to update ping for ping record id -> {}", pingRecordId); - } - } - @Transactional public void deletePingForConnection(final Long connectionId) { try { @@ -148,33 +146,49 @@ public class DistributedPingCache implements DisposableBean { log.debug("*** Delete ping record for SEB connection: {}", connectionId); } - this.clientEventRecordMapper - .deleteByExample() + final Collection records = this.clientEventLastPingMapper + .selectByExample() .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId)) .and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id)) .build() .execute(); + if (records == null || records.isEmpty()) { + return; + } + + final Long id = records.iterator().next().id; + this.pingCache.remove(id); + this.clientEventRecordMapper.deleteByPrimaryKey(id); + } catch (final Exception e) { log.error("Failed to delete ping for connection -> {}", connectionId, e); - } finally { - this.pingCache.remove(connectionId); + try { + log.info( + "Because of failed ping record deletion, " + + "flushing the ping cache to ensure no dead connections pings remain in the cache"); + this.pingCache.clear(); + } catch (final Exception ee) { + log.error("Failed to force flushing the ping cache: ", e); + } } } - public Long getLastPing(final Long pingRecordId) { + public Long getLastPing(final Long pingRecordId, final boolean missing) { try { Long ping = this.pingCache.get(pingRecordId); - if (ping == null) { + if (ping == null && !missing) { if (log.isDebugEnabled()) { log.debug("*** Get and cache ping time: {}", pingRecordId); } ping = this.clientEventLastPingMapper.selectPingTimeByPrimaryKey(pingRecordId); - if (ping != null) { - this.pingCache.put(pingRecordId, ping); - } + } + + // if we have a missing ping we need to check new ping from next update even if the cache was empty + if (ping != null || missing) { + this.pingCache.put(pingRecordId, ping); } return ping; @@ -184,18 +198,24 @@ public class DistributedPingCache implements DisposableBean { } } - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) - public void updateCache() { + private void updatePings() { if (this.pingCache.isEmpty()) { return; } + final long millisecondsNow = Utils.getMillisecondsNow(); + if (millisecondsNow - this.lastUpdate < this.pingUpdateTolerance) { + log.warn("Skip ping update schedule because the last one was less then 2 seconds ago"); + return; + } + if (log.isDebugEnabled()) { log.trace("*** Update distributed ping cache: {}", this.pingCache); } try { + final ArrayList pks = new ArrayList<>(this.pingCache.keySet()); final Map mapping = this.clientEventLastPingMapper .selectByExample() @@ -215,6 +235,8 @@ public class DistributedPingCache implements DisposableBean { } catch (final Exception e) { log.error("Error while trying to update distributed ping cache: {}", this.pingCache, e); } + + this.lastUpdate = millisecondsNow; } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicator.java index 85bb95a6..f22b7b53 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicator.java @@ -10,10 +10,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator; import java.math.BigDecimal; import java.util.Comparator; +import java.util.concurrent.Executor; import org.joda.time.DateTimeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Scope; @@ -22,6 +24,7 @@ import org.springframework.stereotype.Component; import com.fasterxml.jackson.annotation.JsonIgnore; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; @@ -44,8 +47,10 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator { private boolean missingPing = false; private boolean hidden = false; - public PingIntervalClientIndicator(final DistributedPingCache distributedPingCache) { - super(distributedPingCache); + public PingIntervalClientIndicator( + final DistributedPingCache distributedPingCache, + @Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor executor) { + super(distributedPingCache, executor); this.cachingEnabled = true; } @@ -122,16 +127,12 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator { @Override public final double computeValueAt(final long timestamp) { + if (!this.cachingEnabled && super.pingUpdate != null) { - if (!this.cachingEnabled && super.pingRecord != null) { - - // if this indicator is not missing ping - if (!this.isMissingPing()) { - final Long lastPing = this.distributedPingCache.getLastPing(super.pingRecord); - if (lastPing != null) { - final double doubleValue = lastPing.doubleValue(); - return Math.max(Double.isNaN(this.currentValue) ? doubleValue : this.currentValue, doubleValue); - } + final Long lastPing = this.distributedPingCache.getLastPing(super.pingUpdate.pingRecord, this.missingPing); + if (lastPing != null) { + final double doubleValue = lastPing.doubleValue(); + return Math.max(Double.isNaN(this.currentValue) ? doubleValue : this.currentValue, doubleValue); } return this.currentValue; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java index 349c42eb..a1b9180f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java @@ -105,19 +105,13 @@ public class ClientEventController extends ReadonlyEntityController this.clientEventDAO.allMatchingExtended(filterMap, this::hasReadAccess)) - .getOrThrow(); - } catch (final Exception e) { - e.printStackTrace(); - throw e; - } + return this.paginationService.getPage( + pageNumber, + pageSize, + sort, + getSQLTableOfEntity().name(), + () -> this.clientEventDAO.allMatchingExtended(filterMap, this::hasReadAccess)) + .getOrThrow(); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index 4120f6b1..ab391808 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -269,32 +269,31 @@ public class ExamAPI_V1_Controller { final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER); final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM); - if (log.isTraceEnabled()) { - log.trace("****************** SEB client connection: {} ip: {}", - connectionToken, - getClientAddress(request)); - } - - if (instructionConfirm != null) { - this.sebClientConnectionService.confirmInstructionDone(connectionToken, instructionConfirm); + long pingTime; + try { + pingTime = Long.parseLong(timeStampString); + } catch (final Exception e) { + log.error("Invalid ping request: {}", connectionToken); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + return; } final String instruction = this.sebClientConnectionService .notifyPing( connectionToken, - Long.parseLong(timeStampString), - pingNumString != null ? Integer.parseInt(pingNumString) : -1); + pingTime, + pingNumString != null ? Integer.parseInt(pingNumString) : -1, + instructionConfirm); if (instruction == null) { response.setStatus(HttpStatus.NO_CONTENT.value()); - return; - } - - try { - response.setStatus(HttpStatus.OK.value()); - response.getOutputStream().write(instruction.getBytes()); - } catch (final IOException e) { - log.error("Failed to send instruction as response: {}", connectionToken, e); + } else { + try { + response.setStatus(HttpStatus.OK.value()); + response.getOutputStream().write(instruction.getBytes()); + } catch (final IOException e) { + log.error("Failed to send instruction as response: {}", connectionToken, e); + } } } diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index 1c7aa00b..214a61fd 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -13,12 +13,13 @@ spring.datasource.hikari.initializationFailTimeout=30000 spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.maxLifetime=1800000 -spring.datasource.hikari.maximumPoolSize=500 +spring.datasource.hikari.maximumPoolSize=10 +spring.datasource.hikari.leakDetectionThreshold=2000 sebserver.http.client.connect-timeout=15000 sebserver.http.client.connection-request-timeout=10000 sebserver.http.client.read-timeout=20000 - +sebserver.webservice.distributed.pingUpdate=2000 sebserver.webservice.clean-db-on-startup=false # webservice configuration diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index b11db757..ed384d92 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -10,11 +10,16 @@ server.tomcat.uri-encoding=UTF-8 logging.level.ch=INFO logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO logging.level.org.springframework.cache=INFO -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG +#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG +#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper=DEBUG #logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE +logging.level.com.zaxxer.hikari=DEBUG -sebserver.http.client.connect-timeout=150000 -sebserver.http.client.connection-request-timeout=100000 -sebserver.http.client.read-timeout=200000 +sebserver.http.client.connect-timeout=15000 +sebserver.http.client.connection-request-timeout=10000 +sebserver.http.client.read-timeout=60000 diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index a8b7c3e0..88fef4ce 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -10,9 +10,6 @@ sebserver.init.adminaccount.username=sebserver-admin sebserver.init.database.integrity.checks=true sebserver.init.database.integrity.try-fix=true -sebserver.webservice.distributed=false -sebserver.webservice.distributed.pingUpdate=3000 - ### webservice caching spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider spring.cache.jcache.config=classpath:config/ehcache.xml @@ -31,7 +28,8 @@ spring.datasource.hikari.initializationFailTimeout=3000 spring.datasource.hikari.connectionTimeout=30000 spring.datasource.hikari.idleTimeout=600000 spring.datasource.hikari.maxLifetime=1800000 -spring.datasource.hikari.maximumPoolSize=500 +spring.datasource.hikari.maximumPoolSize=100 +spring.datasource.hikari.leakDetectionThreshold=10000 ### webservice security spring.datasource.password=${sebserver.mariadb.password} @@ -41,6 +39,7 @@ sebserver.webservice.internalSecret=${sebserver.password} ### webservice networking sebserver.webservice.forceMaster=false sebserver.webservice.distributed=false +sebserver.webservice.distributed.pingUpdate=3000 sebserver.webservice.http.external.scheme=https sebserver.webservice.http.external.servername= sebserver.webservice.http.external.port= diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicatorTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicatorTest.java index 6fbb52b4..43146d4d 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicatorTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/PingIntervalClientIndicatorTest.java @@ -10,6 +10,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator; import static org.junit.Assert.assertEquals; +import java.util.concurrent.Executor; + import org.joda.time.DateTimeUtils; import org.junit.After; import org.junit.Test; @@ -34,9 +36,10 @@ public class PingIntervalClientIndicatorTest { final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class); final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class); + final Executor executor = Mockito.mock(Executor.class); final PingIntervalClientIndicator pingIntervalClientIndicator = - new PingIntervalClientIndicator(distributedPingCache); + new PingIntervalClientIndicator(distributedPingCache, executor); assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue())); } @@ -47,9 +50,10 @@ public class PingIntervalClientIndicatorTest { final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class); final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class); + final Executor executor = Mockito.mock(Executor.class); final PingIntervalClientIndicator pingIntervalClientIndicator = - new PingIntervalClientIndicator(distributedPingCache); + new PingIntervalClientIndicator(distributedPingCache, executor); assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue())); DateTimeUtils.setCurrentMillisProvider(() -> 10L); @@ -63,9 +67,10 @@ public class PingIntervalClientIndicatorTest { final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class); final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class); + final Executor executor = Mockito.mock(Executor.class); final PingIntervalClientIndicator pingIntervalClientIndicator = - new PingIntervalClientIndicator(distributedPingCache); + new PingIntervalClientIndicator(distributedPingCache, executor); final JSONMapper jsonMapper = new JSONMapper(); final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator); assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);