SEBSERV-250 improved webservice init

This commit is contained in:
anhefti 2021-12-07 10:41:54 +01:00
parent bab4b95609
commit dfb8afb740
18 changed files with 235 additions and 144 deletions

View file

@ -10,11 +10,16 @@ package ch.ethz.seb.sebserver;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import ch.ethz.seb.sebserver.webservice.WebserviceInit;
public class SEBServerInitEvent extends ApplicationEvent { public class SEBServerInitEvent extends ApplicationEvent {
private static final long serialVersionUID = -3608628289559324471L; private static final long serialVersionUID = -3608628289559324471L;
public SEBServerInitEvent(final Object source) { public final WebserviceInit webserviceInit;
super(source);
public SEBServerInitEvent(final WebserviceInit webserviceInit) {
super(webserviceInit);
this.webserviceInit = webserviceInit;
} }
} }

View file

@ -43,16 +43,10 @@ public class GuiInit implements ApplicationListener<ApplicationReadyEvent> {
this.sebServerInit.init(); this.sebServerInit.init();
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("----> **** GUI Service starting up... ****"); SEBServerInit.INIT_LOGGER.info("----> *** GUI Service starting up... ***");
SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> GUI Service successfully successfully started up!");
SEBServerInit.INIT_LOGGER.info("---->");
// final String webServiceProtocol = this.environment.getProperty("sebserver.gui.webservice.protocol", "http");
// final String webServiceAddress = this.environment.getRequiredProperty("sebserver.gui.webservice.address");
// final String webServicePort = this.environment.getProperty("sebserver.gui.webservice.port", "80");
SEBServerInit.INIT_LOGGER.info( SEBServerInit.INIT_LOGGER.info(
"----> Webservice connection: {}", "----> Webservice connection: {}",
@ -82,6 +76,11 @@ public class GuiInit implements ApplicationListener<ApplicationReadyEvent> {
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> Webservice admin API basic access: --" + webServiceAPIBasicAccess + "--"); SEBServerInit.INIT_LOGGER.info("----> Webservice admin API basic access: --" + webServiceAPIBasicAccess + "--");
SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("----> *** GUI Service successfully successfully started up! ***");
SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
} }
} }

View file

