SEBSERV-139 refactoring of townhall and instruction service

This commit is contained in:
anhefti 2020-11-12 13:28:03 +01:00
parent 8e04e43bfa
commit 53bb378d0b
No known key found for this signature in database
GPG key ID: E9AD9471B6BC114D
8 changed files with 184 additions and 73 deletions

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.util;
import java.util.concurrent.ArrayBlockingQueue;
public class SizedArrayNonBlockingQueue<T> extends ArrayBlockingQueue<T> {
private static final long serialVersionUID = -4235702373708064610L;
private final int size;
public SizedArrayNonBlockingQueue(final int size) {
super(size);
this.size = size;
}
@Override
synchronized public boolean add(final T element) {
// Check if queue full already?
if (super.size() == this.size) {
// remove element from queue if queue is full
this.remove();
}
return super.add(element);
}
}

View file

@ -334,26 +334,19 @@ public class MonitoringRunningExam implements TemplateComposer {
if (proctoringSettings != null && proctoringSettings.enableProctoring) {
final RemoteProctoringRoom townhall = restService.getBuilder(GetTownhallRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOr(null);
final boolean townhallActive = townhall != null && townhall.id != null;
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withExec(this::openTownhallRoom)
.withExec(this::toggleTownhallRoom)
.noEventPropagation()
.publish();
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withExec(this::closeTownhallRoom)
.noEventPropagation()
.publish();
if (!townhallActive) {
if (isTownhallRoomActive(entityKey.modelId)) {
this.pageService.firePageEvent(
new ActionActivationEvent(false, ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM),
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)),
pageContext);
}
@ -376,6 +369,40 @@ public class MonitoringRunningExam implements TemplateComposer {
}
}
private boolean isTownhallRoomActive(final String examModelId) {
final RemoteProctoringRoom townhall = this.pageService.getRestService()
.getBuilder(GetTownhallRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examModelId)
.call()
.getOr(null);
return townhall != null && townhall.id != null;
}
private PageAction toggleTownhallRoom(final PageAction action) {
if (isTownhallRoomActive(action.getEntityKey().modelId)) {
closeTownhallRoom(action);
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)),
action.pageContext());
return action;
} else {
openTownhallRoom(action);
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)),
action.pageContext());
return action;
}
}
private PageAction openTownhallRoom(final PageAction action) {
final EntityKey examId = action.getEntityKey();
@ -407,11 +434,6 @@ public class MonitoringRunningExam implements TemplateComposer {
this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script);
proctoringGUIService.registerProctoringWindow(activeAllRoomName);
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM),
action.pageContext());
return action;
}
@ -431,14 +453,29 @@ public class MonitoringRunningExam implements TemplateComposer {
.getProctoringGUIService();
proctoringGUIService.closeRoom(townhall.name);
this.pageService.firePageEvent(
new ActionActivationEvent(
false,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM),
action.pageContext());
return action;
}
private void updateTownhallButton(final EntityKey entityKey, final PageContext pageContext) {
if (isTownhallRoomActive(entityKey.modelId)) {
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)),
pageContext);
} else {
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)),
pageContext);
}
}
private void updateRoomActions(
final EntityKey entityKey,
final PageContext pageContext,
@ -446,6 +483,7 @@ public class MonitoringRunningExam implements TemplateComposer {
final PageActionBuilder actionBuilder,
final ProctoringSettings proctoringSettings) {
updateTownhallButton(entityKey, pageContext);
final I18nSupport i18nSupport = this.pageService.getI18nSupport();
this.pageService.getRestService().getBuilder(GetProcotringRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)

View file

@ -711,7 +711,7 @@ public enum ActionDefinition {
ActionCategory.PROCTORING),
MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM(
new LocTextKey("sebserver.monitoring.exam.action.proctoring.closeTownhall"),
ImageIcon.PROCTOR_ROOM,
ImageIcon.CANCEL,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.PROCTORING),

View file

@ -135,13 +135,11 @@ public class ActionPane implements TemplateComposer {
if (event.decoration != null) {
final TreeItem actionItemToDecorate = findAction(actionTrees, parent, event.decoration._1);
final PageAction action = (PageAction) actionItemToDecorate.getData(ACTION_EVENT_CALL_KEY);
if (actionItemToDecorate != null && event.decoration._2 != null) {
actionItemToDecorate.setImage(0,
event.decoration._2.icon.getImage(parent.getDisplay()));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(
actionItemToDecorate,
(action != null) ? action.getTitle() : event.decoration._2.title);
actionItemToDecorate, event.decoration._2.title);
}
}
});

