Improved Exam Config streaming on SEB Client connection handshake
This commit is contained in:
parent
3d5c05a125
commit
3381d69f8b
6 changed files with 86 additions and 46 deletions
|
@ -137,6 +137,12 @@ public interface ClientConnectionDAO extends
|
||||||
* @return Result refer to the active connection flag or to an error when happened */
|
* @return Result refer to the active connection flag or to an error when happened */
|
||||||
Result<Boolean> isActiveConnection(Long examId, String connectionToken);
|
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
|
/** Filters a set of client connection tokens to a set containing only
|
||||||
* connection tokens of active client connections.
|
* connection tokens of active client connections.
|
||||||
*
|
*
|
||||||
|
|
|
@ -485,6 +485,23 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
||||||
.isPresent());
|
.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
|
@Override
|
||||||
public Result<Set<String>> filterActive(final Long examId, final Set<String> connectionToken) {
|
public Result<Set<String>> filterActive(final Long examId, final Set<String> connectionToken) {
|
||||||
if (connectionToken == null || connectionToken.isEmpty()) {
|
if (connectionToken == null || connectionToken.isEmpty()) {
|
||||||
|
|
|
@ -128,9 +128,13 @@ public interface ExamSessionService {
|
||||||
|
|
||||||
/** Streams the default SEB Exam Configuration to a ClientConnection with given connectionToken.
|
/** 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 connectionToken The connection token that identifiers the ClientConnection
|
||||||
* @param out The OutputStream to stream the data to */
|
* @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.
|
/** Get current ClientConnectionData for a specified active SEB client connection.
|
||||||
*
|
*
|
||||||
|
|
|
@ -174,31 +174,31 @@ public class ExamSessionCacheService {
|
||||||
|
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
||||||
key = "#exam.id",
|
key = "#examId",
|
||||||
sync = true)
|
sync = true)
|
||||||
public InMemorySEBConfig getDefaultSEBConfigForExam(final Exam exam) {
|
public InMemorySEBConfig getDefaultSEBConfigForExam(final Long examId, final Long institutionId) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
||||||
final Long configId = this.sebExamConfigService.exportForExam(
|
final Long configId = this.sebExamConfigService.exportForExam(
|
||||||
byteOut,
|
byteOut,
|
||||||
exam.institutionId,
|
institutionId,
|
||||||
exam.id);
|
examId);
|
||||||
|
|
||||||
return new InMemorySEBConfig(configId, exam.id, byteOut.toByteArray());
|
return new InMemorySEBConfig(configId, examId, byteOut.toByteArray());
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} 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;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CacheEvict(
|
@CacheEvict(
|
||||||
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
||||||
key = "#exam.id")
|
key = "#examId")
|
||||||
public void evictDefaultSEBConfig(final Exam exam) {
|
public void evictDefaultSEBConfig(final Long examId) {
|
||||||
if (log.isDebugEnabled()) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -282,6 +282,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void streamDefaultExamConfig(
|
public void streamDefaultExamConfig(
|
||||||
|
final Long institutionId,
|
||||||
final String connectionToken,
|
final String connectionToken,
|
||||||
final OutputStream out) {
|
final OutputStream out) {
|
||||||
|
|
||||||
|
@ -289,16 +290,17 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
log.debug("SEB exam configuration download request, connectionToken: {}", connectionToken);
|
log.debug("SEB exam configuration download request, connectionToken: {}", connectionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ClientConnection connection = this.clientConnectionDAO
|
final ClientConnectionData clientConnectionData = this.getConnectionData(connectionToken)
|
||||||
.byConnectionToken(connectionToken)
|
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
if (connection == null) {
|
if (clientConnectionData == null || clientConnectionData.clientConnection == null) {
|
||||||
log.warn("SEB exam configuration download request, no active ClientConnection found for token: {}",
|
log.warn("SEB exam configuration download request, no active ClientConnection found for token: {}",
|
||||||
connectionToken);
|
connectionToken);
|
||||||
throw new AccessDeniedException("Illegal connection token. No active ClientConnection found for token");
|
throw new AccessDeniedException("Illegal connection token. No active ClientConnection found for token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ClientConnection connection = clientConnectionData.clientConnection;
|
||||||
|
|
||||||
// exam integrity check
|
// exam integrity check
|
||||||
if (connection.examId == null || !isExamRunning(connection.examId)) {
|
if (connection.examId == null || !isExamRunning(connection.examId)) {
|
||||||
log.error("Missing exam identifier or requested exam is not running for connection: {}", connection);
|
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");
|
log.debug("Trying to get exam from InMemorySEBConfig");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Exam exam = this.getRunningExam(connection.examId)
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService
|
final InMemorySEBConfig sebConfigForExam = this.examSessionCacheService
|
||||||
.getDefaultSEBConfigForExam(exam);
|
.getDefaultSEBConfigForExam(connection.examId, institutionId);
|
||||||
|
|
||||||
if (sebConfigForExam == null) {
|
if (sebConfigForExam == null) {
|
||||||
log.error("Failed to get and cache InMemorySEBConfig for connection: {}", connection);
|
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) {
|
public Result<ClientConnectionData> getConnectionData(final String connectionToken) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||||
.getClientConnection(connectionToken);
|
.getClientConnection(connectionToken);
|
||||||
if (activeClientConnection == null) {
|
if (activeClientConnection == null) {
|
||||||
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
|
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;
|
return activeClientConnection;
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,7 +371,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
if (this.distributedSetup) {
|
if (this.distributedSetup) {
|
||||||
|
|
||||||
// if we run in distributed mode, we have to get the connection tokens of the exam
|
// 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
|
// 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
|
// 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
|
// 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) {
|
public Result<Exam> flushCache(final Exam exam) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
this.examSessionCacheService.evict(exam);
|
this.examSessionCacheService.evict(exam);
|
||||||
this.examSessionCacheService.evictDefaultSEBConfig(exam);
|
this.examSessionCacheService.evictDefaultSEBConfig(exam.id);
|
||||||
this.clientConnectionDAO
|
this.clientConnectionDAO
|
||||||
.getConnectionTokens(exam.id)
|
.getConnectionTokens(exam.id)
|
||||||
.getOrElse(Collections::emptyList)
|
.getOrElse(Collections::emptyList)
|
||||||
|
|
|
@ -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.async.AsyncServiceSpringConfig;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
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.ClientConnectionData;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo;
|
import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
@ -340,12 +339,13 @@ public class ExamAPI_V1_Controller {
|
||||||
final Principal principal,
|
final Principal principal,
|
||||||
final HttpServletResponse response) {
|
final HttpServletResponse response) {
|
||||||
|
|
||||||
|
final Long institutionId = getInstitutionId(principal);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// if an examId is provided with the request, update the connection first
|
// if an examId is provided with the request, update the connection first
|
||||||
if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) {
|
if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) {
|
||||||
final String examId = formParams.getFirst(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(
|
final ClientConnection connection = this.sebClientConnectionService.updateClientConnection(
|
||||||
connectionToken,
|
connectionToken,
|
||||||
institutionId,
|
institutionId,
|
||||||
|
@ -361,36 +361,37 @@ public class ExamAPI_V1_Controller {
|
||||||
|
|
||||||
final ServletOutputStream outputStream = response.getOutputStream();
|
final ServletOutputStream outputStream = response.getOutputStream();
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
|
//
|
||||||
final ClientConnectionData connection = this.examSessionService
|
// final ClientConnectionData connection = this.examSessionService
|
||||||
.getConnectionData(connectionToken)
|
// .getConnectionData(connectionToken)
|
||||||
.getOrThrow();
|
// .getOrThrow();
|
||||||
|
//
|
||||||
// exam integrity check
|
// // exam integrity check
|
||||||
if (connection.clientConnection.examId == null ||
|
// if (connection.clientConnection.examId == null ||
|
||||||
!this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
|
// !this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
|
||||||
|
//
|
||||||
log.error("Missing exam identifier or requested exam is not running for connection: {}",
|
// log.error("Missing exam identifier or requested exam is not running for connection: {}",
|
||||||
connection);
|
// connection);
|
||||||
throw new IllegalStateException("Missing exam identifier or requested exam is not running");
|
// throw new IllegalStateException("Missing exam identifier or requested exam is not running");
|
||||||
}
|
// }
|
||||||
} catch (final Exception e) {
|
// } catch (final Exception e) {
|
||||||
|
//
|
||||||
log.error("Unexpected error: ", e);
|
// log.error("Unexpected error: ", e);
|
||||||
|
//
|
||||||
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
|
// final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
|
||||||
outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
|
// outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
|
||||||
response.setStatus(HttpStatus.BAD_REQUEST.value());
|
// response.setStatus(HttpStatus.BAD_REQUEST.value());
|
||||||
outputStream.flush();
|
// outputStream.flush();
|
||||||
outputStream.close();
|
// outputStream.close();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
this.examSessionService
|
this.examSessionService
|
||||||
.streamDefaultExamConfig(
|
.streamDefaultExamConfig(
|
||||||
|
institutionId,
|
||||||
connectionToken,
|
connectionToken,
|
||||||
outputStream);
|
outputStream);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue