diff --git a/pom.xml b/pom.xml index 485f890d..8ffcc0cb 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ jar - 1.3.0 + 1.3.2 ${sebserver-version} ${sebserver-version} UTF-8 diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java index 8334e9a9..885dcc72 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java @@ -137,7 +137,8 @@ public class ClientConnectionDetails { final ClientConnectionData connectionData = this.restCallBuilder .call() .get(error -> { - log.error("Unexpected error while trying to get current client connection data: ", error); + log.error("Unexpected error while trying to get current client connection data: {}", + error.getMessage()); recoverFromDisposedRestTemplate(error); return null; }); 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 200f641f..3dcd593c 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 @@ -15,6 +15,7 @@ import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; @@ -180,4 +181,10 @@ public interface ClientConnectionDAO extends * @return Result refer to the relevant VDI pair connection if exists or to an error if not */ Result getVDIPairCompanion(Long examId, String clientName); + /** Deletes all client indicator value entries within the client_indicator table for a given exam. + * + * @param exam the Exam to delete all currently registered indicator value entries + * @return Result refer to the given Exam or to an error when happened. */ + Result deleteClientIndicatorValues(Exam exam); + } 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 69f050a1..009fae8a 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 @@ -32,6 +32,7 @@ import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityDependency; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +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.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -694,6 +695,33 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { }); } + @Override + @Transactional + public Result deleteClientIndicatorValues(final Exam exam) { + return Result.tryCatch(() -> { + + final List clientConnections = this.clientConnectionRecordMapper.selectIdsByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(exam.id)) + .build() + .execute(); + + if (clientConnections == null || clientConnections.isEmpty()) { + return exam; + } + + this.clientIndicatorRecordMapper.deleteByExample() + .where( + ClientIndicatorRecordDynamicSqlSupport.clientConnectionId, + SqlBuilder.isIn(clientConnections)) + .build() + .execute(); + + return exam; + }); + } + private Result recordById(final Long id) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index 078227ac..721a2c18 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -59,7 +59,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AccessibilitySettingsData; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.SEBServerData; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.AssignmentData; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.UserData; @@ -252,10 +252,10 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms } private List getAssignments(final RestTemplate restTemplate) { - // NOTE: at the moment, seb_server_enabled cannot be set inside the Ans GUI, + // NOTE: at the moment, seb server cannot be enabled inside the Ans GUI, // only via the API, so we need to list all assignments. Maybe in the future, // we can only list those for which seb server has been enabled in Ans (like in OLAT): - //final String url = "/api/v2/search/assignments?query=seb_server_enabled:true"; + //final String url = "/api/v2/search/assignments?query=integrations.safe_exam_browser_server.enabled:true"; final String url = "/api/v2/search/assignments"; return this.apiGetList(restTemplate, url, new ParameterizedTypeReference>() { }); @@ -346,7 +346,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { final String url = String.format("/api/v2/assignments/%s", id); final AssignmentData assignment = this.apiGet(restTemplate, url, AssignmentData.class); - final AccessibilitySettingsData ts = assignment.accessibility_settings; + final SEBServerData ts = assignment.integrations.safe_exam_browser_server; return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap()); } @@ -354,24 +354,24 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms final SEBRestriction restriction) { final String url = String.format("/api/v2/assignments/%s", id); final AssignmentData assignment = getAssignmentById(restTemplate, id); - assignment.accessibility_settings.config_keys = new ArrayList<>(restriction.configKeys); - assignment.accessibility_settings.seb_server_enabled = true; + assignment.integrations.safe_exam_browser_server.config_keys = new ArrayList<>(restriction.configKeys); + assignment.integrations.safe_exam_browser_server.enabled = true; @SuppressWarnings("unused") final AssignmentData r = this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class); - final AccessibilitySettingsData ts = assignment.accessibility_settings; + final SEBServerData ts = assignment.integrations.safe_exam_browser_server; return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap()); } private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { final String url = String.format("/api/v2/assignments/%s", id); final AssignmentData assignment = getAssignmentById(restTemplate, id); - assignment.accessibility_settings.config_keys = null; - assignment.accessibility_settings.seb_server_enabled = false; + assignment.integrations.safe_exam_browser_server.config_keys = null; + assignment.integrations.safe_exam_browser_server.enabled = false; @SuppressWarnings("unused") final AssignmentData r = this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class); - final AccessibilitySettingsData ts = assignment.accessibility_settings; + final SEBServerData ts = assignment.integrations.safe_exam_browser_server; return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap()); } @@ -406,7 +406,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms private List parseLinks(final String header) { // Extracts the individual links from a header that looks like this: - // ; rel="first",; rel="last" + // ; rel="first",; rel="last" final Stream links = Arrays.stream(header.split(",")); return links .map(s -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java index b0a17dce..7645dcdd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsData.java @@ -15,12 +15,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; public final class AnsLmsData { @JsonIgnoreProperties(ignoreUnknown = true) - static final class AccessibilitySettingsData { + static final class SEBServerData { /* Ans API example: see nested in AssignmentData */ - public boolean seb_server_enabled; + public boolean enabled; public List config_keys; } + @JsonIgnoreProperties(ignoreUnknown = true) + static final class IntegrationsData { + /* Ans API example: see nested in AssignmentData */ + public SEBServerData safe_exam_browser_server; + } + @JsonIgnoreProperties(ignoreUnknown = true) static final class AssignmentData { /* @@ -37,19 +43,10 @@ public final class AnsLmsData { * "updated_at": "2021-08-17T03:41:56.747+02:00", * "trashed": false, * "start_url": "https://staging.ans.app/digital_test/assignments/78805/results/new", - * "accessibility_settings": { - * "attempts": 1, - * "restricted_access_to_other_pages": false, - * "notes": false, - * "spellchecker": false, - * "feedback": false, - * "forced_test_navigation": false, - * "cannot_reopen_question_groups": false, - * "seb_server_enabled": true, - * "config_keys": [ - * "9dd14ac828617116a1230c71b9a1aa9e06f43b32d9fa7db67f4fa113a6896e83e" - * ] - * }, + * "integrations": { + * "safe_exam_browser_server": { + * "enabled": false, + * "config_keys": [ "123" ] } } * "grades_settings": { * "grade_calculation": "formula", * "grade_formula": "1 + 9 * points / total", @@ -70,7 +67,7 @@ public final class AnsLmsData { public String start_at; public String end_at; public String start_url; - public AccessibilitySettingsData accessibility_settings; + public IntegrationsData integrations; } @JsonIgnoreProperties(ignoreUnknown = true) 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 e66ebdaa..7091a992 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 @@ -182,6 +182,13 @@ public interface ExamSessionService { * @return Result refer to the collection of connection tokens or to an error when happened. */ Result> getActiveConnectionTokens(Long examId); + /** Called to notify that the given exam has just been finished. + * This cleanup all exam session caches for the given exam and also cleanup session based stores on the persistent. + * + * @param exam the Exam that has just been finished + * @return Result refer to the finished exam or to an error when happened. */ + Result notifyExamFinished(final Exam exam); + /** Use this to check if the current cached running exam is up to date * and if not to flush the cache. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java index 5be1a549..5aee4fec 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientIndicatorFactory.java @@ -26,8 +26,10 @@ 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.ClientConnection; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.DistributedIndicatorValueService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.PingIntervalClientIndicator; @Lazy @@ -39,23 +41,68 @@ public class ClientIndicatorFactory { private final ApplicationContext applicationContext; private final IndicatorDAO indicatorDAO; + private final DistributedIndicatorValueService distributedPingCache; + private final boolean distributedSetup; private final boolean enableCaching; @Autowired public ClientIndicatorFactory( final ApplicationContext applicationContext, final IndicatorDAO indicatorDAO, + final DistributedIndicatorValueService distributedPingCache, @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup, @Value("${sebserver.webservice.api.exam.enable-indicator-cache:true}") final boolean enableCaching) { this.applicationContext = applicationContext; this.indicatorDAO = indicatorDAO; + this.distributedPingCache = distributedPingCache; + this.distributedSetup = distributedSetup; this.enableCaching = distributedSetup ? false : enableCaching; } - public List createFor(final ClientConnection clientConnection) { - final List result = new ArrayList<>(); + public void initializeDistributedCaches(final ClientConnection clientConnection) { + try { + if (!this.distributedSetup || clientConnection.examId == null) { + return; + } + + final Collection examIndicators = this.indicatorDAO + .allForExam(clientConnection.examId) + .getOrThrow(); + + boolean pingIndicatorAvailable = false; + for (final Indicator indicatorDef : examIndicators) { + + this.distributedPingCache.createIndicatorForConnection( + clientConnection.id, + indicatorDef.type, + indicatorDef.type == IndicatorType.LAST_PING ? Utils.getMillisecondsNow() : 0L); + + if (!pingIndicatorAvailable) { + pingIndicatorAvailable = indicatorDef.type == IndicatorType.LAST_PING; + } + } + + // If there is no ping interval indicator set from the exam, we add a hidden one + // to at least create missing ping events and track missing state + if (!pingIndicatorAvailable) { + this.distributedPingCache.createIndicatorForConnection( + clientConnection.id, + IndicatorType.LAST_PING, + Utils.getMillisecondsNow()); + } + + } catch (final Exception e) { + log.error("Unexpected error while trying to initialize distributed indicator value cache for: {}", + clientConnection, + e); + } + } + + public List createFor(final ClientConnection clientConnection) { + + final List result = new ArrayList<>(); if (clientConnection.examId == null) { return result; } @@ -67,7 +114,6 @@ public class ClientIndicatorFactory { .getOrThrow(); boolean pingIndicatorAvailable = false; - for (final Indicator indicatorDef : examIndicators) { try { @@ -86,7 +132,8 @@ public class ClientIndicatorFactory { result.add(indicator); } catch (final Exception e) { - log.warn("No Indicator with type: {} found as registered bean. Ignore this one.", indicatorDef.type, + log.warn("No Indicator with type: {} found as registered bean. Ignore this one.", + indicatorDef.type, e); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index 5aa76dd7..c5d335c2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; @Service @@ -42,6 +43,7 @@ public class ExamSessionControlTask implements DisposableBean { private final ExamUpdateHandler examUpdateHandler; private final ExamProctoringRoomService examProcotringRoomService; private final WebserviceInfo webserviceInfo; + private final ExamSessionService examSessionService; private final Long examTimePrefix; private final Long examTimeSuffix; @@ -54,6 +56,7 @@ public class ExamSessionControlTask implements DisposableBean { final ExamUpdateHandler examUpdateHandler, final ExamProctoringRoomService examProcotringRoomService, final WebserviceInfo webserviceInfo, + final ExamSessionService examSessionService, @Value("${sebserver.webservice.api.exam.time-prefix:3600000}") final Long examTimePrefix, @Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix, @Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron, @@ -63,6 +66,7 @@ public class ExamSessionControlTask implements DisposableBean { this.sebClientConnectionService = sebClientConnectionService; this.examUpdateHandler = examUpdateHandler; this.webserviceInfo = webserviceInfo; + this.examSessionService = examSessionService; this.examTimePrefix = examTimePrefix; this.examTimeSuffix = examTimeSuffix; this.examTaskCron = examTaskCron; @@ -185,6 +189,7 @@ public class ExamSessionControlTask implements DisposableBean { .filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now)) .flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setFinished(exam, updateId))) .flatMap(exam -> Result.skipOnError(this.examProcotringRoomService.disposeRoomsForExam(exam))) + .flatMap(exam -> Result.skipOnError(this.examSessionService.notifyExamFinished(exam))) .collect(Collectors.toMap(Exam::getId, Exam::getName)); if (!updated.isEmpty()) { 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 c172a014..5ee1bb0c 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 @@ -281,8 +281,8 @@ public class ExamSessionServiceImpl implements ExamSessionService { throw new IllegalStateException("Missing exam identifier or requested exam is not running"); } - if (log.isDebugEnabled()) { - log.debug("Trying to get exam from InMemorySEBConfig"); + if (log.isTraceEnabled()) { + log.trace("Trying to get exam from InMemorySEBConfig"); } final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService @@ -295,14 +295,14 @@ public class ExamSessionServiceImpl implements ExamSessionService { try { - if (log.isDebugEnabled()) { - log.debug("SEB exam configuration download request, start writing SEB exam configuration"); + if (log.isTraceEnabled()) { + log.trace("SEB exam configuration download request, start writing SEB exam configuration"); } out.write(sebConfigForExam.getData()); - if (log.isDebugEnabled()) { - log.debug("SEB exam configuration download request, finished writing SEB exam configuration"); + if (log.isTraceEnabled()) { + log.trace("SEB exam configuration download request, finished writing SEB exam configuration"); } } catch (final IOException e) { @@ -393,6 +393,22 @@ public class ExamSessionServiceImpl implements ExamSessionService { .getActiveConnctionTokens(examId); } + @Override + public Result notifyExamFinished(final Exam exam) { + return Result.tryCatch(() -> { + if (!isExamRunning(exam.id)) { + this.flushCache(exam); + if (this.distributedSetup) { + this.clientConnectionDAO + .deleteClientIndicatorValues(exam) + .getOrThrow(); + } + } + + return exam; + }); + } + @Override public Result updateExamCache(final Long examId) { 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 29f68b4b..3e28fdde 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 @@ -74,8 +74,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic private final SEBClientConfigDAO sebClientConfigDAO; private final SEBClientInstructionService sebInstructionService; private final ExamAdminService examAdminService; - // TODO get rid of this dependency and use application events for signaling client connection state changes private final DistributedIndicatorValueService distributedPingCache; + private final ClientIndicatorFactory clientIndicatorFactory; private final boolean isDistributedSetup; protected SEBClientConnectionServiceImpl( @@ -84,7 +84,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic final SEBClientConfigDAO sebClientConfigDAO, final SEBClientInstructionService sebInstructionService, final ExamAdminService examAdminService, - final DistributedIndicatorValueService distributedPingCache) { + final DistributedIndicatorValueService distributedPingCache, + final ClientIndicatorFactory clientIndicatorFactory) { this.examSessionService = examSessionService; this.examSessionCacheService = examSessionService.getExamSessionCacheService(); @@ -96,6 +97,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic this.examAdminService = examAdminService; this.distributedPingCache = distributedPingCache; this.isDistributedSetup = sebInstructionService.getWebserviceInfo().isDistributed(); + this.clientIndicatorFactory = clientIndicatorFactory; } @Override @@ -165,6 +167,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic null)) .getOrThrow(); + // initialize distributed indicator value caches if possible and needed + if (clientConnection.examId != null && this.isDistributedSetup) { + this.clientIndicatorFactory.initializeDistributedCaches(clientConnection); + } + // load client connection data into cache final ClientConnectionDataInternal activeClientConnection = this.examSessionService .getConnectionDataInternal(connectionToken); @@ -262,6 +269,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic null)) .getOrThrow(); + // initialize distributed indicator value caches if possible and needed + if (examId != null && this.isDistributedSetup) { + this.clientIndicatorFactory.initializeDistributedCaches(clientConnection); + } + final ClientConnectionDataInternal activeClientConnection = reloadConnectionCache(connectionToken); @@ -402,6 +414,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic // check exam integrity for established connection checkExamIntegrity(establishedClientConnection.examId); + // initialize distributed indicator value caches if possible and needed + if (examId != null && this.isDistributedSetup) { + this.clientIndicatorFactory.initializeDistributedCaches(clientConnection); + } + // if proctoring is enabled for exam, mark for room update if (proctoringEnabled) { this.clientConnectionDAO.markForProctoringUpdate(updatedClientConnection.id); @@ -869,13 +886,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic connection.getIndicatorMapping(EventType.ERROR_LOG) .forEach(indicator -> indicator.notifyValueChange(clientEventRecord)); } - - if (this.isDistributedSetup) { - // mark for update and flush the cache - this.clientConnectionDAO.save(connection.clientConnection); - this.examSessionCacheService.evictClientConnection( - connection.clientConnection.connectionToken); - } } }; } 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 06656d71..8473cc52 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 @@ -69,37 +69,34 @@ public abstract class AbstractClientIndicator implements ClientIndicator { this.cachingEnabled = cachingEnabled; if (!this.cachingEnabled && this.active) { - try { - this.ditributedIndicatorValueRecordId = this.distributedPingCache.initIndicatorForConnection( - connectionId, - getType(), - initValue()); - } catch (final Exception e) { - tryRecoverIndicatorRecord(); - } + this.ditributedIndicatorValueRecordId = this.distributedPingCache + .getIndicatorForConnection(connectionId, getType()); +// if (this.ditributedIndicatorValueRecordId == null) { +// tryRecoverIndicatorRecord(); +// } +// try { +// this.ditributedIndicatorValueRecordId = this.distributedPingCache.initIndicatorForConnection( +// connectionId, +// getType(), +// initValue()); +// } catch (final Exception e) { +// tryRecoverIndicatorRecord(); +// } } this.currentValue = computeValueAt(Utils.getMillisecondsNow()); this.initialized = true; } - protected long initValue() { - return 0; - } - protected void tryRecoverIndicatorRecord() { + this.ditributedIndicatorValueRecordId = this.distributedPingCache.getIndicatorForConnection( + this.connectionId, + getType()); - if (log.isWarnEnabled()) { - log.warn("*** Missing indicator value record for connection: {}. Try to recover...", this.connectionId); - } - - try { - this.ditributedIndicatorValueRecordId = this.distributedPingCache.initIndicatorForConnection( + if (this.ditributedIndicatorValueRecordId == null) { + log.warn("Failed to recover from missing indicator value cache record: {} type: {}", this.connectionId, - getType(), - initValue()); - } catch (final Exception e) { - log.error("Failed to recover indicator value record for connection: {}", this.connectionId, e); + getType()); } } 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 ce89c669..516ca0cd 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 @@ -55,8 +55,8 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato @Override public double computeValueAt(final long timestamp) { - if (log.isDebugEnabled()) { - log.debug("computeValueAt: {}", timestamp); + if (log.isTraceEnabled()) { + log.trace("computeValueAt: {}", timestamp); } try { 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 af05e7e5..b2e1224a 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 @@ -73,8 +73,8 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator { @Override public double computeValueAt(final long timestamp) { - if (log.isDebugEnabled()) { - log.debug("computeValueAt: {}", timestamp); + if (log.isTraceEnabled()) { + log.trace("computeValueAt: {}", timestamp); } try { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedIndicatorValueService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedIndicatorValueService.java index a7379e01..48519627 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedIndicatorValueService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/indicator/DistributedIndicatorValueService.java @@ -129,100 +129,89 @@ public class DistributedIndicatorValueService implements DisposableBean { } } - /** This initializes a SEB client indicator on the persistent storage for a given SEB client - * connection identifier and of given IndicatorType. - * If there is already such an indicator for the specified SEB client connection identifier and type, - * this returns the id of the existing one. + /** This creates a distributed indicator value cache record for a given SEB connection and indicator + * if it not already exists and returns the PK for the specified distributed indicator value cache record * - * @param connectionId SEB client connection identifier - * @param type indicator type - * @param value the initial indicator value - * @return SEB client indicator value identifier (PK) */ + * @param connectionId the client connection identifier + * @param type the indicator type + * @param value the initialization value + * @return the PK of the created or existing distributed indicator value cache record or null when a unexpected + * error happened */ @Transactional - public Long initIndicatorForConnection( + public Long createIndicatorForConnection( final Long connectionId, final IndicatorType type, - final Long value) { + final long initValue) { + + if (!this.webserviceInfo.isDistributed()) { + log.warn("No distributed setup, skip createIndicatorForConnection"); + return null; + } try { - if (log.isDebugEnabled()) { - log.trace("*** Initialize indicator value record for SEB connection: {}", connectionId); + // first check if the record already exists + final Long recId = this.clientIndicatorValueMapper.indicatorRecordIdByConnectionId( + connectionId, + type); + if (recId != null) { + log.debug("Distributed indicator value cache already exists for: {}, {}", connectionId, type); + return recId; } - synchronized (this) { + // if not, create new one and return PK + final ClientIndicatorRecord clientEventRecord = new ClientIndicatorRecord( + null, connectionId, type.id, initValue); + this.clientIndicatorRecordMapper.insert(clientEventRecord); - Long recordId = null; + try { + // This also double-check by trying again. If we have more then one entry here + // this will throw an exception that causes a rollback + return this.clientIndicatorValueMapper + .indicatorRecordIdByConnectionId(connectionId, type); - try { - recordId = this.clientIndicatorValueMapper - .indicatorRecordIdByConnectionId(connectionId, type); - } catch (final Exception e) { - // There is already more then one indicator record entry!!! - // delete the second one and work on with the first one + } catch (final Exception e) { - log.warn("Duplicate indicator entry detected for connectionId: {}, type: {} --> try to recover", - connectionId, type); - - try { - final List records = this.clientIndicatorRecordMapper.selectByExample() - .where(ClientIndicatorRecordDynamicSqlSupport.clientConnectionId, - isEqualTo(connectionId)) - .and(ClientIndicatorRecordDynamicSqlSupport.type, isEqualTo(type.id)) - .build() - .execute(); - if (records.size() > 1) { - this.clientIndicatorRecordMapper.deleteByPrimaryKey(records.get(1).getId()); - } - - return records.get(0).getId(); - } catch (final Exception ee) { - log.error("Failed to recover from duplicate indicator entry: ", ee); - return null; - } - } - - if (recordId == null) { - if (!this.webserviceInfo.isMaster()) { - if (log.isDebugEnabled()) { - log.debug("Skip indicator record init because this is no master instance"); - } - return null; - } - - final ClientIndicatorRecord clientEventRecord = new ClientIndicatorRecord( - null, connectionId, type.id, value); - - this.clientIndicatorRecordMapper.insert(clientEventRecord); - - try { - // This also double-check by trying again. If we have more then one entry here - // this will throw an exception that causes a rollback - return this.clientIndicatorValueMapper - .indicatorRecordIdByConnectionId(connectionId, type); - - } catch (final Exception e) { - - log.warn( - "Detected multiple client indicator entries for connection: {} and type: {}. Force rollback to prevent", - connectionId, type); - - // force rollback - TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); - throw new RuntimeException("Detected multiple client indicator value entries"); - } - } - - return recordId; + log.warn( + "Detected multiple client indicator entries for connection: {} and type: {}. Force rollback to prevent", + connectionId, type); + // force rollback + TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); + throw new RuntimeException("Detected multiple client indicator value entries"); } } catch (final Exception e) { + log.error( + "Failed to initialize distributed indicator value cache in persistent store. connectionId: {} type: {}", + connectionId, type, e); - log.error("Failed to initialize indicator value for connection -> {}", connectionId, e); + return null; + } + } - // force rollback - TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); - throw new RuntimeException("Failed to initialize indicator value for connection -> " + connectionId, e); + /** Get the distributed indicator value cache record PK for a given SEB connection and indicator if available. + * If not existing for the specified connection and indicator this return null + * + * @param connectionId the client connection identifier + * @param type the indicator type + * @return the indicator value cache record PK or null of not defined */ + @Transactional(readOnly = true) + public Long getIndicatorForConnection(final Long connectionId, final IndicatorType type) { + try { + + return this.clientIndicatorValueMapper + .indicatorRecordIdByConnectionId(connectionId, type); + + } catch (final Exception e) { + + if (log.isDebugEnabled()) { + log.debug("Failed to get indicator PK for connection: {} type: {} cause: {}", + connectionId, + type, + e.getMessage()); + } + + return null; } } @@ -235,7 +224,7 @@ public class DistributedIndicatorValueService implements DisposableBean { try { if (log.isDebugEnabled()) { - log.debug("*** Delete indicator value record for SEB connection: {}", connectionId); + log.debug("Delete indicator value record for SEB connection: {}", connectionId); } final Collection records = this.clientIndicatorValueMapper @@ -287,10 +276,6 @@ public class DistributedIndicatorValueService implements DisposableBean { if (value == null) { try { - if (log.isDebugEnabled()) { - log.debug("*** Get and cache ping time: {}", indicatorPK); - } - value = this.clientIndicatorValueMapper.selectValueByPrimaryKey(indicatorPK); if (value != null) { this.indicatorValueCache.put(indicatorPK, value); 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 ba933dd9..ffdb07e6 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 @@ -20,7 +20,6 @@ import ch.ethz.seb.sebserver.gbl.Constants; 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.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; @Lazy @@ -40,11 +39,6 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator { this.cachingEnabled = true; } - @Override - protected long initValue() { - return Utils.getMillisecondsNow(); - } - @Override public void init( final Indicator indicatorDefinition, @@ -87,6 +81,7 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator { final long currentTimeMillis = DateTimeUtils.currentTimeMillis(); this.currentValue = computeValueAt(currentTimeMillis); + this.lastUpdate = this.distributedPingCache.lastUpdate(); return (currentTimeMillis < this.currentValue) ? DateTimeUtils.currentTimeMillis() - this.currentValue : currentTimeMillis - this.currentValue; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java index a14265d2..6a09d787 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java @@ -364,6 +364,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService examId, connectionToken, e); + return null; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index fbbe3bce..3be70848 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -555,6 +555,11 @@ public class ZoomProctoringService implements ExamProctoringService { credentials, roomName); + final int statusCodeValue = createUser.getStatusCodeValue(); + if (statusCodeValue >= 400) { + throw new RuntimeException("Failed to create new Zoom user for room: " + createUser.getBody()); + } + final UserResponse userResponse = this.jsonMapper.readValue( createUser.getBody(), UserResponse.class);