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 */
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue