refactoring of LMS API service with overall eh-caching
This commit is contained in:
parent
dd6150ec2a
commit
9214719642
19 changed files with 406 additions and 210 deletions
|
@ -59,6 +59,7 @@ public final class Constants {
|
|||
public static final Character DOUBLE_QUOTE = '"';
|
||||
public static final Character COMMA = ',';
|
||||
public static final Character PIPE = '|';
|
||||
public static final Character UNDERLINE = '_';
|
||||
public static final Character AMPERSAND = '&';
|
||||
public static final Character EQUALITY_SIGN = '=';
|
||||
public static final Character LIST_SEPARATOR_CHAR = COMMA;
|
||||
|
|
|
@ -37,6 +37,8 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
|||
* happened */
|
||||
Result<GrantEntity> examGrantEntityByClientConnection(Long connectionId);
|
||||
|
||||
Result<Exam> getWithQuizDataFromCache(Long id);
|
||||
|
||||
/** Get all active Exams for a given institution.
|
||||
*
|
||||
* @param institutionId the identifier of the institution
|
||||
|
|
|
@ -377,6 +377,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
|
|||
|
||||
private Result<Collection<ExamConfigurationMap>> toDomainModel(
|
||||
final Collection<ExamConfigurationMapRecord> records) {
|
||||
|
||||
return Result.tryCatch(() -> records
|
||||
.stream()
|
||||
.map(model -> this.toDomainModel(model).getOrThrow())
|
||||
|
@ -390,7 +391,8 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
|
|||
.selectByPrimaryKey(record.getConfigurationNodeId());
|
||||
final String status = config.getStatus();
|
||||
|
||||
final Exam exam = this.examDAO.byPK(record.getExamId())
|
||||
final Exam exam = this.examDAO
|
||||
.getWithQuizDataFromCache(record.getExamId())
|
||||
.getOr(null);
|
||||
|
||||
return new ExamConfigurationMap(
|
||||
|
|
|
@ -119,6 +119,13 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.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
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) {
|
||||
|
@ -761,6 +768,17 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
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) {
|
||||
return toDomainModel(
|
||||
record.getLmsSetupId(),
|
||||
|
|
|
@ -165,6 +165,15 @@ public interface LmsAPITemplate {
|
|||
* @return Collection of all {@link QuizData } from the given id set */
|
||||
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,
|
||||
* to LMS examinee account details by requesting them on the LMS API with the given examineeUserId
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -11,12 +11,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
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.async.AsyncService;
|
||||
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.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||
|
@ -101,42 +98,8 @@ public abstract class AbstractCourseAccess {
|
|||
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) {
|
||||
return allQuizzesSupplier().getAll(filterMap);
|
||||
return this.quizzesRequest.protectedRun(allQuizzesSupplier(filterMap));
|
||||
}
|
||||
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
|
||||
|
@ -172,30 +135,41 @@ public abstract class AbstractCourseAccess {
|
|||
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 */
|
||||
protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids);
|
||||
|
||||
/** Provides a AllQuizzesSupplier to supply quiz data either form cache or from LMS */
|
||||
protected abstract AllQuizzesSupplier allQuizzesSupplier();
|
||||
/** Provides a supplier to supply request to use within the circuit breaker */
|
||||
protected abstract Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap);
|
||||
|
||||
/** Provides a supplier for the course chapter data request to use within the circuit breaker */
|
||||
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
|
||||
|
||||
/** Gives a fetch status if asynchronous quiz data fetching is part of the underling implementation */
|
||||
protected abstract FetchStatus getFetchStatus();
|
||||
|
||||
/** Uses to supply quiz data */
|
||||
protected interface AllQuizzesSupplier {
|
||||
/** 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);
|
||||
protected FetchStatus getFetchStatus() {
|
||||
if (this.quizzesRequest.getState() != State.CLOSED) {
|
||||
return FetchStatus.FETCH_ERROR;
|
||||
}
|
||||
return FetchStatus.ALL_FETCHED;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -98,17 +98,20 @@ public class LmsAPIServiceImpl implements LmsAPIService {
|
|||
@Override
|
||||
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
|
||||
return Result.tryCatch(() -> {
|
||||
LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
|
||||
if (lmsAPITemplate == null) {
|
||||
lmsAPITemplate = createLmsSetupTemplate(lmsSetupId);
|
||||
if (lmsAPITemplate != null) {
|
||||
this.cache.put(new CacheKey(lmsSetupId, System.currentTimeMillis()), lmsAPITemplate);
|
||||
synchronized (this) {
|
||||
LmsAPITemplate lmsAPITemplate = getFromCache(lmsSetupId);
|
||||
if (lmsAPITemplate == null) {
|
||||
lmsAPITemplate = createLmsSetupTemplate(lmsSetupId);
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -91,12 +91,10 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
|
|||
public Result<SEBRestriction> getSEBRestrictionFromExam(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
// 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 Collection<String> generatedKeys = this.examConfigService
|
||||
.generateConfigKeys(exam.institutionId, exam.id)
|
||||
.getOrThrow();
|
||||
System.out.println("******* " + (System.currentTimeMillis() - currentTimeMillis));
|
||||
|
||||
configKeys.addAll(generatedKeys);
|
||||
if (generatedKeys != null && !generatedKeys.isEmpty()) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.stream.Collectors;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpEntity;
|
||||
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.api.JSONMapper;
|
||||
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.QuizData;
|
||||
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.servicelayer.dao.FilterMap;
|
||||
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.
|
||||
*
|
||||
* 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);
|
||||
|
||||
|
@ -74,65 +73,34 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
private final JSONMapper jsonMapper;
|
||||
private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
|
||||
private final WebserviceInfo webserviceInfo;
|
||||
private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
|
||||
private final AllQuizzesSupplier allQuizzesSupplier;
|
||||
|
||||
private OAuth2RestTemplate restTemplate;
|
||||
private final Long lmsSetupId;
|
||||
|
||||
public OpenEdxCourseAccess(
|
||||
final JSONMapper jsonMapper,
|
||||
final OpenEdxRestTemplateFactory openEdxRestTemplateFactory,
|
||||
final WebserviceInfo webserviceInfo,
|
||||
final AsyncService asyncService,
|
||||
final Environment environment) {
|
||||
final Environment environment,
|
||||
final CacheManager cacheManager) {
|
||||
|
||||
super(asyncService, environment);
|
||||
super(asyncService, environment, cacheManager);
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
|
||||
this.webserviceInfo = webserviceInfo;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
};
|
||||
this.lmsSetupId = openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup().id;
|
||||
}
|
||||
|
||||
APITemplateDataSupplier getApiTemplateDataSupplier() {
|
||||
return this.openEdxRestTemplateFactory.apiTemplateDataSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long getLmsSetupId() {
|
||||
return this.lmsSetupId;
|
||||
}
|
||||
|
||||
LmsSetupTestResult initAPIAccess() {
|
||||
|
||||
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
|
||||
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||
|
||||
|
@ -231,7 +206,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
getOneCourses(
|
||||
lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
|
||||
getRestTemplate().getOrThrow(),
|
||||
ids.iterator().next()),
|
||||
ids),
|
||||
externalStartURI));
|
||||
};
|
||||
} else {
|
||||
|
@ -241,12 +216,6 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
}
|
||||
}
|
||||
|
||||
private Supplier<List<QuizData>> quizzesSupplier() {
|
||||
return () -> getRestTemplate()
|
||||
.map(this::collectAllQuizzes)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
|
||||
return () -> {
|
||||
|
@ -337,7 +306,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
final List<CourseData> collector = new ArrayList<>();
|
||||
EdXPage page = getEdxPage(pageURI, restTemplate).getBody();
|
||||
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)) {
|
||||
page = getEdxPage(page.next, restTemplate).getBody();
|
||||
if (page != null) {
|
||||
|
@ -355,16 +327,33 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
private CourseData getOneCourses(
|
||||
final String pageURI,
|
||||
final OAuth2RestTemplate restTemplate,
|
||||
final String id) {
|
||||
final Set<String> ids) {
|
||||
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
final ResponseEntity<CourseData> exchange = restTemplate.exchange(
|
||||
pageURI + "/" + id,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders),
|
||||
CourseData.class);
|
||||
System.out.println("********************");
|
||||
|
||||
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) {
|
||||
|
@ -403,7 +392,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
Blocks.class);
|
||||
}
|
||||
|
||||
private static QuizData quizDataOf(
|
||||
private QuizData quizDataOf(
|
||||
final LmsSetup lmsSetup,
|
||||
final CourseData courseData,
|
||||
final String uriPrefix) {
|
||||
|
@ -411,7 +400,7 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
final String startURI = uriPrefix + courseData.id;
|
||||
final Map<String, String> additionalAttrs = new HashMap<>();
|
||||
additionalAttrs.put("blocks_url", courseData.blocks_url);
|
||||
return new QuizData(
|
||||
final QuizData quizData = new QuizData(
|
||||
courseData.id,
|
||||
lmsSetup.getInstitutionId(),
|
||||
lmsSetup.id,
|
||||
|
@ -421,6 +410,10 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
courseData.start,
|
||||
courseData.end,
|
||||
startURI);
|
||||
|
||||
super.putToCache(quizData);
|
||||
|
||||
return quizData;
|
||||
}
|
||||
|
||||
/** Maps a OpenEdX course API course page */
|
||||
|
@ -538,17 +531,4 @@ final class OpenEdxCourseAccess extends AbstractCourseAccess {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -98,12 +98,24 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<QuizData> getQuizFromCache(final String id) {
|
||||
return this.openEdxCourseAccess
|
||||
.getQuizFromCache(id)
|
||||
.orElse(() -> getQuiz(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
|
||||
return this.openEdxCourseAccess.getQuizzesFromCache(ids)
|
||||
.getOrElse(() -> getQuizzes(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
this.openEdxCourseAccess.clearCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Chapters> getCourseChapters(final String courseId) {
|
||||
return Result.tryCatch(() -> this.openEdxCourseAccess
|
||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -36,6 +37,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
private final WebserviceInfo webserviceInfo;
|
||||
private final AsyncService asyncService;
|
||||
private final Environment environment;
|
||||
private final CacheManager cacheManager;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
private final String[] alternativeTokenRequestPaths;
|
||||
|
@ -46,6 +48,7 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
final WebserviceInfo webserviceInfo,
|
||||
final AsyncService asyncService,
|
||||
final Environment environment,
|
||||
final CacheManager cacheManager,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
@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.asyncService = asyncService;
|
||||
this.environment = environment;
|
||||
this.cacheManager = cacheManager;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||
|
@ -84,7 +88,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
openEdxRestTemplateFactory,
|
||||
this.webserviceInfo,
|
||||
this.asyncService,
|
||||
this.environment);
|
||||
this.environment,
|
||||
this.cacheManager);
|
||||
|
||||
final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction(
|
||||
this.jsonMapper,
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -181,6 +183,16 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
|
|||
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
|
||||
public Result<Chapters> getCourseChapters(final String courseId) {
|
||||
return Result.ofError(new UnsupportedOperationException());
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
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.async.AsyncService;
|
||||
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.QuizData;
|
||||
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.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.CourseQuizShort;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
|
||||
|
||||
/** 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 MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader;
|
||||
private final CircuitBreaker<List<QuizData>> allQuizzesRequest;
|
||||
private final AllQuizzesSupplier allQuizzesSupplier;
|
||||
|
||||
private MoodleAPIRestTemplate restTemplate;
|
||||
|
||||
|
@ -119,19 +121,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
"sebserver.webservice.circuitbreaker.allQuizzesRequest.timeToRecover",
|
||||
Long.class,
|
||||
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() {
|
||||
|
@ -223,6 +212,77 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
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
|
||||
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||
return () -> getRestTemplate()
|
||||
|
@ -231,6 +291,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
|
||||
return () -> getRestTemplate()
|
||||
.map(template -> collectAllQuizzes(template, filterMap))
|
||||
|
@ -244,6 +305,9 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
|
||||
@Override
|
||||
protected FetchStatus getFetchStatus() {
|
||||
if (this.allQuizzesRequest.getState() != State.CLOSED) {
|
||||
return FetchStatus.FETCH_ERROR;
|
||||
}
|
||||
if (this.moodleCourseDataAsyncLoader.isRunning()) {
|
||||
return FetchStatus.ASYNC_FETCH_RUNNING;
|
||||
}
|
||||
|
@ -251,9 +315,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
return FetchStatus.ALL_FETCHED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AllQuizzesSupplier allQuizzesSupplier() {
|
||||
return this.allQuizzesSupplier;
|
||||
public void clearCache() {
|
||||
this.moodleCourseDataAsyncLoader.clearCache();
|
||||
}
|
||||
|
||||
private List<QuizData> collectAllQuizzes(
|
||||
|
@ -261,7 +324,6 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
final FilterMap filterMap) {
|
||||
|
||||
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;
|
||||
|
@ -272,7 +334,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
// Verify and call the proper strategy to get the course and quiz data
|
||||
Collection<CourseDataShort> courseQuizData = Collections.emptyList();
|
||||
if (this.moodleCourseDataAsyncLoader.isRunning()) {
|
||||
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
|
||||
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
|
||||
} else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) {
|
||||
// set cut time if available
|
||||
if (fromCutTime >= 0) {
|
||||
|
@ -282,7 +344,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
|
||||
try {
|
||||
Thread.sleep(INITIAL_WAIT_TIME);
|
||||
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
|
||||
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to wait for first load run: ", e);
|
||||
return Collections.emptyList();
|
||||
|
@ -290,7 +352,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
} else if (this.moodleCourseDataAsyncLoader.isLongRunningTask()) {
|
||||
// on long running tasks if we have a different fromCutTime as before
|
||||
// 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()) {
|
||||
this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
|
||||
this.moodleCourseDataAsyncLoader.loadAsync(restTemplate);
|
||||
|
@ -306,7 +368,7 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime);
|
||||
}
|
||||
this.moodleCourseDataAsyncLoader.loadSync(restTemplate);
|
||||
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData();
|
||||
courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData().values();
|
||||
}
|
||||
|
||||
if (courseQuizData.isEmpty()) {
|
||||
|
@ -317,7 +379,8 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
}
|
||||
|
||||
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 String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||
|
@ -534,34 +597,41 @@ public class MoodleCourseAccess extends AbstractCourseAccess {
|
|||
|
||||
final List<QuizData> courseAndQuiz = courseData.quizzes
|
||||
.stream()
|
||||
.map(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);
|
||||
})
|
||||
.map(courseQuizData -> createQuizData(lmsSetup, courseData, uriPrefix, additionalAttrs, courseQuizData))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
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() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
|
||||
|
|
|
@ -65,7 +65,7 @@ public class MoodleCourseDataAsyncLoader {
|
|||
private final int maxSize;
|
||||
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 String lmsSetup = Constants.EMPTY_NOTE;
|
||||
|
@ -122,8 +122,8 @@ public class MoodleCourseDataAsyncLoader {
|
|||
this.fromCutTime = fromCutTime;
|
||||
}
|
||||
|
||||
public Set<CourseDataShort> getCachedCourseData() {
|
||||
return new HashSet<>(this.cachedCourseData);
|
||||
public Map<String, CourseDataShort> getCachedCourseData() {
|
||||
return new HashMap<>(this.cachedCourseData);
|
||||
}
|
||||
|
||||
public long getLastRunTime() {
|
||||
|
@ -138,7 +138,7 @@ public class MoodleCourseDataAsyncLoader {
|
|||
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) {
|
||||
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) {
|
||||
return () -> {
|
||||
//this.cachedCourseData.clear();
|
||||
this.newIds.clear();
|
||||
final long startTime = Utils.getMillisecondsNow();
|
||||
|
||||
loadAllQuizzes(restTemplate);
|
||||
syncCache();
|
||||
this.syncCache();
|
||||
|
||||
this.lastLoadTime = Utils.getMillisecondsNow() - startTime;
|
||||
this.running = false;
|
||||
|
@ -263,7 +269,7 @@ public class MoodleCourseDataAsyncLoader {
|
|||
this.lmsSetup,
|
||||
c);
|
||||
} else {
|
||||
this.cachedCourseData.add(c);
|
||||
this.cachedCourseData.put(c.id, c);
|
||||
this.newIds.add(c.id);
|
||||
}
|
||||
});
|
||||
|
@ -461,13 +467,15 @@ public class MoodleCourseDataAsyncLoader {
|
|||
|
||||
private void syncCache() {
|
||||
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());
|
||||
|
||||
synchronized (this.cachedCourseData) {
|
||||
this.cachedCourseData.clear();
|
||||
this.cachedCourseData.addAll(newData);
|
||||
oldData.stream().forEach(this.cachedCourseData::remove);
|
||||
}
|
||||
}
|
||||
this.newIds.clear();
|
||||
|
|
|
@ -110,6 +110,17 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
|
|||
.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
|
||||
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
|
||||
return this.moodleCourseAccess.getQuizzesFromCache(ids)
|
||||
|
|
|
@ -123,7 +123,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
return Result.tryCatch(() -> {
|
||||
final Collection<APIMessage> result = new ArrayList<>();
|
||||
|
||||
final Exam exam = this.examDAO.byPK(examId)
|
||||
final Exam exam = this.examDAO
|
||||
.getWithQuizDataFromCache(examId)
|
||||
.getOrThrow();
|
||||
|
||||
// check lms connection
|
||||
|
|
|
@ -279,7 +279,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
|
||||
|
||||
checkReadPrivilege(institutionId);
|
||||
return this.examDAO.byPK(modelId)
|
||||
return this.examDAO
|
||||
.getWithQuizDataFromCache(modelId)
|
||||
.flatMap(this.examAdminService::isRestricted)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
@ -407,9 +408,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
@PathVariable final Long modelId) {
|
||||
|
||||
checkReadPrivilege(institutionId);
|
||||
return this.entityDAO.byPK(modelId)
|
||||
.flatMap(this.authorization::checkRead)
|
||||
.flatMap(exam -> this.examAdminService.getProctoringServiceSettings(exam.id))
|
||||
return this.examAdminService.getProctoringServiceSettings(modelId)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<tti unit="hours">24</tti>
|
||||
</expiry>
|
||||
<resources>
|
||||
<heap unit="entries">2000</heap>
|
||||
<heap unit="entries">3000</heap>
|
||||
</resources>
|
||||
</cache>
|
||||
|
||||
|
@ -93,7 +93,16 @@
|
|||
</resources>
|
||||
</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>
|
Loading…
Reference in a new issue