Merge remote-tracking branch 'origin/dev-1.2' into development

Conflicts:
	src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java
	src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java
	src/test/resources/application-test.properties
This commit is contained in:
anhefti 2021-11-24 13:34:14 +01:00
commit c9a1c3a019
33 changed files with 1139 additions and 961 deletions

View file

@ -76,7 +76,7 @@ public class ClientHttpRequestFactoryService {
final ClientCredentialService clientCredentialService,
@Value("${sebserver.http.client.connect-timeout:15000}") final int connectTimeout,
@Value("${sebserver.http.client.connection-request-timeout:20000}") final int connectionRequestTimeout,
@Value("${sebserver.http.client.read-timeout:10000}") final int readTimeout) {
@Value("${sebserver.http.client.read-timeout:20000}") final int readTimeout) {
this.environment = environment;
this.clientCredentialService = clientCredentialService;

View file

@ -17,6 +17,7 @@ import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@EnableAsync
@ -25,6 +26,7 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer {
public static final String EXECUTOR_BEAN_NAME = "AsyncServiceExecutorBean";
/** This ThreadPool is used for internal long running background tasks */
@Bean(name = EXECUTOR_BEAN_NAME)
public Executor threadPoolTaskExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
@ -39,6 +41,9 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer {
public static final String EXAM_API_EXECUTOR_BEAN_NAME = "ExamAPIAsyncServiceExecutorBean";
/** This ThreadPool is used for SEB client connection establishment and
* should be able to handle incoming bursts of SEB client connection requests (handshake)
* when up to 1000 - 2000 clients connect at nearly the same time (start of an exam) */
@Bean(name = EXAM_API_EXECUTOR_BEAN_NAME)
public Executor examAPIThreadPoolTaskExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
@ -52,6 +57,32 @@ public class AsyncServiceSpringConfig implements AsyncConfigurer {
return executor;
}
public static final String EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME = "examAPIPingThreadPoolTaskExecutor";
/** This ThreadPool is used for ping handling in a distributed setup and shall reject
* incoming ping requests as fast as possible if there is to much load on the DB.
* We prefer to loose a shared ping update and respond to the client in time over a client request timeout */
@Bean(name = EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME)
public Executor examAPIPingThreadPoolTaskExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(0);
executor.setThreadNamePrefix("SEBPingService-");
executor.initialize();
executor.setWaitForTasksToCompleteOnShutdown(false);
return executor;
}
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
final ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(5);
threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(false);
threadPoolTaskScheduler.setThreadNamePrefix("SEB-Server-BgTask-");
return threadPoolTaskScheduler;
}
@Override
public Executor getAsyncExecutor() {
return threadPoolTaskExecutor();

View file

@ -110,6 +110,8 @@ public class ExamForm implements TemplateComposer {
new LocTextKey("sebserver.exam.form.quizurl");
private static final LocTextKey FORM_LMSSETUP_TEXT_KEY =
new LocTextKey("sebserver.exam.form.lmssetup");
private final static LocTextKey ACTION_MESSAGE_SEB_RESTRICTION_RELEASE =
new LocTextKey("sebserver.exam.action.sebrestriction.release.confirm");
private static final LocTextKey FORM_EXAM_TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.exam.form.examTemplate");
private static final LocTextKey FORM_EXAM_TEMPLATE_ERROR =
@ -193,6 +195,7 @@ public class ExamForm implements TemplateComposer {
@Override
public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final EntityKey entityKey = pageContext.getEntityKey();
final boolean readonly = pageContext.isReadonly();
@ -432,6 +435,7 @@ public class ExamForm implements TemplateComposer {
&& BooleanUtils.isFalse(isRestricted))
.newAction(ActionDefinition.EXAM_DISABLE_SEB_RESTRICTION)
.withConfirm(() -> ACTION_MESSAGE_SEB_RESTRICTION_RELEASE)
.withEntityKey(entityKey)
.withExec(action -> this.examSEBRestrictionSettings.setSEBRestriction(action, false, this.restService))
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData

View file

@ -83,6 +83,10 @@ public class MonitoringRunningExam implements TemplateComposer {
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected.confirm");
private static final LocTextKey CONFIRM_QUIT_ALL =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm");
private static final LocTextKey CONFIRM_OPEN_TOWNHALL =
new LocTextKey("sebserver.monitoring.exam.connection.action.openTownhall.confirm");
private static final LocTextKey CONFIRM_CLOSE_TOWNHALL =
new LocTextKey("sebserver.monitoring.exam.connection.action.closeTownhall.confirm");
private static final LocTextKey CONFIRM_DISABLE_SELECTED =
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm");
@ -279,6 +283,13 @@ public class MonitoringRunningExam implements TemplateComposer {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withConfirm(action -> {
if (!this.monitoringProctoringService.isTownhallRoomActive(action.getEntityKey().modelId)) {
return CONFIRM_OPEN_TOWNHALL;
} else {
return CONFIRM_CLOSE_TOWNHALL;
}
})
.withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService,
action))
.noEventPropagation()

View file

@ -281,6 +281,10 @@ public interface PageContext {
*
* @param error the original error */
default void notifyUnexpectedError(final Exception error) {
if (error instanceof PageMessageException) {
publishInfo(((PageMessageException) error).getMessageKey());
return;
}
notifyError(UNEXPECTED_ERROR_KEY, error);
}

View file

@ -28,6 +28,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@ -46,11 +47,15 @@ import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
public abstract class RestCall<T> {
private static final Logger log = LoggerFactory.getLogger(RestCall.class);
public static final LocTextKey REQUEST_TIMEOUT_MESSAGE = new LocTextKey("sebserver.overall.message.requesttimeout");
public enum CallType {
UNDEFINED,
GET_SINGLE,
@ -161,6 +166,11 @@ public abstract class RestCall<T> {
}
return Result.ofError(restCallError);
} catch (final ResourceAccessException rae) {
if (rae.getMessage().contains("Read timed out")) {
return Result.ofError(new PageMessageException(REQUEST_TIMEOUT_MESSAGE));
}
return Result.ofError(rae);
} catch (final Exception e) {
final RestCallError restCallError = new RestCallError("Unexpected error while rest call", e);
restCallError.errors.add(APIMessage.ErrorMessage.UNEXPECTED.of(

View file

@ -46,7 +46,6 @@ public class CacheConfig extends JCacheConfigurerSupport {
final CachingProvider cachingProvider = Caching.getCachingProvider();
final javax.cache.CacheManager cacheManager =
cachingProvider.getCacheManager(new URI(this.jCacheConfig), this.getClass().getClassLoader());
System.out.println("cacheManager:" + cacheManager);
final CompositeCacheManager composite = new CompositeCacheManager();
composite.setCacheManagers(Arrays.asList(

View file

@ -86,8 +86,15 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> *** Info:");
SEBServerInit.INIT_LOGGER.info("----> JDBC connection pool max size: {}",
this.environment.getProperty("spring.datasource.hikari.maximumPoolSize"));
if (this.webserviceInfo.isDistributed()) {
SEBServerInit.INIT_LOGGER.info("----> Distributed Setup: {}", this.webserviceInfo.getWebserviceUUID());
SEBServerInit.INIT_LOGGER.info("------> Ping update time: {}",
this.environment.getProperty("sebserver.webservice.distributed.pingUpdate"));
SEBServerInit.INIT_LOGGER.info("------> Connection update time: {}",
this.environment.getProperty("sebserver.webservice.distributed.connectionUpdate"));
}
try {

View file

@ -80,7 +80,7 @@ public interface ClientEventExtensionMapper {
.from(ClientEventRecordDynamicSqlSupport.clientEventRecord)
.leftJoin(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord)
.join(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord)
.on(
ClientEventRecordDynamicSqlSupport.clientEventRecord.clientConnectionId,
equalTo(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord.id));

View file

@ -40,7 +40,7 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
public WebserviceInfoDAOImpl(
final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper,
@Value("${sebserver.webservice.forceMaster:false}") final boolean forceMaster,
@Value("${sebserver.webservice.master.delay.threshold:10000}") final long masterDelayTimeThreshold) {
@Value("${sebserver.webservice.master.delay.threshold:30000}") final long masterDelayTimeThreshold) {
this.webserviceServerInfoRecordMapper = webserviceServerInfoRecordMapper;
this.masterDelayTimeThreshold = masterDelayTimeThreshold;

View file

@ -228,7 +228,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic
private final String sort;
private int pageNumber = 1;
private final int pageSize = 100;
private final int pageSize = 1000;
private Collection<ClientEventRecord> nextRecords;

View file

@ -137,8 +137,9 @@ public interface SEBClientConnectionService {
* @param connectionToken the connection token
* @param timestamp the ping time-stamp
* @param pingNumber the ping number
* @param instructionConfirm instruction confirm sent by the SEB client or null
* @return SEB instruction if available */
String notifyPing(String connectionToken, long timestamp, int pingNumber);
String notifyPing(String connectionToken, long timestamp, int pingNumber, String instructionConfirm);
/** Notify a SEB client event for live indication and storing to database.
*

View file

@ -61,6 +61,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
private final CacheManager cacheManager;
private final SEBRestrictionService sebRestrictionService;
private final boolean distributedSetup;
private final long distributedConnectionUpdate;
private long lastConnectionTokenCacheUpdate = 0;
@ -72,7 +73,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final IndicatorDAO indicatorDAO,
final CacheManager cacheManager,
final SEBRestrictionService sebRestrictionService,
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup) {
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup,
@Value("${sebserver.webservice.distributed.connectionUpdate:2000}") final long distributedConnectionUpdate) {
this.examSessionCacheService = examSessionCacheService;
this.examDAO = examDAO;
@ -82,6 +84,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
this.indicatorDAO = indicatorDAO;
this.sebRestrictionService = sebRestrictionService;
this.distributedSetup = distributedSetup;
this.distributedConnectionUpdate = distributedConnectionUpdate;
}
@Override
@ -114,9 +117,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return Result.tryCatch(() -> {
final Collection<APIMessage> result = new ArrayList<>();
final Exam exam = this.examDAO
.byPK(examId)
.getOrThrow();
final Exam exam = (this.isExamRunning(examId))
? this.examSessionCacheService.getRunningExam(examId)
: this.examDAO
.byPK(examId)
.getOrThrow();
// check lms connection
if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION) {
@ -194,6 +199,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override
public synchronized Result<Exam> getRunningExam(final Long examId) {
if (log.isTraceEnabled()) {
log.trace("Running exam request for exam {}", examId);
}
@ -212,6 +218,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return Result.of(exam);
} else {
if (exam != null) {
log.info("Exam {} is not running anymore. Flush caches", exam);
flushCache(exam);
}
@ -361,6 +368,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override
public Result<Exam> updateExamCache(final Long examId) {
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
if (exam == null) {
return Result.ofEmpty();
@ -395,13 +403,13 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
// If we are in a distributed setup the active connection token cache get flushed
// at least every second. This allows caching over multiple monitoring requests but
// ensure an update every second for new incoming connections
// in specified time interval. This allows caching over multiple monitoring requests but
// ensure an update every now and then for new incoming connections
private void updateClientConnections(final Long examId) {
try {
final long currentTimeMillis = System.currentTimeMillis();
if (this.distributedSetup &&
System.currentTimeMillis() - this.lastConnectionTokenCacheUpdate > Constants.SECOND_IN_MILLIS) {
currentTimeMillis - this.lastConnectionTokenCacheUpdate > this.distributedConnectionUpdate) {
// go trough all client connection and update the ones that not up to date
this.clientConnectionDAO.evictConnectionTokenCache(examId);
@ -420,7 +428,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.stream()
.forEach(this.examSessionCacheService::evictClientConnection);
this.lastConnectionTokenCacheUpdate = System.currentTimeMillis();
this.lastConnectionTokenCacheUpdate = currentTimeMillis;
}
} catch (final Exception e) {
log.error("Unexpected error while trying to update client connections: ", e);

View file

@ -10,21 +10,16 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import org.joda.time.DateTimeUtils;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
public abstract class AbstractClientIndicator implements ClientIndicator {
private static final long PERSISTENT_UPDATE_INTERVAL = Constants.SECOND_IN_MILLIS;
protected Long indicatorId;
protected Long examId;
protected Long connectionId;
protected boolean cachingEnabled;
protected boolean active = true;
protected long persistentUpdateInterval = PERSISTENT_UPDATE_INTERVAL;
protected long lastPersistentUpdate = 0;
protected boolean valueInitializes = false;
protected double currentValue = Double.NaN;
@ -72,15 +67,11 @@ public abstract class AbstractClientIndicator implements ClientIndicator {
final long now = DateTimeUtils.currentTimeMillis();
if (!this.valueInitializes) {
this.currentValue = computeValueAt(now);
this.lastPersistentUpdate = now;
this.valueInitializes = true;
}
if (!this.cachingEnabled && this.active) {
if (now - this.lastPersistentUpdate > this.persistentUpdateInterval) {
this.currentValue = computeValueAt(now);
this.lastPersistentUpdate = now;
}
this.currentValue = computeValueAt(now);
}
return this.currentValue;

View file

@ -24,11 +24,16 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
public abstract class AbstractLogIndicator extends AbstractClientIndicator {
protected static final Long DISTRIBUTED_LOG_UPDATE_INTERVAL = 5 * Constants.SECOND_IN_MILLIS;
protected final Set<EventType> observed;
protected final List<Integer> eventTypeIds;
protected String[] tags;
protected long lastDistributedUpdate = 0L;
protected AbstractLogIndicator(final EventType... eventTypes) {
this.observed = Collections.unmodifiableSet(EnumSet.of(eventTypes[0], eventTypes));
this.eventTypeIds = Utils.immutableListOf(Arrays.stream(eventTypes)
.map(et -> et.id)
@ -44,7 +49,6 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator {
final boolean cachingEnabled) {
super.init(indicatorDefinition, connectionId, active, cachingEnabled);
super.persistentUpdateInterval = 2 * Constants.SECOND_IN_MILLIS;
if (indicatorDefinition == null || StringUtils.isBlank(indicatorDefinition.tags)) {
this.tags = null;
@ -75,4 +79,12 @@ public abstract class AbstractLogIndicator extends AbstractClientIndicator {
return this.observed;
}
protected boolean loadFromPersistent(final long timestamp) {
if (!super.valueInitializes) {
return true;
}
return timestamp - this.lastDistributedUpdate > DISTRIBUTED_LOG_UPDATE_INTERVAL;
}
}

View file

@ -57,6 +57,10 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
@Override
public double computeValueAt(final long timestamp) {
if (!loadFromPersistent(timestamp)) {
return super.currentValue;
}
try {
final Long errors = this.clientEventRecordMapper
@ -72,9 +76,12 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
.execute();
return errors.doubleValue();
} catch (final Exception e) {
log.error("Failed to get indicator count from persistent storage: ", e);
return super.currentValue;
} finally {
super.lastDistributedUpdate = timestamp;
}
}

View file

@ -62,8 +62,15 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
@Override
public double computeValueAt(final long timestamp) {
if (!loadFromPersistent(timestamp)) {
return super.currentValue;
}
try {
System.out.println("************** loadFromPersistent");
final List<ClientEventRecord> execute = this.clientEventRecordMapper.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(this.connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isIn(this.eventTypeIds))
@ -90,6 +97,8 @@ public abstract class AbstractLogNumberIndicator extends AbstractLogIndicator {
} catch (final Exception e) {
log.error("Failed to get indicator number from persistent storage: {}", e.getMessage());
return this.currentValue;
} finally {
super.lastDistributedUpdate = timestamp;
}
}

View file

@ -11,34 +11,38 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.Executor;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
public abstract class AbstractPingIndicator extends AbstractClientIndicator {
private static final Logger log = LoggerFactory.getLogger(AbstractPingIndicator.class);
private static final long INTERVAL_FOR_PERSISTENT_UPDATE = Constants.SECOND_IN_MILLIS;
private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
private final Executor executor;
protected final DistributedPingCache distributedPingCache;
private final long lastUpdate = 0;
protected Long pingRecord = null;
protected AbstractPingIndicator(final DistributedPingCache distributedPingCache) {
//protected Long pingRecord = null;
protected PingUpdate pingUpdate = null;
protected AbstractPingIndicator(
final DistributedPingCache distributedPingCache,
@Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor executor) {
super();
this.executor = executor;
this.distributedPingCache = distributedPingCache;
}
@ -53,33 +57,31 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
if (!this.cachingEnabled && this.active) {
try {
this.pingRecord = this.distributedPingCache.initPingForConnection(this.connectionId);
createPingUpdate();
} catch (final Exception e) {
this.pingRecord = this.distributedPingCache.getPingRecordIdForConnectionId(connectionId);
createPingUpdate();
}
}
}
public final void notifyPing(final long timestamp, final int pingNumber) {
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
super.currentValue = now;
super.lastPersistentUpdate = now;
super.currentValue = timestamp;
if (!this.cachingEnabled) {
if (this.pingRecord == null) {
if (this.pingUpdate == null) {
tryRecoverPingRecord();
if (this.pingRecord == null) {
if (this.pingUpdate == null) {
return;
}
}
// Update last ping time on persistent storage
final long millisecondsNow = DateTimeUtils.currentTimeMillis();
if (millisecondsNow - this.lastUpdate > INTERVAL_FOR_PERSISTENT_UPDATE) {
synchronized (this) {
this.distributedPingCache.updatePing(this.pingRecord, millisecondsNow);
}
// Update last ping time on persistent storage asynchronously within a defines thread pool with no
// waiting queue to skip further ping updates if all update threads are busy
try {
this.executor.execute(this.pingUpdate);
} catch (final Exception e) {
//log.warn("Failed to schedule ping task: {}" + e.getMessage());
}
}
}
@ -91,15 +93,21 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
}
try {
this.pingRecord = this.distributedPingCache.getPingRecordIdForConnectionId(this.connectionId);
if (this.pingRecord == null) {
this.pingRecord = this.distributedPingCache.initPingForConnection(this.connectionId);
createPingUpdate();
if (this.pingUpdate == null) {
createPingUpdate();
}
} catch (final Exception e) {
log.error("Failed to recover ping record for connection: {}", this.connectionId, e);
}
}
private void createPingUpdate() {
this.pingUpdate = new PingUpdate(
this.distributedPingCache.getClientEventLastPingMapper(),
this.distributedPingCache.initPingForConnection(this.connectionId));
}
@Override
public Set<EventType> observedEvents() {
return this.EMPTY_SET;
@ -107,4 +115,46 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
public abstract ClientEventRecord updateLogEvent(final long now);
@Override
public double computeValueAt(final long timestamp) {
// TODO Auto-generated method stub
return 0;
}
@Override
public void notifyValueChange(final ClientEvent event) {
// TODO Auto-generated method stub
}
@Override
public void notifyValueChange(final ClientEventRecord clientEventRecord) {
// TODO Auto-generated method stub
}
@Override
public IndicatorType getType() {
// TODO Auto-generated method stub
return null;
}
static final class PingUpdate implements Runnable {
private final ClientEventLastPingMapper clientEventLastPingMapper;
final Long pingRecord;
public PingUpdate(final ClientEventLastPingMapper clientEventLastPingMapper, final Long pingRecord) {
this.clientEventLastPingMapper = clientEventLastPingMapper;
this.pingRecord = pingRecord;
}
@Override
public void run() {
this.clientEventLastPingMapper
.updatePingTime(this.pingRecord, Utils.getMillisecondsNow());
}
}
}

View file

@ -12,11 +12,12 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.isIn;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.joda.time.DateTimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -25,13 +26,14 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientEventLastPingMapper.ClientEventLastPingRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
@ -45,9 +47,11 @@ public class DistributedPingCache implements DisposableBean {
private final ClientEventLastPingMapper clientEventLastPingMapper;
private final ClientEventRecordMapper clientEventRecordMapper;
private ScheduledFuture<?> taskRef;
private final long pingUpdateTolerance;
private ScheduledFuture<?> taskRef;
private final Map<Long, Long> pingCache = new ConcurrentHashMap<>();
private long lastUpdate = 0L;
public DistributedPingCache(
final ClientEventLastPingMapper clientEventLastPingMapper,
@ -58,9 +62,10 @@ public class DistributedPingCache implements DisposableBean {
this.clientEventLastPingMapper = clientEventLastPingMapper;
this.clientEventRecordMapper = clientEventRecordMapper;
this.pingUpdateTolerance = pingUpdate * 2 / 3;
if (webserviceInfo.isDistributed()) {
try {
this.taskRef = taskScheduler.scheduleAtFixedRate(this::updateCache, pingUpdate);
this.taskRef = taskScheduler.scheduleAtFixedRate(this::updatePings, pingUpdate);
} catch (final Exception e) {
log.error("Failed to initialize distributed ping cache update task");
this.taskRef = null;
@ -70,6 +75,10 @@ public class DistributedPingCache implements DisposableBean {
}
}
public ClientEventLastPingMapper getClientEventLastPingMapper() {
return this.clientEventLastPingMapper;
}
@Transactional
public Long initPingForConnection(final Long connectionId) {
try {
@ -129,17 +138,6 @@ public class DistributedPingCache implements DisposableBean {
}
}
public void updatePing(final Long pingRecordId, final Long pingTime) {
try {
this.clientEventLastPingMapper
.updatePingTime(pingRecordId, pingTime);
} catch (final Exception e) {
log.error("Failed to update ping for ping record id -> {}", pingRecordId);
}
}
@Transactional
public void deletePingForConnection(final Long connectionId) {
try {
@ -148,33 +146,49 @@ public class DistributedPingCache implements DisposableBean {
log.debug("*** Delete ping record for SEB connection: {}", connectionId);
}
this.clientEventRecordMapper
.deleteByExample()
final Collection<ClientEventLastPingRecord> records = this.clientEventLastPingMapper
.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.LAST_PING.id))
.build()
.execute();
if (records == null || records.isEmpty()) {
return;
}
final Long id = records.iterator().next().id;
this.pingCache.remove(id);
this.clientEventRecordMapper.deleteByPrimaryKey(id);
} catch (final Exception e) {
log.error("Failed to delete ping for connection -> {}", connectionId, e);
} finally {
this.pingCache.remove(connectionId);
try {
log.info(
"Because of failed ping record deletion, "
+ "flushing the ping cache to ensure no dead connections pings remain in the cache");
this.pingCache.clear();
} catch (final Exception ee) {
log.error("Failed to force flushing the ping cache: ", e);
}
}
}
public Long getLastPing(final Long pingRecordId) {
public Long getLastPing(final Long pingRecordId, final boolean missing) {
try {
Long ping = this.pingCache.get(pingRecordId);
if (ping == null) {
if (ping == null && !missing) {
if (log.isDebugEnabled()) {
log.debug("*** Get and cache ping time: {}", pingRecordId);
}
ping = this.clientEventLastPingMapper.selectPingTimeByPrimaryKey(pingRecordId);
if (ping != null) {
this.pingCache.put(pingRecordId, ping);
}
}
// if we have a missing ping we need to check new ping from next update even if the cache was empty
if (ping != null || missing) {
this.pingCache.put(pingRecordId, ping);
}
return ping;
@ -184,18 +198,24 @@ public class DistributedPingCache implements DisposableBean {
}
}
@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED)
public void updateCache() {
private void updatePings() {
if (this.pingCache.isEmpty()) {
return;
}
final long millisecondsNow = Utils.getMillisecondsNow();
if (millisecondsNow - this.lastUpdate < this.pingUpdateTolerance) {
log.warn("Skip ping update schedule because the last one was less then 2 seconds ago");
return;
}
if (log.isDebugEnabled()) {
log.trace("*** Update distributed ping cache: {}", this.pingCache);
}
try {
final ArrayList<Long> pks = new ArrayList<>(this.pingCache.keySet());
final Map<Long, Long> mapping = this.clientEventLastPingMapper
.selectByExample()
@ -210,11 +230,14 @@ public class DistributedPingCache implements DisposableBean {
if (mapping != null) {
this.pingCache.clear();
this.pingCache.putAll(mapping);
this.lastUpdate = millisecondsNow;
}
} catch (final Exception e) {
log.error("Error while trying to update distributed ping cache: {}", this.pingCache, e);
}
this.lastUpdate = millisecondsNow;
}
@Override

View file

@ -10,10 +10,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.concurrent.Executor;
import org.joda.time.DateTimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
@ -22,6 +24,7 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.annotation.JsonIgnore;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
@ -44,8 +47,10 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
private boolean missingPing = false;
private boolean hidden = false;
public PingIntervalClientIndicator(final DistributedPingCache distributedPingCache) {
super(distributedPingCache);
public PingIntervalClientIndicator(
final DistributedPingCache distributedPingCache,
@Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor executor) {
super(distributedPingCache, executor);
this.cachingEnabled = true;
}
@ -122,16 +127,12 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
@Override
public final double computeValueAt(final long timestamp) {
if (!this.cachingEnabled && super.pingUpdate != null) {
if (!this.cachingEnabled && super.pingRecord != null) {
// if this indicator is not missing ping
if (!this.isMissingPing()) {
final Long lastPing = this.distributedPingCache.getLastPing(super.pingRecord);
if (lastPing != null) {
final double doubleValue = lastPing.doubleValue();
return Math.max(Double.isNaN(this.currentValue) ? doubleValue : this.currentValue, doubleValue);
}
final Long lastPing = this.distributedPingCache.getLastPing(super.pingUpdate.pingRecord, this.missingPing);
if (lastPing != null) {
final double doubleValue = lastPing.doubleValue();
return Math.max(Double.isNaN(this.currentValue) ? doubleValue : this.currentValue, doubleValue);
}
return this.currentValue;

View file

@ -110,19 +110,13 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString());
populateFilterMap(filterMap, institutionId, sort);
try {
return this.paginationService.getPage(
pageNumber,
pageSize,
sort,
getSQLTableOfEntity().name(),
() -> this.clientEventDAO.allMatchingExtended(filterMap, this::hasReadAccess))
.getOrThrow();
} catch (final Exception e) {
e.printStackTrace();
throw e;
}
return this.paginationService.getPage(
pageNumber,
pageSize,
sort,
getSQLTableOfEntity().name(),
() -> this.clientEventDAO.allMatchingExtended(filterMap, this::hasReadAccess))
.getOrThrow();
}
@Override

View file

@ -270,32 +270,31 @@ public class ExamAPI_V1_Controller {
final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER);
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
if (log.isTraceEnabled()) {
log.trace("****************** SEB client connection: {} ip: {}",
connectionToken,
getClientAddress(request));
}
if (instructionConfirm != null) {
this.sebClientConnectionService.confirmInstructionDone(connectionToken, instructionConfirm);
long pingTime;
try {
pingTime = Long.parseLong(timeStampString);
} catch (final Exception e) {
log.error("Invalid ping request: {}", connectionToken);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
return;
}
final String instruction = this.sebClientConnectionService
.notifyPing(
connectionToken,
Long.parseLong(timeStampString),
pingNumString != null ? Integer.parseInt(pingNumString) : -1);
pingTime,
pingNumString != null ? Integer.parseInt(pingNumString) : -1,
instructionConfirm);
if (instruction == null) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return;
}
try {
response.setStatus(HttpStatus.OK.value());
response.getOutputStream().write(instruction.getBytes());
} catch (final IOException e) {
log.error("Failed to send instruction as response: {}", connectionToken, e);
} else {
try {
response.setStatus(HttpStatus.OK.value());
response.getOutputStream().write(instruction.getBytes());
} catch (final IOException e) {
log.error("Failed to send instruction as response: {}", connectionToken, e);
}
}
}

View file

@ -8,7 +8,7 @@ sebserver.gui.webservice.address=localhost
sebserver.gui.webservice.port=8080
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
#sebserver.gui.webservice.poll-interval=1000
sebserver.gui.webservice.poll-interval=1000
sebserver.gui.theme=css/sebserver.css
sebserver.gui.list.page.size=15

View file

@ -13,12 +13,14 @@ spring.datasource.hikari.initializationFailTimeout=30000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.maximumPoolSize=5
spring.datasource.hikari.maximumPoolSize=10
spring.datasource.hikari.leakDetectionThreshold=2000
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000
sebserver.http.client.read-timeout=20000
sebserver.webservice.distributed.pingUpdate=1000
sebserver.webservice.distributed.connectionUpdate=2000
sebserver.webservice.clean-db-on-startup=false
# webservice configuration

View file

@ -10,12 +10,16 @@ server.tomcat.uri-encoding=UTF-8
logging.level.ch=INFO
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
logging.level.org.springframework.cache=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=TRACE
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=TRACE
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper=DEBUG
#logging.level.ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller=TRACE
logging.level.com.zaxxer.hikari=DEBUG
sebserver.http.client.connect-timeout=150000
sebserver.http.client.connection-request-timeout=100000
sebserver.http.client.read-timeout=200000
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000
sebserver.http.client.read-timeout=60000

View file

@ -23,6 +23,11 @@ sebserver.gui.http.webservice.port=8080
sebserver.gui.http.webservice.contextPath=${server.servlet.context-path}
sebserver.gui.entrypoint=/gui
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000
sebserver.http.client.read-timeout=60000
sebserver.gui.webservice.apipath=${sebserver.webservice.api.admin.endpoint}
# 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=2000

View file

@ -10,9 +10,6 @@ sebserver.init.adminaccount.username=sebserver-admin
sebserver.init.database.integrity.checks=true
sebserver.init.database.integrity.try-fix=true
sebserver.webservice.distributed=false
sebserver.webservice.distributed.pingUpdate=3000
### webservice caching
spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
spring.cache.jcache.config=classpath:config/ehcache.xml
@ -32,6 +29,7 @@ spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.maximumPoolSize=100
spring.datasource.hikari.leakDetectionThreshold=10000
### webservice security
spring.datasource.password=${sebserver.mariadb.password}
@ -41,6 +39,7 @@ sebserver.webservice.internalSecret=${sebserver.password}
### webservice networking
sebserver.webservice.forceMaster=false
sebserver.webservice.distributed=false
sebserver.webservice.distributed.pingUpdate=3000
sebserver.webservice.http.external.scheme=https
sebserver.webservice.http.external.servername=
sebserver.webservice.http.external.port=

View file

@ -86,12 +86,11 @@
<key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.model.exam.QuizData</value-type>
<expiry>
<ttl unit="minutes">1</ttl>
<ttl unit="minutes">10</ttl>
</expiry>
<resources>
<heap unit="entries">10000</heap>
</resources>
</cache>
</config>

View file

@ -9,6 +9,7 @@ sebserver.overall.help=Documentation
sebserver.overall.help.link=https://seb-server.readthedocs.io/en/latest/index.html
sebserver.overall.message.leave.without.save=You have unsaved changes!<br/>Are you sure you want to leave the page? The changes will be lost.
sebserver.overall.message.requesttimeout=There was a request timeout. If this is a search please try to narrow down your search by using the filter above and try again.
sebserver.overall.upload=Please select a file
sebserver.overall.upload.unsupported.file=This file type is not supported. Supported files are: {0}
sebserver.overall.action.modify.cancel=Cancel
@ -462,6 +463,7 @@ sebserver.exam.action.sebrestriction.enable=Apply SEB Lock
sebserver.exam.action.sebrestriction.disable=Release SEB Lock
sebserver.exam.action.sebrestriction.details=SEB Restriction Details
sebserver.exam.action.createClientToStartExam=Export Exam Connection Configuration
sebserver.exam.action.sebrestriction.release.confirm=You are about to release the SEB restriction lock for this exam on the LMS.<br/>Are you sure you want to release the SEB restriction?
sebserver.exam.info.pleaseSelect=At first please select an Exam from the list
@ -1805,6 +1807,8 @@ sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring
sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring
sebserver.monitoring.exam.connection.action.openTownhall.confirm=You are about to open the town-hall room and force all SEB clients to join the town-hall room.<br/>Are you sure to open the town-hall?
sebserver.monitoring.exam.connection.action.closeTownhall.confirm=You are about to close the town-hall room and force all SEB clients to join it's proctoring room.<br/>Are you sure to close the town-hall?
sebserver.monitoring.exam.connection.notificationlist.actions=
sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification

View file

@ -67,7 +67,7 @@ public class WebserviceTest extends AdministrationAPIIntegrationTester {
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
try {
Thread.sleep(5000);
Thread.sleep(500);
} catch (final InterruptedException e) {
}
@ -75,7 +75,7 @@ public class WebserviceTest extends AdministrationAPIIntegrationTester {
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
try {
Thread.sleep(6000);
Thread.sleep(600);
} catch (final InterruptedException e) {
}

View file

@ -10,6 +10,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import static org.junit.Assert.assertEquals;
import java.util.concurrent.Executor;
import org.joda.time.DateTimeUtils;
import org.junit.After;
import org.junit.Test;
@ -34,9 +36,10 @@ public class PingIntervalClientIndicatorTest {
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
final Executor executor = Mockito.mock(Executor.class);
final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(distributedPingCache);
new PingIntervalClientIndicator(distributedPingCache, executor);
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
}
@ -47,9 +50,10 @@ public class PingIntervalClientIndicatorTest {
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
final Executor executor = Mockito.mock(Executor.class);
final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(distributedPingCache);
new PingIntervalClientIndicator(distributedPingCache, executor);
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
DateTimeUtils.setCurrentMillisProvider(() -> 10L);
@ -63,9 +67,10 @@ public class PingIntervalClientIndicatorTest {
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class);
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
final Executor executor = Mockito.mock(Executor.class);
final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(distributedPingCache);
new PingIntervalClientIndicator(distributedPingCache, executor);
final JSONMapper jsonMapper = new JSONMapper();
final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator);
assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);

View file

@ -46,4 +46,5 @@ management.endpoints.web.base-path=/actuator
sebserver.webservice.api.exam.indicator.name=Ping
sebserver.webservice.api.exam.indicator.type=LAST_PING
sebserver.webservice.api.exam.indicator.color=b4b4b4
sebserver.webservice.api.exam.indicator.thresholds=[{"value":5000.0,"color":"22b14c"},{"value":10000.0,"color":"ff7e00"},{"value":15000.0,"color":"ed1c24"}]
sebserver.webservice.api.exam.indicator.thresholds=[{"value":5000.0,"color":"22b14c"},{"value":10000.0,"color":"ff7e00"},{"value":15000.0,"color":"ed1c24"}]
sebserver.webservice.master.delay.threshold=1000