@ -60,6 +60,8 @@ public class WebserviceInfo {
private final boolean isDistributed; private final boolean isDistributed;
private final String webserviceUUID; private final String webserviceUUID;
private final long distributedPingUpdateInterval;
private Map<String, String> lmsExternalAddressAlias; private Map<String, String> lmsExternalAddressAlias;
public WebserviceInfo(final Environment environment) { public WebserviceInfo(final Environment environment) {
@ -74,6 +76,11 @@ public class WebserviceInfo {
this.discoveryEndpoint = environment.getRequiredProperty(WEB_SERVICE_EXAM_API_DISCOVERY_ENDPOINT_KEY); this.discoveryEndpoint = environment.getRequiredProperty(WEB_SERVICE_EXAM_API_DISCOVERY_ENDPOINT_KEY);
this.contextPath = environment.getProperty(WEB_SERVICE_CONTEXT_PATH, ""); this.contextPath = environment.getProperty(WEB_SERVICE_CONTEXT_PATH, "");
this.distributedPingUpdateInterval = environment.getProperty(
"sebserver.webservice.distributed.pingUpdate",
Long.class,
3000L);
if (StringUtils.isEmpty(this.webserverName)) { if (StringUtils.isEmpty(this.webserverName)) {
log.warn("NOTE: External server name, property : 'sebserver.webservice.http.external.servername' " log.warn("NOTE: External server name, property : 'sebserver.webservice.http.external.servername' "
+ "is not set from configuration. The external server name is set to the server address!"); + "is not set from configuration. The external server name is set to the server address!");
@ -160,6 +167,10 @@ public class WebserviceInfo {
return this.serverURLPrefix + this.discoveryEndpoint; return this.serverURLPrefix + this.discoveryEndpoint;
} }
public long getDistributedPingUpdateInterval() {
return this.distributedPingUpdateInterval;
}
public String getLocalHostName() { public String getLocalHostName() {
try { try {
return InetAddress.getLocalHost().getHostName(); return InetAddress.getLocalHost().getHostName();

View file

@ -15,6 +15,7 @@ import javax.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -31,6 +32,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
@Import(DataSourceAutoConfiguration.class) @Import(DataSourceAutoConfiguration.class)
public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent> { public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent> {
private final ApplicationContext applicationContext;
private final SEBServerInit sebServerInit; private final SEBServerInit sebServerInit;
private final Environment environment; private final Environment environment;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
@ -41,20 +43,26 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
protected WebserviceInit( protected WebserviceInit(
final SEBServerInit sebServerInit, final SEBServerInit sebServerInit,
final Environment environment,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AdminUserInitializer adminUserInitializer, final AdminUserInitializer adminUserInitializer,
final ApplicationEventPublisher applicationEventPublisher, final ApplicationEventPublisher applicationEventPublisher,
final WebserviceInfoDAO webserviceInfoDAO, final WebserviceInfoDAO webserviceInfoDAO,
final DBIntegrityChecker dbIntegrityChecker) { final DBIntegrityChecker dbIntegrityChecker,
final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
this.sebServerInit = sebServerInit; this.sebServerInit = sebServerInit;
this.environment = environment; this.environment = applicationContext.getEnvironment();
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.adminUserInitializer = adminUserInitializer; this.adminUserInitializer = adminUserInitializer;
this.applicationEventPublisher = applicationEventPublisher; this.applicationEventPublisher = applicationEventPublisher;
this.webserviceInfoDAO = webserviceInfoDAO; this.webserviceInfoDAO = webserviceInfoDAO;
this.dbIntegrityChecker = dbIntegrityChecker; this.dbIntegrityChecker = dbIntegrityChecker;
}
public ApplicationContext getApplicationContext() {
return this.applicationContext;
} }
@Override @Override
@ -62,17 +70,21 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
this.sebServerInit.init(); this.sebServerInit.init();
SEBServerInit.INIT_LOGGER.info("----> **** Webservice starting up... ****"); SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("----> *** Webservice starting up... ***");
SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("----> "); SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Register Webservice: {}", this.webserviceInfo.getWebserviceUUID()); SEBServerInit.INIT_LOGGER.info("----> Register Webservice: {}", this.webserviceInfo.getWebserviceUUID());
try { try {
this.webserviceInfoDAO.register( final boolean register = this.webserviceInfoDAO.register(
this.webserviceInfo.getWebserviceUUID(), this.webserviceInfo.getWebserviceUUID(),
InetAddress.getLocalHost().getHostAddress()); InetAddress.getLocalHost().getHostAddress());
if (register) {
SEBServerInit.INIT_LOGGER.info("----> Successfully register Webservice instance");
}
} catch (final Exception e) { } catch (final Exception e) {
SEBServerInit.INIT_LOGGER.error("Failed to register webservice: ", e); SEBServerInit.INIT_LOGGER.error("----> Failed to register webservice: ", e);
} }
SEBServerInit.INIT_LOGGER.info("----> "); SEBServerInit.INIT_LOGGER.info("----> ");
@ -81,23 +93,31 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
this.applicationEventPublisher.publishEvent(new SEBServerInitEvent(this)); this.applicationEventPublisher.publishEvent(new SEBServerInitEvent(this));
SEBServerInit.INIT_LOGGER.info("----> "); // Run the data base integrity checks and fixes if configured
SEBServerInit.INIT_LOGGER.info("----> **** Webservice successfully started up! **** "); this.dbIntegrityChecker.checkIntegrity();
// Create an initial admin account if requested and not already in the data-base
this.adminUserInitializer.initAdminAccount();
SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("----> *** Webservice Info: ***");
SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> *** Info:");
SEBServerInit.INIT_LOGGER.info("----> JDBC connection pool max size: {}", SEBServerInit.INIT_LOGGER.info("----> JDBC connection pool max size: {}",
this.environment.getProperty("spring.datasource.hikari.maximumPoolSize")); this.environment.getProperty("spring.datasource.hikari.maximumPoolSize"));
if (this.webserviceInfo.isDistributed()) { if (this.webserviceInfo.isDistributed()) {
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Distributed Setup: {}", this.webserviceInfo.getWebserviceUUID()); SEBServerInit.INIT_LOGGER.info("----> Distributed Setup: {}", this.webserviceInfo.getWebserviceUUID());
SEBServerInit.INIT_LOGGER.info("------> Ping update time: {}", SEBServerInit.INIT_LOGGER.info("----> Ping update time: {}",
this.environment.getProperty("sebserver.webservice.distributed.pingUpdate")); this.environment.getProperty("sebserver.webservice.distributed.pingUpdate"));
SEBServerInit.INIT_LOGGER.info("------> Connection update time: {}", SEBServerInit.INIT_LOGGER.info("----> Connection update time: {}",
this.environment.getProperty("sebserver.webservice.distributed.connectionUpdate", "2000")); this.environment.getProperty("sebserver.webservice.distributed.connectionUpdate", "2000"));
} }
try { try {
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address")); SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address"));
SEBServerInit.INIT_LOGGER.info("----> Server port: {}", this.environment.getProperty("server.port")); SEBServerInit.INIT_LOGGER.info("----> Server port: {}", this.environment.getProperty("server.port"));
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");
@ -122,19 +142,18 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> Property Override Test: {}", this.webserviceInfo.getTestProperty()); SEBServerInit.INIT_LOGGER.info("----> Property Override Test: {}", this.webserviceInfo.getTestProperty());
// Run the data base integrity checks and fixes if configured SEBServerInit.INIT_LOGGER.info("---->");
this.dbIntegrityChecker.checkIntegrity(); SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
SEBServerInit.INIT_LOGGER.info("----> *** Webservice successfully started up! ***");
// Create an initial admin account if requested and not already in the data-base SEBServerInit.INIT_LOGGER.info("----> *********************************************************");
this.adminUserInitializer.initAdminAccount();
} }
@PreDestroy @PreDestroy
public void gracefulShutdown() { public void gracefulShutdown() {
SEBServerInit.INIT_LOGGER.info("**** Gracefully Shutdown of SEB Server instance {} ****", SEBServerInit.INIT_LOGGER.info("*********************************************************");
SEBServerInit.INIT_LOGGER.info("**** Gracefully Shutdown of SEB Server instance {}",
this.webserviceInfo.getHostAddress()); this.webserviceInfo.getHostAddress());
SEBServerInit.INIT_LOGGER.info("---->"); SEBServerInit.INIT_LOGGER.info("---->");
SEBServerInit.INIT_LOGGER.info("----> Unregister Webservice: {}", this.webserviceInfo.getWebserviceUUID()); SEBServerInit.INIT_LOGGER.info("----> Unregister Webservice: {}", this.webserviceInfo.getWebserviceUUID());

View file

@ -50,12 +50,12 @@ public interface ClientPingMapper {
int update(UpdateStatementProvider updateStatement); int update(UpdateStatementProvider updateStatement);
@SelectProvider(type = SqlProviderAdapter.class, method = "select") @SelectProvider(type = SqlProviderAdapter.class, method = "select")
@ResultType(ClientEventLastPingRecord.class) @ResultType(ClientLastPingRecord.class)
@ConstructorArgs({ @ConstructorArgs({
@Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT), @Arg(column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
@Arg(column = "value", javaType = Long.class, jdbcType = JdbcType.BIGINT) @Arg(column = "value", javaType = Long.class, jdbcType = JdbcType.BIGINT)
}) })
Collection<ClientEventLastPingRecord> selectMany(SelectStatementProvider select); Collection<ClientLastPingRecord> selectMany(SelectStatementProvider select);
default Long selectPingTimeByPrimaryKey(final Long id_) { default Long selectPingTimeByPrimaryKey(final Long id_) {
return SelectDSL.selectWithMapper( return SelectDSL.selectWithMapper(
@ -78,7 +78,7 @@ public interface ClientPingMapper {
.execute(); .execute();
} }
default QueryExpressionDSL<MyBatis3SelectModelAdapter<Collection<ClientEventLastPingRecord>>> selectByExample() { default QueryExpressionDSL<MyBatis3SelectModelAdapter<Collection<ClientLastPingRecord>>> selectByExample() {
return SelectDSL.selectWithMapper( return SelectDSL.selectWithMapper(
this::selectMany, this::selectMany,
@ -95,12 +95,12 @@ public interface ClientPingMapper {
.execute(); .execute();
} }
final class ClientEventLastPingRecord { final class ClientLastPingRecord {
public final Long id; public final Long id;
public final Long lastPingTime; public final Long lastPingTime;
public ClientEventLastPingRecord( public ClientLastPingRecord(
final Long id, final Long id,
final Long value) { final Long value) {

View file

@ -41,6 +41,8 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionR
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;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientIndicatorRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientIndicatorRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientInstructionRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientInstructionRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientInstructionRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientInstructionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport;
@ -63,15 +65,18 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
private final ClientConnectionRecordMapper clientConnectionRecordMapper; private final ClientConnectionRecordMapper clientConnectionRecordMapper;
private final ClientEventRecordMapper clientEventRecordMapper; private final ClientEventRecordMapper clientEventRecordMapper;
private final ClientInstructionRecordMapper clientInstructionRecordMapper; private final ClientInstructionRecordMapper clientInstructionRecordMapper;
private final ClientIndicatorRecordMapper clientIndicatorRecordMapper;
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) {
this.clientConnectionRecordMapper = clientConnectionRecordMapper; this.clientConnectionRecordMapper = clientConnectionRecordMapper;
this.clientEventRecordMapper = clientEventRecordMapper; this.clientEventRecordMapper = clientEventRecordMapper;
this.clientInstructionRecordMapper = clientInstructionRecordMapper; this.clientInstructionRecordMapper = clientInstructionRecordMapper;
this.clientIndicatorRecordMapper = clientIndicatorRecordMapper;
} }
@Override @Override
@ -474,7 +479,15 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
return Collections.emptyList(); return Collections.emptyList();
} }
// first delete all related client events // delete all related client indicators
this.clientIndicatorRecordMapper.deleteByExample()
.where(
ClientIndicatorRecordDynamicSqlSupport.clientConnectionId,
SqlBuilder.isIn(ids))
.build()
.execute();
// delete all related client events
this.clientEventRecordMapper.deleteByExample() this.clientEventRecordMapper.deleteByExample()
.where( .where(
ClientEventRecordDynamicSqlSupport.clientConnectionId, ClientEventRecordDynamicSqlSupport.clientConnectionId,

View file

@ -237,18 +237,19 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override @Override
public Result<Boolean> isProctoringEnabled(final Long examId) { public Result<Boolean> isProctoringEnabled(final Long examId) {
final Result<Boolean> result = this.additionalAttributesDAO.getAdditionalAttribute( return this.additionalAttributesDAO.getAdditionalAttribute(
EntityType.EXAM, EntityType.EXAM,
examId, examId,
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING) ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)
.map(rec -> BooleanUtils.toBoolean(rec.getValue())) .map(rec -> BooleanUtils.toBoolean(rec.getValue()))
.onError(error -> log.warn("Failed to verify proctoring enabled for exam: {}, {}", .onErrorDo(error -> {
examId, if (log.isDebugEnabled()) {
error.getMessage())); log.warn("Failed to verify proctoring enabled for exam: {}, {}",
if (result.hasError()) { examId,
return Result.of(false); error.getMessage());
} }
return result; return false;
});
} }
@Override @Override

View file

@ -15,6 +15,7 @@ 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.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue; import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.ClientIndicatorType;
/** A client indicator is a indicator value holder for a specific Indicator /** A client indicator is a indicator value holder for a specific Indicator
* on a running client connection. * on a running client connection.
@ -31,6 +32,11 @@ public interface ClientIndicator extends IndicatorValue {
* @param cachingEnabled defines whether indicator value caching is enabled or not. */ * @param cachingEnabled defines whether indicator value caching is enabled or not. */
void init(Indicator indicatorDefinition, Long connectionId, boolean active, boolean cachingEnabled); void init(Indicator indicatorDefinition, Long connectionId, boolean active, boolean cachingEnabled);
/** Get the client indicator type
*
* @return the client indicator type */
ClientIndicatorType indicatorType();
/** Get the exam identifier of the client connection of this ClientIndicator /** Get the exam identifier of the client connection of this ClientIndicator
* *
* @return the exam identifier of the client connection of this ClientIndicator */ * @return the exam identifier of the client connection of this ClientIndicator */

View file

@ -44,7 +44,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.DistributedPingCache; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.DistributedPingService;
import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException; import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException;
@Lazy @Lazy
@ -70,7 +70,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final SEBClientNotificationService sebClientNotificationService; private final SEBClientNotificationService sebClientNotificationService;
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final DistributedPingCache distributedPingCache; private final DistributedPingService distributedPingCache;
private final boolean isDistributedSetup; private final boolean isDistributedSetup;
protected SEBClientConnectionServiceImpl( protected SEBClientConnectionServiceImpl(
@ -80,7 +80,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
final SEBClientInstructionService sebInstructionService, final SEBClientInstructionService sebInstructionService,
final SEBClientNotificationService sebClientNotificationService, final SEBClientNotificationService sebClientNotificationService,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final DistributedPingCache distributedPingCache) { final DistributedPingService distributedPingCache) {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionService.getExamSessionCacheService(); this.examSessionCacheService = examSessionService.getExamSessionCacheService();

View file

@ -11,18 +11,14 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
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;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; 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.ClientPingMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator.DistributedPingService.PingUpdate;
public abstract class AbstractPingIndicator extends AbstractClientIndicator { public abstract class AbstractPingIndicator extends AbstractClientIndicator {
@ -30,17 +26,11 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class)); private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
private final Executor executor; protected final DistributedPingService distributedPingCache;
protected final DistributedPingCache distributedPingCache;
protected PingUpdate pingUpdate = null; protected PingUpdate pingUpdate = null;
protected AbstractPingIndicator( protected AbstractPingIndicator(final DistributedPingService distributedPingCache) {
final DistributedPingCache distributedPingCache,
@Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor executor) {
super(); super();
this.executor = executor;
this.distributedPingCache = distributedPingCache; this.distributedPingCache = distributedPingCache;
} }
@ -55,9 +45,9 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
if (!this.cachingEnabled && this.active) { if (!this.cachingEnabled && this.active) {
try { try {
createPingUpdate(); this.pingUpdate = this.distributedPingCache.createPingUpdate(connectionId);
} catch (final Exception e) { } catch (final Exception e) {
createPingUpdate(); this.pingUpdate = this.distributedPingCache.createPingUpdate(connectionId);
} }
} }
} }
@ -74,15 +64,7 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
} }
} }
// Update last ping time on persistent storage asynchronously within a defines thread pool with no this.distributedPingCache.updatePingAsync(this.pingUpdate);
// waiting queue to skip further ping updates if all update threads are busy
try {
this.executor.execute(this.pingUpdate);
} catch (final Exception e) {
if (log.isDebugEnabled()) {
log.warn("Failed to schedule ping task: {}" + e.getMessage());
}
}
} }
} }
@ -93,21 +75,15 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
} }
try { try {
createPingUpdate(); this.pingUpdate = this.distributedPingCache.createPingUpdate(this.connectionId);
if (this.pingUpdate == null) { if (this.pingUpdate == null) {
createPingUpdate(); this.pingUpdate = this.distributedPingCache.createPingUpdate(this.connectionId);
} }
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to recover ping record for connection: {}", this.connectionId, e); log.error("Failed to recover ping record for connection: {}", this.connectionId, e);
} }
} }
private void createPingUpdate() {
this.pingUpdate = new PingUpdate(
this.distributedPingCache.getClientPingMapper(),
this.distributedPingCache.initPingForConnection(this.connectionId));
}
@Override @Override
public Set<EventType> observedEvents() { public Set<EventType> observedEvents() {
return this.EMPTY_SET; return this.EMPTY_SET;
@ -115,26 +91,4 @@ public abstract class AbstractPingIndicator extends AbstractClientIndicator {
public abstract ClientEventRecord updateLogEvent(final long now); public abstract ClientEventRecord updateLogEvent(final long now);
static final class PingUpdate implements Runnable {
private final ClientPingMapper clientPingMapper;
final Long pingRecord;
public PingUpdate(final ClientPingMapper clientPingMapper, final Long pingRecord) {
this.clientPingMapper = clientPingMapper;
this.pingRecord = pingRecord;
}
@Override
public void run() {
try {
this.clientPingMapper
.updatePingTime(this.pingRecord, Utils.getMillisecondsNow());
} catch (final Exception e) {
log.error("Failed to update ping: {}", e.getMessage());
}
}
}
} }

View file

@ -45,4 +45,9 @@ public class BatteryStatusIndicator extends AbstractLogNumberIndicator {
return IndicatorType.BATTERY_STATUS; return IndicatorType.BATTERY_STATUS;
} }
@Override
public ClientIndicatorType indicatorType() {
return ClientIndicatorType.BATTERY_STATUS;
}
} }

View file

@ -13,6 +13,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -20,50 +21,78 @@ import org.joda.time.DateTimeUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.SEBServerInit;
import ch.ethz.seb.sebserver.SEBServerInitEvent;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientPingMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientPingMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientPingMapper.ClientEventLastPingRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.ClientPingMapper.ClientLastPingRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientIndicatorRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientIndicatorRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientIndicatorRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientIndicatorRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientIndicatorRecord;
@Lazy @Lazy
@Component @Component
@WebServiceProfile @WebServiceProfile
public class DistributedPingCache implements DisposableBean { public class DistributedPingService implements DisposableBean {
private static final Logger log = LoggerFactory.getLogger(DistributedPingCache.class); private static final Logger log = LoggerFactory.getLogger(DistributedPingService.class);
private final Executor pingUpdateExecutor;
private final ClientIndicatorRecordMapper clientIndicatorRecordMapper; private final ClientIndicatorRecordMapper clientIndicatorRecordMapper;
private final ClientPingMapper clientPingMapper; private final ClientPingMapper clientPingMapper;
private final long pingUpdateTolerance; private long pingUpdateTolerance;
private ScheduledFuture<?> taskRef; private ScheduledFuture<?> taskRef;
private final Map<Long, Long> pingCache = new ConcurrentHashMap<>(); private final Map<Long, Long> pingCache = new ConcurrentHashMap<>();
private long lastUpdate = 0L; private long lastUpdate = 0L;
public DistributedPingCache( public DistributedPingService(
@Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor pingUpdateExecutor,
final ClientIndicatorRecordMapper clientIndicatorRecordMapper, final ClientIndicatorRecordMapper clientIndicatorRecordMapper,
final ClientPingMapper clientPingMapper, final ClientPingMapper clientPingMapper) {
final WebserviceInfo webserviceInfo,
final TaskScheduler taskScheduler,
@Value("${sebserver.webservice.distributed.pingUpdate:3000}") final long pingUpdate) {
this.pingUpdateExecutor = pingUpdateExecutor;
this.clientIndicatorRecordMapper = clientIndicatorRecordMapper; this.clientIndicatorRecordMapper = clientIndicatorRecordMapper;
this.clientPingMapper = clientPingMapper; this.clientPingMapper = clientPingMapper;
this.pingUpdateTolerance = pingUpdate * 2 / 3; }
@EventListener(SEBServerInitEvent.class)
public void init(final SEBServerInitEvent initEvent) {
final ApplicationContext applicationContext = initEvent.webserviceInit.getApplicationContext();
final WebserviceInfo webserviceInfo = applicationContext.getBean(WebserviceInfo.class);
if (webserviceInfo.isDistributed()) { if (webserviceInfo.isDistributed()) {
SEBServerInit.INIT_LOGGER.info("------>");
SEBServerInit.INIT_LOGGER.info("------> Activate distributed ping service:");
final TaskScheduler taskScheduler = applicationContext.getBean(TaskScheduler.class);
final long distributedPingUpdateInterval = webserviceInfo.getDistributedPingUpdateInterval();
this.pingUpdateTolerance = distributedPingUpdateInterval * 2 / 3;
SEBServerInit.INIT_LOGGER.info("------> with distributedPingUpdateInterval: {}",
distributedPingUpdateInterval);
SEBServerInit.INIT_LOGGER.info("------> with taskScheduler: {}", taskScheduler);
try { try {
this.taskRef = taskScheduler.scheduleAtFixedRate(this::updatePings, pingUpdate); this.taskRef = taskScheduler.scheduleAtFixedRate(
this::persistentPingUpdate,
distributedPingUpdateInterval);
SEBServerInit.INIT_LOGGER.info("------> distributed ping service successfully initialized!");
} catch (final Exception e) { } catch (final Exception e) {
SEBServerInit.INIT_LOGGER.error("------> Failed to initialize distributed ping service:", e);
log.error("Failed to initialize distributed ping cache update task"); log.error("Failed to initialize distributed ping cache update task");
this.taskRef = null; this.taskRef = null;
} }
@ -141,10 +170,10 @@ public class DistributedPingCache implements DisposableBean {
log.debug("*** Delete ping record for SEB connection: {}", connectionId); log.debug("*** Delete ping record for SEB connection: {}", connectionId);
} }
final Collection<ClientEventLastPingRecord> records = this.clientPingMapper final Collection<ClientLastPingRecord> records = this.clientPingMapper
.selectByExample() .selectByExample()
.where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId)) .where(ClientIndicatorRecordDynamicSqlSupport.clientConnectionId, isEqualTo(connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(ClientIndicatorType.LAST_PING.id)) .and(ClientIndicatorRecordDynamicSqlSupport.type, isEqualTo(ClientIndicatorType.LAST_PING.id))
.build() .build()
.execute(); .execute();
@ -194,8 +223,7 @@ public class DistributedPingCache implements DisposableBean {
} }
} }
private void updatePings() { public void persistentPingUpdate() {
if (this.pingCache.isEmpty()) { if (this.pingCache.isEmpty()) {
return; return;
} }
@ -215,7 +243,7 @@ public class DistributedPingCache implements DisposableBean {
final Map<Long, Long> mapping = this.clientPingMapper final Map<Long, Long> mapping = this.clientPingMapper
.selectByExample() .selectByExample()
.where( .where(
ClientEventRecordDynamicSqlSupport.type, ClientIndicatorRecordDynamicSqlSupport.type,
isEqualTo(ClientIndicatorType.LAST_PING.id)) isEqualTo(ClientIndicatorType.LAST_PING.id))
.build() .build()
.execute() .execute()
@ -235,6 +263,45 @@ public class DistributedPingCache implements DisposableBean {
this.lastUpdate = millisecondsNow; this.lastUpdate = 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
void updatePingAsync(final PingUpdate pingUpdate) {
try {
this.pingUpdateExecutor.execute(pingUpdate);
} catch (final Exception e) {
if (log.isDebugEnabled()) {
log.warn("Failed to schedule ping task: {}" + e.getMessage());
}
}
}
PingUpdate createPingUpdate(final Long connectionId) {
return new PingUpdate(
this.getClientPingMapper(),
this.initPingForConnection(connectionId));
}
static final class PingUpdate implements Runnable {
private final ClientPingMapper clientPingMapper;
final Long pingRecord;
public PingUpdate(final ClientPingMapper clientPingMapper, final Long pingRecord) {
this.clientPingMapper = clientPingMapper;
this.pingRecord = pingRecord;
}
@Override
public void run() {
try {
this.clientPingMapper
.updatePingTime(this.pingRecord, Utils.getMillisecondsNow());
} catch (final Exception e) {
log.error("Failed to update ping: {}", e.getMessage());
}
}
}
@Override @Override
public void destroy() throws Exception { public void destroy() throws Exception {
if (this.taskRef != null) { if (this.taskRef != null) {

View file

@ -31,4 +31,9 @@ public final class ErrorLogCountClientIndicator extends AbstractLogLevelCountInd
return IndicatorType.ERROR_COUNT; return IndicatorType.ERROR_COUNT;
} }
@Override
public ClientIndicatorType indicatorType() {
return ClientIndicatorType.ERROR_LOG_COUNT;
}
} }

View file

@ -31,4 +31,9 @@ public class InfoLogCountClientIndicator extends AbstractLogLevelCountIndicator
return IndicatorType.INFO_COUNT; return IndicatorType.INFO_COUNT;
} }
@Override
public ClientIndicatorType indicatorType() {
return ClientIndicatorType.INFO_LOG_COUNT;
}
} }

View file

@ -10,12 +10,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Comparator; import java.util.Comparator;
import java.util.concurrent.Executor;
import org.joda.time.DateTimeUtils; import org.joda.time.DateTimeUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
@ -24,7 +22,6 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import ch.ethz.seb.sebserver.gbl.Constants; 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;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; 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;
@ -47,10 +44,8 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
private boolean missingPing = false; private boolean missingPing = false;
private boolean hidden = false; private boolean hidden = false;
public PingIntervalClientIndicator( public PingIntervalClientIndicator(final DistributedPingService distributedPingCache) {
final DistributedPingCache distributedPingCache, super(distributedPingCache);
@Qualifier(AsyncServiceSpringConfig.EXAM_API_PING_SERVICE_EXECUTOR_BEAN_NAME) final Executor executor) {
super(distributedPingCache, executor);
this.cachingEnabled = true; this.cachingEnabled = true;
} }
@ -89,6 +84,11 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
} }
@Override
public ClientIndicatorType indicatorType() {
return ClientIndicatorType.LAST_PING;
}
@JsonIgnore @JsonIgnore
public final boolean isMissingPing() { public final boolean isMissingPing() {
return this.missingPing; return this.missingPing;

View file

@ -45,4 +45,9 @@ public class WLANStatusIndicator extends AbstractLogNumberIndicator {
return IndicatorType.WLAN_STATUS; return IndicatorType.WLAN_STATUS;
} }
@Override
public ClientIndicatorType indicatorType() {
return ClientIndicatorType.WLAN_STATUS;
}
} }

View file

@ -30,4 +30,9 @@ public class WarnLogCountClientIndicator extends AbstractLogLevelCountIndicator
public IndicatorType getType() { public IndicatorType getType() {
return IndicatorType.WARN_COUNT; return IndicatorType.WARN_COUNT;
} }
@Override
public ClientIndicatorType indicatorType() {
return ClientIndicatorType.WARN_LOG_COUNT;
}
} }

View file

@ -10,8 +10,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.util.concurrent.Executor;
import org.joda.time.DateTimeUtils; import org.joda.time.DateTimeUtils;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
@ -20,7 +18,6 @@ import org.mockito.Mockito;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
public class PingIntervalClientIndicatorTest { public class PingIntervalClientIndicatorTest {
@ -34,12 +31,10 @@ public class PingIntervalClientIndicatorTest {
DateTimeUtils.setCurrentMillisProvider(() -> 1L); DateTimeUtils.setCurrentMillisProvider(() -> 1L);
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class); final DistributedPingService distributedPingCache = Mockito.mock(DistributedPingService.class);
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
final Executor executor = Mockito.mock(Executor.class);
final PingIntervalClientIndicator pingIntervalClientIndicator = final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(distributedPingCache, executor); new PingIntervalClientIndicator(distributedPingCache);
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue())); assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
} }
@ -48,12 +43,10 @@ public class PingIntervalClientIndicatorTest {
DateTimeUtils.setCurrentMillisProvider(() -> 1L); DateTimeUtils.setCurrentMillisProvider(() -> 1L);
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class); final DistributedPingService distributedPingCache = Mockito.mock(DistributedPingService.class);
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
final Executor executor = Mockito.mock(Executor.class);
final PingIntervalClientIndicator pingIntervalClientIndicator = final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(distributedPingCache, executor); new PingIntervalClientIndicator(distributedPingCache);
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue())); assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
DateTimeUtils.setCurrentMillisProvider(() -> 10L); DateTimeUtils.setCurrentMillisProvider(() -> 10L);
@ -65,12 +58,10 @@ public class PingIntervalClientIndicatorTest {
public void testSerialization() throws JsonProcessingException { public void testSerialization() throws JsonProcessingException {
DateTimeUtils.setCurrentMillisProvider(() -> 1L); DateTimeUtils.setCurrentMillisProvider(() -> 1L);
final ClientEventDAO clientEventDAO = Mockito.mock(ClientEventDAO.class); final DistributedPingService distributedPingCache = Mockito.mock(DistributedPingService.class);
final DistributedPingCache distributedPingCache = Mockito.mock(DistributedPingCache.class);
final Executor executor = Mockito.mock(Executor.class);
final PingIntervalClientIndicator pingIntervalClientIndicator = final PingIntervalClientIndicator pingIntervalClientIndicator =
new PingIntervalClientIndicator(distributedPingCache, executor); new PingIntervalClientIndicator(distributedPingCache);
final JSONMapper jsonMapper = new JSONMapper(); final JSONMapper jsonMapper = new JSONMapper();
final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator); final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator);
assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json); assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);