Merge remote-tracking branch 'origin/dev-1.1-patch-1' into development

Conflicts:
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java
This commit is contained in:
anhefti 2021-06-16 21:11:42 +02:00
commit 31fbfa373c
6 changed files with 82 additions and 37 deletions

View file

@ -242,7 +242,7 @@ public class MonitoringClientConnection implements TemplateComposer {
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION) .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
.withParentEntityKey(parentEntityKey) .withParentEntityKey(parentEntityKey)
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY) .withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
.withExec(action -> this.confirmNotification(action, connectionData)) .withExec(action -> this.confirmNotification(action, connectionData, t))
.noEventPropagation() .noEventPropagation()
.create()) .create())
.withSelectionListener(this.pageService.getSelectionPublisher( .withSelectionListener(this.pageService.getSelectionPublisher(
@ -256,8 +256,10 @@ public class MonitoringClientConnection implements TemplateComposer {
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY) .withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
.withSelect( .withSelect(
() -> notificationTable.getSelection(), () -> notificationTable.getSelection(),
action -> this.confirmNotification(action, connectionData), action -> this.confirmNotification(action, connectionData, notificationTable),
NOTIFICATION_LIST_NO_SELECTION_KEY) NOTIFICATION_LIST_NO_SELECTION_KEY)
.noEventPropagation()
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false); .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
@ -409,9 +411,10 @@ public class MonitoringClientConnection implements TemplateComposer {
private PageAction confirmNotification( private PageAction confirmNotification(
final PageAction pageAction, final PageAction pageAction,
final ClientConnectionData connectionData) { final ClientConnectionData connectionData,
final EntityTable<ClientNotification> table) {
final EntityKey entityKey = pageAction.getSingleSelection(); final EntityKey entityKey = table.getSingleSelection();
final EntityKey parentEntityKey = pageAction.getParentEntityKey(); final EntityKey parentEntityKey = pageAction.getParentEntityKey();
this.pageService.getRestService() this.pageService.getRestService()
@ -422,13 +425,8 @@ public class MonitoringClientConnection implements TemplateComposer {
.call() .call()
.getOrThrow(); .getOrThrow();
return pageAction table.reset();
.withEntityKey( return pageAction;
new EntityKey(connectionData.getConnectionId(),
EntityType.CLIENT_CONNECTION))
.withAttribute(
Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
connectionData.clientConnection.connectionToken);
} }
private String getClientTime(final ClientEvent event) { private String getClientTime(final ClientEvent event) {

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
@ -28,10 +29,29 @@ public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
FilterMap filterMap, FilterMap filterMap,
Predicate<ExtendedClientEvent> predicate); Predicate<ExtendedClientEvent> predicate);
/** Get a specified notification by id (PK)
*
* @param notificationId The PK of the notification
* @return Result refer to the specified ClientNotification or to an error when happened */
Result<ClientNotification> getPendingNotification(Long notificationId); Result<ClientNotification> getPendingNotification(Long notificationId);
/** Get all pending notifications for a given client connection.
*
* @param clientConnectionId The client connection identifier
* @return Result refer to the list of pending notifications or to an error when happened */
Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId); Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);
/** Get all identifiers (PKs) of client connections of a given exam that has any pending notification
*
* @param examId the exam identifier
* @return Result refer to a set of identifiers of client connections or to an error when happened */
Result<Set<Long>> getClientConnectionIdsWithPendingNotification(Long examId);
/** Used to confirm a pending notification so that the notification is not pending anymore
*
* @param notificationId the notification identifier
* @param clientConnectionId the client connection identifier
* @return Result refer to the confirmed notification or to en error when happened */
Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId); Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId);
} }

View file

