refactoring of LMS API service with overall eh-caching

This commit is contained in:
anhefti 2021-05-15 17:43:26 +02:00
parent dd6150ec2a
commit 9214719642
19 changed files with 406 additions and 210 deletions

View file

@ -59,6 +59,7 @@ public final class Constants {
public static final Character DOUBLE_QUOTE = '"'; public static final Character DOUBLE_QUOTE = '"';
public static final Character COMMA = ','; public static final Character COMMA = ',';
public static final Character PIPE = '|'; public static final Character PIPE = '|';
public static final Character UNDERLINE = '_';
public static final Character AMPERSAND = '&'; public static final Character AMPERSAND = '&';
public static final Character EQUALITY_SIGN = '='; public static final Character EQUALITY_SIGN = '=';
public static final Character LIST_SEPARATOR_CHAR = COMMA; public static final Character LIST_SEPARATOR_CHAR = COMMA;

View file

@ -37,6 +37,8 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
* happened */ * happened */
Result<GrantEntity> examGrantEntityByClientConnection(Long connectionId); Result<GrantEntity> examGrantEntityByClientConnection(Long connectionId);
Result<Exam> getWithQuizDataFromCache(Long id);
/** Get all active Exams for a given institution. /** Get all active Exams for a given institution.
* *
* @param institutionId the identifier of the institution * @param institutionId the identifier of the institution

View file

@ -377,6 +377,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
private Result<Collection<ExamConfigurationMap>> toDomainModel( private Result<Collection<ExamConfigurationMap>> toDomainModel(
final Collection<ExamConfigurationMapRecord> records) { final Collection<ExamConfigurationMapRecord> records) {
return Result.tryCatch(() -> records return Result.tryCatch(() -> records
.stream() .stream()
.map(model -> this.toDomainModel(model).getOrThrow()) .map(model -> this.toDomainModel(model).getOrThrow())
@ -390,7 +391,8 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.selectByPrimaryKey(record.getConfigurationNodeId()); .selectByPrimaryKey(record.getConfigurationNodeId());
final String status = config.getStatus(); final String status = config.getStatus();
final Exam exam = this.examDAO.byPK(record.getExamId()) final Exam exam = this.examDAO
.getWithQuizDataFromCache(record.getExamId())
.getOr(null); .getOr(null);
return new ExamConfigurationMap( return new ExamConfigurationMap(

View file

@ -119,6 +119,13 @@ public class ExamDAOImpl implements ExamDAO {
.map(record -> toDomainModel(record, null, null).getOrThrow()); .map(record -> toDomainModel(record, null, null).getOrThrow());
} }
@Override
@Transactional(readOnly = true)
public Result<Exam> getWithQuizDataFromCache(final Long id) {
return recordById(id)
.flatMap(this::toDomainModelFromCache);
}
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) { public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) {
@ -761,6 +768,17 @@ public class ExamDAOImpl implements ExamDAO {
exam.getDescription()); exam.getDescription());
} }
private Result<Exam> toDomainModelFromCache(final ExamRecord record) {
return this.lmsAPIService
.getLmsAPITemplate(record.getLmsSetupId())
.flatMap(template -> this.toDomainModel(
record,
template.getQuizFromCache(record.getExternalId())
.getOrThrow(),
null));
}
private Result<Exam> toDomainModel(final ExamRecord record) { private Result<Exam> toDomainModel(final ExamRecord record) {
return toDomainModel( return toDomainModel(
record.getLmsSetupId(), record.getLmsSetupId(),

View file

@ -165,6 +165,15 @@ public interface LmsAPITemplate {
* @return Collection of all {@link QuizData } from the given id set */ * @return Collection of all {@link QuizData } from the given id set */
Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids); Collection<Result<QuizData>> getQuizzesFromCache(Set<String> ids);
/** Get a particular quiz data from cache if available. If not, tries to get it from the LMS.
*
* @param id the quiz identifier, external identifier of the exam.
* @return Result refer to the {@link QuizData } or to an error when happended */
Result<QuizData> getQuizFromCache(String id);
/** Clears the underling caches if there are some for a particular implementation. */
void clearCache();
/** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login, /** Convert an anonymous or temporary examineeUserId, sent by the SEB Client on LMS login,
* to LMS examinee account details by requesting them on the LMS API with the given examineeUserId * to LMS examinee account details by requesting them on the LMS API with the given examineeUserId
* *

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2021 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.webservice.servicelayer.lms.impl;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.util.Result;
public abstract class AbstractCachedCourseAccess extends AbstractCourseAccess {
public static final String CACHE_NAME_QUIZ_DATA = "QUIZ_DATA_CACHE";
private static final String NO_QUIZ_DATA_FOUND_ERROR = "NO_QUIZ_DATA_FOUND_ERROR";
private final Cache cache;
protected AbstractCachedCourseAccess(
final AsyncService asyncService,
final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment);
this.cache = cacheManager.getCache(CACHE_NAME_QUIZ_DATA);
}
public void clearCache() {
this.cache.clear();
}
protected QuizData getFromCache(final String id) {
return this.cache.get(createCacheKey(id), QuizData.class);
}
protected void putToCache(final QuizData quizData) {
this.cache.put(createCacheKey(quizData.id), quizData);
}
protected void putToCache(final Collection<QuizData> quizData) {
quizData.stream().forEach(q -> this.cache.put(createCacheKey(q.id), q));
}
protected void evict(final String id) {
this.cache.evict(createCacheKey(id));
}
@Override
public Result<QuizData> getQuizFromCache(final String id) {
final QuizData fromCache = getFromCache(id);
if (fromCache != null) {
return Result.of(fromCache);
} else {
return Result.ofRuntimeError(NO_QUIZ_DATA_FOUND_ERROR);
}
}
@Override
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.of(ids.stream().map(this::getQuizFromCache).collect(Collectors.toList()));
}
protected abstract Long getLmsSetupId();
private final String createCacheKey(final String id) {
return id + Constants.UNDERLINE + getLmsSetupId();
}
}

View file

@ -11,12 +11,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -25,6 +21,7 @@ import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
@ -101,42 +98,8 @@ public abstract class AbstractCourseAccess {
Constants.SECOND_IN_MILLIS * 10)); Constants.SECOND_IN_MILLIS * 10));
} }
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = allQuizzesSupplier().getAllCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> collect = quizzesSupplier(ids).get()
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
if (collect != null) {
quizMapping.clear();
quizMapping.putAll(collect);
}
}
return ids
.stream()
.map(id -> {
final QuizData q = quizMapping.get(id);
return (q == null)
? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
: Result.of(q);
})
.collect(Collectors.toList());
});
}
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) { public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return allQuizzesSupplier().getAll(filterMap); return this.quizzesRequest.protectedRun(allQuizzesSupplier(filterMap));
} }
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) { public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
@ -172,30 +135,41 @@ public abstract class AbstractCourseAccess {
Collections.emptyMap()); Collections.emptyMap());
} }
/** This abstraction has no cache implementation and therefore this returns a Result
* with an "No cache supported error.
* </p>
* To implement and use caching, this must be overridden and implemented
*
* @param id The identifier of the QuizData to get from cache
* @return Result with an "No cache supported error */
public Result<QuizData> getQuizFromCache(final String id) {
return Result.ofRuntimeError("No cache supported");
}
/** This abstraction has no cache implementation and therefore this returns a Result
* with an "No cache supported error.
* </p>
* To implement and use caching, this must be overridden and implemented
*
* @param ids Collection of quiz data identifier to get from the cache
* @return Result with an "No cache supported error */
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.ofRuntimeError("No cache supported");
}
/** Provides a supplier for the quiz data request to use within the circuit breaker */ /** Provides a supplier for the quiz data request to use within the circuit breaker */
protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids); protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids);
/** Provides a AllQuizzesSupplier to supply quiz data either form cache or from LMS */ /** Provides a supplier to supply request to use within the circuit breaker */
protected abstract AllQuizzesSupplier allQuizzesSupplier(); protected abstract Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap);
/** Provides a supplier for the course chapter data request to use within the circuit breaker */ /** Provides a supplier for the course chapter data request to use within the circuit breaker */
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId); protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
/** Gives a fetch status if asynchronous quiz data fetching is part of the underling implementation */ protected FetchStatus getFetchStatus() {
protected abstract FetchStatus getFetchStatus(); if (this.quizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
/** Uses to supply quiz data */ }
protected interface AllQuizzesSupplier { return FetchStatus.ALL_FETCHED;
/** Get all currently cached quiz data if supported by the underling implementation
*
* @return List containing all cached quiz data objects */
List<QuizData> getAllCached();
/** Get a list of all quiz data filtered by the given filter map from LMS.
*
* @param filterMap Map containing the filter criteria
* @return Result refer to the list of filtered quiz data or to an error when happened */
Result<List<QuizData>> getAll(final FilterMap filterMap);
} }
} }

