From: Sébastien Lesaint Date: Thu, 8 Jun 2017 15:51:17 +0000 (+0200) Subject: SONAR-8785 cache DbSession in ThreadLocals for HTTP requests X-Git-Tag: 6.5-RC1~49 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=refs%2Fpull%2F2158%2Fhead;p=sonarqube.git SONAR-8785 cache DbSession in ThreadLocals for HTTP requests --- diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 5f50464eb51..59e18e5f049 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -22,6 +22,7 @@ package org.sonar.ce.container; import com.google.common.annotations.VisibleForTesting; import java.util.List; import javax.annotation.CheckForNull; +import org.sonar.db.DBSessionsImpl; import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarQubeVersion; import org.sonar.api.config.EmailSettings; @@ -247,6 +248,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { // DB DaoModule.class, ReadOnlyPropertiesDao.class, + DBSessionsImpl.class, DbClient.class, // Elasticsearch diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 9b009c3452f..b9eec535920 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -134,7 +134,7 @@ public class ComputeEngineContainerImplTest { ); assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION - + 24 // level 1 + + 25 // level 1 + 47 // content of DaoModule + 3 // content of EsSearchModule + 58 // content of CorePropertyDefinitions diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DBSessions.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DBSessions.java new file mode 100644 index 00000000000..878816e7351 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DBSessions.java @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +public interface DBSessions { + DbSession openSession(boolean batch); + + void enableCaching(); + + void disableCaching(); +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DBSessionsImpl.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DBSessionsImpl.java new file mode 100644 index 00000000000..e331480cb90 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DBSessionsImpl.java @@ -0,0 +1,151 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import java.lang.reflect.Field; +import java.util.Objects; +import java.util.function.Supplier; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.defaults.DefaultSqlSession; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; +import static java.lang.Thread.currentThread; + +public class DBSessionsImpl implements DBSessions { + private static final Logger LOG = Loggers.get(DBSessionsImpl.class); + + private static final ThreadLocal CACHING_ENABLED = ThreadLocal.withInitial(() -> Boolean.FALSE); + private final ThreadLocal regularDbSession = ThreadLocal.withInitial(this::buildRegularDbSessionSupplier); + private final ThreadLocal batchDbSession = ThreadLocal.withInitial(this::buildBatchDbSessionSupplier); + + private final MyBatis myBatis; + + public DBSessionsImpl(MyBatis myBatis) { + this.myBatis = myBatis; + } + + private DelegatingDbSessionSupplier buildRegularDbSessionSupplier() { + LOG.trace("{} called buildRegularDbSessionSupplier", currentThread()); + return new DelegatingDbSessionSupplier(() -> { + DbSession res = myBatis.openSession(false); + ensureAutoCommitFalse(res); + LOG.trace("{} created regular DbSession {}", currentThread(), res); + return res; + }); + } + + private DelegatingDbSessionSupplier buildBatchDbSessionSupplier() { + LOG.trace("{} called buildBatchDbSessionSupplier", currentThread()); + return new DelegatingDbSessionSupplier(() -> { + DbSession res = myBatis.openSession(true); + ensureAutoCommitFalse(res); + LOG.trace("{} created batch DbSession {}", currentThread(), res); + return res; + }); + } + + private static void ensureAutoCommitFalse(DbSession dbSession) { + try { + SqlSession sqlSession = dbSession.getSqlSession(); + if (sqlSession instanceof DefaultSqlSession) { + Field f = sqlSession.getClass().getDeclaredField("autoCommit"); + f.setAccessible(true); + checkState(!((boolean) f.get(sqlSession)), "Autocommit must be false"); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + LOG.debug("Failed to check the autocommit status of SqlSession", e); + } + } + + @Override + public void enableCaching() { + LOG.trace("{} enabled caching", currentThread()); + CACHING_ENABLED.set(Boolean.TRUE); + } + + @Override + public DbSession openSession(boolean batch) { + LOG.trace("{} called openSession({}) (caching={})", currentThread(), batch, CACHING_ENABLED.get()); + if (!CACHING_ENABLED.get()) { + DbSession res = myBatis.openSession(batch); + LOG.trace("{} created non cached {} session (batch={})", currentThread(), res, batch); + return res; + } + if (batch) { + return new NonClosingDbSession(batchDbSession.get().get()); + } + return new NonClosingDbSession(regularDbSession.get().get()); + } + + @Override + public void disableCaching() { + LOG.trace("{} disabled caching", currentThread()); + close(regularDbSession, "regular"); + close(batchDbSession, "batch"); + regularDbSession.remove(); + batchDbSession.remove(); + CACHING_ENABLED.remove(); + } + + public void close(ThreadLocal dbSessionThreadLocal, String label) { + DelegatingDbSessionSupplier delegatingDbSessionSupplier = dbSessionThreadLocal.get(); + boolean getCalled = delegatingDbSessionSupplier.isPopulated(); + LOG.trace("{} attempts closing on {} session (getCalled={})", currentThread(), label, getCalled); + if (getCalled) { + try { + DbSession res = delegatingDbSessionSupplier.get(); + LOG.trace("{} closes {}", currentThread(), res); + res.close(); + } catch (Exception e) { + LOG.error(format("Failed to close %s connection in %s", label, currentThread()), e); + } + } + } + + /** + * A {@link Supplier} of {@link DelegatingDbSession} which logs whether {@link Supplier#get() get} has been called at + * least once, delegates the actual supplying to the a specific {@link Supplier} instance and + * caches the supplied {@link NonClosingDbSession}. + */ + private static final class DelegatingDbSessionSupplier implements Supplier { + private final Supplier delegate; + private DbSession dbSession; + + DelegatingDbSessionSupplier(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public DbSession get() { + if (dbSession == null) { + dbSession = Objects.requireNonNull(delegate.get()); + } + return dbSession; + } + + boolean isPopulated() { + return dbSession != null; + } + } + +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java index 6eb7c664a25..bd0fd55735e 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java @@ -73,6 +73,7 @@ public class DbClient { private final Database database; private final MyBatis myBatis; + private final DBSessions dbSessions; private final SchemaMigrationDao schemaMigrationDao; private final AuthorizationDao authorizationDao; private final OrganizationDao organizationDao; @@ -121,9 +122,10 @@ public class DbClient { private final DefaultQProfileDao defaultQProfileDao; private final EsQueueDao esQueueDao; - public DbClient(Database database, MyBatis myBatis, Dao... daos) { + public DbClient(Database database, MyBatis myBatis, DBSessions dbSessions, Dao... daos) { this.database = database; this.myBatis = myBatis; + this.dbSessions = dbSessions; Map map = new IdentityHashMap<>(); for (Dao dao : daos) { @@ -179,7 +181,7 @@ public class DbClient { } public DbSession openSession(boolean batch) { - return myBatis.openSession(batch); + return dbSessions.openSession(batch); } public Database getDatabase() { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbSession.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbSession.java index 7ae9d3803fa..dc757acd9f0 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbSession.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbSession.java @@ -22,4 +22,5 @@ package org.sonar.db; import org.apache.ibatis.session.SqlSession; public interface DbSession extends SqlSession { + SqlSession getSqlSession(); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbSessionImpl.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbSessionImpl.java index 6e22851f463..3e1fa8d2daf 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbSessionImpl.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbSessionImpl.java @@ -175,4 +175,9 @@ public class DbSessionImpl implements DbSession { public Connection getConnection() { return session.getConnection(); } + + @Override + public SqlSession getSqlSession() { + return session; + } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DelegatingDbSession.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DelegatingDbSession.java new file mode 100644 index 00000000000..d065f42262f --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DelegatingDbSession.java @@ -0,0 +1,194 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import java.sql.Connection; +import java.util.List; +import java.util.Map; +import org.apache.ibatis.executor.BatchResult; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.apache.ibatis.session.SqlSession; +import org.sonar.db.DbSession; + +/** + * A wrapper of a {@link DbSession} instance which does not call the wrapped {@link DbSession}'s + * {@link DbSession#close() close} method but throws a {@link UnsupportedOperationException} instead. + */ +abstract class DelegatingDbSession implements DbSession { + private final DbSession delegate; + + DelegatingDbSession(DbSession delegate) { + this.delegate = delegate; + } + + public DbSession getDelegate() { + return delegate; + } + + /////////////////////// + // overridden with change of behavior + /////////////////////// + @Override + public void close() { + doClose(); + } + + protected abstract void doClose(); + + /////////////////////// + // overridden with NO change of behavior + /////////////////////// + @Override + public T selectOne(String statement) { + return delegate.selectOne(statement); + } + + @Override + public T selectOne(String statement, Object parameter) { + return delegate.selectOne(statement, parameter); + } + + @Override + public List selectList(String statement) { + return delegate.selectList(statement); + } + + @Override + public List selectList(String statement, Object parameter) { + return delegate.selectList(statement, parameter); + } + + @Override + public List selectList(String statement, Object parameter, RowBounds rowBounds) { + return delegate.selectList(statement, parameter, rowBounds); + } + + @Override + public Map selectMap(String statement, String mapKey) { + return delegate.selectMap(statement, mapKey); + } + + @Override + public Map selectMap(String statement, Object parameter, String mapKey) { + return delegate.selectMap(statement, parameter, mapKey); + } + + @Override + public Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) { + return delegate.selectMap(statement, parameter, mapKey, rowBounds); + } + + @Override + public void select(String statement, Object parameter, ResultHandler handler) { + delegate.select(statement, parameter, handler); + } + + @Override + public void select(String statement, ResultHandler handler) { + delegate.select(statement, handler); + } + + @Override + public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { + delegate.select(statement, parameter, rowBounds, handler); + } + + @Override + public int insert(String statement) { + return delegate.insert(statement); + } + + @Override + public int insert(String statement, Object parameter) { + return delegate.insert(statement, parameter); + } + + @Override + public int update(String statement) { + return delegate.update(statement); + } + + @Override + public int update(String statement, Object parameter) { + return delegate.update(statement, parameter); + } + + @Override + public int delete(String statement) { + return delegate.delete(statement); + } + + @Override + public int delete(String statement, Object parameter) { + return delegate.delete(statement, parameter); + } + + @Override + public void commit() { + delegate.commit(); + } + + @Override + public void commit(boolean force) { + delegate.commit(force); + } + + @Override + public void rollback() { + delegate.rollback(); + } + + @Override + public void rollback(boolean force) { + delegate.rollback(force); + } + + @Override + public List flushStatements() { + return delegate.flushStatements(); + } + + @Override + public void clearCache() { + delegate.clearCache(); + } + + @Override + public Configuration getConfiguration() { + return delegate.getConfiguration(); + } + + @Override + public T getMapper(Class type) { + return delegate.getMapper(type); + } + + @Override + public Connection getConnection() { + return delegate.getConnection(); + } + + @Override + public SqlSession getSqlSession() { + return delegate.getSqlSession(); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/NonClosingDbSession.java b/server/sonar-db-dao/src/main/java/org/sonar/db/NonClosingDbSession.java new file mode 100644 index 00000000000..09ee82ca615 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/NonClosingDbSession.java @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +/** + * A {@link DelegatingDbSession} subclass which tracks calls to insert/update/delete methods and commit/rollback + */ +class NonClosingDbSession extends DelegatingDbSession { + private boolean dirty; + + NonClosingDbSession(DbSession delegate) { + super(delegate); + } + + @Override + public void doClose() { + // rollback when session is dirty so that no statement leaks from one use of the DbSession to another + // super.close() would do such rollback before actually closing **if autocommit is true** + // we are going to assume autocommit is true and keep this behavior + if (dirty) { + getDelegate().rollback(); + } + } + + @Override + public int insert(String statement) { + dirty = true; + return super.insert(statement); + } + + @Override + public int insert(String statement, Object parameter) { + dirty = true; + return super.insert(statement, parameter); + } + + @Override + public int update(String statement) { + dirty = true; + return super.update(statement); + } + + @Override + public int update(String statement, Object parameter) { + dirty = true; + return super.update(statement, parameter); + } + + @Override + public int delete(String statement) { + dirty = true; + return super.delete(statement); + } + + @Override + public int delete(String statement, Object parameter) { + dirty = true; + return super.delete(statement, parameter); + } + + @Override + public void commit() { + super.commit(); + dirty = false; + } + + @Override + public void commit(boolean force) { + super.commit(force); + dirty = false; + } + + @Override + public void rollback() { + super.rollback(); + dirty = false; + } + + @Override + public void rollback(boolean force) { + super.rollback(force); + dirty = false; + } +} diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/DBSessionsImplTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/DBSessionsImplTest.java new file mode 100644 index 00000000000..deea8de9c21 --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/DBSessionsImplTest.java @@ -0,0 +1,522 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +import com.google.common.base.Throwables; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.core.util.stream.MoreCollectors; + +import static java.lang.Math.abs; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class DBSessionsImplTest { + @Rule + public LogTester logTester = new LogTester(); + + private final MyBatis myBatis = mock(MyBatis.class); + private final DbSession myBatisDbSession = mock(DbSession.class); + private final Random random = new Random(); + private final DBSessionsImpl underTest = new DBSessionsImpl(myBatis); + + @After + public void tearDown() throws Exception { + underTest.disableCaching(); + } + + @Test + public void openSession_without_caching_always_returns_a_new_regular_session_when_parameter_is_false() { + DbSession[] expected = {mock(DbSession.class), mock(DbSession.class), mock(DbSession.class), mock(DbSession.class)}; + when(myBatis.openSession(false)) + .thenReturn(expected[0]) + .thenReturn(expected[1]) + .thenReturn(expected[2]) + .thenReturn(expected[3]) + .thenThrow(oneCallTooMuch()); + + assertThat(Arrays.stream(expected).map(ignored -> underTest.openSession(false)).collect(MoreCollectors.toList())) + .containsExactly(expected); + } + + @Test + public void openSession_without_caching_always_returns_a_new_batch_session_when_parameter_is_true() { + DbSession[] expected = {mock(DbSession.class), mock(DbSession.class), mock(DbSession.class), mock(DbSession.class)}; + when(myBatis.openSession(true)) + .thenReturn(expected[0]) + .thenReturn(expected[1]) + .thenReturn(expected[2]) + .thenReturn(expected[3]) + .thenThrow(oneCallTooMuch()); + + assertThat(Arrays.stream(expected).map(ignored -> underTest.openSession(true)).collect(MoreCollectors.toList())) + .containsExactly(expected); + } + + @Test + public void openSession_with_caching_always_returns_the_same_regular_session_when_parameter_is_false() { + DbSession expected = mock(DbSession.class); + when(myBatis.openSession(false)) + .thenReturn(expected) + .thenThrow(oneCallTooMuch()); + underTest.enableCaching(); + + int size = 1 + abs(random.nextInt(10)); + Set dbSessions = IntStream.range(0, size).mapToObj(ignored -> underTest.openSession(false)).collect(MoreCollectors.toSet()); + assertThat(dbSessions).hasSize(size); + assertThat(getWrappedDbSessions(dbSessions)) + .hasSize(1) + .containsOnly(expected); + } + + @Test + public void openSession_with_caching_always_returns_the_same_batch_session_when_parameter_is_true() { + DbSession expected = mock(DbSession.class); + when(myBatis.openSession(true)) + .thenReturn(expected) + .thenThrow(oneCallTooMuch()); + underTest.enableCaching(); + + int size = 1 + abs(random.nextInt(10)); + Set dbSessions = IntStream.range(0, size).mapToObj(ignored -> underTest.openSession(true)).collect(MoreCollectors.toSet()); + assertThat(dbSessions).hasSize(size); + assertThat(getWrappedDbSessions(dbSessions)) + .hasSize(1) + .containsOnly(expected); + } + + @Test + public void openSession_with_caching_returns_a_session_per_thread() { + boolean batchOrRegular = random.nextBoolean(); + DbSession[] expected = {mock(DbSession.class), mock(DbSession.class), mock(DbSession.class), mock(DbSession.class)}; + when(myBatis.openSession(batchOrRegular)) + .thenReturn(expected[0]) + .thenReturn(expected[1]) + .thenReturn(expected[2]) + .thenReturn(expected[3]) + .thenThrow(oneCallTooMuch()); + List collector = new ArrayList<>(); + Runnable runnable = () -> { + underTest.enableCaching(); + collector.add(underTest.openSession(batchOrRegular)); + underTest.disableCaching(); + }; + Thread[] threads = {new Thread(runnable, "T1"), new Thread(runnable, "T2"), new Thread(runnable, "T3")}; + + executeThreadsAndCurrent(runnable, threads); + + // verify each DbSession was closed and then reset mock for next verification + Arrays.stream(expected).forEach(s -> { + verify(s).close(); + reset(s); + }); + // verify whether each thread got the expected DbSession from MyBatis + // by verifying to which each returned DbSession delegates to + DbSession[] dbSessions = collector.toArray(new DbSession[0]); + for (int i = 0; i < expected.length; i++) { + dbSessions[i].rollback(); + verify(expected[i]).rollback(); + + List sub = new ArrayList<>(Arrays.asList(expected)); + sub.remove(expected[i]); + sub.forEach(Mockito::verifyNoMoreInteractions); + reset(expected); + } + } + + private static void executeThreadsAndCurrent(Runnable runnable, Thread[] threads) { + Arrays.stream(threads).forEach(thread -> { + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + Throwables.propagate(e); + } + }); + runnable.run(); + } + + @Test + public void openSession_with_caching_returns_wrapper_of_MyBatis_DbSession_which_delegates_all_methods_but_close() { + boolean batchOrRegular = random.nextBoolean(); + + underTest.enableCaching(); + + verifyFirstDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + dbSession.rollback(); + verify(myBatisDbSession).rollback(); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + boolean flag = random.nextBoolean(); + dbSession.rollback(flag); + verify(myBatisDbSession).rollback(flag); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + dbSession.commit(); + verify(myBatisDbSession).commit(); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + boolean flag = random.nextBoolean(); + dbSession.commit(flag); + verify(myBatisDbSession).commit(flag); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + dbSession.selectOne(str); + verify(myBatisDbSession).selectOne(str); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object object = new Object(); + dbSession.selectOne(str, object); + verify(myBatisDbSession).selectOne(str, object); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + dbSession.selectList(str); + verify(myBatisDbSession).selectList(str); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object object = new Object(); + dbSession.selectList(str, object); + verify(myBatisDbSession).selectList(str, object); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object parameter = new Object(); + RowBounds rowBounds = new RowBounds(); + dbSession.selectList(str, parameter, rowBounds); + verify(myBatisDbSession).selectList(str, parameter, rowBounds); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + String mapKey = randomAlphabetic(10); + dbSession.selectMap(str, mapKey); + verify(myBatisDbSession).selectMap(str, mapKey); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object parameter = new Object(); + String mapKey = randomAlphabetic(10); + dbSession.selectMap(str, parameter, mapKey); + verify(myBatisDbSession).selectMap(str, parameter, mapKey); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object parameter = new Object(); + String mapKey = randomAlphabetic(10); + RowBounds rowBounds = new RowBounds(); + dbSession.selectMap(str, parameter, mapKey, rowBounds); + verify(myBatisDbSession).selectMap(str, parameter, mapKey, rowBounds); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + ResultHandler handler = mock(ResultHandler.class); + dbSession.select(str, handler); + verify(myBatisDbSession).select(str, handler); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object parameter = new Object(); + ResultHandler handler = mock(ResultHandler.class); + dbSession.select(str, parameter, handler); + verify(myBatisDbSession).select(str, parameter, handler); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object parameter = new Object(); + ResultHandler handler = mock(ResultHandler.class); + RowBounds rowBounds = new RowBounds(); + dbSession.select(str, parameter, rowBounds, handler); + verify(myBatisDbSession).select(str, parameter, rowBounds, handler); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + dbSession.insert(str); + verify(myBatisDbSession).insert(str); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object object = new Object(); + dbSession.insert(str, object); + verify(myBatisDbSession).insert(str, object); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + dbSession.update(str); + verify(myBatisDbSession).update(str); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object object = new Object(); + dbSession.update(str, object); + verify(myBatisDbSession).update(str, object); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + dbSession.delete(str); + verify(myBatisDbSession).delete(str); + }); + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + String str = randomAlphabetic(10); + Object object = new Object(); + dbSession.delete(str, object); + verify(myBatisDbSession).delete(str, object); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + dbSession.flushStatements(); + verify(myBatisDbSession).flushStatements(); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + dbSession.clearCache(); + verify(myBatisDbSession).clearCache(); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + Configuration expected = mock(Configuration.class); + when(myBatisDbSession.getConfiguration()).thenReturn(expected); + assertThat(dbSession.getConfiguration()).isSameAs(expected); + verify(myBatisDbSession).getConfiguration(); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + Class clazz = Object.class; + Object expected = new Object(); + when(myBatisDbSession.getMapper(clazz)).thenReturn(expected); + assertThat(dbSession.getMapper(clazz)).isSameAs(expected); + verify(myBatisDbSession).getMapper(clazz); + }); + + verifyDelegation(batchOrRegular, (myBatisDbSession, dbSession) -> { + Connection connection = mock(Connection.class); + when(myBatisDbSession.getConnection()).thenReturn(connection); + assertThat(dbSession.getConnection()).isSameAs(connection); + verify(myBatisDbSession).getConnection(); + }); + } + + @Test + public void openSession_with_caching_returns_DbSession_that_rolls_back_on_close_if_any_mutation_call_was_not_followed_by_commit_nor_rollback() throws SQLException { + DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls(); + + dbSession.close(); + + verify(myBatisDbSession).rollback(); + } + + @Test + public void openSession_with_caching_returns_DbSession_that_does_not_roll_back_on_close_if_any_mutation_call_was_followed_by_commit() throws SQLException { + DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls(); + COMMIT_CALLS[random.nextBoolean() ? 0 : 1].consume(dbSession); + + dbSession.close(); + + verify(myBatisDbSession, times(0)).rollback(); + } + + @Test + public void openSession_with_caching_returns_DbSession_that_does_not_roll_back_on_close_if_any_mutation_call_was_followed_by_rollback_without_parameters() throws SQLException { + DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls(); + dbSession.rollback(); + + dbSession.close(); + + verify(myBatisDbSession, times(1)).rollback(); + } + + @Test + public void openSession_with_caching_returns_DbSession_that_does_not_roll_back_on_close_if_any_mutation_call_was_followed_by_rollback_with_parameters() throws SQLException { + boolean force = random.nextBoolean(); + DbSession dbSession = openSessionAndDoSeveralMutatingAndNeutralCalls(); + dbSession.rollback(force); + + dbSession.close(); + + verify(myBatisDbSession, times(1)).rollback(force); + verify(myBatisDbSession, times(0)).rollback(); + } + + private DbSession openSessionAndDoSeveralMutatingAndNeutralCalls() throws SQLException { + boolean batchOrRegular = random.nextBoolean(); + when(myBatis.openSession(batchOrRegular)) + .thenReturn(myBatisDbSession) + .thenThrow(oneCallTooMuch()); + underTest.enableCaching(); + DbSession dbSession = underTest.openSession(batchOrRegular); + + int dirtyingCallsCount = 1 + abs(random.nextInt(5)); + int neutralCallsCount = abs(random.nextInt(5)); + int[] dirtyCallIndices = IntStream.range(0, dirtyingCallsCount).map(ignored -> abs(random.nextInt(DIRTYING_CALLS.length))).toArray(); + int[] neutralCallsIndices = IntStream.range(0, neutralCallsCount).map(ignored -> abs(random.nextInt(NEUTRAL_CALLS.length))).toArray(); + for (int index : dirtyCallIndices) { + DIRTYING_CALLS[index].consume(dbSession); + } + for (int index : neutralCallsIndices) { + NEUTRAL_CALLS[index].consume(dbSession); + } + return dbSession; + } + + private static DbSessionCaller[] DIRTYING_CALLS = { + session -> session.insert(randomAlphabetic(3)), + session -> session.insert(randomAlphabetic(2), new Object()), + session -> session.update(randomAlphabetic(3)), + session -> session.update(randomAlphabetic(3), new Object()), + session -> session.delete(randomAlphabetic(3)), + session -> session.delete(randomAlphabetic(3), new Object()), + }; + + private static DbSessionCaller[] COMMIT_CALLS = { + session -> session.commit(), + session -> session.commit(new Random().nextBoolean()), + }; + + private static DbSessionCaller[] ROLLBACK_CALLS = { + session -> session.rollback(), + session -> session.rollback(new Random().nextBoolean()), + }; + + private static DbSessionCaller[] NEUTRAL_CALLS = { + session -> session.selectOne(randomAlphabetic(3)), + session -> session.selectOne(randomAlphabetic(3), new Object()), + session -> session.select(randomAlphabetic(3), mock(ResultHandler.class)), + session -> session.select(randomAlphabetic(3), new Object(), mock(ResultHandler.class)), + session -> session.select(randomAlphabetic(3), new Object(), new RowBounds(), mock(ResultHandler.class)), + session -> session.selectList(randomAlphabetic(3)), + session -> session.selectList(randomAlphabetic(3), new Object()), + session -> session.selectList(randomAlphabetic(3), new Object(), new RowBounds()), + session -> session.selectMap(randomAlphabetic(3), randomAlphabetic(3)), + session -> session.selectMap(randomAlphabetic(3), new Object(), randomAlphabetic(3)), + session -> session.selectMap(randomAlphabetic(3), new Object(), randomAlphabetic(3), new RowBounds()), + session -> session.getMapper(Object.class), + session -> session.getConfiguration(), + session -> session.getConnection(), + session -> session.clearCache(), + session -> session.flushStatements() + }; + + private interface DbSessionCaller { + void consume(DbSession t) throws SQLException; + } + + @Test + public void disableCaching_does_not_open_DB_connection_if_openSession_was_never_called() { + when(myBatis.openSession(anyBoolean())) + .thenThrow(oneCallTooMuch()); + underTest.enableCaching(); + + underTest.disableCaching(); + + verifyNoMoreInteractions(myBatis); + } + + @Test + public void disableCaching_has_no_effect_if_enabledCaching_has_not_been_called() { + underTest.disableCaching(); + + verifyNoMoreInteractions(myBatis); + } + + @Test + public void disableCaching_does_not_fail_but_logs_if_closing_MyBatis_session_close_throws_an_exception() { + boolean batchOrRegular = random.nextBoolean(); + IllegalStateException toBeThrown = new IllegalStateException("Faking MyBatisSession#close failing"); + + when(myBatis.openSession(batchOrRegular)) + .thenReturn(myBatisDbSession) + .thenThrow(oneCallTooMuch()); + Mockito.doThrow(toBeThrown) + .when(myBatisDbSession).close(); + underTest.enableCaching(); + underTest.openSession(batchOrRegular); + + underTest.disableCaching(); + + List errorLogs = logTester.logs(LoggerLevel.ERROR); + assertThat(errorLogs) + .hasSize(1) + .containsOnly("Failed to close " + (batchOrRegular ? "batch" : "regular") + " connection in " + Thread.currentThread()); + } + + private void verifyFirstDelegation(boolean batchOrRegular, BiConsumer r) { + verifyDelegation(batchOrRegular, true, r); + } + + private void verifyDelegation(boolean batchOrRegular, BiConsumer r) { + verifyDelegation(batchOrRegular, false, r); + } + + private void verifyDelegation(boolean batchOrRegular, boolean firstCall, BiConsumer r) { + when(myBatis.openSession(batchOrRegular)) + .thenReturn(myBatisDbSession) + .thenThrow(oneCallTooMuch()); + r.accept(myBatisDbSession, underTest.openSession(batchOrRegular)); + verify(myBatisDbSession, times(firstCall ? 1 : 0)).getSqlSession(); + verifyNoMoreInteractions(myBatisDbSession); + reset(myBatis, myBatisDbSession); + } + + private static IllegalStateException oneCallTooMuch() { + return new IllegalStateException("one call too much"); + } + + private Set getWrappedDbSessions(Set dbSessions) { + return dbSessions.stream() + .filter(NonClosingDbSession.class::isInstance) + .map(NonClosingDbSession.class::cast) + .map(NonClosingDbSession::getDelegate) + .collect(Collectors.toSet()); + } +} diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java b/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java index be89d738207..cb707617cb6 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java @@ -121,7 +121,7 @@ public class DbTester extends AbstractDbTester { ioc.addComponent(daoClass); } List daos = ioc.getComponents(Dao.class); - client = new DbClient(db.getDatabase(), db.getMyBatis(), daos.toArray(new Dao[daos.size()])); + client = new DbClient(db.getDatabase(), db.getMyBatis(), new TestDBSessions(db.getMyBatis()), daos.toArray(new Dao[daos.size()])); } public DbTester setDisableDefaultOrganization(boolean b) { diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/TestDBSessions.java b/server/sonar-db-dao/src/test/java/org/sonar/db/TestDBSessions.java new file mode 100644 index 00000000000..87670145ac6 --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/TestDBSessions.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.db; + +public final class TestDBSessions implements DBSessions { + private final MyBatis myBatis; + + public TestDBSessions(MyBatis myBatis) { + this.myBatis = myBatis; + } + + @Override + public DbSession openSession(boolean batch) { + return myBatis.openSession(false); + } + + @Override + public void enableCaching() { + // ignored + } + + @Override + public void disableCaching() { + // ignored + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java index 10bffed4ced..6de6ea8288a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java @@ -21,6 +21,7 @@ package org.sonar.server.platform.platformlevel; import java.util.Properties; import javax.annotation.Nullable; +import org.sonar.db.DBSessionsImpl; import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarQubeVersion; import org.sonar.api.internal.ApiVersion; @@ -102,6 +103,7 @@ public class PlatformLevel1 extends PlatformLevel { ThreadLocalUserSession.class, // DB + DBSessionsImpl.class, DbClient.class, DaoModule.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java index d20232aae20..26c23e569bd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java @@ -30,12 +30,16 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.sonar.db.DBSessions; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; import org.sonar.server.authentication.UserSessionInitializer; import org.sonar.server.organization.DefaultOrganizationCache; import org.sonar.server.platform.Platform; import org.sonar.server.setting.ThreadLocalSettings; public class UserSessionFilter implements Filter { + private static final Logger LOG = Loggers.get(UserSessionFilter.class); private final Platform platform; public UserSessionFilter() { @@ -52,20 +56,27 @@ public class UserSessionFilter implements Filter { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; + DBSessions dbSessions = platform.getContainer().getComponentByType(DBSessions.class); ThreadLocalSettings settings = platform.getContainer().getComponentByType(ThreadLocalSettings.class); DefaultOrganizationCache defaultOrganizationCache = platform.getContainer().getComponentByType(DefaultOrganizationCache.class); UserSessionInitializer userSessionInitializer = platform.getContainer().getComponentByType(UserSessionInitializer.class); - defaultOrganizationCache.load(); + LOG.trace("{} serves {}", Thread.currentThread(), request.getRequestURI()); + dbSessions.enableCaching(); try { - settings.load(); + defaultOrganizationCache.load(); try { - doFilter(request, response, chain, userSessionInitializer); + settings.load(); + try { + doFilter(request, response, chain, userSessionInitializer); + } finally { + settings.unload(); + } } finally { - settings.unload(); + defaultOrganizationCache.unload(); } } finally { - defaultOrganizationCache.unload(); + dbSessions.disableCaching(); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/metric/DefaultMetricFinderTest.java b/server/sonar-server/src/test/java/org/sonar/server/metric/DefaultMetricFinderTest.java index c72140fd88a..504249c23cf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/metric/DefaultMetricFinderTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/metric/DefaultMetricFinderTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbTester; +import org.sonar.db.TestDBSessions; import org.sonar.db.metric.MetricDao; import static org.hamcrest.core.Is.is; @@ -43,7 +44,7 @@ public class DefaultMetricFinderTest { @Before public void setUp() { dbTester.prepareDbUnit(DefaultMetricFinderTest.class, "shared.xml"); - finder = new DefaultMetricFinder(new DbClient(dbTester.database(), dbTester.myBatis(), new MetricDao())); + finder = new DefaultMetricFinder(new DbClient(dbTester.database(), dbTester.myBatis(), new TestDBSessions(dbTester.myBatis()), new MetricDao())); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/metric/ws/DomainsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/metric/ws/DomainsActionTest.java index 84c89561028..cbee45038ed 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/metric/ws/DomainsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/metric/ws/DomainsActionTest.java @@ -27,6 +27,7 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import org.sonar.db.TestDBSessions; import org.sonar.db.metric.MetricDao; import org.sonar.db.metric.MetricDto; import org.sonar.server.ws.WsTester; @@ -45,7 +46,7 @@ public class DomainsActionTest { @Before public void setUp() { - dbClient = new DbClient(db.database(), db.myBatis(), new MetricDao()); + dbClient = new DbClient(db.database(), db.myBatis(), new TestDBSessions(db.myBatis()), new MetricDao()); dbSession = dbClient.openSession(false); ws = new WsTester(new MetricsWs(new DomainsAction(dbClient))); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java index 4ea52d16890..79ef9e2d898 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java @@ -29,6 +29,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; +import org.sonar.db.DBSessions; import org.sonar.server.authentication.UserSessionInitializer; import org.sonar.server.organization.DefaultOrganizationCache; import org.sonar.server.platform.Platform; @@ -51,12 +52,14 @@ public class UserSessionFilterTest { private HttpServletRequest request = mock(HttpServletRequest.class); private HttpServletResponse response = mock(HttpServletResponse.class); private FilterChain chain = mock(FilterChain.class); + private DBSessions dbSessions = mock(DBSessions.class); private ThreadLocalSettings settings = mock(ThreadLocalSettings.class); private DefaultOrganizationCache defaultOrganizationCache = mock(DefaultOrganizationCache.class); private UserSessionFilter underTest = new UserSessionFilter(platform); @Before public void setUp() { + when(platform.getContainer().getComponentByType(DBSessions.class)).thenReturn(dbSessions); when(platform.getContainer().getComponentByType(ThreadLocalSettings.class)).thenReturn(settings); when(platform.getContainer().getComponentByType(DefaultOrganizationCache.class)).thenReturn(defaultOrganizationCache); } @@ -117,6 +120,32 @@ public class UserSessionFilterTest { } } + @Test + public void doFilter_enables_and_disables_caching_in_DbSessions() throws Exception { + mockNoUserSessionInitializer(); + + underTest.doFilter(request, response, chain); + + InOrder inOrder = inOrder(dbSessions); + inOrder.verify(dbSessions).enableCaching(); + inOrder.verify(dbSessions).disableCaching(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void doFilter_disables_caching_in_DbSessions_even_if_chain_throws_exception() throws Exception { + mockNoUserSessionInitializer(); + RuntimeException thrown = mockChainDoFilterError(); + + try { + underTest.doFilter(request, response, chain); + fail("A RuntimeException should have been thrown"); + } catch (RuntimeException e) { + assertThat(e).isSameAs(thrown); + verify(dbSessions).disableCaching(); + } + } + @Test public void doFilter_unloads_Settings_even_if_DefaultOrganizationCache_unload_fails() throws Exception { mockNoUserSessionInitializer();