@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; 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.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
@ -212,6 +213,31 @@ public class ClientEventDAOImpl implements ClientEventDAO {
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
@Override
@Transactional(readOnly = true)
public Result<Set<Long>> getClientConnectionIdsWithPendingNotification(final Long examId) {
return Result.tryCatch(() -> this.clientEventRecordMapper
.selectByExample()
.leftJoin(ClientConnectionRecordDynamicSqlSupport.clientConnectionRecord)
.on(
ClientConnectionRecordDynamicSqlSupport.id,
equalTo(ClientEventRecordDynamicSqlSupport.clientConnectionId))
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
isEqualToWhenPresent(examId))
.and(
ClientConnectionRecordDynamicSqlSupport.status,
isEqualTo(ConnectionStatus.ACTIVE.name()))
.and(
ClientEventRecordDynamicSqlSupport.type,
isEqualTo(EventType.NOTIFICATION.id))
.build()
.execute()
.stream()
.map(ClientEventRecord::getClientConnectionId)
.collect(Collectors.toSet()));
}
@Override @Override
@Transactional @Transactional
public Result<ClientNotification> confirmPendingNotification(final Long notificationId, public Result<ClientNotification> confirmPendingNotification(final Long notificationId,

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.List; import java.util.List;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -24,7 +25,7 @@ public interface SEBClientNotificationService {
* *
* @param clientConnectionId the client connection identifier * @param clientConnectionId the client connection identifier
* @return true if there is any pending notification for the specified client connection */ * @return true if there is any pending notification for the specified client connection */
Boolean hasAnyPendingNotification(Long clientConnectionId); Boolean hasAnyPendingNotification(final ClientConnection clientConnection);
Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId); Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);

View file

@ -47,7 +47,7 @@ public class InternalClientConnectionDataFactory {
return new ClientConnectionDataInternal( return new ClientConnectionDataInternal(
clientConnection, clientConnection,
() -> this.sebClientNotificationService () -> this.sebClientNotificationService
.hasAnyPendingNotification(clientConnection.id), .hasAnyPendingNotification(clientConnection),
this.clientIndicatorFactory.createFor(clientConnection)); this.clientIndicatorFactory.createFor(clientConnection));
} }

View file

@ -20,6 +20,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
@ -43,6 +45,9 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
private final ClientEventDAO clientEventDAO; private final ClientEventDAO clientEventDAO;
private final SEBClientInstructionService sebClientInstructionService; private final SEBClientInstructionService sebClientInstructionService;
private final Set<Long> pendingNotifications = new HashSet<>(); private final Set<Long> pendingNotifications = new HashSet<>();
private final Set<Long> examUpdate = new HashSet<>();
private long lastUpdate = 0;
public SEBClientNotificationServiceImpl( public SEBClientNotificationServiceImpl(
final ClientEventDAO clientEventDAO, final ClientEventDAO clientEventDAO,
@ -52,31 +57,10 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
this.sebClientInstructionService = sebClientInstructionService; this.sebClientInstructionService = sebClientInstructionService;
} }
// TODO find a better (faster) way to update pending notifications
// get them all at certain interval with background update for example
// or update them all on every monitoring call
@Override @Override
public Boolean hasAnyPendingNotification(final Long clientConnectionId) { public Boolean hasAnyPendingNotification(final ClientConnection clientConnection) {
updateCache(clientConnection.examId);
if (this.pendingNotifications.contains(clientConnectionId)) { return this.pendingNotifications.contains(clientConnection.id);
return true;
}
final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId)
.getOr(Collections.emptyList())
.isEmpty();
if (hasAnyPendingNotification) {
// NOTE this is a quick and dirty way to keep cache pendingNotifications cache size short.
// TODO find a better way to do this.
if (this.pendingNotifications.size() > 100) {
this.pendingNotifications.clear();
}
this.pendingNotifications.add(clientConnectionId);
}
return hasAnyPendingNotification;
} }
@Override @Override
@ -146,4 +130,20 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
return notification; return notification;
} }
private final void updateCache(final Long examId) {
if (System.currentTimeMillis() - this.lastUpdate > 5 * Constants.SECOND_IN_MILLIS) {
this.examUpdate.clear();
this.pendingNotifications.clear();
this.lastUpdate = System.currentTimeMillis();
}
if (!this.examUpdate.contains(examId)) {
this.pendingNotifications.addAll(
this.clientEventDAO
.getClientConnectionIdsWithPendingNotification(examId)
.getOr(Collections.emptySet()));
this.examUpdate.add(examId);
}
}
} }