View file

@ -98,17 +98,20 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override @Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) { public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId); synchronized (this) {
if (lmsAPITemplate == null) { LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
lmsAPITemplate = createLmsSetupTemplate(lmsSetupId); if (lmsAPITemplate == null) {
if (lmsAPITemplate != null) { lmsAPITemplate = createLmsSetupTemplate(lmsSetupId);
this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate); if (lmsAPITemplate != null) {
this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate);
}
} }
if (lmsAPITemplate == null) {
throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId);
}
return lmsAPITemplate;
} }
if (lmsAPITemplate == null) {
throw new ResourceNotFoundException(EntityType.LMS_SETUP, lmsSetupId);
}
return lmsAPITemplate;
}); });
} }

View file

@ -91,12 +91,10 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) { public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
// load the config keys from restriction and merge with new generated config keys // load the config keys from restriction and merge with new generated config keys
final long currentTimeMillis = System.currentTimeMillis();
final Set<String> configKeys = new HashSet<>(); final Set<String> configKeys = new HashSet<>();
final Collection<String> generatedKeys = this.examConfigService final Collection<String> generatedKeys = this.examConfigService
.generateConfigKeys(exam.institutionId, exam.id) .generateConfigKeys(exam.institutionId, exam.id)
.getOrThrow(); .getOrThrow();
System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis));
configKeys.addAll(generatedKeys); configKeys.addAll(generatedKeys);
if (generatedKeys != null && !generatedKeys.isEmpty()) { if (generatedKeys != null && !generatedKeys.isEmpty()) {

View file

@ -21,6 +21,7 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -43,8 +44,6 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; 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;
@ -55,12 +54,12 @@ 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.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;
/** Implements the LmsAPITemplate for Open edX LMS Course API access. /** Implements the LmsAPITemplate for Open edX LMS Course API access.
* *
* See also: https://course-catalog-api-guide.readthedocs.io */ * See also: https://course-catalog-api-guide.readthedocs.io */
final class OpenEdxCourseAccess extends AbstractCourseAccess { final class OpenEdxCourseAccess extends AbstractCachedCourseAccess {
private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
@ -74,65 +73,34 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
private final AllQuizzesSupplier allQuizzesSupplier;
private OAuth2RestTemplate restTemplate; private OAuth2RestTemplate restTemplate;
private final Long lmsSetupId;
public OpenEdxCourseAccess( public OpenEdxCourseAccess(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment) { final Environment environment,
final CacheManager cacheManager) {
super(asyncService, environment); super(asyncService, environment, cacheManager);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.lmsSetupId = openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup().id;
this.allQuizzesRequest = asyncService.createMemoizingCircuitBreaker(
quizzesSupplier(),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.attempts",
Integer.class,
3),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.blockingTime",
Long.class,
Constants.MINUTE_IN_MILLIS),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.memoize",
Boolean.class,
true),
environment.getProperty(
"sebserver.webservice.circuitbreaker.allQuizzesRequest.memoizingTime",
Long.class,
Constants.HOUR_IN_MILLIS));
this.allQuizzesSupplier = new AllQuizzesSupplier() {
@Override
public List<QuizData> getAllCached() {
return OpenEdxCourseAccess.this.allQuizzesRequest.getCached();
}
@Override
public Result<List<QuizData>> getAll(final FilterMap filterMap) {
return OpenEdxCourseAccess.this.allQuizzesRequest.get();
}
};
} }
APITemplateDataSupplier getApiTemplateDataSupplier() { APITemplateDataSupplier getApiTemplateDataSupplier() {
return this.openEdxRestTemplateFactory.apiTemplateDataSupplier; return this.openEdxRestTemplateFactory.apiTemplateDataSupplier;
} }
@Override
protected Long getLmsSetupId() {
return this.lmsSetupId;
}
LmsSetupTestResult initAPIAccess() { LmsSetupTestResult initAPIAccess() {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
@ -219,6 +187,13 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
}); });
} }
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
}
@Override @Override
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
@ -231,7 +206,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
getOneCourses( getOneCourses(
lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
getRestTemplate().getOrThrow(), getRestTemplate().getOrThrow(),
ids.iterator().next()), ids),
externalStartURI)); externalStartURI));
}; };
} else { } else {
@ -241,12 +216,6 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
} }
} }
private Supplier<List<QuizData>> quizzesSupplier() {
return () -> getRestTemplate()
.map(this::collectAllQuizzes)
.getOrThrow();
}
@Override @Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) { protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> { return () -> {
@ -337,7 +306,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
final List<CourseData> collector = new ArrayList<>(); final List<CourseData> collector = new ArrayList<>();
EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); EdXPage page = getEdxPage(pageURI, restTemplate).getBody();
if (page != null) { if (page != null) {
collector.addAll(page.results); page.results
.stream()
.filter(cd -> ids.contains(cd.id))
.forEach(collector::add);
while (page != null && StringUtils.isNotBlank(page.next)) { while (page != null && StringUtils.isNotBlank(page.next)) {
page = getEdxPage(page.next, restTemplate).getBody(); page = getEdxPage(page.next, restTemplate).getBody();
if (page != null) { if (page != null) {
@ -355,16 +327,33 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
private CourseData getOneCourses( private CourseData getOneCourses(
final String pageURI, final String pageURI,
final OAuth2RestTemplate restTemplate, final OAuth2RestTemplate restTemplate,
final String id) { final Set<String> ids) {
final HttpHeaders httpHeaders = new HttpHeaders(); System.out.println("********************");
final ResponseEntity<CourseData> exchange = restTemplate.exchange(
pageURI + "/" + id,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
CourseData.class);
return exchange.getBody(); // NOTE: try first to get the course data by id. This seems to be possible
// when the SEB restriction is not set. Once the SEB restriction is set,
// this gives a 403 response.
// We haven't found another way to get course data by id in this case so far
// Workaround is to search the course by paging (slow)
try {
final HttpHeaders httpHeaders = new HttpHeaders();
final String uri = pageURI + ids.iterator().next();
final ResponseEntity<CourseData> exchange = restTemplate.exchange(
uri,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
CourseData.class);
return exchange.getBody();
} catch (final Exception e) {
// try with paging
final List<CourseData> collectCourses = collectCourses(pageURI, restTemplate, ids);
if (collectCourses.isEmpty()) {
return null;
}
return collectCourses.get(0);
}
} }
private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) {
@ -403,7 +392,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
Blocks.class); Blocks.class);
} }
private static QuizData quizDataOf( private QuizData quizDataOf(
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final CourseData courseData, final CourseData courseData,
final String uriPrefix) { final String uriPrefix) {
@ -411,7 +400,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
final String startURI = uriPrefix + courseData.id; final String startURI = uriPrefix + courseData.id;
final Map<String, String> additionalAttrs = new HashMap<>(); final Map<String, String> additionalAttrs = new HashMap<>();
additionalAttrs.put("blocks_url", courseData.blocks_url); additionalAttrs.put("blocks_url", courseData.blocks_url);
return new QuizData( final QuizData quizData = new QuizData(
courseData.id, courseData.id,
lmsSetup.getInstitutionId(), lmsSetup.getInstitutionId(),
lmsSetup.id, lmsSetup.id,
@ -421,6 +410,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
courseData.start, courseData.start,
courseData.end, courseData.end,
startURI); startURI);
super.putToCache(quizData);
return quizData;
} }
/** Maps a OpenEdX course API course page */ /** Maps a OpenEdX course API course page */
@ -538,17 +531,4 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
return Result.of(this.restTemplate); return Result.of(this.restTemplate);
} }
@Override
protected FetchStatus getFetchStatus() {
if (this.allQuizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
}
return FetchStatus.ALL_FETCHED;
}
@Override
protected AllQuizzesSupplier allQuizzesSupplier() {
return this.allQuizzesSupplier;
}
} }

