# WARNING: head commit changed in the meantime

Merge remote-tracking branch 'origin/master' into dev-1.3
plus more unit tests
plus CircuitBreaker fix
This commit is contained in:
anhefti 2022-03-02 17:09:37 +01:00
parent 501db30fa8
commit c21f0ef463
11 changed files with 269 additions and 20 deletions

View file

@ -18,7 +18,7 @@
<packaging>jar</packaging>
<properties>
<sebserver-version>1.3-rc2</sebserver-version>
<sebserver-version>1.3-rc3</sebserver-version>
<build-version>${sebserver-version}</build-version>
<revision>${sebserver-version}</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View file

@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
/** A circuit breaker with three states (CLOSED, HALF_OPEN, OPEN)
* <p>
@ -70,6 +71,7 @@ public final class CircuitBreaker<T> {
private State state = State.CLOSED;
private final AtomicInteger failingCount = new AtomicInteger(0);
private long lastSuccessTime;
private long lastOpenTime;
/** Create new CircuitBreakerSupplier.
*
@ -99,6 +101,8 @@ public final class CircuitBreaker<T> {
this.maxFailingAttempts = maxFailingAttempts;
this.maxBlockingTime = maxBlockingTime;
this.timeToRecover = timeToRecover;
// Initialize with creation time to get expected cool-down phase time if never was successful since
this.lastOpenTime = Utils.getMillisecondsNow();
}
public int getMaxFailingAttempts() {
@ -122,7 +126,7 @@ public final class CircuitBreaker<T> {
}
public synchronized Result<T> protectedRun(final Supplier<T> supplier) {
final long currentTime = System.currentTimeMillis();
final long currentTime = Utils.getMillisecondsNow();
if (log.isDebugEnabled()) {
log.debug("Called on: {} current state is: {} failing count: {}",
@ -163,7 +167,7 @@ public final class CircuitBreaker<T> {
log.debug("Attempt failed. failing count: {}", this.failingCount);
}
final long currentBlockingTime = System.currentTimeMillis() - startTime;
final long currentBlockingTime = Utils.getMillisecondsNow() - startTime;
final int failing = this.failingCount.incrementAndGet();
if (failing > this.maxFailingAttempts || currentBlockingTime > this.maxBlockingTime) {
// brake thought to HALF_OPEN state and return error
@ -174,14 +178,14 @@ public final class CircuitBreaker<T> {
this.state = State.HALF_OPEN;
this.failingCount.set(0);
return Result.ofError(new RuntimeException(
"Set CircuitBeaker to half-open state. Cause: " + result.getError().getMessage(),
"Set CircuitBeaker to half-open state. Cause: " + result.getError(),
result.getError()));
} else {
// try again
return protectedRun(supplier);
}
} else {
this.lastSuccessTime = System.currentTimeMillis();
this.lastSuccessTime = Utils.getMillisecondsNow();
return result;
}
}
@ -202,9 +206,10 @@ public final class CircuitBreaker<T> {
log.debug("Changing state from Half Open to Open and return cached value");
}
this.lastOpenTime = Utils.getMillisecondsNow();
this.state = State.OPEN;
return Result.ofError(new RuntimeException(
"Set CircuitBeaker to open state. Cause: " + result.getError().getMessage(),
"Set CircuitBeaker to open state. Cause: " + result.getError(),
result.getError()));
} else {
// on success go to CLOSED state
@ -228,7 +233,7 @@ public final class CircuitBreaker<T> {
log.debug("Handle Open on: {}", startTime);
}
if (startTime - this.lastSuccessTime >= this.timeToRecover) {
if (startTime - this.lastOpenTime >= this.timeToRecover) {
// if cool-down period is over, go back to HALF_OPEN state and try again
if (log.isDebugEnabled()) {
log.debug("Time to recover reached. Changing state from Open to Half Open");
@ -247,6 +252,7 @@ public final class CircuitBreaker<T> {
return Result.of(future.get(this.maxBlockingTime, TimeUnit.MILLISECONDS));
} catch (final Exception e) {
future.cancel(false);
log.warn("Max blocking timeout exceeded: {}, {}", this.maxBlockingTime, this.state);
return Result.ofError(e);
}
}

View file

@ -818,7 +818,8 @@ public class ExamDAOImpl implements ExamDAO {
log.debug("Using short-name: {} for recovering", shortname);
final QuizData recoveredQuizData = this.lmsAPIService.getLmsAPITemplate(lmsSetup.id)
final QuizData recoveredQuizData = this.lmsAPIService
.getLmsAPITemplate(lmsSetup.id)
.map(template -> template.getQuizzes(new FilterMap())
.getOrThrow()
.stream()

View file

@ -80,7 +80,7 @@ public abstract class AbstractCourseAccess {
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
Long.class,
Constants.MINUTE_IN_MILLIS),
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
Long.class,
@ -94,7 +94,7 @@ public abstract class AbstractCourseAccess {
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime",
Long.class,
Constants.MINUTE_IN_MILLIS),
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.timeToRecover",
Long.class,
@ -112,7 +112,7 @@ public abstract class AbstractCourseAccess {
environment.getProperty(
"sebserver.webservice.circuitbreaker.chaptersRequest.timeToRecover",
Long.class,
Constants.MINUTE_IN_MILLIS));
Constants.SECOND_IN_MILLIS * 30));
this.accountDetailRequest = asyncService.createCircuitBreaker(
environment.getProperty(
@ -126,19 +126,28 @@ public abstract class AbstractCourseAccess {
environment.getProperty(
"sebserver.webservice.circuitbreaker.accountDetailRequest.timeToRecover",
Long.class,
Constants.SECOND_IN_MILLIS * 10));
Constants.SECOND_IN_MILLIS * 30));
}
public Result<List<QuizData>> protectedQuizzesRequest(final FilterMap filterMap) {
return this.allQuizzesRequest.protectedRun(allQuizzesSupplier(filterMap));
return this.allQuizzesRequest.protectedRun(allQuizzesSupplier(filterMap))
.onError(error -> log.error(
"Failed to run protectedQuizzesRequest: {}",
error.getMessage()));
}
public Result<Collection<QuizData>> protectedQuizzesRequest(final Set<String> ids) {
return this.quizzesRequest.protectedRun(quizzesSupplier(ids));
return this.quizzesRequest.protectedRun(quizzesSupplier(ids))
.onError(error -> log.error(
"Failed to run protectedQuizzesRequest: {}",
error.getMessage()));
}
public Result<QuizData> protectedQuizRequest(final String id) {
return this.quizRequest.protectedRun(quizSupplier(id));
return this.quizRequest.protectedRun(quizSupplier(id))
.onError(error -> log.error(
"Failed to run protectedQuizRequest: {}",
error.getMessage()));
}
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
@ -165,7 +174,10 @@ public abstract class AbstractCourseAccess {
}
public Result<Chapters> getCourseChapters(final String courseId) {
return this.chaptersRequest.protectedRun(getCourseChaptersSupplier(courseId));
return this.chaptersRequest.protectedRun(getCourseChaptersSupplier(courseId))
.onError(error -> log.error(
"Failed to run getCourseChapters: {}",
error.getMessage()));
}
protected abstract Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId);

View file

@ -9,8 +9,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -24,9 +26,17 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory;
@WebServiceProfile
public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory {
private final AsyncService asyncService;
private final WebserviceInfo webserviceInfo;
private final Environment environment;
public MockLmsAPITemplateFactory(final WebserviceInfo webserviceInfo) {
public MockLmsAPITemplateFactory(
final AsyncService asyncService,
final Environment environment,
final WebserviceInfo webserviceInfo) {
this.environment = environment;
this.asyncService = asyncService;
this.webserviceInfo = webserviceInfo;
}
@ -38,6 +48,8 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory {
@Override
public Result<LmsAPITemplate> create(final APITemplateDataSupplier apiTemplateDataSupplier) {
return Result.tryCatch(() -> new MockupLmsAPITemplate(
this.asyncService,
this.environment,
apiTemplateDataSupplier,
this.webserviceInfo));
}

View file

@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
@ -19,9 +20,11 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
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.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
@ -38,6 +41,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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
public class MockupLmsAPITemplate implements LmsAPITemplate {
@ -48,7 +52,11 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
private final WebserviceInfo webserviceInfo;
private final APITemplateDataSupplier apiTemplateDataSupplier;
private final AbstractCourseAccess abstractCourseAccess;
MockupLmsAPITemplate(
final AsyncService asyncService,
final Environment environment,
final APITemplateDataSupplier apiTemplateDataSupplier,
final WebserviceInfo webserviceInfo) {
@ -56,6 +64,37 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
this.webserviceInfo = webserviceInfo;
this.mockups = new ArrayList<>();
this.abstractCourseAccess = new AbstractCourseAccess(asyncService, environment) {
@Override
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
return () -> MockupLmsAPITemplate.this
.getExamineeAccountDetails_protected(examineeSessionId)
.getOrThrow();
}
@Override
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> MockupLmsAPITemplate.this.getQuizzes_protected(filterMap).getOrThrow();
}
@Override
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> MockupLmsAPITemplate.this.getQuizzes_protected(ids).getOrThrow();
}
@Override
protected Supplier<QuizData> quizSupplier(final String id) {
return () -> MockupLmsAPITemplate.this.getQuiz_protected(id).getOrThrow();
}
@Override
protected Supplier<Chapters> getCourseChaptersSupplier(final String courseId) {
return () -> MockupLmsAPITemplate.this.getCourseChapters_protected(courseId).getOrThrow();
}
};
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final Long lmsSetupId = lmsSetup.id;
final Long institutionId = lmsSetup.getInstitutionId();
@ -149,6 +188,10 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.abstractCourseAccess.protectedQuizzesRequest(filterMap);
}
private Result<List<QuizData>> getQuizzes_protected(final FilterMap filterMap) {
return Result.tryCatch(() -> {
if (!authenticate()) {
throw new IllegalArgumentException("Wrong clientId or secret");
@ -164,6 +207,10 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<QuizData> getQuiz(final String id) {
return this.abstractCourseAccess.protectedQuizRequest(id);
}
private Result<QuizData> getQuiz_protected(final String id) {
return Result.of(this.mockups
.stream()
.filter(q -> id.equals(q.id))
@ -173,6 +220,11 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
return this.abstractCourseAccess.protectedQuizzesRequest(ids);
}
private Result<Collection<QuizData>> getQuizzes_protected(final Set<String> ids) {
return Result.tryCatch(() -> {
if (!authenticate()) {
throw new IllegalArgumentException("Wrong clientId or secret");
@ -193,11 +245,19 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<Chapters> getCourseChapters(final String courseId) {
return this.abstractCourseAccess.getCourseChapters(courseId);
}
private Result<Chapters> getCourseChapters_protected(final String courseId) {
return Result.ofError(new UnsupportedOperationException());
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return this.abstractCourseAccess.getExamineeAccountDetails(examineeSessionId);
}
private Result<ExamineeAccountDetails> getExamineeAccountDetails_protected(final String examineeSessionId) {
return Result.ofError(new UnsupportedOperationException());
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2022 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;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.client.ProxyData;
import ch.ethz.seb.sebserver.gbl.util.Result;
public class ClientHttpRequestFactoryServiceTest {
@Mock
Environment environment;
@Mock
ClientCredentialService clientCredentialService;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testGetClientHttpRequestFactory() {
final ClientHttpRequestFactoryService clientHttpRequestFactoryService = new ClientHttpRequestFactoryService(
this.environment,
this.clientCredentialService,
1, 1, 1);
final ProxyData proxyData = new ProxyData("testPoxy", 8000, new ClientCredentials("test", "test"));
Mockito.when(this.environment.getActiveProfiles()).thenReturn(new String[] { "dev-gui", "test" });
Mockito.when(this.clientCredentialService.getPlainClientSecret(Mockito.any())).thenReturn(Result.of("test"));
Result<ClientHttpRequestFactory> clientHttpRequestFactory = clientHttpRequestFactoryService
.getClientHttpRequestFactory();
assertNotNull(clientHttpRequestFactory);
assertFalse(clientHttpRequestFactory.hasError());
ClientHttpRequestFactory instance = clientHttpRequestFactory.get();
assertTrue(instance instanceof HttpComponentsClientHttpRequestFactory);
clientHttpRequestFactory = clientHttpRequestFactoryService
.getClientHttpRequestFactory(proxyData);
assertNotNull(clientHttpRequestFactory);
assertFalse(clientHttpRequestFactory.hasError());
instance = clientHttpRequestFactory.get();
assertTrue(instance instanceof HttpComponentsClientHttpRequestFactory);
Mockito.when(this.environment.getActiveProfiles()).thenReturn(new String[] { "prod-gui", "prod-ws" });
clientHttpRequestFactory = clientHttpRequestFactoryService
.getClientHttpRequestFactory();
assertNotNull(clientHttpRequestFactory);
assertFalse(clientHttpRequestFactory.hasError());
instance = clientHttpRequestFactory.get();
assertTrue(instance instanceof HttpComponentsClientHttpRequestFactory);
clientHttpRequestFactory = clientHttpRequestFactoryService
.getClientHttpRequestFactory(proxyData);
assertNotNull(clientHttpRequestFactory);
assertFalse(clientHttpRequestFactory.hasError());
instance = clientHttpRequestFactory.get();
assertTrue(instance instanceof HttpComponentsClientHttpRequestFactory);
}
}

View file

@ -76,7 +76,7 @@ public class CircuitBreakerTest {
assertEquals(CircuitBreaker.OPEN_STATE_EXCEPTION, result.getError());
// wait time to recover
Thread.sleep(500);
Thread.sleep(1000);
result = circuitBreaker.protectedRun(tester); // 11. call...
assertEquals(State.CLOSED, circuitBreaker.getState());
assertEquals("Hello back again", result.get());

View file

@ -77,7 +77,7 @@ public class MemoizingCircuitBreakerTest {
assertEquals(State.OPEN, circuitBreaker.getState());
// wait time to recover
Thread.sleep(500);
Thread.sleep(1000);
result = circuitBreaker.get(); // 11. call...
assertEquals(State.CLOSED, circuitBreaker.getState());
assertEquals("Hello back again", result.get());

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022 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.integration.api.admin;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Collection;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.jdbc.Sql;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityName;
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql" })
public class WebserviceInfoTest extends AdministrationAPIIntegrationTester {
@Test
public void testGetLogo() throws Exception {
String result = new RestAPITestHelper()
.withAccessToken(getAdminInstitution1Access())
.withPath(API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT)
.withPath("/inst1")
.withMethod(HttpMethod.GET)
.withExpectedStatus(HttpStatus.OK)
.getAsString();
assertEquals("", result);
result = new RestAPITestHelper()
.withAccessToken(getAdminInstitution1Access())
.withPath(API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT)
.withPath("/inst2")
.withMethod(HttpMethod.GET)
.withExpectedStatus(HttpStatus.OK)
.getAsString();
assertEquals("AAA", result);
}
@Test
public void test_getInstitutionInfo() throws Exception {
final Collection<EntityName> result = new RestAPITestHelper()
.withAccessToken(getAdminInstitution1Access())
.withPath(API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT)
.withMethod(HttpMethod.GET)
.withExpectedStatus(HttpStatus.OK)
.getAsObject(new TypeReference<Collection<EntityName>>() {
});
assertNotNull(result);
assertTrue(result.stream().filter(en -> "Institution1".equals(en.name)).findFirst().isPresent());
assertTrue(result.stream().filter(en -> "Institution2".equals(en.name)).findFirst().isPresent());
assertFalse(result.stream().filter(en -> "Institution3".equals(en.name)).findFirst().isPresent());
}
}

View file

@ -1,6 +1,6 @@
INSERT INTO institution VALUES
(1, 'Institution1', 'inst1', null, null, 1),
(2, 'Institution2', 'inst2', null, null, 1),
(2, 'Institution2', 'inst2', 'AAA', null, 1),
(3, 'Institution3', 'inst3', null, null, 0)
;