From eec4392f78c4f15f797358d55c58d2cd49a9a03f Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 12 Jan 2021 10:10:30 +0100 Subject: [PATCH] Added EHcache for caching and improved Moodle asnyc loading --- pom.xml | 14 +- .../java/ch/ethz/seb/sebserver/SEBServer.java | 2 - .../sebserver/gui/content/QuizLookupList.java | 9 +- .../seb/sebserver/webservice/CacheConfig.java | 66 ++++ .../servicelayer/lms/impl/CourseAccess.java | 84 +++-- .../lms/impl/edx/OpenEdxCourseAccess.java | 63 +++- .../edx/OpenEdxLmsAPITemplateFactory.java | 7 +- .../lms/impl/moodle/MoodleCourseAccess.java | 186 +++++++--- ....java => MoodleCourseDataAsyncLoader.java} | 325 ++++++++++++------ .../moodle/MoodleLmsAPITemplateFactory.java | 14 +- .../impl/SEBClientConnectionServiceImpl.java | 4 +- .../SEBClientNotificationServiceImpl.java | 12 +- .../config/application-dev-ws.properties | 2 - .../config/application-dev.properties | 2 + .../config/application-ws.properties | 4 + .../resources/config/application.properties | 2 +- src/main/resources/config/ehcache.xml | 85 +++++ src/main/resources/logback-spring.xml | 1 + .../impl/moodle/MoodleCourseAccessTest.java | 18 +- .../resources/application-test.properties | 3 + 20 files changed, 685 insertions(+), 218 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/{MoodleCourseDataLazyLoader.java => MoodleCourseDataAsyncLoader.java} (51%) create mode 100644 src/main/resources/config/ehcache.xml diff --git a/pom.xml b/pom.xml index 47304872..1ed4d64b 100644 --- a/pom.xml +++ b/pom.xml @@ -290,11 +290,15 @@ flyway-core - - - - - + + + org.ehcache + ehcache + + + javax.cache + cache-api + diff --git a/src/main/java/ch/ethz/seb/sebserver/SEBServer.java b/src/main/java/ch/ethz/seb/sebserver/SEBServer.java index 2bb3ba30..f0ae0de5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/SEBServer.java +++ b/src/main/java/ch/ethz/seb/sebserver/SEBServer.java @@ -15,7 +15,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; @@ -40,7 +39,6 @@ import ch.ethz.seb.sebserver.gbl.profile.ProdWebServiceProfile; @SpringBootApplication(exclude = { UserDetailsServiceAutoConfiguration.class, }) -@EnableCaching public class SEBServer { public static void main(final String[] args) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizLookupList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizLookupList.java index 2df044aa..a6ae673e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizLookupList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/QuizLookupList.java @@ -45,6 +45,7 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamImported; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser.GrantCheck; @@ -297,7 +298,11 @@ public class QuizLookupList implements TemplateComposer { final QuizData quizData, final Function institutionNameFunction) { - action.getSingleSelection(); + final QuizData fullQuizData = this.pageService.getRestService().getBuilder(GetQuizData.class) + .withURIVariable(API.PARAM_MODEL_ID, quizData.getModelId()) + .withQueryParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, String.valueOf(quizData.lmsSetupId)) + .call() + .getOr(quizData); final ModalInputDialog dialog = new ModalInputDialog( action.pageContext().getParent().getShell(), @@ -307,7 +312,7 @@ public class QuizLookupList implements TemplateComposer { dialog.open( DETAILS_TITLE_TEXT_KEY, action.pageContext(), - pc -> createDetailsForm(quizData, pc, institutionNameFunction)); + pc -> createDetailsForm(fullQuizData, pc, institutionNameFunction)); return action; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java new file mode 100644 index 00000000..19240cfe --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/CacheConfig.java @@ -0,0 +1,66 @@ +/* + * 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; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; + +import javax.cache.Caching; +import javax.cache.spi.CachingProvider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.cache.jcache.config.JCacheConfigurerSupport; +import org.springframework.cache.support.CompositeCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; + +@EnableCaching +@WebServiceProfile +@Configuration +public class CacheConfig extends JCacheConfigurerSupport { + + private static final Logger log = LoggerFactory.getLogger(CacheConfig.class); + + @Value("${spring.cache.jcache.config}") + String jCacheConfig; + + @Override + @Bean + public CacheManager cacheManager() { + try { + final CachingProvider cachingProvider = Caching.getCachingProvider(); + final javax.cache.CacheManager cacheManager = + cachingProvider.getCacheManager(new URI(this.jCacheConfig), this.getClass().getClassLoader()); + System.out.println("cacheManager:" + cacheManager); + + final CompositeCacheManager composite = new CompositeCacheManager(); + composite.setCacheManagers(Arrays.asList( + new JCacheCacheManager(cacheManager), + new ConcurrentMapCacheManager())); + composite.setFallbackToNoOpCache(true); + + return composite; + + } catch (final URISyntaxException e) { + log.error("Failed to initialize caching with EHCache. Fallback to simple caching"); + return new ConcurrentMapCacheManager(); + } + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java index f5e02d24..edc58243 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/CourseAccess.java @@ -20,55 +20,81 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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.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.user.ExamineeAccountDetails; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; public abstract class CourseAccess { private static final Logger log = LoggerFactory.getLogger(CourseAccess.class); - protected final MemoizingCircuitBreaker> allQuizzesRequest; + public enum FetchStatus { + ALL_FETCHED, + ASYNC_FETCH_RUNNING, + FETCH_ERROR + } + protected final CircuitBreaker> quizzesRequest; protected final CircuitBreaker chaptersRequest; protected final CircuitBreaker accountDetailRequest; - protected CourseAccess(final AsyncService asyncService) { - this.allQuizzesRequest = asyncService.createMemoizingCircuitBreaker( - allQuizzesSupplier(null), - 3, - Constants.MINUTE_IN_MILLIS, - Constants.MINUTE_IN_MILLIS, - true, - Constants.HOUR_IN_MILLIS); + protected CourseAccess( + final AsyncService asyncService, + final Environment environment) { this.quizzesRequest = asyncService.createCircuitBreaker( - 3, - Constants.MINUTE_IN_MILLIS, - Constants.MINUTE_IN_MILLIS); + environment.getProperty( + "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", + Integer.class, + 3), + environment.getProperty( + "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime", + Long.class, + Constants.MINUTE_IN_MILLIS), + environment.getProperty( + "sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover", + Long.class, + Constants.MINUTE_IN_MILLIS)); this.chaptersRequest = asyncService.createCircuitBreaker( - 3, - Constants.SECOND_IN_MILLIS * 10, - Constants.MINUTE_IN_MILLIS); + environment.getProperty( + "sebserver.webservice.circuitbreaker.chaptersRequest.attempts", + Integer.class, + 3), + environment.getProperty( + "sebserver.webservice.circuitbreaker.chaptersRequest.blockingTime", + Long.class, + Constants.SECOND_IN_MILLIS * 10), + environment.getProperty( + "sebserver.webservice.circuitbreaker.chaptersRequest.timeToRecover", + Long.class, + Constants.MINUTE_IN_MILLIS)); this.accountDetailRequest = asyncService.createCircuitBreaker( - 1, - Constants.SECOND_IN_MILLIS * 10, - Constants.SECOND_IN_MILLIS * 10); + environment.getProperty( + "sebserver.webservice.circuitbreaker.accountDetailRequest.attempts", + Integer.class, + 2), + environment.getProperty( + "sebserver.webservice.circuitbreaker.accountDetailRequest.blockingTime", + Long.class, + Constants.SECOND_IN_MILLIS * 10), + environment.getProperty( + "sebserver.webservice.circuitbreaker.accountDetailRequest.timeToRecover", + Long.class, + Constants.SECOND_IN_MILLIS * 10)); } public Result>> getQuizzesFromCache(final Set ids) { return Result.tryCatch(() -> { - final List cached = this.allQuizzesRequest.getCached(); + final List cached = allQuizzesSupplier().getAllCached(); final List available = (cached != null) ? cached : quizzesSupplier(ids).get(); @@ -101,11 +127,7 @@ public abstract class CourseAccess { } public Result> getQuizzes(final FilterMap filterMap) { - if (filterMap != null) { - this.allQuizzesRequest.setSupplier(allQuizzesSupplier(filterMap)); - } - return this.allQuizzesRequest.get() - .map(LmsAPIService.quizzesFilterFunction(filterMap)); + return allQuizzesSupplier().getAll(filterMap); } public Result getExamineeAccountDetails(final String examineeSessionId) { @@ -139,8 +161,16 @@ public abstract class CourseAccess { protected abstract Supplier> quizzesSupplier(final Set ids); - protected abstract Supplier> allQuizzesSupplier(final FilterMap filterMap); + protected abstract AllQuizzesSupplier allQuizzesSupplier(); protected abstract Supplier getCourseChaptersSupplier(final String courseId); + protected abstract FetchStatus getFetchStatus(); + + protected interface AllQuizzesSupplier { + List getAllCached(); + + Result> getAll(final FilterMap filterMap); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java index 95b7587b..69af7ac4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,6 +42,8 @@ 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; @@ -70,6 +73,8 @@ final class OpenEdxCourseAccess extends CourseAccess { private final LmsSetup lmsSetup; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; private final WebserviceInfo webserviceInfo; + private final MemoizingCircuitBreaker> allQuizzesRequest; + private final AllQuizzesSupplier allQuizzesSupplier; private OAuth2RestTemplate restTemplate; @@ -78,13 +83,51 @@ final class OpenEdxCourseAccess extends CourseAccess { final LmsSetup lmsSetup, final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, final WebserviceInfo webserviceInfo, - final AsyncService asyncService) { + final AsyncService asyncService, + final Environment environment) { - super(asyncService); + super(asyncService, environment); this.jsonMapper = jsonMapper; this.lmsSetup = lmsSetup; 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 getAllCached() { + return OpenEdxCourseAccess.this.allQuizzesRequest.getCached(); + } + + @Override + public Result> getAll(final FilterMap filterMap) { + return OpenEdxCourseAccess.this.allQuizzesRequest.get(); + } + + }; } LmsSetupTestResult initAPIAccess() { @@ -173,8 +216,7 @@ final class OpenEdxCourseAccess extends CourseAccess { .getOrThrow(); } - @Override - protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { + private Supplier> quizzesSupplier() { return () -> getRestTemplate() .map(this::collectAllQuizzes) .getOrThrow(); @@ -451,4 +493,17 @@ final class OpenEdxCourseAccess extends CourseAccess { 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; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java index 8133165f..faa55804 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java @@ -11,6 +11,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.context.annotation.Lazy; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; @@ -33,6 +34,7 @@ public class OpenEdxLmsAPITemplateFactory { private final JSONMapper jsonMapper; private final WebserviceInfo webserviceInfo; private final AsyncService asyncService; + private final Environment environment; private final ClientCredentialService clientCredentialService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final String[] alternativeTokenRequestPaths; @@ -42,6 +44,7 @@ public class OpenEdxLmsAPITemplateFactory { final JSONMapper jsonMapper, final WebserviceInfo webserviceInfo, final AsyncService asyncService, + final Environment environment, final ClientCredentialService clientCredentialService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService, @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths, @@ -50,6 +53,7 @@ public class OpenEdxLmsAPITemplateFactory { this.jsonMapper = jsonMapper; this.webserviceInfo = webserviceInfo; this.asyncService = asyncService; + this.environment = environment; this.clientCredentialService = clientCredentialService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) @@ -78,7 +82,8 @@ public class OpenEdxLmsAPITemplateFactory { lmsSetup, openEdxRestTemplateFactory, this.webserviceInfo, - this.asyncService); + this.asyncService, + this.environment); final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( lmsSetup, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index d2641b83..918186b5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -23,6 +23,7 @@ import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -34,6 +35,7 @@ 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; 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; @@ -43,6 +45,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseDataAsyncLoader.CourseDataShort; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; /** Implements the LmsAPITemplate for Open edX LMS Course API access. @@ -73,7 +76,9 @@ public class MoodleCourseAccess extends CourseAccess { private final JSONMapper jsonMapper; private final LmsSetup lmsSetup; private final MoodleRestTemplateFactory moodleRestTemplateFactory; - private final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader; + private final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader; + private final CircuitBreaker> allQuizzesRequest; + private final AllQuizzesSupplier allQuizzesSupplier; private MoodleAPIRestTemplate restTemplate; @@ -81,14 +86,42 @@ public class MoodleCourseAccess extends CourseAccess { final JSONMapper jsonMapper, final LmsSetup lmsSetup, final MoodleRestTemplateFactory moodleRestTemplateFactory, - final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader, - final AsyncService asyncService) { + final MoodleCourseDataAsyncLoader moodleCourseDataAsyncLoader, + final AsyncService asyncService, + final Environment environment) { - super(asyncService); + super(asyncService, environment); this.jsonMapper = jsonMapper; this.lmsSetup = lmsSetup; - this.moodleCourseDataLazyLoader = moodleCourseDataLazyLoader; + this.moodleCourseDataAsyncLoader = moodleCourseDataAsyncLoader; this.moodleRestTemplateFactory = moodleRestTemplateFactory; + + this.allQuizzesRequest = asyncService.createCircuitBreaker( + 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)); + + this.allQuizzesSupplier = new AllQuizzesSupplier() { + + @Override + public List getAllCached() { + return getCached(); + } + + @Override + public Result> getAll(final FilterMap filterMap) { + return MoodleCourseAccess.this.allQuizzesRequest.protectedRun(allQuizzesSupplier(filterMap)); + } + }; } @Override @@ -175,7 +208,6 @@ public class MoodleCourseAccess extends CourseAccess { } - @Override protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { return () -> getRestTemplate() .map(template -> collectAllQuizzes(template, filterMap)) @@ -187,6 +219,20 @@ public class MoodleCourseAccess extends CourseAccess { throw new UnsupportedOperationException("not available yet"); } + @Override + protected FetchStatus getFetchStatus() { + if (this.moodleCourseDataAsyncLoader.isRunning()) { + return FetchStatus.ASYNC_FETCH_RUNNING; + } + + return FetchStatus.ALL_FETCHED; + } + + @Override + protected AllQuizzesSupplier allQuizzesSupplier() { + return this.allQuizzesSupplier; + } + private List collectAllQuizzes( final MoodleAPIRestTemplate restTemplate, final FilterMap filterMap) { @@ -198,42 +244,42 @@ public class MoodleCourseAccess extends CourseAccess { final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; - Collection courseQuizData = Collections.emptyList(); - if (this.moodleCourseDataLazyLoader.isRunning()) { - courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); - } else if (this.moodleCourseDataLazyLoader.getLastRunTime() <= 0) { + Collection courseQuizData = Collections.emptyList(); + if (this.moodleCourseDataAsyncLoader.isRunning()) { + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); + } else if (this.moodleCourseDataAsyncLoader.getLastRunTime() <= 0) { // set cut time if available if (fromCutTime >= 0) { - this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); + this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); } // first run async and wait some time, get what is there - this.moodleCourseDataLazyLoader.loadAsync(restTemplate); + this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); try { Thread.sleep(INITIAL_WAIT_TIME); - courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); } catch (final Exception e) { log.error("Failed to wait for first load run: ", e); return Collections.emptyList(); } - } else if (this.moodleCourseDataLazyLoader.isLongRunningTask()) { + } 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 - if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataLazyLoader.getFromCutTime()) { - this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); - this.moodleCourseDataLazyLoader.loadAsync(restTemplate); + if (fromCutTime > 0 && fromCutTime != this.moodleCourseDataAsyncLoader.getFromCutTime()) { + this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); + this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); // otherwise kick off only if the last fetch task was then minutes ago - } else if (Utils.getMillisecondsNow() - this.moodleCourseDataLazyLoader.getLastRunTime() > 10 + } else if (Utils.getMillisecondsNow() - this.moodleCourseDataAsyncLoader.getLastRunTime() > 10 * Constants.MINUTE_IN_MILLIS) { - this.moodleCourseDataLazyLoader.loadAsync(restTemplate); + this.moodleCourseDataAsyncLoader.loadAsync(restTemplate); } - courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); } else { // just run the task in sync if (fromCutTime >= 0) { - this.moodleCourseDataLazyLoader.setFromCutTime(fromCutTime); + this.moodleCourseDataAsyncLoader.setFromCutTime(fromCutTime); } - this.moodleCourseDataLazyLoader.loadSync(restTemplate); - courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds(); + this.moodleCourseDataAsyncLoader.loadSync(restTemplate); + courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); } if (courseQuizData.isEmpty()) { @@ -257,6 +303,29 @@ public class MoodleCourseAccess extends CourseAccess { }); } + private List getCached() { + final Collection courseQuizData = this.moodleCourseDataAsyncLoader.getCachedCourseData(); + + final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR)) + ? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH + : this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH; + return courseQuizData + .stream() + .reduce( + new ArrayList<>(), + (list, courseData) -> { + list.addAll(quizDataOf( + this.lmsSetup, + courseData, + urlPrefix)); + return list; + }, + (list1, list2) -> { + list1.addAll(list2); + return list1; + }); + } + private List getQuizzesForIds( final MoodleAPIRestTemplate restTemplate, final Set quizIds) { @@ -405,6 +474,46 @@ public class MoodleCourseAccess extends CourseAccess { return courseAndQuiz; } + private List quizDataOf( + final LmsSetup lmsSetup, + final CourseDataShort courseData, + final String uriPrefix) { + + additionalAttrs.clear(); + 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 List 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); + }) + .collect(Collectors.toList()); + + return courseAndQuiz; + } + private Result getRestTemplate() { if (this.restTemplate == null) { final Result templateRequest = this.moodleRestTemplateFactory @@ -482,7 +591,7 @@ public class MoodleCourseAccess extends CourseAccess { /** Maps the Moodle course API course data */ @JsonIgnoreProperties(ignoreUnknown = true) - static final class CourseData { + private static final class CourseData { final String id; final String short_name; final String idnumber; @@ -516,35 +625,10 @@ public class MoodleCourseAccess extends CourseAccess { this.end_date = end_date; this.time_created = time_created; } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((this.id == null) ? 0 : this.id.hashCode()); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - final CourseData other = (CourseData) obj; - if (this.id == null) { - if (other.id != null) - return false; - } else if (!this.id.equals(other.id)) - return false; - return true; - } } @JsonIgnoreProperties(ignoreUnknown = true) - static final class Courses { + private static final class Courses { final Collection courses; @JsonCreator @@ -555,7 +639,7 @@ public class MoodleCourseAccess extends CourseAccess { } @JsonIgnoreProperties(ignoreUnknown = true) - static final class CourseQuizData { + private static final class CourseQuizData { final Collection quizzes; @JsonCreator diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataLazyLoader.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java similarity index 51% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataLazyLoader.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java index 89474626..c264befc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataLazyLoader.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseDataAsyncLoader.java @@ -28,8 +28,10 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Scope; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -40,40 +42,72 @@ import com.fasterxml.jackson.databind.JsonMappingException; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.async.AsyncRunner; +import ch.ethz.seb.sebserver.gbl.async.AsyncService; +import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseData; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseQuiz; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseQuizData; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.Courses; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate; @Lazy @Component @WebServiceProfile @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) -public class MoodleCourseDataLazyLoader { +public class MoodleCourseDataAsyncLoader { - private static final Logger log = LoggerFactory.getLogger(MoodleCourseDataLazyLoader.class); + private static final Logger log = LoggerFactory.getLogger(MoodleCourseDataAsyncLoader.class); private final JSONMapper jsonMapper; private final AsyncRunner asyncRunner; + private final CircuitBreaker moodleRestCall; + private final int maxSize; + private final int pageSize; - private final Set preFilteredCourseIds = new HashSet<>(); + private final Set cachedCourseData = new HashSet<>(); + private String lmsSetup = Constants.EMPTY_NOTE; private long lastRunTime = 0; private long lastLoadTime = 0; private boolean running = false; private long fromCutTime; - public MoodleCourseDataLazyLoader( + public MoodleCourseDataAsyncLoader( final JSONMapper jsonMapper, - final AsyncRunner asyncRunner) { + final AsyncService asyncService, + final AsyncRunner asyncRunner, + final Environment environment) { this.jsonMapper = jsonMapper; - this.asyncRunner = asyncRunner; this.fromCutTime = Utils.toUnixTimeInSeconds(DateTime.now(DateTimeZone.UTC).minusYears(3)); + this.asyncRunner = asyncRunner; + + this.moodleRestCall = asyncService.createCircuitBreaker( + environment.getProperty( + "sebserver.webservice.circuitbreaker.moodleRestCall.attempts", + Integer.class, + 2), + environment.getProperty( + "sebserver.webservice.circuitbreaker.moodleRestCall.blockingTime", + Long.class, + Constants.SECOND_IN_MILLIS * 20), + environment.getProperty( + "sebserver.webservice.circuitbreaker.moodleRestCall.timeToRecover", + Long.class, + Constants.MINUTE_IN_MILLIS)); + + this.maxSize = + environment.getProperty("sebserver.webservice.cache.moodle.course.maxSize", Integer.class, 10000); + this.pageSize = + environment.getProperty("sebserver.webservice.cache.moodle.course.pageSize", Integer.class, 500); + } + + public void init(final String lmsSetupName) { + if (Constants.EMPTY_NOTE.equals(this.lmsSetup)) { + this.lmsSetup = lmsSetupName; + } else { + throw new IllegalStateException( + "Invalid initialization of MoodleCourseDataAsyncLoader. It has already been initialized yet"); + } } public long getFromCutTime() { @@ -84,8 +118,8 @@ public class MoodleCourseDataLazyLoader { this.fromCutTime = fromCutTime; } - public Set getPreFilteredCourseIds() { - return this.preFilteredCourseIds; + public Set getCachedCourseData() { + return new HashSet<>(this.cachedCourseData); } public long getLastRunTime() { @@ -100,7 +134,7 @@ public class MoodleCourseDataLazyLoader { return this.lastLoadTime > 30 * Constants.SECOND_IN_MILLIS; } - public Set loadSync(final MoodleAPIRestTemplate restTemplate) { + public Set loadSync(final MoodleAPIRestTemplate restTemplate) { if (this.running) { throw new IllegalStateException("Is already running asynchronously"); } @@ -109,9 +143,11 @@ public class MoodleCourseDataLazyLoader { loadAndCache(restTemplate).run(); this.lastRunTime = Utils.getMillisecondsNow(); - log.info("Loaded {} courses synchronously", this.preFilteredCourseIds.size()); + log.info("LMS Setup: {} loaded {} courses synchronously", + this.lmsSetup, + this.cachedCourseData.size()); - return this.preFilteredCourseIds; + return this.cachedCourseData; } public void loadAsync(final MoodleAPIRestTemplate restTemplate) { @@ -126,6 +162,7 @@ public class MoodleCourseDataLazyLoader { private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) { return () -> { + this.cachedCourseData.clear(); final long startTime = Utils.getMillisecondsNow(); loadAllQuizzes(restTemplate); @@ -133,7 +170,9 @@ public class MoodleCourseDataLazyLoader { this.lastLoadTime = Utils.getMillisecondsNow() - startTime; this.running = false; - log.info("Loaded {} courses asynchronously", this.preFilteredCourseIds.size()); + log.info("LMS Setup: {} loaded {} courses asynchronously", + this.lmsSetup, + this.cachedCourseData.size()); }; } @@ -151,8 +190,8 @@ public class MoodleCourseDataLazyLoader { try { // first get courses from Moodle for page - final Map courseData = new HashMap<>(); - final Collection coursesPage = getCoursesPage(restTemplate, page, 1000); + final Map courseData = new HashMap<>(); + final Collection coursesPage = getCoursesPage(restTemplate, page, this.pageSize); if (coursesPage == null || coursesPage.isEmpty()) { return false; @@ -168,7 +207,8 @@ public class MoodleCourseDataLazyLoader { MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS, new ArrayList<>(courseData.keySet())); - final String quizzesJSON = restTemplate.callMoodleAPIFunction( + final String quizzesJSON = callMoodleRestAPI( + restTemplate, MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME, attributes); @@ -185,28 +225,36 @@ public class MoodleCourseDataLazyLoader { .stream() .filter(getQuizFilter()) .forEach(quiz -> { - final CourseData data = courseData.get(quiz.course); + final CourseDataShort data = courseData.get(quiz.course); if (data != null) { data.quizzes.add(quiz); } }); - this.preFilteredCourseIds.addAll( - courseData.values().stream() - .filter(c -> !c.quizzes.isEmpty()) - .collect(Collectors.toList())); + courseData.values().stream() + .filter(c -> !c.quizzes.isEmpty()) + .forEach(c -> { + if (this.cachedCourseData.size() >= this.maxSize) { + log.error( + "LMS Setup: {} Cache is full and has reached its maximal size. Skip data: -> {}", + this.lmsSetup, + c); + } else { + this.cachedCourseData.add(c); + } + }); return true; } else { return false; } } catch (final Exception e) { - log.error("Unexpected exception while trying to get course data: ", e); + log.error("LMS Setup: {} Unexpected exception while trying to get course data: ", this.lmsSetup, e); return false; } } - private Collection getCoursesPage( + private Collection getCoursesPage( final MoodleAPIRestTemplate restTemplate, final int page, final int size) throws JsonParseException, JsonMappingException, IOException { @@ -219,7 +267,8 @@ public class MoodleCourseDataLazyLoader { attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE, String.valueOf(page)); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE_SIZE, String.valueOf(size)); - final String courseKeyPageJSON = restTemplate.callMoodleAPIFunction( + final String courseKeyPageJSON = callMoodleRestAPI( + restTemplate, MoodleCourseAccess.MOODLE_COURSE_SEARCH_API_FUNCTION_NAME, attributes); @@ -228,7 +277,9 @@ public class MoodleCourseDataLazyLoader { CoursePage.class); if (keysPage == null || keysPage.courseKeys == null || keysPage.courseKeys.isEmpty()) { - log.info("No courses found on page: {}", page); + if (log.isDebugEnabled()) { + log.debug("LMS Setup: {} No courses found on page: {}", this.lmsSetup, page); + } return Collections.emptyList(); } @@ -238,7 +289,7 @@ public class MoodleCourseDataLazyLoader { .map(key -> key.id) .collect(Collectors.toSet()); - final Collection result = getCoursesForIds(restTemplate, ids) + final Collection result = getCoursesForIds(restTemplate, ids) .stream() .filter(getCourseFilter()) .collect(Collectors.toList()); @@ -247,19 +298,19 @@ public class MoodleCourseDataLazyLoader { return result; } catch (final Exception e) { - log.error("Unexpected error while trying to get courses page: ", e); + log.error("LMS Setup: {} Unexpected error while trying to get courses page: ", this.lmsSetup, e); return Collections.emptyList(); } } - private Collection getCoursesForIds( + private Collection getCoursesForIds( final MoodleAPIRestTemplate restTemplate, final Set ids) { try { if (log.isDebugEnabled()) { - log.debug("Get courses for ids: {}", ids); + log.debug("LMS Setup: {} Get courses for ids: {}", this.lmsSetup, ids); } final String joinedIds = StringUtils.join(ids, Constants.COMMA); @@ -267,32 +318,52 @@ public class MoodleCourseDataLazyLoader { final LinkedMultiValueMap attributes = new LinkedMultiValueMap<>(); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_NAME, MoodleCourseAccess.MOODLE_COURSE_API_IDS); attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_VALUE, joinedIds); - final String coursePageJSON = restTemplate.callMoodleAPIFunction( + final String coursePageJSON = callMoodleRestAPI( + restTemplate, MoodleCourseAccess.MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME, attributes); - return this.jsonMapper. readValue( + return this.jsonMapper.readValue( coursePageJSON, Courses.class).courses; } catch (final Exception e) { - log.error("Unexpected error while trying to get courses for ids", e); + log.error("LMS Setup: {} Unexpected error while trying to get courses for ids", this.lmsSetup, e); return Collections.emptyList(); } } - private Predicate getQuizFilter() { + private String callMoodleRestAPI( + final MoodleAPIRestTemplate restTemplate, + final String function, + final MultiValueMap queryAttributes) { + + return this.moodleRestCall + .protectedRun(() -> restTemplate.callMoodleAPIFunction( + function, + queryAttributes)) + .getOrThrow(); + + } + + private Predicate getQuizFilter() { final long now = Utils.getSecondsNow(); return quiz -> { if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) { return true; } - log.info("remove quiz {} end_time {} now {}", quiz.name, quiz.time_close, now); + if (log.isDebugEnabled()) { + log.debug("LMS Setup: {} remove quiz {} end_time {} now {}", + this.lmsSetup, + quiz.name, + quiz.time_close, + now); + } return false; }; } - private Predicate getCourseFilter() { + private Predicate getCourseFilter() { final long now = Utils.getSecondsNow(); return course -> { if (course.start_date < this.fromCutTime) { @@ -303,7 +374,13 @@ public class MoodleCourseDataLazyLoader { return true; } - log.info("remove course {} end_time {} now {}", course.short_name, course.end_date, now); + if (log.isDebugEnabled()) { + log.info("LMS Setup: {} remove course {} end_time {} now {}", + this.lmsSetup, + course.short_name, + course.end_date, + now); + } return false; }; } @@ -356,77 +433,107 @@ public class MoodleCourseDataLazyLoader { } -// @JsonIgnoreProperties(ignoreUnknown = true) -// static final class CourseKeys { -// final Collection courses; -// -// @JsonCreator -// protected CourseKeys( -// @JsonProperty(value = "courses") final Collection courses) { -// this.courses = courses; -// } -// } + /** Maps the Moodle course API course data */ + @JsonIgnoreProperties(ignoreUnknown = true) + static final class CourseDataShort { + final String id; + final String short_name; + final String idnumber; + final Long start_date; // unix-time seconds UTC + final Long end_date; // unix-time seconds UTC + final Long time_created; // unix-time seconds UTC + final Collection quizzes = new ArrayList<>(); -// /** Maps the Moodle course API course data */ -// @JsonIgnoreProperties(ignoreUnknown = true) -// static final class CourseDataKey { -// final String id; -// final String short_name; -// final Long start_date; // unix-time seconds UTC -// final Long end_date; // unix-time seconds UTC -// final Long time_created; // unix-time seconds UTC -// final Collection quizzes = new ArrayList<>(); -// -// @JsonCreator -// protected CourseDataKey( -// @JsonProperty(value = "id") final String id, -// @JsonProperty(value = "shortname") final String short_name, -// @JsonProperty(value = "startdate") final Long start_date, -// @JsonProperty(value = "enddate") final Long end_date, -// @JsonProperty(value = "timecreated") final Long time_created) { -// -// this.id = id; -// this.short_name = short_name; -// this.start_date = start_date; -// this.end_date = end_date; -// this.time_created = time_created; -// } -// -// } + @JsonCreator + protected CourseDataShort( + @JsonProperty(value = "id") final String id, + @JsonProperty(value = "shortname") final String short_name, + @JsonProperty(value = "idnumber") final String idnumber, + @JsonProperty(value = "startdate") final Long start_date, + @JsonProperty(value = "enddate") final Long end_date, + @JsonProperty(value = "timecreated") final Long time_created) { -// @JsonIgnoreProperties(ignoreUnknown = true) -// static final class CourseQuizKeys { -// final Collection quizzes; -// -// @JsonCreator -// protected CourseQuizKeys( -// @JsonProperty(value = "quizzes") final Collection quizzes) { -// this.quizzes = quizzes; -// } -// } -// -// @JsonIgnoreProperties(ignoreUnknown = true) -// static final class CourseQuizKey { -// final String id; -// final String course; -// final String name; -// final Long time_open; // unix-time seconds UTC -// final Long time_close; // unix-time seconds UTC -// -// @JsonCreator -// protected CourseQuizKey( -// @JsonProperty(value = "id") final String id, -// @JsonProperty(value = "course") final String course, -// @JsonProperty(value = "name") final String name, -// @JsonProperty(value = "timeopen") final Long time_open, -// @JsonProperty(value = "timeclose") final Long time_close) { -// -// this.id = id; -// this.course = course; -// this.name = name; -// this.time_open = time_open; -// this.time_close = time_close; -// } -// } + this.id = id; + this.short_name = short_name; + this.idnumber = idnumber; + this.start_date = start_date; + this.end_date = end_date; + this.time_created = time_created; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.id == null) ? 0 : this.id.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final CourseDataShort other = (CourseDataShort) obj; + if (this.id == null) { + if (other.id != null) + return false; + } else if (!this.id.equals(other.id)) + return false; + return true; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static final class Courses { + final Collection courses; + + @JsonCreator + protected Courses( + @JsonProperty(value = "courses") final Collection courses) { + this.courses = courses; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class CourseQuizData { + final Collection quizzes; + + @JsonCreator + protected CourseQuizData( + @JsonProperty(value = "quizzes") final Collection quizzes) { + this.quizzes = quizzes; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class CourseQuizShort { + final String id; + final String course; + final String course_module; + final String name; + final Long time_open; // unix-time seconds UTC + final Long time_close; // unix-time seconds UTC + + @JsonCreator + protected CourseQuizShort( + @JsonProperty(value = "id") final String id, + @JsonProperty(value = "course") final String course, + @JsonProperty(value = "coursemodule") final String course_module, + @JsonProperty(value = "name") final String name, + @JsonProperty(value = "timeopen") final Long time_open, + @JsonProperty(value = "timeclose") final Long time_close) { + + this.id = id; + this.course = course; + this.course_module = course_module; + this.name = name; + this.time_open = time_open; + this.time_close = time_close; + } + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java index 0afdabb0..f49523e7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplateFactory.java @@ -12,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; @@ -32,6 +33,7 @@ public class MoodleLmsAPITemplateFactory { private final JSONMapper jsonMapper; private final AsyncService asyncService; + private final Environment environment; private final ClientCredentialService clientCredentialService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ApplicationContext applicationContext; @@ -40,6 +42,7 @@ public class MoodleLmsAPITemplateFactory { protected MoodleLmsAPITemplateFactory( final JSONMapper jsonMapper, final AsyncService asyncService, + final Environment environment, final ClientCredentialService clientCredentialService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ApplicationContext applicationContext, @@ -47,6 +50,7 @@ public class MoodleLmsAPITemplateFactory { this.jsonMapper = jsonMapper; this.asyncService = asyncService; + this.environment = environment; this.clientCredentialService = clientCredentialService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.applicationContext = applicationContext; @@ -62,8 +66,9 @@ public class MoodleLmsAPITemplateFactory { return Result.tryCatch(() -> { - final MoodleCourseDataLazyLoader lazyLoaderPrototype = - this.applicationContext.getBean(MoodleCourseDataLazyLoader.class); + final MoodleCourseDataAsyncLoader asyncLoaderPrototype = + this.applicationContext.getBean(MoodleCourseDataAsyncLoader.class); + asyncLoaderPrototype.init(lmsSetup.name); final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory( this.jsonMapper, @@ -78,8 +83,9 @@ public class MoodleLmsAPITemplateFactory { this.jsonMapper, lmsSetup, moodleRestTemplateFactory, - lazyLoaderPrototype, - this.asyncService); + asyncLoaderPrototype, + this.asyncService, + this.environment); final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction( this.jsonMapper, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 3c525551..004fc8bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -489,9 +489,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic .filter(Objects::nonNull) .filter(connection -> connection.pingIndicator != null && connection.clientConnection.status.establishedStatus) - .map(connection -> connection.pingIndicator.updateLogEvent()) - .filter(Objects::nonNull) - .forEach(this.eventHandlingStrategy); + .forEach(connection -> connection.pingIndicator.updateLogEvent()); } catch (final Exception e) { log.error("Failed to update ping events: ", e); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java index 19254f92..93059772 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientNotificationServiceImpl.java @@ -42,7 +42,7 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe private final ClientEventDAO clientEventDAO; private final SEBClientInstructionService sebClientInstructionService; - private final Set pendingNotifications; + private final Set pendingNotifications = new HashSet<>(); public SEBClientNotificationServiceImpl( final ClientEventDAO clientEventDAO, @@ -50,19 +50,25 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe this.clientEventDAO = clientEventDAO; this.sebClientInstructionService = sebClientInstructionService; - this.pendingNotifications = new HashSet<>(); } @Override public Boolean hasAnyPendingNotification(final Long clientConnectionId) { - if (this.pendingNotifications.contains(clientConnectionId)) { + + if (this.pendingNotifications.add(clientConnectionId)) { return true; } final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId) .getOr(Collections.emptyList()) .isEmpty(); + if (hasAnyPendingNotification) { + // NOTE this is a quick and dirty way to keep cache pendingNotifications cache size short. + // TODO find a better way to do this. + if (this.pendingNotifications.size() > 100) { + this.pendingNotifications.clear(); + } this.pendingNotifications.add(clientConnectionId); } diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index 50dd97d5..3966d495 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -1,8 +1,6 @@ server.address=localhost server.port=8090 -#logging.file=log/sebserver.log - # data source configuration spring.datasource.initialize=true spring.datasource.initialization-mode=always diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index 507d76bf..5ec2e5a1 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -6,6 +6,8 @@ server.servlet.context-path=/ server.tomcat.uri-encoding=UTF-8 logging.level.ch=INFO +logging.level.org.springframework.cache=INFO +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG sebserver.http.client.connect-timeout=150000 sebserver.http.client.connection-request-timeout=100000 diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index 559f1061..efa28906 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -8,6 +8,10 @@ sebserver.init.adminaccount.gen-on-init=true sebserver.init.organisation.name=SEB Server sebserver.init.adminaccount.username=sebserver-admin +### webservice caching +spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider +spring.cache.jcache.config=classpath:config/ehcache.xml + ### webservice data source configuration spring.datasource.username=root spring.datasource.initialize=true diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties index eff5fc27..023e512e 100644 --- a/src/main/resources/config/application.properties +++ b/src/main/resources/config/application.properties @@ -13,7 +13,7 @@ server.port=8080 server.servlet.context-path=/ # Tomcat -server.tomcat.max-threads=1000 +server.tomcat.max-threads=2000 server.tomcat.accept-count=300 server.tomcat.uri-encoding=UTF-8 diff --git a/src/main/resources/config/ehcache.xml b/src/main/resources/config/ehcache.xml new file mode 100644 index 00000000..1791f8b0 --- /dev/null +++ b/src/main/resources/config/ehcache.xml @@ -0,0 +1,85 @@ + + + + java.lang.Long + ch.ethz.seb.sebserver.gbl.model.exam.Exam + + 24 + + + 100 + + + + + java.lang.String + ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal + + 24 + + + 2000 + + + + + java.lang.Long + ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.InMemorySEBConfig + + 24 + + + 20 + + + + + java.lang.String + ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord + + 24 + + + 2000 + + + + + java.lang.Long + ch.ethz.seb.sebserver.gbl.util.Result + + 24 + + + 100 + + + + + org.springframework.security.oauth2.common.OAuth2AccessToken + org.springframework.security.oauth2.provider.OAuth2Authentication + + 24 + + + 100 + + + + + java.lang.String + ch.ethz.seb.sebserver.gbl.util.Result + + 24 + + + 2000 + + + + \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 1e8ea704..2b12492d 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -19,6 +19,7 @@ + diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java index 9c2244b8..ec1da20a 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccessTest.java @@ -15,6 +15,9 @@ import static org.mockito.Mockito.*; import java.util.TreeMap; import org.junit.Test; +import org.mockito.Mock; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -28,6 +31,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT public class MoodleCourseAccessTest { + @Mock + Environment env = new MockEnvironment(); + @Test public void testGetExamineeAccountDetails() { @@ -68,7 +74,8 @@ public class MoodleCourseAccessTest { null, moodleRestTemplateFactory, null, - mock(AsyncService.class)); + mock(AsyncService.class), + this.env); final String examId = "123"; final Result examineeAccountDetails = @@ -116,7 +123,8 @@ public class MoodleCourseAccessTest { null, moodleRestTemplateFactory, null, - mock(AsyncService.class)); + mock(AsyncService.class), + this.env); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); assertNotNull(initAPIAccess); @@ -138,7 +146,8 @@ public class MoodleCourseAccessTest { null, moodleRestTemplateFactory, null, - mock(AsyncService.class)); + mock(AsyncService.class), + this.env); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); assertNotNull(initAPIAccess); @@ -159,7 +168,8 @@ public class MoodleCourseAccessTest { null, moodleRestTemplateFactory, null, - mock(AsyncService.class)); + mock(AsyncService.class), + this.env); final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess(); assertNotNull(initAPIAccess); diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 853b0aae..59e6955f 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -12,6 +12,9 @@ server.servlet.context-path=/ spring.main.allow-bean-definition-overriding=true sebserver.password=test-password +spring.cache.jcache.provider= +spring.cache.jcache.config=classpath:config/ehcache.xml + spring.h2.console.enabled=true spring.datasource.platform=h2 spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE