From 3381d69f8b3f683f03c077d006a8328e7552dd8b Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 14 Apr 2021 09:15:21 +0200 Subject: [PATCH] Improved Exam Config streaming on SEB Client connection handshake --- .../servicelayer/dao/ClientConnectionDAO.java | 6 ++ .../dao/impl/ClientConnectionDAOImpl.java | 17 ++++++ .../session/ExamSessionService.java | 6 +- .../session/impl/ExamSessionCacheService.java | 18 +++--- .../session/impl/ExamSessionServiceImpl.java | 30 +++++++--- .../weblayer/api/ExamAPI_V1_Controller.java | 55 ++++++++++--------- 6 files changed, 86 insertions(+), 46 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index 811aae2c..6a7e2767 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -137,6 +137,12 @@ public interface ClientConnectionDAO extends * @return Result refer to the active connection flag or to an error when happened */ Result isActiveConnection(Long examId, String connectionToken); + /** Use this to check whether a single ClientConnection is up to date or needs a refresh. + * + * @param clientConnection the actual ClientConnection (from the internal cache) + * @return Result refer to true if the given ClientConnection is up to date */ + Result isUpToDate(ClientConnection clientConnection); + /** Filters a set of client connection tokens to a set containing only * connection tokens of active client connections. * 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 bf68d880..646702b4 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 @@ -485,6 +485,23 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { .isPresent()); } + @Override + public Result isUpToDate(final ClientConnection clientConnection) { + return Result.tryCatch(() -> this.clientConnectionRecordMapper + .selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.connectionToken, + SqlBuilder.isEqualTo(clientConnection.connectionToken)) + .and( + ClientConnectionRecordDynamicSqlSupport.updateTime, + SqlBuilder.isEqualTo(clientConnection.updateTime)) + .build() + .execute() + .stream() + .findFirst() + .isPresent()); + } + @Override public Result> filterActive(final Long examId, final Set connectionToken) { if (connectionToken == null || connectionToken.isEmpty()) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index afdcd698..f9a6e7ea 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -128,9 +128,13 @@ public interface ExamSessionService { /** Streams the default SEB Exam Configuration to a ClientConnection with given connectionToken. * + * @param institutionId the Institution identifier * @param connectionToken The connection token that identifiers the ClientConnection * @param out The OutputStream to stream the data to */ - void streamDefaultExamConfig(String connectionToken, OutputStream out); + void streamDefaultExamConfig( + Long institutionId, + String connectionToken, + OutputStream out); /** Get current ClientConnectionData for a specified active SEB client connection. * 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 b26c2434..c95c9564 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 @@ -174,31 +174,31 @@ public class ExamSessionCacheService { @Cacheable( cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, - key = "#exam.id", + key = "#examId", sync = true) - public InMemorySEBConfig getDefaultSEBConfigForExam(final Exam exam) { + public InMemorySEBConfig getDefaultSEBConfigForExam(final Long examId, final Long institutionId) { try { final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); final Long configId = this.sebExamConfigService.exportForExam( byteOut, - exam.institutionId, - exam.id); + institutionId, + examId); - return new InMemorySEBConfig(configId, exam.id, byteOut.toByteArray()); + return new InMemorySEBConfig(configId, examId, byteOut.toByteArray()); } catch (final Exception e) { - log.error("Unexpected error while getting default exam configuration for running exam; {}", exam, e); + log.error("Unexpected error while getting default exam configuration for running exam; {}", examId, e); throw e; } } @CacheEvict( cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, - key = "#exam.id") - public void evictDefaultSEBConfig(final Exam exam) { + key = "#examId") + public void evictDefaultSEBConfig(final Long examId) { if (log.isDebugEnabled()) { - log.debug("Eviction of default SEB Configuration from cache for exam: {}", exam.id); + log.debug("Eviction of default SEB Configuration from cache for exam: {}", 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 edd56c82..67965c7b 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 @@ -282,6 +282,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { @Override public void streamDefaultExamConfig( + final Long institutionId, final String connectionToken, final OutputStream out) { @@ -289,16 +290,17 @@ public class ExamSessionServiceImpl implements ExamSessionService { log.debug("SEB exam configuration download request, connectionToken: {}", connectionToken); } - final ClientConnection connection = this.clientConnectionDAO - .byConnectionToken(connectionToken) + final ClientConnectionData clientConnectionData = this.getConnectionData(connectionToken) .getOrThrow(); - if (connection == null) { + if (clientConnectionData == null || clientConnectionData.clientConnection == null) { log.warn("SEB exam configuration download request, no active ClientConnection found for token: {}", connectionToken); throw new AccessDeniedException("Illegal connection token. No active ClientConnection found for token"); } + final ClientConnection connection = clientConnectionData.clientConnection; + // exam integrity check if (connection.examId == null || !isExamRunning(connection.examId)) { log.error("Missing exam identifier or requested exam is not running for connection: {}", connection); @@ -309,11 +311,8 @@ public class ExamSessionServiceImpl implements ExamSessionService { log.debug("Trying to get exam from InMemorySEBConfig"); } - final Exam exam = this.getRunningExam(connection.examId) - .getOrThrow(); - final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService - .getDefaultSEBConfigForExam(exam); + .getDefaultSEBConfigForExam(connection.examId, institutionId); if (sebConfigForExam == null) { log.error("Failed to get and cache InMemorySEBConfig for connection: {}", connection); @@ -341,13 +340,26 @@ public class ExamSessionServiceImpl implements ExamSessionService { public Result getConnectionData(final String connectionToken) { return Result.tryCatch(() -> { + final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService .getClientConnection(connectionToken); if (activeClientConnection == null) { throw new NoSuchElementException("Client Connection with token: " + connectionToken); } + if (this.distributedSetup) { + + final Boolean upToDate = this.clientConnectionDAO + .isUpToDate(activeClientConnection.clientConnection) + .getOr(false); + if (!upToDate) { + this.examSessionCacheService.evictClientConnection(connectionToken); + return this.examSessionCacheService.getClientConnection(connectionToken); + } + } + return activeClientConnection; + }); } @@ -359,7 +371,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { if (this.distributedSetup) { // if we run in distributed mode, we have to get the connection tokens of the exam - // always form the persistent storage and update the client connection cache + // always from the persistent storage and update the client connection cache // before by remove out-dated client connection. This is done within the update_time // of the client connection record that is set on every update in the persistent // storage. So if the update_time of the cached client connection doesen't match the @@ -409,7 +421,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { public Result flushCache(final Exam exam) { return Result.tryCatch(() -> { this.examSessionCacheService.evict(exam); - this.examSessionCacheService.evictDefaultSEBConfig(exam); + this.examSessionCacheService.evictDefaultSEBConfig(exam.id); this.clientConnectionDAO .getConnectionTokens(exam.id) .getOrElse(Collections::emptyList) 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 b55e74b6..f0c1c019 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 @@ -44,7 +44,6 @@ import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -340,12 +339,13 @@ public class ExamAPI_V1_Controller { final Principal principal, final HttpServletResponse response) { + final Long institutionId = getInstitutionId(principal); + try { // if an examId is provided with the request, update the connection first if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) { final String examId = formParams.getFirst(API.EXAM_API_PARAM_EXAM_ID); - final Long institutionId = getInstitutionId(principal); final ClientConnection connection = this.sebClientConnectionService.updateClientConnection( connectionToken, institutionId, @@ -361,36 +361,37 @@ public class ExamAPI_V1_Controller { final ServletOutputStream outputStream = response.getOutputStream(); - try { - - final ClientConnectionData connection = this.examSessionService - .getConnectionData(connectionToken) - .getOrThrow(); - - // exam integrity check - if (connection.clientConnection.examId == null || - !this.examSessionService.isExamRunning(connection.clientConnection.examId)) { - - log.error("Missing exam identifier or requested exam is not running for connection: {}", - connection); - throw new IllegalStateException("Missing exam identifier or requested exam is not running"); - } - } catch (final Exception e) { - - log.error("Unexpected error: ", e); - - final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage()); - outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage))); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - outputStream.flush(); - outputStream.close(); - return; - } +// try { +// +// final ClientConnectionData connection = this.examSessionService +// .getConnectionData(connectionToken) +// .getOrThrow(); +// +// // exam integrity check +// if (connection.clientConnection.examId == null || +// !this.examSessionService.isExamRunning(connection.clientConnection.examId)) { +// +// log.error("Missing exam identifier or requested exam is not running for connection: {}", +// connection); +// throw new IllegalStateException("Missing exam identifier or requested exam is not running"); +// } +// } catch (final Exception e) { +// +// log.error("Unexpected error: ", e); +// +// final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage()); +// outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage))); +// response.setStatus(HttpStatus.BAD_REQUEST.value()); +// outputStream.flush(); +// outputStream.close(); +// return; +// } try { this.examSessionService .streamDefaultExamConfig( + institutionId, connectionToken, outputStream);