Merge remote-tracking branch 'origin/dev-1.1.0' into development

This commit is contained in:
anhefti 2021-02-17 11:49:26 +01:00
commit 1796ff4a7a
9 changed files with 193 additions and 46 deletions

View file

@ -68,9 +68,19 @@ public class SEBExamConfigImportPopup {
this.pageService = pageService;
}
public Function<PageAction, PageAction> importFunction(final boolean newConfig) {
public Function<PageAction, PageAction> importFunction() {
return importFunction(null);
}
public Function<PageAction, PageAction> importFunction(final Supplier<String> tabSelectionSupplier) {
return action -> {
final boolean newConfig = tabSelectionSupplier == null || tabSelectionSupplier.get() == null;
final PageContext context = (tabSelectionSupplier != null)
? action.pageContext()
.withAttribute(SEBSettingsForm.ATTR_VIEW_INDEX, tabSelectionSupplier.get())
: action.pageContext();
final ModalInputDialog<FormHandle<ConfigurationNode>> dialog =
new ModalInputDialog<FormHandle<ConfigurationNode>>(
action.pageContext().getParent().getShell(),
@ -79,7 +89,7 @@ public class SEBExamConfigImportPopup {
final ImportFormContext importFormContext = new ImportFormContext(
this.pageService,
action.pageContext(),
context,
newConfig);
dialog.open(
@ -147,18 +157,6 @@ public class SEBExamConfigImportPopup {
if (!configuration.hasError()) {
context.publishInfo(SEBExamConfigForm.FORM_IMPORT_CONFIRM_TEXT_KEY);
final PageAction action = (newConfig)
? this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.create()
: this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.create();
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
} else {
final Exception error = configuration.getError();
if (error instanceof RestCallError) {
@ -177,14 +175,15 @@ public class SEBExamConfigImportPopup {
.notifyImportError(EntityType.CONFIGURATION_NODE, error);
}
});
return true;
}
} else {
formHandle.getContext().notifyError(
SEBExamConfigForm.FORM_TITLE,
configuration.getError());
}
}
reloadPage(newConfig, context);
return true;
} else {
formHandle.getContext().publishPageMessage(
@ -194,11 +193,26 @@ public class SEBExamConfigImportPopup {
return false;
} catch (final Exception e) {
reloadPage(newConfig, formHandle.getContext());
formHandle.getContext().notifyError(SEBExamConfigForm.FORM_TITLE, e);
return true;
}
}
private void reloadPage(final boolean newConfig, final PageContext context) {
final PageAction action = (newConfig)
? this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.create()
: this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.create();
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
}
private boolean checkInput(final FormHandle<ConfigurationNode> formHandle, final boolean newConfig,
final Form form) {
if (newConfig) {

View file

@ -170,7 +170,7 @@ public class SEBExamConfigList implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.withSelect(
configTable.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
this.sebExamConfigImportPopup.importFunction(true),
this.sebExamConfigImportPopup.importFunction(),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(examConfigGrant::im);

View file

@ -69,6 +69,8 @@ public class SEBSettingsForm implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(SEBSettingsForm.class);
public static final String ATTR_VIEW_INDEX = "VIEW_INDEX";
private static final String VIEW_TEXT_KEY_PREFIX =
"sebserver.examconfig.props.form.views.";
private static final String KEY_SAVE_TO_HISTORY_SUCCESS =
@ -194,7 +196,7 @@ public class SEBSettingsForm implements TemplateComposer {
}
// set selection if available
final String viewIndex = pageContext.getAttribute("VIEW_INDEX");
final String viewIndex = pageContext.getAttribute(ATTR_VIEW_INDEX);
if (StringUtils.isNotBlank(viewIndex)) {
try {
tabFolder.setSelection(Integer.parseInt(viewIndex));
@ -217,7 +219,7 @@ public class SEBSettingsForm implements TemplateComposer {
.call()
.onError(t -> notifyErrorOnSave(t, pageContext));
return action.withAttribute(
"VIEW_INDEX",
ATTR_VIEW_INDEX,
String.valueOf(tabFolder.getSelectionIndex()));
})
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
@ -232,7 +234,7 @@ public class SEBSettingsForm implements TemplateComposer {
.call()
.getOrThrow();
return action.withAttribute(
"VIEW_INDEX",
ATTR_VIEW_INDEX,
String.valueOf(tabFolder.getSelectionIndex()));
})
.withSuccess(KEY_UNDO_SUCCESS)
@ -254,7 +256,9 @@ public class SEBSettingsForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_EXISTING_CONFIG)
.withEntityKey(entityKey)
.withExec(this.sebExamConfigImportPopup.importFunction(false))
.withExec(this.sebExamConfigImportPopup.importFunction(
() -> String.valueOf(tabFolder.getSelectionIndex())))
.noEventPropagation()
.publishIf(() -> examConfigGrant.iw() && !readonly && !isAttachedToExam)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)

View file

@ -35,13 +35,16 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
private final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper;
private final long masterDelayTimeThreshold;
private final boolean forceMaster;
public WebserviceInfoDAOImpl(
final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper,
@Value("${sebserver.webservice.forceMaster:false}") final boolean forceMaster,
@Value("${sebserver.webservice.master.delay.threshold:10000}") final long masterDelayTimeThreshold) {
this.webserviceServerInfoRecordMapper = webserviceServerInfoRecordMapper;
this.masterDelayTimeThreshold = masterDelayTimeThreshold;
this.forceMaster = forceMaster;
}
@Transactional
@ -68,18 +71,24 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
if (masters != null && !masters.isEmpty()) {
if (masters.size() > 1) {
log.error("There are more then one master registered: ", masters);
log.info("Reset masters and set this webservice as new master");
masters.stream()
.forEach(masterRec -> this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(masterRec.getId(), null, null, 0, 0L)));
this.setMasterTo(uuid);
return true;
masters.stream().forEach(masterRec -> this.webserviceServerInfoRecordMapper
.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(
masterRec.getId(),
null,
null,
0,
0L)));
return this.setMasterTo(uuid);
}
final WebserviceServerInfoRecord masterRec = masters.get(0);
if (masterRec.getUuid().equals(uuid)) {
// this webservice was the master and update time to remain being master
// This webservice is the master. Update time-stamp to remain being master
final long now = Utils.getMillisecondsNow();
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(masterRec.getId(), null, null, null, now));
@ -91,20 +100,18 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
return true;
} else {
// Another webservice is master. Check if still alive...
// Force this service to become master if the other master is not alive anymore
// Or if this service is forced to be the master service
final long now = Utils.getMillisecondsNow();
final long lastUpdateSince = now - masterRec.getUpdateTime();
if (lastUpdateSince > this.masterDelayTimeThreshold) {
log.info("Change webservice master form uuid: {} to uuid: {}", masterRec.getUuid(), uuid);
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(masterRec.getId(), null, null, 0, 0L));
setMasterTo(uuid);
if (lastUpdateSince > this.masterDelayTimeThreshold || this.forceMaster) {
forceMaster(uuid, masterRec.getUuid(), masterRec.getId());
return true;
}
}
} else {
// if we have no master yet
setMasterTo(uuid);
return true;
// We have no master yet so set this as master service
return setMasterTo(uuid);
}
return false;
@ -115,14 +122,41 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
}
}
private void setMasterTo(final String uuid) {
private void forceMaster(final String uuid, final String otherUUID, final Long otherId) {
log.info("Change webservice master form uuid: {} to uuid: {}", otherUUID, uuid);
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(otherId, null, null, 0, 0L));
setMasterTo(uuid);
}
private boolean setMasterTo(final String uuid) {
log.info("Set webservice {} as master", uuid);
final long now = Utils.getMillisecondsNow();
this.webserviceServerInfoRecordMapper.updateByExampleSelective(
// check if this is registered
final List<WebserviceServerInfoRecord> entries = this.webserviceServerInfoRecordMapper.selectByExample()
.where(WebserviceServerInfoRecordDynamicSqlSupport.uuid, SqlBuilder.isEqualTo(uuid))
.build()
.execute();
if (entries == null || entries.isEmpty()) {
log.warn("The webservice with uuid: {} is not registered and cannot become a master", uuid);
return false;
}
final Integer execute = this.webserviceServerInfoRecordMapper.updateByExampleSelective(
new WebserviceServerInfoRecord(null, null, null, 1, now))
.where(WebserviceServerInfoRecordDynamicSqlSupport.uuid, SqlBuilder.isEqualTo(uuid))
.build()
.execute();
if (execute == null || execute.intValue() <= 0) {
log.error("Failed to update webservice with uuid: {} to become master", uuid);
return false;
}
return true;
}
@Transactional

View file

@ -49,12 +49,6 @@ class ExamSessionControlTask implements DisposableBean {
private final String examTaskCron;
private final long pingUpdateRate;
// TODO considering to have some caching of running exams end dates here to improve performance
// the end check task has than only to first update missing running exams and then
// check the exam ending within the cached end date of the exam. if an exam has ended by
// applying the check to the cached value, the process can double-check if the end date has
// no change and update if needed or end the exam and remove from cache.
protected ExamSessionControlTask(
final ExamDAO examDAO,
final SEBClientConnectionService sebClientConnectionService,

View file

@ -32,6 +32,7 @@ sebserver.webservice.api.admin.clientSecret=${sebserver.password}
sebserver.webservice.internalSecret=${sebserver.password}
### webservice networking
sebserver.webservice.forceMaster=false
sebserver.webservice.distributed=false
sebserver.webservice.http.external.scheme=https
sebserver.webservice.http.external.servername=

View file

@ -36,6 +36,8 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestServiceImpl;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.OAuth2AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
@RunWith(SpringRunner.class)
@SpringBootTest(
@ -59,13 +61,19 @@ public abstract class GuiIntegrationTest {
protected JSONMapper jsonMapper;
@Autowired
protected FilterChainProxy springSecurityFilterChain;
@Autowired
private WebserviceInfoDAO webserviceInfoDAO;
@Autowired
private WebserviceInfo webserviceInfo;
protected MockMvc mockMvc;
@Before
public void setup() {
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(this.springSecurityFilterChain).build();
}
protected OAuth2AuthorizationContextHolder getAuthorizationContextHolder() {

View file

@ -0,0 +1,85 @@
/*
* 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.integration.api.admin;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
public class WebserviceTest extends AdministrationAPIIntegrationTester {
private static final String WEBSERVICE_1 = "WEBSERVICE_1";
private static final String WEBSERVICE_2 = "WEBSERVICE_2";
@Autowired
private WebserviceInfoDAO webserviceInfoDAO;
@Autowired
private WebserviceInfo webserviceInfo;
@Before
public void init() {
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
this.webserviceInfoDAO.register(WEBSERVICE_1, "0.0.0.1");
this.webserviceInfoDAO.register(WEBSERVICE_2, "0.0.0.1");
}
@After
public void cleanup() {
this.webserviceInfoDAO.unregister(WEBSERVICE_1);
this.webserviceInfoDAO.unregister(WEBSERVICE_2);
this.webserviceInfoDAO.register(
this.webserviceInfo.getWebserviceUUID(),
this.webserviceInfo.getLocalHostAddress());
}
@Test
public void testFistBecomeMaster() {
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
}
@Test
public void testUnregister_OtherBecomeMaster() {
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertTrue(this.webserviceInfoDAO.unregister(WEBSERVICE_1));
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
}
@Test
public void testOtherBecomeMasterAfterTimout() {
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
try {
Thread.sleep(5000);
} catch (final InterruptedException e) {
}
// Still not master
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
try {
Thread.sleep(6000);
} catch (final InterruptedException e) {
}
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
}
}

View file

@ -51,6 +51,8 @@ import ch.ethz.seb.sebserver.SEBServer;
import ch.ethz.seb.sebserver.WebSecurityConfig;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration;
@ -74,6 +76,10 @@ public abstract class ExamAPIIntegrationTester {
protected JSONMapper jsonMapper;
@Autowired
protected FilterChainProxy springSecurityFilterChain;
@Autowired
private WebserviceInfoDAO webserviceInfoDAO;
@Autowired
private WebserviceInfo webserviceInfo;
protected MockMvc mockMvc;
@ -82,6 +88,7 @@ public abstract class ExamAPIIntegrationTester {
@Before
public void setup() {
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(this.springSecurityFilterChain).build();
// clear all caches before a test