SEBSERV-317 also archive exam config and release SEB restriction

This commit is contained in:
anhefti 2022-06-13 16:17:26 +02:00
parent 22c0dd872d
commit b44c5f4eb2
12 changed files with 162 additions and 38 deletions

View file

@ -87,7 +87,7 @@ public final class CircuitBreaker<T> {
/** Create new CircuitBreakerSupplier.
*
* @param asyncRunner the AsyncRunner used to create asynchronous calls on the given supplier function
* @param maxFailingAttempts the number of maximal failing attempts before go form CLOSE into HALF_OPEN state
* @param maxFailingAttempts the number of maximal failing attempts before go from CLOSE into HALF_OPEN state
* @param maxBlockingTime the maximal time that an call attempt can block until an error is responded
* @param timeToRecover the time the circuit breaker needs to cool-down on OPEN-STATE before going back to HALF_OPEN
* state */
@ -169,7 +169,7 @@ public final class CircuitBreaker<T> {
final long currentBlockingTime = Utils.getMillisecondsNow() - startTime;
final int failing = this.failingCount.incrementAndGet();
if (failing > this.maxFailingAttempts || currentBlockingTime > this.maxBlockingTime) {
if (failing >= this.maxFailingAttempts || currentBlockingTime > this.maxBlockingTime) {
// brake thought to HALF_OPEN state and return error
if (log.isDebugEnabled()) {
log.debug("Changing state from Open to Half Open");

View file

@ -445,21 +445,21 @@ public class ExamForm implements TemplateComposer {
.withEntityKey(entityKey)
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
.withAttribute(ExamSEBRestrictionSettings.PAGE_CONTEXT_ATTR_LMS_ID, String.valueOf(exam.lmsSetupId))
.withAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant))
.withAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant || !editable))
.noEventPropagation()
.publishIf(() -> sebRestrictionAvailable && readonly)
.newAction(ActionDefinition.EXAM_ENABLE_SEB_RESTRICTION)
.withEntityKey(entityKey)
.withExec(action -> this.examSEBRestrictionSettings.setSEBRestriction(action, true, this.restService))
.publishIf(() -> sebRestrictionAvailable && readonly && editable && !importFromQuizData
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
&& 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 && editable && !importFromQuizData
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
&& BooleanUtils.isTrue(isRestricted))
.newAction(ActionDefinition.EXAM_PROCTORING_ON)

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -64,6 +65,10 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
@WebServiceProfile
public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
private static final List<String> ACTIVE_EXAM_STATE_NAMES = Arrays.asList(
ExamStatus.FINISHED.name(),
ExamStatus.ARCHIVED.name());
private final ExamRecordMapper examRecordMapper;
private final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper;
private final ConfigurationNodeRecordMapper configurationNodeRecordMapper;
@ -344,7 +349,8 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
@Override
@Transactional(readOnly = true)
public Result<Collection<Long>> getExamIdsForConfigNodeId(final Long configurationNodeId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectByExample()
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectByExample()
.where(
ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configurationNodeId))
@ -383,16 +389,16 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.build()
.execute()
.stream()
.filter(rec -> !isExamFinished(rec.getExamId()))
.filter(rec -> !isExamActive(rec.getExamId()))
.findFirst()
.isPresent());
}
private boolean isExamFinished(final Long examId) {
private boolean isExamActive(final Long examId) {
try {
return this.examRecordMapper.countByExample()
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
.and(ExamRecordDynamicSqlSupport.status, isEqualTo(ExamStatus.FINISHED.name()))
.and(ExamRecordDynamicSqlSupport.status, isIn(ACTIVE_EXAM_STATE_NAMES))
.build()
.execute() >= 1;
} catch (final Exception e) {

View file

@ -97,6 +97,13 @@ public interface ExamAdminService {
* @return ExamProctoringService instance */
Result<ExamProctoringService> getExamProctoringService(final Long examId);
/** This archives a finished exam and set it to archived state as well as the assigned
* exam configurations that are also set to archived state.
*
* @param exam The exam to archive
* @return Result refer to the archived exam or to an error when happened */
Result<Exam> archiveExam(Exam exam);
/** Used to check threshold consistency for a given list of thresholds.
* Checks if all values are present (none null value)
* Checks if there are duplicates

View file

@ -20,17 +20,24 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService;
@ -49,17 +56,23 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private final ExamDAO examDAO;
private final ProctoringAdminService proctoringServiceSettingsService;
private final AdditionalAttributesDAO additionalAttributesDAO;
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final LmsAPIService lmsAPIService;
protected ExamAdminServiceImpl(
final ExamDAO examDAO,
final ProctoringAdminService proctoringServiceSettingsService,
final AdditionalAttributesDAO additionalAttributesDAO,
final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final LmsAPIService lmsAPIService) {
this.examDAO = examDAO;
this.proctoringServiceSettingsService = proctoringServiceSettingsService;
this.additionalAttributesDAO = additionalAttributesDAO;
this.configurationNodeDAO = configurationNodeDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.lmsAPIService = lmsAPIService;
}
@ -114,7 +127,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
return this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.map(lmsAPI -> !lmsAPI.hasSEBClientRestriction(exam));
.map(lmsAPI -> lmsAPI.hasSEBClientRestriction(exam))
.onError(error -> log.error("Failed to check SEB restriction: ", error));
}
@Override
@ -182,4 +196,55 @@ public class ExamAdminServiceImpl implements ExamAdminService {
});
}
@Override
public Result<Exam> archiveExam(final Exam exam) {
return Result.tryCatch(() -> {
if (exam.status != ExamStatus.FINISHED) {
throw new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION.of("Exam is in wrong status to archive."));
}
if (log.isDebugEnabled()) {
log.debug("Archiving exam: {}", exam);
}
if (this.isRestricted(exam).getOr(false)) {
if (log.isDebugEnabled()) {
log.debug("Archiving exam, SEB restriction still active, try to release: {}", exam);
}
this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.flatMap(lms -> lms.releaseSEBClientRestriction(exam))
.onError(error -> log.error(
"Failed to release SEB client restriction for archiving exam: ",
error));
}
final Exam result = this.examDAO
.updateState(exam.id, ExamStatus.ARCHIVED, null)
.getOrThrow();
this.examConfigurationMapDAO
.getConfigurationNodeIds(result.id)
.getOrThrow()
.stream()
.forEach(configNodeId -> {
if (this.examConfigurationMapDAO.checkNoActiveExamReferences(configNodeId).getOr(false)) {
log.debug("Also set exam configuration to archived: ", configNodeId);
this.configurationNodeDAO.save(
new ConfigurationNode(
configNodeId, null, null, null, null, null,
null, ConfigurationStatus.ARCHIVED, null, null))
.onError(error -> log.error("Failed to set exam configuration to archived state: ",
error));
}
});
return result;
});
}
}

View file

@ -37,7 +37,7 @@ public interface SEBRestrictionAPI {
*
* A SEB Restriction is available if there it can get from LMS and if there is either a Config-Key
* or a BrowserExam-Key set or both. If none of this keys is set, the SEB Restriction is been
* considdered to not set on the LMS.
* considered to not set on the LMS.
*
* @param exam exam the exam to get the SEB restriction data for
* @return true if there is a SEB restriction set on the LMS for the exam or false otherwise */

View file

@ -130,7 +130,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.attempts",
Integer.class,
3),
1),
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.blockingTime",
Long.class,
@ -158,7 +158,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.attempts",
Integer.class,
2),
1),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.blockingTime",
Long.class,
@ -388,20 +388,24 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.getSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to get SEB restrictions: {}",
error.getMessage()))
.onError(error -> {
if (error instanceof NoSEBRestrictionException) {
return;
}
log.error("Failed to get SEB restrictions: {}", error.getMessage());
})
.getOrThrow());
}
@Override
public boolean hasSEBClientRestriction(final Exam exam) {
if (this.sebRestrictionAPI == null) {
final Result<SEBRestriction> sebClientRestriction = getSEBClientRestriction(exam);
if (sebClientRestriction.hasError()) {
return false;
}
return this.sebRestrictionAPI
.getSEBClientRestriction(exam).hasError();
final SEBRestriction sebRestriction = sebClientRestriction.get();
return !sebRestriction.configKeys.isEmpty() || !sebRestriction.browserExamKeys.isEmpty();
}
@Override

View file

@ -30,7 +30,6 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
/** The open edX SEB course restriction API implementation.
*
@ -134,8 +133,9 @@ public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
}
return SEBRestriction.from(exam.id, data);
} catch (final HttpClientErrorException ce) {
if (ce.getStatusCode() == HttpStatus.NOT_FOUND || ce.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new NoSEBRestrictionException(ce);
if (ce.getStatusCode() == HttpStatus.NOT_FOUND) {
// No SEB restriction is set for the specified exam, return an empty one
return new SEBRestriction(exam.id, null, null, null);
}
throw ce;
}

View file

@ -47,7 +47,6 @@ import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
@ -229,8 +228,9 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
checkWritePrivilege(institutionId);
return this.examDAO.byPK(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(this::checkArchive)
.flatMap(exam -> this.examDAO.updateState(exam.id, ExamStatus.ARCHIVED, null))
.flatMap(this.examAdminService::archiveExam)
// .flatMap(this::checkArchive)
// .flatMap(exam -> this.examDAO.updateState(exam.id, ExamStatus.ARCHIVED, null))
.flatMap(this::logModify)
.getOrThrow();
}
@ -581,15 +581,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
});
}
private Result<Exam> checkArchive(final Exam exam) {
if (exam.status != ExamStatus.FINISHED) {
throw new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION.of("Exam is in wrong status to archive."));
}
return Result.of(exam);
}
static Function<Collection<Exam>, List<Exam>> pageSort(final String sort) {
final String sortBy = PageSortOrder.decode(sort);

View file

@ -1,11 +1,11 @@
server.address=localhost
server.port=8090
server.port=8080
sebserver.gui.http.external.scheme=http
sebserver.gui.entrypoint=/gui
sebserver.gui.webservice.protocol=http
sebserver.gui.webservice.address=localhost
sebserver.gui.webservice.port=8090
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

View file

@ -39,10 +39,61 @@ public class CircuitBreakerTest {
assertNotNull(this.asyncService);
}
@Test
public void testMaxAttempts1() {
final CircuitBreaker<String> circuitBreaker =
this.asyncService.createCircuitBreaker(1, 500, 1000);
final AtomicInteger attemptCounter = new AtomicInteger(0);
final Supplier<String> tester = () -> {
attemptCounter.getAndIncrement();
throw new RuntimeException("Test Error");
};
final Result<String> result = circuitBreaker.protectedRun(tester); // 1. call...
assertTrue(result.hasError());
assertEquals(State.HALF_OPEN, circuitBreaker.getState());
assertEquals("1", String.valueOf(attemptCounter.get()));
}
@Test
public void testMaxAttempts4() {
final CircuitBreaker<String> circuitBreaker =
this.asyncService.createCircuitBreaker(4, 500, 1000);
final AtomicInteger attemptCounter = new AtomicInteger(0);
final Supplier<String> tester = () -> {
attemptCounter.getAndIncrement();
throw new RuntimeException("Test Error");
};
final Result<String> result = circuitBreaker.protectedRun(tester); // 1. call...
assertTrue(result.hasError());
assertEquals(State.HALF_OPEN, circuitBreaker.getState());
assertEquals("4", String.valueOf(attemptCounter.get()));
}
@Test
public void testMaxAttempts0() {
final CircuitBreaker<String> circuitBreaker =
this.asyncService.createCircuitBreaker(0, 500, 1000);
final AtomicInteger attemptCounter = new AtomicInteger(0);
final Supplier<String> tester = () -> {
attemptCounter.getAndIncrement();
throw new RuntimeException("Test Error");
};
final Result<String> result = circuitBreaker.protectedRun(tester); // 1. call...
assertTrue(result.hasError());
assertEquals(State.HALF_OPEN, circuitBreaker.getState());
assertEquals("1", String.valueOf(attemptCounter.get()));
}
@Test
public void roundtrip1() throws InterruptedException {
final CircuitBreaker<String> circuitBreaker =
this.asyncService.createCircuitBreaker(3, 500, 1000);
this.asyncService.createCircuitBreaker(4, 500, 1000);
final Supplier<String> tester = tester(100, 5, 10);

View file

@ -42,7 +42,7 @@ public class MemoizingCircuitBreakerTest {
@Test
public void roundtrip1() throws InterruptedException {
final MemoizingCircuitBreaker<String> circuitBreaker = this.asyncService.createMemoizingCircuitBreaker(
tester(100, 5, 10), 3, 500, 1000, true, 1000);
tester(100, 5, 10), 4, 500, 1000, true, 1000);
assertNull(circuitBreaker.getCached());