diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/CommonServerTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/CommonServerTest.java index 4fe247a0f37a57428dacee4ec0ecfd9517434935..5c75c3ff9f0d609c8c68cdde6a2b4438787f214c 100644 --- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/CommonServerTest.java +++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/CommonServerTest.java @@ -18,16 +18,28 @@ package ch.systemsx.cisd.openbis.systemtest; import static org.testng.AssertJUnit.assertEquals; +import java.sql.Connection; +import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; +import org.apache.log4j.Logger; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.testng.annotations.Test; +import ch.systemsx.cisd.common.concurrent.MessageChannel; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.openbis.generic.shared.ICommonServer; import ch.systemsx.cisd.openbis.generic.shared.basic.TechId; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AbstractExternalData; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Attachment; @@ -35,6 +47,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.AuthorizationGroup; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ContainerDataSet; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataSetType; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DeletionType; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DisplaySettings; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityType; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityTypePropertyType; @@ -42,6 +55,8 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Experiment; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Material; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.NewAuthorizationGroup; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.displaysettings.IDisplaySettingsUpdate; +import ch.systemsx.cisd.openbis.generic.shared.dto.SessionContextDTO; /** * @author Franz-Josef Elmer @@ -49,6 +64,8 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.Sample; public class CommonServerTest extends SystemTestCase { + private Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, getClass()); + @Test public void testDeleteGroupWithPersons() { @@ -224,4 +241,336 @@ public class CommonServerTest extends SystemTestCase Collections.sort(propertyCodes); assertEquals(expected, propertyCodes.toString()); } + + @Test(timeOut = 5000, enabled = false) + @Transactional(propagation = Propagation.NEVER) + public void testConcurrentDisplaySettingsUpdateForOneUserIsSafe() + { + testConcurrentDisplaySettingsUpdateForUsersIsSafe(new String[] { "test" }, 10, 10); + } + + @Test(timeOut = 5000, enabled = false) + @Transactional(propagation = Propagation.NEVER) + public void testConcurrentDisplaySettingsUpdateForDifferentUsersIsSafe() + { + testConcurrentDisplaySettingsUpdateForUsersIsSafe(new String[] { "test", "test_role" }, 5, 10); + } + + @SuppressWarnings("deprecation") + private void testConcurrentDisplaySettingsUpdateForUsersIsSafe(String[] users, int numberOfThreads, int numberOfIterations) + { + final String PASSWORD = "password"; + final String PANEL_ID = "panel_id"; + final String FINISHED_MESSAGE = "finished"; + + MessageChannel sendChannel = new MessageChannel(5000); + List<Thread> threads = new ArrayList<Thread>(); + SessionContextDTO[] sessionContext = new SessionContextDTO[users.length]; + + for (int u = 0; u < users.length; u++) + { + for (int i = 0; i < numberOfThreads; i++) + { + sessionContext[u] = commonServer.tryAuthenticate(users[u], PASSWORD); + IncrementPanelSizeRunnable runnable = + new IncrementPanelSizeRunnable(commonServer, sessionContext[u].getSessionToken(), PANEL_ID, numberOfIterations); + runnable.setSendChannel(sendChannel); + runnable.setFinishedMessage(FINISHED_MESSAGE); + Thread thread = new Thread(runnable); + thread.setDaemon(true); + threads.add(thread); + } + } + + for (Thread thread : threads) + { + thread.start(); + } + + for (int i = 0; i < threads.size(); i++) + { + sendChannel.assertNextMessage(FINISHED_MESSAGE); + } + + for (int u = 0; u < users.length; u++) + { + sessionContext[u] = commonServer.tryGetSession(sessionContext[u].getSessionToken()); + assertEquals(Integer.valueOf(numberOfThreads * numberOfIterations), + sessionContext[u].getDisplaySettings().getPanelSizeSettings().get(PANEL_ID)); + } + } + + @Test(timeOut = 5000, enabled = false) + @Transactional(propagation = Propagation.NEVER) + public void testLongRunninngDisplaySettingsUpdateForOneUserBlocksOtherUpdatesForThisUser() throws Exception + { + final String USER_ID = "test"; + final String PASSWORD = "password"; + final String PANEL_ID = "testPanelId"; + final String FINISHED_MESSAGE = "finished"; + final long TIMEOUT = 1000; + + DataSource dataSource = (DataSource) applicationContext.getBean("data-source"); + Connection connection = dataSource.getConnection(); + + try + { + connection.setAutoCommit(false); + + /* + * DummyAuthenticationService always returns random principals and triggers a person update during login. As we don't want to hold any + * locks on the persons table at this point, we call tryAuthenticate methods in the main thread. The test method is marked with + * Propagation.NEVER, which makes each of these calls to be executed in a separate transaction that is auto-committed. + */ + SessionContextDTO sessionContext1 = commonServer.tryAuthenticate(USER_ID, PASSWORD); + operationLog.info("User '" + USER_ID + "' authenticated"); + + SessionContextDTO sessionContext2 = commonServer.tryAuthenticate(USER_ID, PASSWORD); + operationLog.info("User '" + USER_ID + "' authenticated"); + + /* + * Acquire a database lock on USER_ID_1 person. It will block updating the display settings for that person. + */ + PreparedStatement statement = connection.prepareStatement("UPDATE persons SET registration_timestamp = now() WHERE user_id = ?"); + statement.setString(1, USER_ID); + statement.executeUpdate(); + operationLog.info("User '" + USER_ID + "' locked by a SQL query"); + + MessageChannel sendChannel = new MessageChannel(TIMEOUT); + + /* + * Will concurrently update the same person in two separate transactions. + */ + IncrementPanelSizeRunnable runnable1 = new IncrementPanelSizeRunnable(commonServer, sessionContext1.getSessionToken(), PANEL_ID, 1); + IncrementPanelSizeRunnable runnable2 = new IncrementPanelSizeRunnable(commonServer, sessionContext2.getSessionToken(), PANEL_ID, 1); + + runnable1.setSendChannel(sendChannel); + runnable2.setSendChannel(sendChannel); + + runnable1.setFinishedMessage(FINISHED_MESSAGE); + runnable2.setFinishedMessage(FINISHED_MESSAGE); + + Thread thread1 = new Thread(runnable1); + Thread thread2 = new Thread(runnable2); + + thread1.setDaemon(true); + thread2.setDaemon(true); + + operationLog.info("Will try to update user '" + USER_ID + "' display settings"); + /* + * First try to update the person without releasing the database lock. + */ + thread1.start(); + thread2.start(); + + Thread.sleep(TIMEOUT); + sendChannel.assertEmpty(); + + operationLog.info("Still waiting to update user '" + USER_ID + "' display settings"); + + /* + * After releasing the database lock, updating the person should succeed. + */ + connection.rollback(); + + operationLog.info("Releasing SQL lock on user '" + USER_ID + "'"); + + sendChannel.assertNextMessage(FINISHED_MESSAGE); + sendChannel.assertNextMessage(FINISHED_MESSAGE); + + operationLog.info("Successfully updated user '" + USER_ID + "' display settings"); + + } finally + { + connection.rollback(); + connection.setAutoCommit(true); + connection.close(); + } + } + + @Test(timeOut = 5000, enabled = false) + @Transactional(propagation = Propagation.NEVER) + public void testLongRunninngDisplaySettingsUpdateForOneUserDoesNotBlockUpdatesForOtherUsers() throws Exception + { + final String USER_ID_1 = "test"; + final String USER_ID_2 = "test_role"; + final String PASSWORD = "password"; + final String PANEL_ID = "testPanelId"; + final String FINISHED_MESSAGE_1 = "finished1"; + final String FINISHED_MESSAGE_2 = "finished2"; + final long TIMEOUT = 1000; + + DataSource dataSource = (DataSource) applicationContext.getBean("data-source"); + Connection connection = dataSource.getConnection(); + + try + { + connection.setAutoCommit(false); + + /* + * DummyAuthenticationService always returns random principals and triggers a person update during login. As we don't want to hold any + * locks on the persons table at this point, we call tryAuthenticate methods in the main thread. The test method is marked with + * Propagation.NEVER, which makes each of these calls to be executed in a separate transaction that is auto-committed. + */ + SessionContextDTO sessionContext1 = commonServer.tryAuthenticate(USER_ID_1, PASSWORD); + operationLog.info("User '" + USER_ID_1 + "' authenticated"); + + SessionContextDTO sessionContext2 = commonServer.tryAuthenticate(USER_ID_2, PASSWORD); + operationLog.info("User '" + USER_ID_2 + "' authenticated"); + + /* + * Acquire a database lock on USER_ID_1 person. It will block updating the display settings for that person. + */ + PreparedStatement statement = connection.prepareStatement("UPDATE persons SET registration_timestamp = now() WHERE user_id = ?"); + statement.setString(1, USER_ID_1); + statement.executeUpdate(); + operationLog.info("User '" + USER_ID_1 + "' locked by a SQL query"); + + MessageChannel sendChannel = new MessageChannel(TIMEOUT); + + /* + * Will concurrently update two different persons in two separate transactions. + */ + IncrementPanelSizeRunnable runnable1 = new IncrementPanelSizeRunnable(commonServer, sessionContext1.getSessionToken(), PANEL_ID, 1); + IncrementPanelSizeRunnable runnable2 = new IncrementPanelSizeRunnable(commonServer, sessionContext2.getSessionToken(), PANEL_ID, 1); + + runnable1.setSendChannel(sendChannel); + runnable2.setSendChannel(sendChannel); + + runnable1.setFinishedMessage(FINISHED_MESSAGE_1); + runnable2.setFinishedMessage(FINISHED_MESSAGE_2); + + Thread thread1 = new Thread(runnable1); + Thread thread2 = new Thread(runnable2); + thread1.setDaemon(true); + thread2.setDaemon(true); + + operationLog.info("Will try to update user '" + USER_ID_1 + "' display settings"); + /* + * First try to update the USER_ID_1 person that is blocked by the database lock. + */ + thread1.start(); + + Thread.sleep(TIMEOUT); + sendChannel.assertEmpty(); + + operationLog.info("Still waiting to update user '" + USER_ID_1 + "' display settings"); + operationLog.info("Will try to update user '" + USER_ID_2 + "' display settings"); + + /* + * Now try to update USER_ID_2 person that is not blocked by any database lock. + */ + thread2.start(); + + sendChannel.assertNextMessage(FINISHED_MESSAGE_2); + + operationLog.info("Successfully update user '" + USER_ID_2 + "' display settings"); + + /* + * After releasing the database lock, updating USER_ID_1 person should also succeed. + */ + connection.rollback(); + + operationLog.info("Releasing SQL lock on user '" + USER_ID_1 + "'"); + + sendChannel.assertNextMessage(FINISHED_MESSAGE_1); + + operationLog.info("Successfully updated user '" + USER_ID_1 + "' display settings"); + + } finally + { + operationLog.info("Cleaning up"); + connection.rollback(); + connection.setAutoCommit(true); + connection.close(); + } + } + + private static class IncrementPanelSizeRunnable implements Runnable + { + private ICommonServer server; + + private String sessionToken; + + private String panelId; + + private int count; + + private MessageChannel sendChannel; + + private String finishedMessage; + + public IncrementPanelSizeRunnable(ICommonServer server, String sessionToken, String panelId, int count) + { + this.server = server; + this.sessionToken = sessionToken; + this.panelId = panelId; + this.count = count; + } + + @Override + public void run() + { + IDisplaySettingsUpdate update = new IDisplaySettingsUpdate() + { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("deprecation") + @Override + public DisplaySettings update(DisplaySettings displaySettings) + { + Map<String, Integer> panelSizeSettings = displaySettings.getPanelSizeSettings(); + Integer panelSize = panelSizeSettings.get(panelId); + if (panelSize == null) + { + panelSize = 0; + } + try + { + // increase probability of race condition + Thread.sleep(5); + } + catch (Exception e) + { + + } + panelSizeSettings.put(panelId, panelSize + 1); + return displaySettings; + } + }; + + for (int value = 0; value < count; value++) + { + server.updateDisplaySettings(sessionToken, update); + } + + if (getSendChannel() != null && getFinishedMessage() != null) + { + getSendChannel().send(getFinishedMessage()); + } + } + + public MessageChannel getSendChannel() + { + return sendChannel; + } + + public void setSendChannel(MessageChannel sendChannel) + { + this.sendChannel = sendChannel; + } + + public String getFinishedMessage() + { + return finishedMessage; + } + + public void setFinishedMessage(String finishedMessage) + { + this.finishedMessage = finishedMessage; + } + + } + }