SEBSERV-314 partially fixed

This commit is contained in:
anhefti 2022-06-10 09:03:39 +02:00
parent bfe15f794a
commit 0ba33c66e0
9 changed files with 94 additions and 83 deletions

View file

@ -8,8 +8,10 @@
package ch.ethz.seb.sebserver.gbl.model.session; package ch.ethz.seb.sebserver.gbl.model.session;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
@ -46,6 +48,11 @@ public final class ClientConnection implements GrantEntity {
} }
} }
public final static List<String> ACTIVE_STATES = Arrays.asList(
ConnectionStatus.ACTIVE.name(),
ConnectionStatus.AUTHENTICATED.name(),
ConnectionStatus.CONNECTION_REQUESTED.name());
public static final ClientConnection EMPTY_CLIENT_CONNECTION = new ClientConnection( public static final ClientConnection EMPTY_CLIENT_CONNECTION = new ClientConnection(
-1L, -1L, -1L, -1L, -1L, -1L,
ConnectionStatus.UNDEFINED, ConnectionStatus.UNDEFINED,

View file

@ -25,20 +25,19 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport;
@Mapper @Mapper
public interface ClientConnectionMinMapper { public interface ClientConnectionTokenMapper {
@SelectProvider(type = SqlProviderAdapter.class, method = "select") @SelectProvider(type = SqlProviderAdapter.class, method = "select")
Long num(SelectStatementProvider selectStatement); Long num(SelectStatementProvider selectStatement);
@SelectProvider(type = SqlProviderAdapter.class, method = "select") @SelectProvider(type = SqlProviderAdapter.class, method = "select")
@ResultType(ClientConnectionMinRecord.class) @ResultType(ClientConnectionTokenRecord.class)
@ConstructorArgs({ @ConstructorArgs({
@Arg(column = "connection_token", javaType = String.class, jdbcType = JdbcType.VARCHAR, id = true), @Arg(column = "connection_token", javaType = String.class, jdbcType = JdbcType.VARCHAR, id = true)
@Arg(column = "update_time", javaType = Long.class, jdbcType = JdbcType.BIGINT),
}) })
Collection<ClientConnectionMinRecord> selectMany(SelectStatementProvider select); Collection<ClientConnectionTokenRecord> selectMany(SelectStatementProvider select);
default QueryExpressionDSL<MyBatis3SelectModelAdapter<Collection<ClientConnectionMinRecord>>> selectByExample() { default QueryExpressionDSL<MyBatis3SelectModelAdapter<Collection<ClientConnectionTokenRecord>>> selectByExample() {
return SelectDSL.selectWithMapper( return SelectDSL.selectWithMapper(
this::selectMany, this::selectMany,
@ -48,17 +47,12 @@ public interface ClientConnectionMinMapper {
.from(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord); .from(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord);
} }
final class ClientConnectionMinRecord { final class ClientConnectionTokenRecord {
public final String connection_token; public final String connection_token;
public final Long update_time;
public ClientConnectionMinRecord(
final String connection_token,
final Long update_time) {
public ClientConnectionTokenRecord(final String connection_token) {
this.connection_token = connection_token; this.connection_token = connection_token;
this.update_time = update_time;
} }
} }

View file

@ -48,22 +48,19 @@ public interface ClientConnectionDAO extends
} }
/** Get a list of all connection tokens of all connections (no matter what state)
* of an exam.
*
* @param examId The exam identifier
* @return list of all connection tokens of all connections (no matter what state)
* of an exam */
default Result<Collection<String>> getConnectionTokensNoCache(final Long examId) {
return getConnectionTokens(examId);
}
/** Get a list of all connection tokens of all connections of an exam /** Get a list of all connection tokens of all connections of an exam
* that are in state active * that are in state <code>ConnectionStatus.ACTIVE</code>
* *
* @param examId The exam identifier * @param examId The exam identifier
* @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>> getActiveConnctionTokens(Long examId); Result<Collection<String>> getActiveConnectionTokens(Long examId);
/** Get a list of all connection tokens of all connections of an exam
* that are in state an active state. See <code>ClientConnection</code>
*
* @param examId The exam identifier
* @return Result refer to the collection of connection tokens or to an error when happened */
Result<Collection<String>> getAllActiveConnectionTokens(Long examId);
/** Get all inactive connection tokens from the set of given tokens. /** Get all inactive connection tokens from the set of given tokens.
* This is usually used for cleanup purposes to filter a bunch of connection tokens * This is usually used for cleanup purposes to filter a bunch of connection tokens

View file

@ -38,6 +38,7 @@ 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;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientConnectionTokenMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
@ -70,19 +71,22 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
private final ClientInstructionRecordMapper clientInstructionRecordMapper; private final ClientInstructionRecordMapper clientInstructionRecordMapper;
private final ClientIndicatorRecordMapper clientIndicatorRecordMapper; private final ClientIndicatorRecordMapper clientIndicatorRecordMapper;
private final ClientNotificationRecordMapper clientNotificationRecordMapper; private final ClientNotificationRecordMapper clientNotificationRecordMapper;
private final ClientConnectionTokenMapper clientConnectionMinMapper;
protected ClientConnectionDAOImpl( protected ClientConnectionDAOImpl(
final ClientConnectionRecordMapper clientConnectionRecordMapper, final ClientConnectionRecordMapper clientConnectionRecordMapper,
final ClientEventRecordMapper clientEventRecordMapper, final ClientEventRecordMapper clientEventRecordMapper,
final ClientInstructionRecordMapper clientInstructionRecordMapper, final ClientInstructionRecordMapper clientInstructionRecordMapper,
final ClientIndicatorRecordMapper clientIndicatorRecordMapper, final ClientIndicatorRecordMapper clientIndicatorRecordMapper,
final ClientNotificationRecordMapper clientNotificationRecordMapper) { final ClientNotificationRecordMapper clientNotificationRecordMapper,
final ClientConnectionTokenMapper clientConnectionMinMapper) {
this.clientConnectionRecordMapper = clientConnectionRecordMapper; this.clientConnectionRecordMapper = clientConnectionRecordMapper;
this.clientEventRecordMapper = clientEventRecordMapper; this.clientEventRecordMapper = clientEventRecordMapper;
this.clientInstructionRecordMapper = clientInstructionRecordMapper; this.clientInstructionRecordMapper = clientInstructionRecordMapper;
this.clientIndicatorRecordMapper = clientIndicatorRecordMapper; this.clientIndicatorRecordMapper = clientIndicatorRecordMapper;
this.clientNotificationRecordMapper = clientNotificationRecordMapper; this.clientNotificationRecordMapper = clientNotificationRecordMapper;
this.clientConnectionMinMapper = clientConnectionMinMapper;
} }
@Override @Override
@ -162,7 +166,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<String>> getConnectionTokens(final Long examId) { public Result<Collection<String>> getConnectionTokens(final Long examId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper return Result.tryCatch(() -> this.clientConnectionMinMapper
.selectByExample() .selectByExample()
.where( .where(
ClientConnectionRecordDynamicSqlSupport.examId, ClientConnectionRecordDynamicSqlSupport.examId,
@ -170,15 +174,15 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.build() .build()
.execute() .execute()
.stream() .stream()
.map(ClientConnectionRecord::getConnectionToken) .map(rec -> rec.connection_token)
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<String>> getActiveConnctionTokens(final Long examId) { public Result<Collection<String>> getActiveConnectionTokens(final Long examId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper return Result.tryCatch(() -> this.clientConnectionMinMapper
.selectByExample() .selectByExample()
.where( .where(
ClientConnectionRecordDynamicSqlSupport.examId, ClientConnectionRecordDynamicSqlSupport.examId,
@ -189,7 +193,26 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.build() .build()
.execute() .execute()
.stream() .stream()
.map(ClientConnectionRecord::getConnectionToken) .map(rec -> rec.connection_token)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList()));
}
@Override
@Transactional(readOnly = true)
public Result<Collection<String>> getAllActiveConnectionTokens(final Long examId) {
return Result.tryCatch(() -> this.clientConnectionMinMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.and(
ClientConnectionRecordDynamicSqlSupport.status,
SqlBuilder.isIn(ClientConnection.ACTIVE_STATES))
.build()
.execute()
.stream()
.map(rec -> rec.connection_token)
.filter(StringUtils::isNotBlank) .filter(StringUtils::isNotBlank)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }

View file

@ -210,7 +210,7 @@ public interface ExamSessionService {
* @return Result with reference to the given Exam or to an error if happened */ * @return Result with reference to the given Exam or to an error if happened */
Result<Exam> flushCache(final Exam exam); Result<Exam> flushCache(final Exam exam);
/** Is is supposed to be the single access point to internally get client connection /** This is supposed to be the single access point to internally get client connection
* data for a specified connection token. * data for a specified connection token.
* This uses the client connection data cache for lookup and also synchronizes asynchronous * This uses the client connection data cache for lookup and also synchronizes asynchronous
* cache calls to prevent parallel creation of ClientConnectionDataInternal * cache calls to prevent parallel creation of ClientConnectionDataInternal

View file

@ -424,7 +424,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override @Override
public Result<Collection<String>> getActiveConnectionTokens(final Long examId) { public Result<Collection<String>> getActiveConnectionTokens(final Long examId) {
return this.clientConnectionDAO return this.clientConnectionDAO
.getActiveConnctionTokens(examId); .getActiveConnectionTokens(examId);
} }
@EventListener @EventListener

View file

@ -11,9 +11,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.security.Principal; import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -22,8 +22,6 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -69,7 +67,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final ExamSessionCacheService examSessionCacheService; private final ExamSessionCacheService examSessionCacheService;
private final CacheManager cacheManager;
private final EventHandlingStrategy eventHandlingStrategy; private final EventHandlingStrategy eventHandlingStrategy;
private final ClientConnectionDAO clientConnectionDAO; private final ClientConnectionDAO clientConnectionDAO;
private final SEBClientConfigDAO sebClientConfigDAO; private final SEBClientConfigDAO sebClientConfigDAO;
@ -90,7 +87,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionService.getExamSessionCacheService(); this.examSessionCacheService = examSessionService.getExamSessionCacheService();
this.cacheManager = examSessionService.getCacheManager();
this.clientConnectionDAO = examSessionService.getClientConnectionDAO(); this.clientConnectionDAO = examSessionService.getClientConnectionDAO();
this.eventHandlingStrategy = eventHandlingStrategyFactory.get(); this.eventHandlingStrategy = eventHandlingStrategyFactory.get();
this.sebClientConfigDAO = sebClientConfigDAO; this.sebClientConfigDAO = sebClientConfigDAO;
@ -645,28 +641,19 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
public void updatePingEvents() { public void updatePingEvents() {
try { try {
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
final long now = Utils.getMillisecondsNow();
final Consumer<ClientConnectionDataInternal> missingPingUpdate = missingPingUpdate(now);
this.examSessionService this.examSessionService
.getExamDAO() .getExamDAO()
.allRunningExamIds() .allRunningExamIds()
.getOrThrow() .getOrThrow()
.stream() .stream()
.flatMap(examId -> this.isDistributedSetup .flatMap(examId -> this.clientConnectionDAO
? this.clientConnectionDAO .getAllActiveConnectionTokens(examId)
.getConnectionTokensNoCache(examId) .getOr(Collections.emptyList())
.getOrThrow() .stream())
.stream() .map(this.examSessionService::getConnectionDataInternal)
: this.clientConnectionDAO
.getConnectionTokens(examId)
.getOrThrow()
.stream())
.map(token -> cache.get(token, ClientConnectionDataInternal.class))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(connection -> connection.pingIndicator != null && .filter(connection -> connection.pingIndicator != null)
connection.clientConnection.status.clientActiveStatus) .forEach(this::missingPingUpdate);
.forEach(connection -> missingPingUpdate.accept(connection));
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to update ping events: ", e); log.error("Failed to update ping events: ", e);
@ -905,31 +892,29 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
return this.examSessionService.getConnectionDataInternal(connectionToken); return this.examSessionService.getConnectionDataInternal(connectionToken);
} }
private Consumer<ClientConnectionDataInternal> missingPingUpdate(final long now) { private void missingPingUpdate(final ClientConnectionDataInternal connection) {
if (connection.pingIndicator.changeOnIncident()) {
return connection -> { final boolean missingPing = connection.getMissingPing();
final long millisecondsNow = Utils.getMillisecondsNow();
final ClientEventRecord clientEventRecord = new ClientEventRecord(
null,
connection.getConnectionId(),
(missingPing) ? EventType.ERROR_LOG.id : EventType.INFO_LOG.id,
millisecondsNow,
millisecondsNow,
new BigDecimal(connection.pingIndicator.getValue()),
(missingPing) ? "Missing Client Ping" : "Client Ping Back To Normal");
if (connection.pingIndicator.missingPingUpdate(now)) { // store event and and flush cache
final boolean missingPing = connection.getMissingPing(); this.eventHandlingStrategy.accept(clientEventRecord);
final ClientEventRecord clientEventRecord = new ClientEventRecord(
null,
connection.getConnectionId(),
(missingPing) ? EventType.ERROR_LOG.id : EventType.INFO_LOG.id,
now,
now,
new BigDecimal(connection.pingIndicator.getValue()),
(missingPing) ? "Missing Client Ping" : "Client Ping Back To Normal");
// store event and and flush cache // update indicators
this.eventHandlingStrategy.accept(clientEventRecord); if (clientEventRecord.getType() != null && EventType.ERROR_LOG.id == clientEventRecord.getType()) {
connection.getIndicatorMapping(EventType.ERROR_LOG)
// update indicators .forEach(indicator -> indicator.notifyValueChange(clientEventRecord));
if (clientEventRecord.getType() != null && EventType.ERROR_LOG.id == clientEventRecord.getType()) {
connection.getIndicatorMapping(EventType.ERROR_LOG)
.forEach(indicator -> indicator.notifyValueChange(clientEventRecord));
}
} }
}; }
} }
} }

View file

@ -118,22 +118,27 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
@Override @Override
public final boolean hasIncident() { public final boolean hasIncident() {
if (!this.active) {
return false;
}
return getValue() >= super.incidentThreshold; return getValue() >= super.incidentThreshold;
} }
private double lastCheckVal = 0; private double lastCheckVal = 0;
public final boolean missingPingUpdate(final long now) { public final boolean changeOnIncident() {
if (this.currentValue <= 0) { if (!this.active || this.currentValue <= 0) {
return false; return false;
} }
final double val = now - this.currentValue; final double val = getValue();
// check if incidentThreshold was passed (up or down) since last update // check if incident threshold has passed (up or down) since last update
final boolean result = (this.lastCheckVal < this.incidentThreshold && val >= this.incidentThreshold) || final boolean changed = (this.lastCheckVal < this.incidentThreshold && val >= this.incidentThreshold) ||
(this.lastCheckVal >= this.incidentThreshold && val < this.incidentThreshold); (this.lastCheckVal >= this.incidentThreshold && val < this.incidentThreshold);
this.lastCheckVal = val; this.lastCheckVal = val;
return result; return changed;
} }
} }

View file

@ -1,11 +1,11 @@
server.address=localhost server.address=localhost
server.port=8080 server.port=8090
sebserver.gui.http.external.scheme=http sebserver.gui.http.external.scheme=http
sebserver.gui.entrypoint=/gui sebserver.gui.entrypoint=/gui
sebserver.gui.webservice.protocol=http sebserver.gui.webservice.protocol=http
sebserver.gui.webservice.address=localhost sebserver.gui.webservice.address=localhost
sebserver.gui.webservice.port=8080 sebserver.gui.webservice.port=8090
sebserver.gui.webservice.apipath=/admin-api/v1 sebserver.gui.webservice.apipath=/admin-api/v1
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
sebserver.gui.webservice.poll-interval=1000 sebserver.gui.webservice.poll-interval=1000