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);