Improved Exam Config streaming on SEB Client connection handshake

This commit is contained in:
anhefti 2021-04-14 09:15:21 +02:00
parent 3d5c05a125
commit 3381d69f8b
6 changed files with 86 additions and 46 deletions

View file

@ -137,6 +137,12 @@ public interface ClientConnectionDAO extends
* @return Result refer to the active connection flag or to an error when happened */
Result<Boolean> isActiveConnection(Long examId, String connectionToken);
/** Use this to check whether a single ClientConnection is up to date or needs a refresh.
*
* @param clientConnection the actual ClientConnection (from the internal cache)
* @return Result refer to true if the given ClientConnection is up to date */
Result<Boolean> isUpToDate(ClientConnection clientConnection);
/** Filters a set of client connection tokens to a set containing only
* connection tokens of active client connections.
*

View file

@ -485,6 +485,23 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.isPresent());
}
@Override
public Result<Boolean> isUpToDate(final ClientConnection clientConnection) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isEqualTo(clientConnection.connectionToken))
.and(
ClientConnectionRecordDynamicSqlSupport.updateTime,
SqlBuilder.isEqualTo(clientConnection.updateTime))
.build()
.execute()
.stream()
.findFirst()
.isPresent());
}
@Override
public Result<Set<String>> filterActive(final Long examId, final Set<String> connectionToken) {
if (connectionToken == null || connectionToken.isEmpty()) {

View file

@ -128,9 +128,13 @@ public interface ExamSessionService {
/** Streams the default SEB Exam Configuration to a ClientConnection with given connectionToken.
*
* @param institutionId the Institution identifier
* @param connectionToken The connection token that identifiers the ClientConnection
* @param out The OutputStream to stream the data to */
void streamDefaultExamConfig(String connectionToken, OutputStream out);
void streamDefaultExamConfig(
Long institutionId,
String connectionToken,
OutputStream out);
/** Get current ClientConnectionData for a specified active SEB client connection.
*

View file

@ -174,31 +174,31 @@ public class ExamSessionCacheService {
@Cacheable(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#exam.id",
key = "#examId",
sync = true)
public InMemorySEBConfig getDefaultSEBConfigForExam(final Exam exam) {
public InMemorySEBConfig getDefaultSEBConfigForExam(final Long examId, final Long institutionId) {
try {
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
final Long configId = this.sebExamConfigService.exportForExam(
byteOut,
exam.institutionId,
exam.id);
institutionId,
examId);
return new InMemorySEBConfig(configId, exam.id, byteOut.toByteArray());
return new InMemorySEBConfig(configId, examId, byteOut.toByteArray());
} catch (final Exception e) {
log.error("Unexpected error while getting default exam configuration for running exam; {}", exam, e);
log.error("Unexpected error while getting default exam configuration for running exam; {}", examId, e);
throw e;
}
}
@CacheEvict(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#exam.id")
public void evictDefaultSEBConfig(final Exam exam) {
key = "#examId")
public void evictDefaultSEBConfig(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Eviction of default SEB Configuration from cache for exam: {}", exam.id);
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId);
}
}

View file

@ -282,6 +282,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override
public void streamDefaultExamConfig(
final Long institutionId,
final String connectionToken,
final OutputStream out) {
@ -289,16 +290,17 @@ public class ExamSessionServiceImpl implements ExamSessionService {
log.debug("SEB exam configuration download request, connectionToken: {}", connectionToken);
}
final ClientConnection connection = this.clientConnectionDAO
.byConnectionToken(connectionToken)
final ClientConnectionData clientConnectionData = this.getConnectionData(connectionToken)
.getOrThrow();
if (connection == null) {
if (clientConnectionData == null || clientConnectionData.clientConnection == null) {
log.warn("SEB exam configuration download request, no active ClientConnection found for token: {}",
connectionToken);
throw new AccessDeniedException("Illegal connection token. No active ClientConnection found for token");
}
final ClientConnection connection = clientConnectionData.clientConnection;
// exam integrity check
if (connection.examId == null || !isExamRunning(connection.examId)) {
log.error("Missing exam identifier or requested exam is not running for connection: {}", connection);
@ -309,11 +311,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
log.debug("Trying to get exam from InMemorySEBConfig");
}
final Exam exam = this.getRunningExam(connection.examId)
.getOrThrow();
final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService
.getDefaultSEBConfigForExam(exam);
.getDefaultSEBConfigForExam(connection.examId, institutionId);
if (sebConfigForExam == null) {
log.error("Failed to get and cache InMemorySEBConfig for connection: {}", connection);
@ -341,13 +340,26 @@ public class ExamSessionServiceImpl implements ExamSessionService {
public Result<ClientConnectionData> getConnectionData(final String connectionToken) {
return Result.tryCatch(() -> {
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
.getClientConnection(connectionToken);
if (activeClientConnection == null) {
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
}
if (this.distributedSetup) {
final Boolean upToDate = this.clientConnectionDAO
.isUpToDate(activeClientConnection.clientConnection)
.getOr(false);
if (!upToDate) {
this.examSessionCacheService.evictClientConnection(connectionToken);
return this.examSessionCacheService.getClientConnection(connectionToken);
}
}
return activeClientConnection;
});
}
@ -359,7 +371,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
if (this.distributedSetup) {
// if we run in distributed mode, we have to get the connection tokens of the exam
// always form the persistent storage and update the client connection cache
// always from the persistent storage and update the client connection cache
// before by remove out-dated client connection. This is done within the update_time
// of the client connection record that is set on every update in the persistent
// storage. So if the update_time of the cached client connection doesen't match the
@ -409,7 +421,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
public Result<Exam> flushCache(final Exam exam) {
return Result.tryCatch(() -> {
this.examSessionCacheService.evict(exam);
this.examSessionCacheService.evictDefaultSEBConfig(exam);
this.examSessionCacheService.evictDefaultSEBConfig(exam.id);
this.clientConnectionDAO
.getConnectionTokens(exam.id)
.getOrElse(Collections::emptyList)

View file

@ -44,7 +44,6 @@ import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -340,12 +339,13 @@ public class ExamAPI_V1_Controller {
final Principal principal,
final HttpServletResponse response) {
final Long institutionId = getInstitutionId(principal);
try {
// if an examId is provided with the request, update the connection first
if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) {
final String examId = formParams.getFirst(API.EXAM_API_PARAM_EXAM_ID);
final Long institutionId = getInstitutionId(principal);
final ClientConnection connection = this.sebClientConnectionService.updateClientConnection(
connectionToken,
institutionId,
@ -361,36 +361,37 @@ public class ExamAPI_V1_Controller {
final ServletOutputStream outputStream = response.getOutputStream();
try {
final ClientConnectionData connection = this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
// exam integrity check
if (connection.clientConnection.examId == null ||
!this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
log.error("Missing exam identifier or requested exam is not running for connection: {}",
connection);
throw new IllegalStateException("Missing exam identifier or requested exam is not running");
}
} catch (final Exception e) {
log.error("Unexpected error: ", e);
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
response.setStatus(HttpStatus.BAD_REQUEST.value());
outputStream.flush();
outputStream.close();
return;
}
// try {
//
// final ClientConnectionData connection = this.examSessionService
// .getConnectionData(connectionToken)
// .getOrThrow();
//
// // exam integrity check
// if (connection.clientConnection.examId == null ||
// !this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
//
// log.error("Missing exam identifier or requested exam is not running for connection: {}",
// connection);
// throw new IllegalStateException("Missing exam identifier or requested exam is not running");
// }
// } catch (final Exception e) {
//
// log.error("Unexpected error: ", e);
//
// final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
// outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
// response.setStatus(HttpStatus.BAD_REQUEST.value());
// outputStream.flush();
// outputStream.close();
// return;
// }
try {
this.examSessionService
.streamDefaultExamConfig(
institutionId,
connectionToken,
outputStream);