dispose client connection token and connection token caching

This commit is contained in:
anhefti 2022-06-22 08:51:13 +02:00
parent 4ab317d763
commit 1c896c827d
8 changed files with 78 additions and 54 deletions

View file

@ -11,10 +11,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@ -26,6 +27,8 @@ public interface ClientConnectionDAO extends
EntityDAO<ClientConnection, ClientConnection>,
BulkActionSupportDAO<ClientConnection> {
Logger log = LoggerFactory.getLogger(ClientConnectionDAO.class);
String CONNECTION_TOKENS_CACHE = "CONNECTION_TOKENS_CACHE";
/** Get a list of all connection tokens of all connections (no matter what state)
@ -43,9 +46,13 @@ public interface ClientConnectionDAO extends
unless = "#result.hasError()")
Result<Collection<String>> getConnectionTokens(Long examId);
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, key = "#examId")
@CacheEvict(
cacheNames = CONNECTION_TOKENS_CACHE,
key = "#examId")
default void evictConnectionTokenCache(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Evict SEB connection tokens for exam: {}", examId);
}
}
/** Get a list of all connection tokens of all connections of an exam
@ -101,22 +108,6 @@ public interface ClientConnectionDAO extends
* @return Result refer to a collection of all ClientConnection of the room or to an error if happened */
Result<Collection<ClientConnection>> getCollectingRoomConnections(final Long examId, final String roomName);
/** Creates new ClientConnection from the given ClientConnection data.
*
* This evicts all entries from the CONNECTION_TOKENS_CACHE.
*
* TODO improvement: Use the examId to evict only the relevant cache entry
*
* @param data ClientConnection instance
* @return Result refer to the newly created ClientConnection data or to an error if happened */
@Override
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, allEntries = true)
Result<ClientConnection> createNew(ClientConnection data);
@Override
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, allEntries = true)
Result<ClientConnection> save(ClientConnection data);
@CacheEvict(
cacheNames = ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionToken")
@ -130,19 +121,6 @@ public interface ClientConnectionDAO extends
/** Used to re-mark a client connection record for room update in error case. */
Result<Void> markForProctoringUpdate(Long id);
/** Deletes the given ClientConnection data.
*
* This evicts all entries from the CONNECTION_TOKENS_CACHE.
*
* TODO improvement: Use the examId to evict only the relevant cache entry
*
* @param all Set of EntityKey for entities to delete
* @return Result refer to a collection of deleted entities or to an error if happened */
@Override
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, allEntries = true)
// TODO this probably is nor working when called from BulkActionSupportDAO
Result<Collection<EntityKey>> delete(Set<EntityKey> all);
/** Get a ClientConnection by connection token.
*
* @param connectionToken the connection token

View file

@ -24,6 +24,7 @@ import org.apache.commons.lang3.BooleanUtils;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@ -72,6 +73,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
private final ClientIndicatorRecordMapper clientIndicatorRecordMapper;
private final ClientNotificationRecordMapper clientNotificationRecordMapper;
private final ClientConnectionTokenMapper clientConnectionMinMapper;
private final CacheManager cacheManager;
protected ClientConnectionDAOImpl(
final ClientConnectionRecordMapper clientConnectionRecordMapper,
@ -79,7 +81,8 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
final ClientInstructionRecordMapper clientInstructionRecordMapper,
final ClientIndicatorRecordMapper clientIndicatorRecordMapper,
final ClientNotificationRecordMapper clientNotificationRecordMapper,
final ClientConnectionTokenMapper clientConnectionMinMapper) {
final ClientConnectionTokenMapper clientConnectionMinMapper,
final CacheManager cacheManager) {
this.clientConnectionRecordMapper = clientConnectionRecordMapper;
this.clientEventRecordMapper = clientEventRecordMapper;
@ -87,6 +90,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
this.clientIndicatorRecordMapper = clientIndicatorRecordMapper;
this.clientNotificationRecordMapper = clientNotificationRecordMapper;
this.clientConnectionMinMapper = clientConnectionMinMapper;
this.cacheManager = cacheManager;
}
@Override
@ -529,6 +533,8 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
return Collections.emptyList();
}
ids.stream().forEach(this::clearConnecionTokenCache);
// delete all related client indicators
this.clientIndicatorRecordMapper.deleteByExample()
.where(
@ -879,4 +885,22 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
return record.getConnectionToken();
}
private Long clearConnecionTokenCache(final Long id) {
try {
final ClientConnectionRecord rec = recordById(id)
.getOrThrow();
this.cacheManager
.getCache(CONNECTION_TOKENS_CACHE)
.evictIfPresent(rec.getExamId());
} catch (final Exception e) {
log.error("Failed to clear connection token cache: ", e);
}
return id;
}
}

View file

@ -215,6 +215,9 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
return Collections.emptyList();
}
// clear caches and revoke tokens first
ids.stream().forEach(this::disposeSEBClientConfig);
final SebClientConfigRecord record = new SebClientConfigRecord(
null, null, null, null, null, null, null,
BooleanUtils.toIntegerObject(active),
@ -228,7 +231,6 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
return ids.stream()
.map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION))
.map(this::revokeClientConnection)
.collect(Collectors.toList());
});
}
@ -308,6 +310,9 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
return Collections.emptyList();
}
// clear caches and revoke tokens first
ids.stream().forEach(this::disposeSEBClientConfig);
this.sebClientConfigRecordMapper.deleteByExample()
.where(SebClientConfigRecordDynamicSqlSupport.id, isIn(ids))
.build()
@ -315,7 +320,6 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
return ids.stream()
.map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION))
.map(this::revokeClientConnection)
.collect(Collectors.toList());
});
}
@ -670,10 +674,10 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
}
}
private EntityKey revokeClientConnection(final EntityKey key) {
private Long disposeSEBClientConfig(final Long pk) {
try {
final SebClientConfigRecord rec = recordById(Long.parseLong(key.modelId))
final SebClientConfigRecord rec = recordById(pk)
.getOrThrow();
// revoke token
@ -681,17 +685,18 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
this.applicationEventPublisher
.publishEvent(new RevokeExamTokenEvent(rec.getClientName()));
} catch (final Exception e) {
log.error("Failed to revoke token for SEB client connection. Connection Configuration: {}", key, e);
log.error("Failed to revoke token for SEB client connection. Connection Configuration: {}", pk, e);
}
// clear cache
this.cacheManager.getCache(ClientConfigService.EXAM_CLIENT_DETAILS_CACHE)
this.cacheManager
.getCache(ClientConfigService.EXAM_CLIENT_DETAILS_CACHE)
.evictIfPresent(rec.getClientName());
} catch (final Exception e) {
log.error("Failed to revoke SEB client connection. Connection Configuration: {}", key, e);
log.error("Failed to revoke SEB client connection. Connection Configuration: {}", pk, e);
}
return key;
return pk;
}
}

View file

@ -180,6 +180,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.clientIndicatorFactory.initializeDistributedCaches(clientConnection);
}
// flash connection token cache for exam if available
if (examId != null) {
this.clientConnectionDAO.evictConnectionTokenCache(examId);
}
// load client connection data into cache
final ClientConnectionDataInternal activeClientConnection = this.examSessionService
.getConnectionDataInternal(connectionToken);
@ -286,8 +291,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.clientIndicatorFactory.initializeDistributedCaches(clientConnection);
}
final ClientConnectionDataInternal activeClientConnection =
reloadConnectionCache(connectionToken);
final ClientConnectionDataInternal activeClientConnection = reloadConnectionCache(
connectionToken,
examId);
if (activeClientConnection == null) {
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
@ -441,8 +447,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
}
// flush and reload caches to work with actual connection data
final ClientConnectionDataInternal activeClientConnection =
reloadConnectionCache(connectionToken);
final ClientConnectionDataInternal activeClientConnection = reloadConnectionCache(
connectionToken,
examId);
if (activeClientConnection == null) {
log.warn("Failed to load ClientConnectionDataInternal into cache on update");
@ -496,13 +503,14 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
establishedClientConnection.remoteProctoringRoomUpdate);
// Update other connection with token and exam id
this.clientConnectionDAO
final ClientConnection connection = this.clientConnectionDAO
.save(new ClientConnection(
vdiPairCompanion.getId(), null,
vdiExamId, null, null, null, null, null, null,
establishedClientConnection.connectionToken, null, null, null, null, null, null, null))
.getOrThrow();
reloadConnectionCache(vdiPairCompanion.getConnectionToken());
reloadConnectionCache(vdiPairCompanion.getConnectionToken(), connection.examId);
return updatedConnection;
}
@ -557,7 +565,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.deleteIndicatorValues(updatedClientConnection.id);
}
reloadConnectionCache(connectionToken);
reloadConnectionCache(connectionToken, clientConnection.examId);
return updatedClientConnection;
});
}
@ -619,7 +627,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.deleteIndicatorValues(updatedClientConnection.id);
}
reloadConnectionCache(connectionToken);
reloadConnectionCache(connectionToken, clientConnection.examId);
return updatedClientConnection;
});
}
@ -886,7 +894,14 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.getOrThrow();
}
private ClientConnectionDataInternal reloadConnectionCache(final String connectionToken) {
private ClientConnectionDataInternal reloadConnectionCache(
final String connectionToken,
final Long examId) {
if (examId != null) {
// evict connection tokens for exam
this.clientConnectionDAO.evictConnectionTokenCache(examId);
}
// evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache

View file

@ -79,8 +79,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
log.warn(
"Unauthorized Request: {}, {}",
request,
"Unauthorized Request: {}",
exception != null ? exception.getMessage() : Constants.EMPTY_NOTE);
response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }");
});

View file

@ -66,7 +66,9 @@ public class WebClientDetailsService implements ClientDetailsService {
return getForExamClientAPI(clientId)
.get(t -> {
log.error("Active ClientConfig not found: {} cause: {}", clientId, t.getMessage());
if (log.isDebugEnabled()) {
log.warn("Active ClientConfig not found: {} cause: {}", clientId, t.getMessage());
}
throw new UsernameNotFoundException(t.getMessage());
});
}

View file

@ -25,7 +25,7 @@ sebserver.webservice.clean-db-on-startup=false
# webservice configuration
sebserver.init.adminaccount.gen-on-init=false
sebserver.webservice.distributed=true
sebserver.webservice.distributed=false
#sebserver.webservice.master.delay.threshold=10000
sebserver.webservice.http.external.scheme=http
sebserver.webservice.http.external.servername=localhost

View file

@ -22,6 +22,7 @@ logging.level.ch.ethz.seb.sebserver.webservice.weblayer.oauth=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
logging.level.com.zaxxer.hikari=INFO
#logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao=DEBUG
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000