View file

@ -98,12 +98,24 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return this.openEdxCourseAccess
.getQuizFromCache(id)
.orElse(() -> getQuiz(id));
}
@Override @Override
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) { public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return this.openEdxCourseAccess.getQuizzesFromCache(ids) return this.openEdxCourseAccess.getQuizzesFromCache(ids)
.getOrElse(() -> getQuizzes(ids)); .getOrElse(() -> getQuizzes(ids));
} }
@Override
public void clearCache() {
this.openEdxCourseAccess.clearCache();
}
@Override @Override
public Result<Chapters> getCourseChapters(final String courseId) { public Result<Chapters> getCourseChapters(final String courseId) {
return Result.tryCatch(() -> this.openEdxCourseAccess return Result.tryCatch(() -> this.openEdxCourseAccess

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -36,6 +37,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final AsyncService asyncService; private final AsyncService asyncService;
private final Environment environment; private final Environment environment;
private final CacheManager cacheManager;
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final String[] alternativeTokenRequestPaths; private final String[] alternativeTokenRequestPaths;
@ -46,6 +48,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final CacheManager cacheManager,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
@Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths, @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths,
@ -55,6 +58,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.asyncService = asyncService; this.asyncService = asyncService;
this.environment = environment; this.environment = environment;
this.cacheManager = cacheManager;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
@ -84,7 +88,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
openEdxRestTemplateFactory, openEdxRestTemplateFactory,
this.webserviceInfo, this.webserviceInfo,
this.asyncService, this.asyncService,
this.environment); this.environment,
this.cacheManager);
final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction(
this.jsonMapper, this.jsonMapper,

View file

@ -9,7 +9,9 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -181,6 +183,16 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
return getQuizzes(ids); return getQuizzes(ids);
} }
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return getQuizzes(new HashSet<>(Arrays.asList(id))).iterator().next();
}
@Override
public void clearCache() {
}
@Override @Override
public Result<Chapters> getCourseChapters(final String courseId) { public Result<Chapters> getCourseChapters(final String courseId) {
return Result.ofError(new UnsupportedOperationException()); return Result.ofError(new UnsupportedOperationException());

View file

@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -37,6 +38,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker.State;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters; import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; 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;
@ -48,6 +50,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseQuizShort;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
/** Implements the LmsAPITemplate for Open edX LMS Course API access. /** Implements the LmsAPITemplate for Open edX LMS Course API access.
@ -90,7 +93,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
private final CircuitBreaker<List<QuizData>> allQuizzesRequest; private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
private final AllQuizzesSupplier allQuizzesSupplier;
private MoodleAPIRestTemplate restTemplate; private MoodleAPIRestTemplate restTemplate;
@ -119,19 +121,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
"sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover", "sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover",
Long.class, Long.class,
Constants.MINUTE_IN_MILLIS)); Constants.MINUTE_IN_MILLIS));
this.allQuizzesSupplier = new AllQuizzesSupplier() {
@Override
public List<QuizData> getAllCached() {
return getCached();
}
@Override
public Result<List<QuizData>> getAll(final FilterMap filterMap) {
return MoodleCourseAccess.this.allQuizzesRequest.protectedRun(allQuizzesSupplier(filterMap));
}
};
} }
APITemplateDataSupplier getApiTemplateDataSupplier() { APITemplateDataSupplier getApiTemplateDataSupplier() {
@ -223,6 +212,77 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
return LmsSetupTestResult.ofOkay(); return LmsSetupTestResult.ofOkay();
} }
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return Result.tryCatch(() -> {
final Map<String, CourseDataShort> cachedCourseData = this.moodleCourseDataAsyncLoader
.getCachedCourseData();
final String courseId = getCourseId(id);
final String quizId = getQuizId(id);
if (cachedCourseData.containsKey(courseId)) {
final CourseDataShort courseData = cachedCourseData.get(courseId);
final CourseQuizShort quiz = courseData.quizzes
.stream()
.filter(q -> q.id.equals(quizId))
.findFirst()
.orElse(null);
if (quiz != null) {
final Map<String, String> additionalAttrs = new HashMap<>();
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_CREATION_TIME,
String.valueOf(courseData.time_created));
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SHORT_NAME, courseData.short_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_ID_NUMBER, courseData.idnumber);
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
return createQuizData(lmsSetup, courseData, urlPrefix, additionalAttrs, quiz);
}
}
throw new RuntimeException("No quiz found in cache");
});
}
@Override
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
return Result.tryCatch(() -> {
final List<QuizData> cached = getCached();
final List<QuizData> available = (cached != null)
? cached
: Collections.emptyList();
final Map<String, QuizData> quizMapping = available
.stream()
.collect(Collectors.toMap(q -> q.id, Function.identity()));
if (!quizMapping.keySet().containsAll(ids)) {
final Map<String, QuizData> collect = quizzesSupplier(ids).get()
.stream()
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
if (collect != null) {
quizMapping.clear();
quizMapping.putAll(collect);
}
}
return ids
.stream()
.map(id -> {
final QuizData q = quizMapping.get(id);
return (q == null)
? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
: Result.of(q);
})
.collect(Collectors.toList());
});
}
@Override @Override
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> getRestTemplate() return () -> getRestTemplate()
@ -231,6 +291,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
} }
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate() return () -> getRestTemplate()
.map(template -> collectAllQuizzes(template, filterMap)) .map(template -> collectAllQuizzes(template, filterMap))
@ -244,6 +305,9 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
@Override @Override
protected FetchStatus getFetchStatus() { protected FetchStatus getFetchStatus() {
if (this.allQuizzesRequest.getState() != State.CLOSED) {
return FetchStatus.FETCH_ERROR;
}
if (this.moodleCourseDataAsyncLoader.isRunning()) { if (this.moodleCourseDataAsyncLoader.isRunning()) {
return FetchStatus.ASYNC_FETCH_RUNNING; return FetchStatus.ASYNC_FETCH_RUNNING;
} }
@ -251,9 +315,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
return FetchStatus.ALL_FETCHED; return FetchStatus.ALL_FETCHED;
} }
@Override public void clearCache() {
protected AllQuizzesSupplier allQuizzesSupplier() { this.moodleCourseDataAsyncLoader.clearCache();
return this.allQuizzesSupplier;
} }
private List<QuizData> collectAllQuizzes( private List<QuizData> collectAllQuizzes(
@ -261,7 +324,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
final FilterMap filterMap) { final FilterMap filterMap) {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; : lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
@ -272,7 +334,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
// Verify and call the proper strategy to get the course and quiz data // Verify and call the proper strategy to get the course and quiz data
Collection<CourseDataShort> courseQuizData = Collections.emptyList(); Collection<CourseDataShort> courseQuizData = Collections.emptyList();
if (this.moodleCourseDataAsyncLoader.isRunning()) { if (this.moodleCourseDataAsyncLoader.isRunning()) {
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
} else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) { } else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) {
// set cut time if available // set cut time if available
if (fromCutTime >= 0) { if (fromCutTime >= 0) {
@ -282,7 +344,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
try { try {
Thread.sleep(INITIAL_WAIT_TIME); Thread.sleep(INITIAL_WAIT_TIME);
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to wait for first load run: ", e); log.error("Failed to wait for first load run: ", e);
return Collections.emptyList(); return Collections.emptyList();
@ -290,7 +352,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
} else if (this.moodleCourseDataAsyncLoader.isLongRunningTask()) { } else if (this.moodleCourseDataAsyncLoader.isLongRunningTask()) {
// on long running tasks if we have a different fromCutTime as before // on long running tasks if we have a different fromCutTime as before
// kick off the lazy loading task immediately with the new time filter // kick off the lazy loading task immediately with the new time filter
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataAsyncLoader.getFromCutTime()) { if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataAsyncLoader.getFromCutTime()) {
this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
@ -306,7 +368,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
} }
this.moodleCourseDataAsyncLoader.loadSync(restTemplate); this.moodleCourseDataAsyncLoader.loadSync(restTemplate);
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
} }
if (courseQuizData.isEmpty()) { if (courseQuizData.isEmpty()) {
@ -317,7 +379,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
} }
private List<QuizData> getCached() { private List<QuizData> getCached() {
final Collection<CourseDataShort> courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); final Collection<CourseDataShort> courseQuizData =
this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup(); final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH ? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
@ -534,34 +597,41 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
final List<QuizData> courseAndQuiz = courseData.quizzes final List<QuizData> courseAndQuiz = courseData.quizzes
.stream() .stream()
.map(courseQuizData -> { .map(courseQuizData -> createQuizData(lmsSetup, courseData, uriPrefix, additionalAttrs, courseQuizData))
final String startURI = uriPrefix + courseQuizData.course_module;
//additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
return new QuizData(
getInternalQuizId(
courseQuizData.course_module,
courseData.id,
courseData.short_name,
courseData.idnumber),
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
courseQuizData.name,
Constants.EMPTY_NOTE,
(courseQuizData.time_open != null && courseQuizData.time_open > 0)
? Utils.toDateTimeUTCUnix(courseQuizData.time_open)
: Utils.toDateTimeUTCUnix(courseData.start_date),
(courseQuizData.time_close != null && courseQuizData.time_close > 0)
? Utils.toDateTimeUTCUnix(courseQuizData.time_close)
: Utils.toDateTimeUTCUnix(courseData.end_date),
startURI,
additionalAttrs);
})
.collect(Collectors.toList()); .collect(Collectors.toList());
return courseAndQuiz; return courseAndQuiz;
} }
private QuizData createQuizData(
final LmsSetup lmsSetup,
final CourseDataShort courseData,
final String uriPrefix,
final Map<String, String> additionalAttrs,
final CourseQuizShort courseQuizData) {
final String startURI = uriPrefix + courseQuizData.course_module;
return new QuizData(
getInternalQuizId(
courseQuizData.course_module,
courseData.id,
courseData.short_name,
courseData.idnumber),
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
courseQuizData.name,
Constants.EMPTY_NOTE,
(courseQuizData.time_open != null && courseQuizData.time_open > 0)
? Utils.toDateTimeUTCUnix(courseQuizData.time_open)
: Utils.toDateTimeUTCUnix(courseData.start_date),
(courseQuizData.time_close != null && courseQuizData.time_close > 0)
? Utils.toDateTimeUTCUnix(courseQuizData.time_close)
: Utils.toDateTimeUTCUnix(courseData.end_date),
startURI,
additionalAttrs);
}
private Result<MoodleAPIRestTemplate> getRestTemplate() { private Result<MoodleAPIRestTemplate> getRestTemplate() {
if (this.restTemplate == null) { if (this.restTemplate == null) {
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory

View file

@ -65,7 +65,7 @@ public class MoodleCourseDataAsyncLoader {
private final int maxSize; private final int maxSize;
private final int pageSize; private final int pageSize;
private final Set<CourseDataShort> cachedCourseData = new HashSet<>(); private final Map<String, CourseDataShort> cachedCourseData = new HashMap<>();
private final Set<String> newIds = new HashSet<>(); private final Set<String> newIds = new HashSet<>();
private String lmsSetup = Constants.EMPTY_NOTE; private String lmsSetup = Constants.EMPTY_NOTE;
@ -122,8 +122,8 @@ public class MoodleCourseDataAsyncLoader {
this.fromCutTime = fromCutTime; this.fromCutTime = fromCutTime;
} }
public Set<CourseDataShort> getCachedCourseData() { public Map<String, CourseDataShort> getCachedCourseData() {
return new HashSet<>(this.cachedCourseData); return new HashMap<>(this.cachedCourseData);
} }
public long getLastRunTime() { public long getLastRunTime() {
@ -138,7 +138,7 @@ public class MoodleCourseDataAsyncLoader {
return this.lastLoadTime > 3 * Constants.SECOND_IN_MILLIS; return this.lastLoadTime > 3 * Constants.SECOND_IN_MILLIS;
} }
public Set<CourseDataShort> loadSync(final MoodleAPIRestTemplate restTemplate) { public Map<String, CourseDataShort> loadSync(final MoodleAPIRestTemplate restTemplate) {
if (this.running) { if (this.running) {
throw new IllegalStateException("Is already running asynchronously"); throw new IllegalStateException("Is already running asynchronously");
} }
@ -168,14 +168,20 @@ public class MoodleCourseDataAsyncLoader {
} }
public void clearCache() {
if (!isRunning()) {
this.cachedCourseData.clear();
this.newIds.clear();
}
}
private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) { private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) {
return () -> { return () -> {
//this.cachedCourseData.clear();
this.newIds.clear(); this.newIds.clear();
final long startTime = Utils.getMillisecondsNow(); final long startTime = Utils.getMillisecondsNow();
loadAllQuizzes(restTemplate); loadAllQuizzes(restTemplate);
syncCache(); this.syncCache();
this.lastLoadTime = Utils.getMillisecondsNow() - startTime; this.lastLoadTime = Utils.getMillisecondsNow() - startTime;
this.running = false; this.running = false;
@ -263,7 +269,7 @@ public class MoodleCourseDataAsyncLoader {
this.lmsSetup, this.lmsSetup,
c); c);
} else { } else {
this.cachedCourseData.add(c); this.cachedCourseData.put(c.id, c);
this.newIds.add(c.id); this.newIds.add(c.id);
} }
}); });
@ -461,13 +467,15 @@ public class MoodleCourseDataAsyncLoader {
private void syncCache() { private void syncCache() {
if (!this.cachedCourseData.isEmpty()) { if (!this.cachedCourseData.isEmpty()) {
final Set<CourseDataShort> newData = this.cachedCourseData.stream()
.filter(data -> this.newIds.contains(data.id)) final Set<String> oldData = this.cachedCourseData
.keySet()
.stream()
.filter(id -> !this.newIds.contains(id))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
synchronized (this.cachedCourseData) { synchronized (this.cachedCourseData) {
this.cachedCourseData.clear(); oldData.stream().forEach(this.cachedCourseData::remove);
this.cachedCourseData.addAll(newData);
} }
} }
this.newIds.clear(); this.newIds.clear();

View file

@ -110,6 +110,17 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public Result<QuizData> getQuizFromCache(final String id) {
return this.moodleCourseAccess.getQuizFromCache(id)
.orElse(() -> getQuiz(id));
}
@Override
public void clearCache() {
this.moodleCourseAccess.clearCache();
}
@Override @Override
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) { public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
return this.moodleCourseAccess.getQuizzesFromCache(ids) return this.moodleCourseAccess.getQuizzesFromCache(ids)

View file

@ -123,7 +123,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Collection<APIMessage> result = new ArrayList<>(); final Collection<APIMessage> result = new ArrayList<>();
final Exam exam = this.examDAO.byPK(examId) final Exam exam = this.examDAO
.getWithQuizDataFromCache(examId)
.getOrThrow(); .getOrThrow();
// check lms connection // check lms connection

View file

@ -279,7 +279,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
checkReadPrivilege(institutionId); checkReadPrivilege(institutionId);
return this.examDAO.byPK(modelId) return this.examDAO
.getWithQuizDataFromCache(modelId)
.flatMap(this.examAdminService::isRestricted) .flatMap(this.examAdminService::isRestricted)
.getOrThrow(); .getOrThrow();
} }
@ -407,9 +408,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@PathVariable final Long modelId) { @PathVariable final Long modelId) {
checkReadPrivilege(institutionId); checkReadPrivilege(institutionId);
return this.entityDAO.byPK(modelId) return this.examAdminService.getProctoringServiceSettings(modelId)
.flatMap(this.authorization::checkRead)
.flatMap(exam -> this.examAdminService.getProctoringServiceSettings(exam.id))
.getOrThrow(); .getOrThrow();
} }

View file

@ -23,7 +23,7 @@
<tti unit="hours">24</tti> <tti unit="hours">24</tti>
</expiry> </expiry>
<resources> <resources>
<heap unit="entries">2000</heap> <heap unit="entries">3000</heap>
</resources> </resources>
</cache> </cache>
@ -93,7 +93,16 @@
</resources> </resources>
</cache> </cache>
<cache alias="QUIZ_DATA_CACHE">
<key-type>java.lang.String</key-type>
<value-type>ch.ethz.seb.sebserver.gbl.model.exam.QuizData</value-type>
<expiry>
<tti unit="minutes">10</tti>
</expiry>
<resources>
<heap unit="entries">10000</heap>
</resources>
</cache>
</config> </config>