diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java index 6f0f808e..6638187a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java @@ -42,17 +42,20 @@ public class OpenEdxCourseRestriction { private final LmsSetup lmsSetup; private final JSONMapper jsonMapper; private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; + private final int restrictionAPIPushCount; private OAuth2RestTemplate restTemplate; protected OpenEdxCourseRestriction( final LmsSetup lmsSetup, final JSONMapper jsonMapper, - final OpenEdxRestTemplateFactory openEdxRestTemplateFactory) { + final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, + final int restrictionAPIPushCount) { this.lmsSetup = lmsSetup; this.jsonMapper = jsonMapper; this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; + this.restrictionAPIPushCount = restrictionAPIPushCount; } LmsSetupTestResult initAPIAccess() { @@ -140,24 +143,9 @@ public class OpenEdxCourseRestriction { log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction); } - return handleSebRestriction(() -> { - final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId); - final HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - - final OpenEdxCourseRestrictionData confirm = this.restTemplate.exchange( - url, - HttpMethod.PUT, - new HttpEntity<>(toJson(restriction), httpHeaders), - OpenEdxCourseRestrictionData.class) - .getBody(); - - if (log.isDebugEnabled()) { - log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, confirm); - } - - return true; - }); + return handleSebRestriction(processSebRestrictionUpdate(pushSebRestrictionFunction( + restriction, + courseId))); } Result deleteSebRestriction(final String courseId) { @@ -166,24 +154,97 @@ public class OpenEdxCourseRestriction { log.debug("DELETE SEB Client restriction on course: {}", courseId); } - return handleSebRestriction(() -> { - final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId); - final ResponseEntity exchange = this.restTemplate.exchange( - url, - HttpMethod.DELETE, - new HttpEntity<>(new HttpHeaders()), - Object.class); + return handleSebRestriction(processSebRestrictionUpdate(deleteSebRestrictionFunction(courseId))); + } - if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { - if (log.isDebugEnabled()) { - log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); + private BooleanSupplier processSebRestrictionUpdate(final BooleanSupplier restrictionUpdate) { + return () -> { + if (this.restrictionAPIPushCount > 0) { + // NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to + // apply on load-balanced infrastructure or infrastructure that has several layers of cache. + // The reason for this is that the API (Open edX system) internally don't apply a resource-change that is + // done within HTTP API call immediately from an outside perspective. + // After a resource-change on the API is done, the system toggles between the old and the new resource + // while constantly calling GET. This usually happens for about a minute or two then it stabilizes on the new resource + // + // This may source on load-balancing or internally caching on Open edX side. + // To mitigate this effect the SEB Server can be configured to apply a resource-change on the + // API several times in a row to flush as match caches and reach as match as possible server instances. + // + // Since this is a brute-force method to mitigate the problem, this should only be a temporary + // work-around until a better solution on Open edX SEB integration side has been found and applied. + + log.warn("SEB restriction update with multiple API push " + + "(this is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin)"); + + for (int i = 0; i < this.restrictionAPIPushCount; i++) { + if (!restrictionUpdate.getAsBoolean()) { + Result.ofRuntimeError( + "Failed to process SEB restriction update. See logs for more information"); + } } return true; } else { - throw new RuntimeException("Unexpected response for deletion: " + exchange); + return restrictionUpdate.getAsBoolean(); } - }); + }; + } + + private BooleanSupplier pushSebRestrictionFunction( + final OpenEdxCourseRestrictionData restriction, + final String courseId) { + + final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + return () -> { + try { + final OpenEdxCourseRestrictionData body = this.restTemplate.exchange( + url, + HttpMethod.PUT, + new HttpEntity<>(toJson(restriction), httpHeaders), + OpenEdxCourseRestrictionData.class) + .getBody(); + + if (log.isDebugEnabled()) { + log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, body); + } + } catch (final Exception e) { + log.error("Unexpected error while trying to call API for PUT. Course: ", courseId, e); + return false; + } + + return true; + }; + } + + private BooleanSupplier deleteSebRestrictionFunction(final String courseId) { + + final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId); + return () -> { + try { + final ResponseEntity exchange = this.restTemplate.exchange( + url, + HttpMethod.DELETE, + new HttpEntity<>(new HttpHeaders()), + Object.class); + + if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { + if (log.isDebugEnabled()) { + log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); + } + } else { + log.error("Unexpected response for deletion: {}", exchange); + return false; + } + } catch (final Exception e) { + log.error("Unexpected error while trying to call API for DELETE. Course: ", courseId, e); + return false; + } + + return true; + }; } private Result handleSebRestriction(final BooleanSupplier task) { 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 880068d4..87ef5114 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 @@ -36,6 +36,7 @@ public class OpenEdxLmsAPITemplateFactory { private final ClientCredentialService clientCredentialService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final String[] alternativeTokenRequestPaths; + private final int restrictionAPIPushCount; protected OpenEdxLmsAPITemplateFactory( final JSONMapper jsonMapper, @@ -43,7 +44,8 @@ public class OpenEdxLmsAPITemplateFactory { final AsyncService asyncService, final ClientCredentialService clientCredentialService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService, - @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths) { + @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths, + @Value("${sebserver.webservice.lms.openedx.seb.restriction.push-count:0}") final int restrictionAPIPushCount) { this.jsonMapper = jsonMapper; this.webserviceInfo = webserviceInfo; @@ -53,6 +55,7 @@ public class OpenEdxLmsAPITemplateFactory { this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR) : null; + this.restrictionAPIPushCount = restrictionAPIPushCount; } public Result create( @@ -79,7 +82,8 @@ public class OpenEdxLmsAPITemplateFactory { final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( lmsSetup, this.jsonMapper, - openEdxRestTemplateFactory); + openEdxRestTemplateFactory, + this.restrictionAPIPushCount); return new OpenEdxLmsAPITemplate( lmsSetup, diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index a59caf8e..bebb83f3 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -40,6 +40,21 @@ sebserver.webservice.api.pagination.maxPageSize=500 sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias +# NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to +# apply on load-balanced infrastructure or infrastructure that has several layers of cache. +# The reason for this is that the API (Open edX system) internally don't apply a resource-change that is +# done within HTTP API call immediately from an outside perspective. +# After a resource-change on the API is done, the system toggles between the old and the new resource +# while constantly calling GET. This usually happens for about a minute or two then it stabilizes on the new resource +# +# This may source on load-balancing or internally caching on Open edX side. +# To mitigate this effect the SEB Server can be configured to apply a resource-change on the +# API several times in a row to flush as match caches and reach as match as possible server instances. +# +# Since this is a brute-force method to mitigate the problem, this should only be a temporary +# work-around until a better solution on Open edX SEB integration side has been found and applied. +sebserver.webservice.lms.openedx.seb.restriction.push-count=10 + # actuator configuration management.endpoints.web.base-path=/actuator management.endpoints.web.exposure.include=logfile,loggers \ No newline at end of file