Merge branch 'rel-1.3.2'

Conflicts:
	pom.xml
This commit is contained in:
anhefti 2022-03-30 15:25:14 +02:00
commit 1aab22d267
18 changed files with 263 additions and 162 deletions

View file

@ -18,7 +18,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<sebserver-version>1.3.0</sebserver-version> <sebserver-version>1.3.2</sebserver-version>
<build-version>${sebserver-version}</build-version> <build-version>${sebserver-version}</build-version>
<revision>${sebserver-version}</revision> <revision>${sebserver-version}</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View file

@ -137,7 +137,8 @@ public class ClientConnectionDetails {
final ClientConnectionData connectionData = this.restCallBuilder final ClientConnectionData connectionData = this.restCallBuilder
.call() .call()
.get(error -> { .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); recoverFromDisposedRestTemplate(error);
return null; return null;
}); });

View file

@ -15,6 +15,7 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; 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 */ * @return Result refer to the relevant VDI pair connection if exists or to an error if not */
Result<ClientConnectionRecord> getVDIPairCompanion(Long examId, String clientName); Result<ClientConnectionRecord> 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<Exam> deleteClientIndicatorValues(Exam exam);
} }

View file

@ -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.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityDependency; import ch.ethz.seb.sebserver.gbl.model.EntityDependency;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -694,6 +695,33 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
}); });
} }
@Override
@Transactional
public Result<Exam> deleteClientIndicatorValues(final Exam exam) {
return Result.tryCatch(() -> {
final List<Long> 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<ClientConnectionRecord> recordById(final Long id) { private Result<ClientConnectionRecord> recordById(final Long id) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

@ -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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; 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.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.AssignmentData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.ans.AnsLmsData.UserData; 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<AssignmentData> getAssignments(final RestTemplate restTemplate) { private List<AssignmentData> 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, // 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): // 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"; final String url = "/api/v2/search/assignments";
return this.apiGetList(restTemplate, url, new ParameterizedTypeReference<List<AssignmentData>>() { return this.apiGetList(restTemplate, url, new ParameterizedTypeReference<List<AssignmentData>>() {
}); });
@ -346,7 +346,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
final String url = String.format("/api/v2/assignments/%s", id); final String url = String.format("/api/v2/assignments/%s", id);
final AssignmentData assignment = this.apiGet(restTemplate, url, AssignmentData.class); 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<String, String>()); return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
} }
@ -354,24 +354,24 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
final SEBRestriction restriction) { final SEBRestriction restriction) {
final String url = String.format("/api/v2/assignments/%s", id); final String url = String.format("/api/v2/assignments/%s", id);
final AssignmentData assignment = getAssignmentById(restTemplate, id); final AssignmentData assignment = getAssignmentById(restTemplate, id);
assignment.accessibility_settings.config_keys = new ArrayList<>(restriction.configKeys); assignment.integrations.safe_exam_browser_server.config_keys = new ArrayList<>(restriction.configKeys);
assignment.accessibility_settings.seb_server_enabled = true; assignment.integrations.safe_exam_browser_server.enabled = true;
@SuppressWarnings("unused") @SuppressWarnings("unused")
final AssignmentData r = final AssignmentData r =
this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class); 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<String, String>()); return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
} }
private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) {
final String url = String.format("/api/v2/assignments/%s", id); final String url = String.format("/api/v2/assignments/%s", id);
final AssignmentData assignment = getAssignmentById(restTemplate, id); final AssignmentData assignment = getAssignmentById(restTemplate, id);
assignment.accessibility_settings.config_keys = null; assignment.integrations.safe_exam_browser_server.config_keys = null;
assignment.accessibility_settings.seb_server_enabled = false; assignment.integrations.safe_exam_browser_server.enabled = false;
@SuppressWarnings("unused") @SuppressWarnings("unused")
final AssignmentData r = final AssignmentData r =
this.apiPatch(restTemplate, url, assignment, AssignmentData.class, AssignmentData.class); 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<String, String>()); return new SEBRestriction(Long.valueOf(id), ts.config_keys, null, new HashMap<String, String>());
} }
@ -406,7 +406,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
private List<PageLink> parseLinks(final String header) { private List<PageLink> parseLinks(final String header) {
// Extracts the individual links from a header that looks like this: // Extracts the individual links from a header that looks like this:
// <https://staging.ans.app/api/v2/search/assignments?query=seb_server_enabled%3Atrue&page=1&items=20>; rel="first",<https://staging.ans.app/api/v2/search/assignments?query=seb_server_enabled%3Atrue&page=1&items=20>; rel="last" // <https://staging.ans.app/api/v2/search/assignments?page=1&items=20>; rel="first",<https://staging.ans.app/api/v2/search/assignments?page=1&items=20>; rel="last"
final Stream<String> links = Arrays.stream(header.split(",")); final Stream<String> links = Arrays.stream(header.split(","));
return links return links
.map(s -> { .map(s -> {

View file

@ -15,12 +15,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
public final class AnsLmsData { public final class AnsLmsData {
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static final class AccessibilitySettingsData { static final class SEBServerData {
/* Ans API example: see nested in AssignmentData */ /* Ans API example: see nested in AssignmentData */
public boolean seb_server_enabled; public boolean enabled;
public List<String> config_keys; public List<String> 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) @JsonIgnoreProperties(ignoreUnknown = true)
static final class AssignmentData { static final class AssignmentData {
/* /*
@ -37,19 +43,10 @@ public final class AnsLmsData {
* "updated_at": "2021-08-17T03:41:56.747+02:00", * "updated_at": "2021-08-17T03:41:56.747+02:00",
* "trashed": false, * "trashed": false,
* "start_url": "https://staging.ans.app/digital_test/assignments/78805/results/new", * "start_url": "https://staging.ans.app/digital_test/assignments/78805/results/new",
* "accessibility_settings": { * "integrations": {
* "attempts": 1, * "safe_exam_browser_server": {
* "restricted_access_to_other_pages": false, * "enabled": false,
* "notes": false, * "config_keys": [ "123" ] } }
* "spellchecker": false,
* "feedback": false,
* "forced_test_navigation": false,
* "cannot_reopen_question_groups": false,
* "seb_server_enabled": true,
* "config_keys": [
* "9dd14ac828617116a1230c71b9a1aa9e06f43b32d9fa7db67f4fa113a6896e83e"
* ]
* },
* "grades_settings": { * "grades_settings": {
* "grade_calculation": "formula", * "grade_calculation": "formula",
* "grade_formula": "1 + 9 * points / total", * "grade_formula": "1 + 9 * points / total",
@ -70,7 +67,7 @@ public final class AnsLmsData {
public String start_at; public String start_at;
public String end_at; public String end_at;
public String start_url; public String start_url;
public AccessibilitySettingsData accessibility_settings; public IntegrationsData integrations;
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -182,6 +182,13 @@ public interface ExamSessionService {
* @return Result refer to the collection of connection tokens or to an error when happened. */ * @return Result refer to the collection of connection tokens or to an error when happened. */
Result<Collection<String>> getActiveConnectionTokens(Long examId); Result<Collection<String>> 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<Exam> notifyExamFinished(final Exam exam);
/** Use this to check if the current cached running exam is up to date /** Use this to check if the current cached running exam is up to date
* and if not to flush the cache. * and if not to flush the cache.
* *

View file

@ -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.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; 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.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator; 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; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.PingIntervalClientIndicator;
@Lazy @Lazy
@ -39,23 +41,68 @@ public class ClientIndicatorFactory {
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final IndicatorDAO indicatorDAO; private final IndicatorDAO indicatorDAO;
private final DistributedIndicatorValueService distributedPingCache;
private final boolean distributedSetup;
private final boolean enableCaching; private final boolean enableCaching;
@Autowired @Autowired
public ClientIndicatorFactory( public ClientIndicatorFactory(
final ApplicationContext applicationContext, final ApplicationContext applicationContext,
final IndicatorDAO indicatorDAO, final IndicatorDAO indicatorDAO,
final DistributedIndicatorValueService distributedPingCache,
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup, @Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup,
@Value("${sebserver.webservice.api.exam.enable-indicator-cache:true}") final boolean enableCaching) { @Value("${sebserver.webservice.api.exam.enable-indicator-cache:true}") final boolean enableCaching) {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.indicatorDAO = indicatorDAO; this.indicatorDAO = indicatorDAO;
this.distributedPingCache = distributedPingCache;
this.distributedSetup = distributedSetup;
this.enableCaching = distributedSetup ? false : enableCaching; this.enableCaching = distributedSetup ? false : enableCaching;
} }
public List<ClientIndicator> createFor(final ClientConnection clientConnection) { public void initializeDistributedCaches(final ClientConnection clientConnection) {
final List<ClientIndicator> result = new ArrayList<>(); try {
if (!this.distributedSetup || clientConnection.examId == null) {
return;
}
final Collection<Indicator> 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<ClientIndicator> createFor(final ClientConnection clientConnection) {
final List<ClientIndicator> result = new ArrayList<>();
if (clientConnection.examId == null) { if (clientConnection.examId == null) {
return result; return result;
} }
@ -67,7 +114,6 @@ public class ClientIndicatorFactory {
.getOrThrow(); .getOrThrow();
boolean pingIndicatorAvailable = false; boolean pingIndicatorAvailable = false;
for (final Indicator indicatorDef : examIndicators) { for (final Indicator indicatorDef : examIndicators) {
try { try {
@ -86,7 +132,8 @@ public class ClientIndicatorFactory {
result.add(indicator); result.add(indicator);
} catch (final Exception e) { } 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); e);
} }
} }

View file

@ -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.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; 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.ExamProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
@Service @Service
@ -42,6 +43,7 @@ public class ExamSessionControlTask implements DisposableBean {
private final ExamUpdateHandler examUpdateHandler; private final ExamUpdateHandler examUpdateHandler;
private final ExamProctoringRoomService examProcotringRoomService; private final ExamProctoringRoomService examProcotringRoomService;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final ExamSessionService examSessionService;
private final Long examTimePrefix; private final Long examTimePrefix;
private final Long examTimeSuffix; private final Long examTimeSuffix;
@ -54,6 +56,7 @@ public class ExamSessionControlTask implements DisposableBean {
final ExamUpdateHandler examUpdateHandler, final ExamUpdateHandler examUpdateHandler,
final ExamProctoringRoomService examProcotringRoomService, final ExamProctoringRoomService examProcotringRoomService,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final ExamSessionService examSessionService,
@Value("${sebserver.webservice.api.exam.time-prefix:3600000}") final Long examTimePrefix, @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.time-suffix:3600000}") final Long examTimeSuffix,
@Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron, @Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron,
@ -63,6 +66,7 @@ public class ExamSessionControlTask implements DisposableBean {
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
this.examUpdateHandler = examUpdateHandler; this.examUpdateHandler = examUpdateHandler;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.examSessionService = examSessionService;
this.examTimePrefix = examTimePrefix; this.examTimePrefix = examTimePrefix;
this.examTimeSuffix = examTimeSuffix; this.examTimeSuffix = examTimeSuffix;
this.examTaskCron = examTaskCron; this.examTaskCron = examTaskCron;
@ -185,6 +189,7 @@ public class ExamSessionControlTask implements DisposableBean {
.filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now)) .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.examUpdateHandler.setFinished(exam, updateId)))
.flatMap(exam -> Result.skipOnError(this.examProcotringRoomService.disposeRoomsForExam(exam))) .flatMap(exam -> Result.skipOnError(this.examProcotringRoomService.disposeRoomsForExam(exam)))
.flatMap(exam -> Result.skipOnError(this.examSessionService.notifyExamFinished(exam)))
.collect(Collectors.toMap(Exam::getId, Exam::getName)); .collect(Collectors.toMap(Exam::getId, Exam::getName));
if (!updated.isEmpty()) { if (!updated.isEmpty()) {

View file

@ -281,8 +281,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
throw new IllegalStateException("Missing exam identifier or requested exam is not running"); throw new IllegalStateException("Missing exam identifier or requested exam is not running");
} }
if (log.isDebugEnabled()) { if (log.isTraceEnabled()) {
log.debug("Trying to get exam from InMemorySEBConfig"); log.trace("Trying to get exam from InMemorySEBConfig");
} }
final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService
@ -295,14 +295,14 @@ public class ExamSessionServiceImpl implements ExamSessionService {
try { try {
if (log.isDebugEnabled()) { if (log.isTraceEnabled()) {
log.debug("SEB exam configuration download request, start writing SEB exam configuration"); log.trace("SEB exam configuration download request, start writing SEB exam configuration");
} }
out.write(sebConfigForExam.getData()); out.write(sebConfigForExam.getData());
if (log.isDebugEnabled()) { if (log.isTraceEnabled()) {
log.debug("SEB exam configuration download request, finished writing SEB exam configuration"); log.trace("SEB exam configuration download request, finished writing SEB exam configuration");
} }
} catch (final IOException e) { } catch (final IOException e) {
@ -393,6 +393,22 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.getActiveConnctionTokens(examId); .getActiveConnctionTokens(examId);
} }
@Override
public Result<Exam> 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 @Override
public Result<Exam> updateExamCache(final Long examId) { public Result<Exam> updateExamCache(final Long examId) {

View file

@ -74,8 +74,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final SEBClientConfigDAO sebClientConfigDAO; private final SEBClientConfigDAO sebClientConfigDAO;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final ExamAdminService examAdminService; 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 DistributedIndicatorValueService distributedPingCache;
private final ClientIndicatorFactory clientIndicatorFactory;
private final boolean isDistributedSetup; private final boolean isDistributedSetup;
protected SEBClientConnectionServiceImpl( protected SEBClientConnectionServiceImpl(
@ -84,7 +84,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
final SEBClientConfigDAO sebClientConfigDAO, final SEBClientConfigDAO sebClientConfigDAO,
final SEBClientInstructionService sebInstructionService, final SEBClientInstructionService sebInstructionService,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final DistributedIndicatorValueService distributedPingCache) { final DistributedIndicatorValueService distributedPingCache,
final ClientIndicatorFactory clientIndicatorFactory) {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionService.getExamSessionCacheService(); this.examSessionCacheService = examSessionService.getExamSessionCacheService();
@ -96,6 +97,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.distributedPingCache = distributedPingCache; this.distributedPingCache = distributedPingCache;
this.isDistributedSetup = sebInstructionService.getWebserviceInfo().isDistributed(); this.isDistributedSetup = sebInstructionService.getWebserviceInfo().isDistributed();
this.clientIndicatorFactory = clientIndicatorFactory;
} }
@Override @Override
@ -165,6 +167,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
null)) null))
.getOrThrow(); .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 // load client connection data into cache
final ClientConnectionDataInternal activeClientConnection = this.examSessionService final ClientConnectionDataInternal activeClientConnection = this.examSessionService
.getConnectionDataInternal(connectionToken); .getConnectionDataInternal(connectionToken);
@ -262,6 +269,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
null)) null))
.getOrThrow(); .getOrThrow();
// initialize distributed indicator value caches if possible and needed
if (examId != null && this.isDistributedSetup) {
this.clientIndicatorFactory.initializeDistributedCaches(clientConnection);
}
final ClientConnectionDataInternal activeClientConnection = final ClientConnectionDataInternal activeClientConnection =
reloadConnectionCache(connectionToken); reloadConnectionCache(connectionToken);
@ -402,6 +414,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
// check exam integrity for established connection // check exam integrity for established connection
checkExamIntegrity(establishedClientConnection.examId); 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 proctoring is enabled for exam, mark for room update
if (proctoringEnabled) { if (proctoringEnabled) {
this.clientConnectionDAO.markForProctoringUpdate(updatedClientConnection.id); this.clientConnectionDAO.markForProctoringUpdate(updatedClientConnection.id);
@ -869,13 +886,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
connection.getIndicatorMapping(EventType.ERROR_LOG) connection.getIndicatorMapping(EventType.ERROR_LOG)
.forEach(indicator -> indicator.notifyValueChange(clientEventRecord)); .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);
}
} }
}; };
} }

View file

@ -69,37 +69,34 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
this.cachingEnabled = cachingEnabled; this.cachingEnabled = cachingEnabled;
if (!this.cachingEnabled && this.active) { if (!this.cachingEnabled && this.active) {
try { this.ditributedIndicatorValueRecordId = this.distributedPingCache
this.ditributedIndicatorValueRecordId = this.distributedPingCache.initIndicatorForConnection( .getIndicatorForConnection(connectionId, getType());
connectionId, // if (this.ditributedIndicatorValueRecordId == null) {
getType(), // tryRecoverIndicatorRecord();
initValue()); // }
} catch (final Exception e) { // try {
tryRecoverIndicatorRecord(); // this.ditributedIndicatorValueRecordId = this.distributedPingCache.initIndicatorForConnection(
} // connectionId,
// getType(),
// initValue());
// } catch (final Exception e) {
// tryRecoverIndicatorRecord();
// }
} }
this.currentValue = computeValueAt(Utils.getMillisecondsNow()); this.currentValue = computeValueAt(Utils.getMillisecondsNow());
this.initialized = true; this.initialized = true;
} }
protected long initValue() {
return 0;
}
protected void tryRecoverIndicatorRecord() { protected void tryRecoverIndicatorRecord() {
this.ditributedIndicatorValueRecordId = this.distributedPingCache.getIndicatorForConnection(
this.connectionId,
getType());
if (log.isWarnEnabled()) { if (this.ditributedIndicatorValueRecordId == null) {
log.warn("*** Missing indicator value record for connection: {}. Try to recover...", this.connectionId); log.warn("Failed to recover from missing indicator value cache record: {} type: {}",
}
try {
this.ditributedIndicatorValueRecordId = this.distributedPingCache.initIndicatorForConnection(
this.connectionId, this.connectionId,
getType(), getType());
initValue());
} catch (final Exception e) {
log.error("Failed to recover indicator value record for connection: {}", this.connectionId, e);
} }
} }

View file

@ -55,8 +55,8 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
@Override @Override
public double computeValueAt(final long timestamp) { public double computeValueAt(final long timestamp) {
if (log.isDebugEnabled()) { if (log.isTraceEnabled()) {
log.debug("computeValueAt: {}", timestamp); log.trace("computeValueAt: {}", timestamp);
} }
try { try {

View file

@ -73,8 +73,8 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
@Override @Override
public double computeValueAt(final long timestamp) { public double computeValueAt(final long timestamp) {
if (log.isDebugEnabled()) { if (log.isTraceEnabled()) {
log.debug("computeValueAt: {}", timestamp); log.trace("computeValueAt: {}", timestamp);
} }
try { try {

View file

@ -129,100 +129,89 @@ public class DistributedIndicatorValueService implements DisposableBean {
} }
} }
/** This initializes a SEB client indicator on the persistent storage for a given SEB client /** This creates a distributed indicator value cache record for a given SEB connection and indicator
* connection identifier and of given IndicatorType. * if it not already exists and returns the PK for the specified distributed indicator value cache record
* If there is already such an indicator for the specified SEB client connection identifier and type,
* this returns the id of the existing one.
* *
* @param connectionId SEB client connection identifier * @param connectionId the client connection identifier
* @param type indicator type * @param type the indicator type
* @param value the initial indicator value * @param value the initialization value
* @return SEB client indicator value identifier (PK) */ * @return the PK of the created or existing distributed indicator value cache record or null when a unexpected
* error happened */
@Transactional @Transactional
public Long initIndicatorForConnection( public Long createIndicatorForConnection(
final Long connectionId, final Long connectionId,
final IndicatorType type, final IndicatorType type,
final Long value) { final long initValue) {
if (!this.webserviceInfo.isDistributed()) {
log.warn("No distributed setup, skip createIndicatorForConnection");
return null;
}
try { try {
if (log.isDebugEnabled()) { // first check if the record already exists
log.trace("*** Initialize indicator value record for SEB connection: {}", connectionId); 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 { } catch (final Exception e) {
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
log.warn("Duplicate indicator entry detected for connectionId: {}, type: {} --> try to recover", log.warn(
connectionId, type); "Detected multiple client indicator entries for connection: {} and type: {}. Force rollback to prevent",
connectionId, type);
try {
final List<ClientIndicatorRecord> 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;
// force rollback
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException("Detected multiple client indicator value entries");
} }
} catch (final Exception e) { } 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 /** Get the distributed indicator value cache record PK for a given SEB connection and indicator if available.
TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); * If not existing for the specified connection and indicator this return null
throw new RuntimeException("Failed to initialize indicator value for connection -> " + connectionId, e); *
* @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 { try {
if (log.isDebugEnabled()) { 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<ClientIndicatorValueRecord> records = this.clientIndicatorValueMapper final Collection<ClientIndicatorValueRecord> records = this.clientIndicatorValueMapper
@ -287,10 +276,6 @@ public class DistributedIndicatorValueService implements DisposableBean {
if (value == null) { if (value == null) {
try { try {
if (log.isDebugEnabled()) {
log.debug("*** Get and cache ping time: {}", indicatorPK);
}
value = this.clientIndicatorValueMapper.selectValueByPrimaryKey(indicatorPK); value = this.clientIndicatorValueMapper.selectValueByPrimaryKey(indicatorPK);
if (value != null) { if (value != null) {
this.indicatorValueCache.put(indicatorPK, value); this.indicatorValueCache.put(indicatorPK, value);

View file

@ -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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; 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;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
@Lazy @Lazy
@ -40,11 +39,6 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
this.cachingEnabled = true; this.cachingEnabled = true;
} }
@Override
protected long initValue() {
return Utils.getMillisecondsNow();
}
@Override @Override
public void init( public void init(
final Indicator indicatorDefinition, final Indicator indicatorDefinition,
@ -87,6 +81,7 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
final long currentTimeMillis = DateTimeUtils.currentTimeMillis(); final long currentTimeMillis = DateTimeUtils.currentTimeMillis();
this.currentValue = computeValueAt(currentTimeMillis); this.currentValue = computeValueAt(currentTimeMillis);
this.lastUpdate = this.distributedPingCache.lastUpdate();
return (currentTimeMillis < this.currentValue) return (currentTimeMillis < this.currentValue)
? DateTimeUtils.currentTimeMillis() - this.currentValue ? DateTimeUtils.currentTimeMillis() - this.currentValue
: currentTimeMillis - this.currentValue; : currentTimeMillis - this.currentValue;

View file

@ -364,6 +364,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
examId, examId,
connectionToken, connectionToken,
e); e);
return null; return null;
} }
} }

View file

@ -555,6 +555,11 @@ public class ZoomProctoringService implements ExamProctoringService {
credentials, credentials,
roomName); 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( final UserResponse userResponse = this.jsonMapper.readValue(
createUser.getBody(), createUser.getBody(),
UserResponse.class); UserResponse.class);