View file

@ -29,12 +29,10 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerT
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.GuiServiceInfo;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringBroadcastAttributes;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService.ProctoringWindowData;
@ -92,7 +90,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell);
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData, pageContext));
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData));
final String url = this.guiServiceInfo
.getExternalServerURIBuilder()
@ -122,7 +120,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY);
closeAction.setLayoutData(new RowData(150, 30));
closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData, pageContext));
closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData));
final BroadcastActionState broadcastActionState = new BroadcastActionState();
final String connectionTokens = getConnectionTokens(proctoringWindowData);
@ -258,16 +256,11 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
boolean chat = false;
}
private void closeRoom(final ProctoringWindowData proctoringWindowData, final PageContext pageContext) {
private void closeRoom(final ProctoringWindowData proctoringWindowData) {
this.pageService
.getCurrentUser()
.getProctoringGUIService()
.closeRoom(proctoringWindowData.connectionData.roomName);
this.pageService.firePageEvent(
new ActionActivationEvent(
false,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM),
pageContext);
}
}

View file

@ -199,6 +199,11 @@ public class ProctoringGUIService {
this.restService.getBuilder(SendProctoringBroadcastAttributes.class)
.withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
.withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomConnectionData.roomName)
.withFormParam(
API.EXAM_API_SEB_CONNECTION_TOKEN,
roomConnectionData.connections.isEmpty()
? ""
: StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR))
.call()
.onError(error -> log.error(
"Failed to send reset broadcast attribute instruction call for room: {}, cause: {}",
@ -229,7 +234,9 @@ public class ProctoringGUIService {
public void clear() {
if (!this.rooms.isEmpty()) {
this.rooms.keySet().stream().forEach(this::closeRoom);
this.rooms.keySet()
.stream()
.forEach(this::closeRoom);
this.rooms.clear();
}

View file

@ -16,8 +16,6 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
@ -31,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.SizedArrayNonBlockingQueue;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientInstructionRecord;
@ -45,6 +44,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
private static final Logger log = LoggerFactory.getLogger(SEBInstructionServiceImpl.class);
private static final int INSTRUCTION_QUEUE_MAX_SIZE = 10;
private static final String JSON_INST = "instruction";
private static final String JSON_ATTR = "attributes";
@ -53,7 +53,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
private final ClientInstructionDAO clientInstructionDAO;
private final JSONMapper jsonMapper;
private final Map<String, ClientInstructionRecord> instructions;
private final Map<String, SizedArrayNonBlockingQueue<ClientInstructionRecord>> instructions;
private long lastRefresh = 0;
@ -113,7 +113,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
final String attributesString = this.jsonMapper.writeValueAsString(attributes);
this.clientInstructionDAO
.insert(examId, type, attributesString, connectionToken, needsConfirm)
.map(this::chacheInstruction)
.map(this::putToCacheIfAbsent)
.onError(error -> log.error("Failed to register instruction: {}", error.getMessage()))
.getOrThrow();
} catch (final Exception e) {
@ -146,7 +146,7 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
error -> log.error("Failed to register instruction: {}", error.getMessage()),
() -> null))
.filter(Objects::nonNull)
.forEach(this::chacheInstruction);
.forEach(this::putToCacheIfAbsent);
});
}
@ -162,10 +162,19 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
return null;
}
final ClientInstructionRecord clientInstruction = this.instructions.get(connectionToken);
final SizedArrayNonBlockingQueue<ClientInstructionRecord> queue = this.instructions.get(connectionToken);
if (queue.isEmpty()) {
return null;
}
final ClientInstructionRecord clientInstruction = queue.peek();
if (clientInstruction == null) {
return null;
}
final boolean needsConfirm = BooleanUtils.toBoolean(clientInstruction.getNeedsConfirmation());
if (!needsConfirm) {
this.instructions.remove(connectionToken);
queue.poll();
final Result<Void> delete = this.clientInstructionDAO.delete(clientInstruction.getId());
if (delete.hasError()) {
log.error("Failed to delete SEB client instruction on persistent storage: ", delete.getError());
@ -207,13 +216,26 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
@Override
public void confirmInstructionDone(final String connectionToken, final String instructionConfirm) {
try {
this.instructions.remove(connectionToken);
this.clientInstructionDAO.delete(Long.valueOf(instructionConfirm));
final SizedArrayNonBlockingQueue<ClientInstructionRecord> queue = this.instructions.get(connectionToken);
if (queue.isEmpty()) {
return;
}
final ClientInstructionRecord instruction = queue.peek();
if (String.valueOf(instruction.getId()).equals(String.valueOf(instruction.getId()))) {
queue.poll();
this.clientInstructionDAO.delete(Long.valueOf(instructionConfirm));
} else {
log.warn("SEB instruction confirmation mismatch. Sent instructionConfirm: {} pending instruction: {}",
instructionConfirm,
instruction.getId());
}
} catch (final Exception e) {
log.error(
"Failed to remove SEB instruction after confirmation: connectionToken: {} instructionConfirm: {}",
"Failed to remove SEB instruction after confirmation: connectionToken: {} instructionConfirm: {} connectionToken: {}",
connectionToken,
instructionConfirm);
instructionConfirm,
connectionToken);
}
}
@ -236,33 +258,53 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
private Result<Void> loadInstructions() {
return Result.tryCatch(() -> this.clientInstructionDAO.getAllActive()
.getOrThrow()
.forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst)));
.forEach(this::putToCacheIfAbsent));
}
private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) {
// private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) {
//
//
//
// final String connectionToken = instruction.getConnectionToken();
// if (this.instructions.containsKey(connectionToken)) {
// // check if previous instruction is still valid
// final ClientInstructionRecord clientInstructionRecord = this.instructions.get(connectionToken);
//
// System.out.println("************* previous instruction still active: " + clientInstructionRecord);
//
// if (BooleanUtils.toBoolean(BooleanUtils.toBooleanObject(clientInstructionRecord.getNeedsConfirmation()))) {
// // check if time is out
// final long now = DateTime.now(DateTimeZone.UTC).getMillis();
// final Long timestamp = clientInstructionRecord.getTimestamp();
// if (timestamp != null && now - timestamp > Constants.MINUTE_IN_MILLIS) {
// // remove old instruction and add new one
// System.out.println("************* remove old instruction and put new: ");
// this.instructions.put(connectionToken, instruction);
// }
// }
// } else {
// this.instructions.put(connectionToken, instruction);
// }
// return instruction;
// }
private ClientInstructionRecord putToCacheIfAbsent(final ClientInstructionRecord instruction) {
final SizedArrayNonBlockingQueue<ClientInstructionRecord> queue = this.instructions.computeIfAbsent(
instruction.getConnectionToken(),
key -> new SizedArrayNonBlockingQueue<>(INSTRUCTION_QUEUE_MAX_SIZE));
if (queue.contains(instruction)) {
log.warn("Instruction alread in the queue: {}", instruction);
return instruction;
}
if (log.isDebugEnabled()) {
log.debug("Put SEB instruction into instruction queue: {}", instruction);
}
System.out.println("************* register instruction: " + instruction);
final String connectionToken = instruction.getConnectionToken();
if (this.instructions.containsKey(connectionToken)) {
// check if previous instruction is still valid
final ClientInstructionRecord clientInstructionRecord = this.instructions.get(connectionToken);
System.out.println("************* previous instruction still active: " + clientInstructionRecord);
if (BooleanUtils.toBoolean(BooleanUtils.toBooleanObject(clientInstructionRecord.getNeedsConfirmation()))) {
// check if time is out
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
final Long timestamp = clientInstructionRecord.getTimestamp();
if (timestamp != null && now - timestamp > Constants.MINUTE_IN_MILLIS) {
// remove old instruction and add new one
System.out.println("************* remove old instruction and put new: ");
this.instructions.put(connectionToken, instruction);
}
}
} else {
this.instructions.put(connectionToken, instruction);
}
queue.add(instruction);
return instruction;
}

View file

@ -1,4 +1,3 @@
################################
# Overall
################################