]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8785 cache DbSession in ThreadLocals for HTTP requests 2158/head
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 8 Jun 2017 15:51:17 +0000 (17:51 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 11 Jul 2017 08:15:10 +0000 (10:15 +0200)
17 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/DBSessions.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/DBSessionsImpl.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
server/sonar-db-dao/src/main/java/org/sonar/db/DbSession.java
server/sonar-db-dao/src/main/java/org/sonar/db/DbSessionImpl.java
server/sonar-db-dao/src/main/java/org/sonar/db/DelegatingDbSession.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/NonClosingDbSession.java [new file with mode: 0644]
server/sonar-db-dao/src/test/java/org/sonar/db/DBSessionsImplTest.java [new file with mode: 0644]
server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java
server/sonar-db-dao/src/test/java/org/sonar/db/TestDBSessions.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java
server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java
server/sonar-server/src/test/java/org/sonar/server/metric/DefaultMetricFinderTest.java
server/sonar-server/src/test/java/org/sonar/server/metric/ws/DomainsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java

index 5f50464eb512c5259940c28e93898e04e1051ddf..59e18e5f04937cd257576fa9532dda2cd7f5735e 100644 (file)
@@ -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
index 9b009c3452fb7d63e464fd5f5e6e08e7da81f9b4..b9eec535920e21653338b70de201ff8d0616cd7a 100644 (file)
@@ -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 (file)
index 0000000..878816e
--- /dev/null
@@ -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 (file)
index 0000000..e331480
--- /dev/null
@@ -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<Boolean> CACHING_ENABLED = ThreadLocal.withInitial(() -> Boolean.FALSE);
+  private final ThreadLocal<DelegatingDbSessionSupplier> regularDbSession = ThreadLocal.withInitial(this::buildRegularDbSessionSupplier);
+  private final ThreadLocal<DelegatingDbSessionSupplier> 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<DelegatingDbSessionSupplier> 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<NonClosingDbSession>} instance and
+   * caches the supplied {@link NonClosingDbSession}.
+   */
+  private static final class DelegatingDbSessionSupplier implements Supplier<DbSession> {
+    private final Supplier<DbSession> delegate;
+    private DbSession dbSession;
+
+    DelegatingDbSessionSupplier(Supplier<DbSession> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public DbSession get() {
+      if (dbSession == null) {
+        dbSession = Objects.requireNonNull(delegate.get());
+      }
+      return dbSession;
+    }
+
+    boolean isPopulated() {
+      return dbSession != null;
+    }
+  }
+
+}
index 6eb7c664a2541aa0db46fd85bd36f52610edbc92..bd0fd55735e0785f436948530ef6742a05366a8d 100644 (file)
@@ -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<Class, Dao> 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() {
index 7ae9d3803fa6c11ab6b96aeb8c5a53998625ce55..dc757acd9f090db65721a19f738b856530ea9fc8 100644 (file)
@@ -22,4 +22,5 @@ package org.sonar.db;
 import org.apache.ibatis.session.SqlSession;
 
 public interface DbSession extends SqlSession {
+  SqlSession getSqlSession();
 }
index 6e22851f4631d82c5e68eccfefb2b239aaf7dbb1..3e1fa8d2daff7d755988fe0c7d84ac8f295cd67a 100644 (file)
@@ -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 (file)
index 0000000..d065f42
--- /dev/null
@@ -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> T selectOne(String statement) {
+    return delegate.selectOne(statement);
+  }
+
+  @Override
+  public <T> T selectOne(String statement, Object parameter) {
+    return delegate.selectOne(statement, parameter);
+  }
+
+  @Override
+  public <E> List<E> selectList(String statement) {
+    return delegate.selectList(statement);
+  }
+
+  @Override
+  public <E> List<E> selectList(String statement, Object parameter) {
+    return delegate.selectList(statement, parameter);
+  }
+
+  @Override
+  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
+    return delegate.selectList(statement, parameter, rowBounds);
+  }
+
+  @Override
+  public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
+    return delegate.selectMap(statement, mapKey);
+  }
+
+  @Override
+  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
+    return delegate.selectMap(statement, parameter, mapKey);
+  }
+
+  @Override
+  public <K, V> Map<K, V> 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<BatchResult> flushStatements() {
+    return delegate.flushStatements();
+  }
+
+  @Override
+  public void clearCache() {
+    delegate.clearCache();
+  }
+
+  @Override
+  public Configuration getConfiguration() {
+    return delegate.getConfiguration();
+  }
+
+  @Override
+  public <T> T getMapper(Class<T> 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 (file)
index 0000000..09ee82c
--- /dev/null
@@ -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 (file)
index 0000000..deea8de
--- /dev/null
@@ -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<DbSession> 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<DbSession> 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<DbSession> 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<DbSession> 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<Object> 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<String> 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<DbSession, DbSession> r) {
+    verifyDelegation(batchOrRegular, true, r);
+  }
+
+  private void verifyDelegation(boolean batchOrRegular, BiConsumer<DbSession, DbSession> r) {
+    verifyDelegation(batchOrRegular, false, r);
+  }
+
+  private void verifyDelegation(boolean batchOrRegular, boolean firstCall, BiConsumer<DbSession, DbSession> 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<DbSession> getWrappedDbSessions(Set<DbSession> dbSessions) {
+    return dbSessions.stream()
+      .filter(NonClosingDbSession.class::isInstance)
+      .map(NonClosingDbSession.class::cast)
+      .map(NonClosingDbSession::getDelegate)
+      .collect(Collectors.toSet());
+  }
+}
index be89d738207ff2926202bbe8d1c688105eb253e8..cb707617cb6197853c02fb35c382c93e26bd1acb 100644 (file)
@@ -121,7 +121,7 @@ public class DbTester extends AbstractDbTester<TestDb> {
       ioc.addComponent(daoClass);
     }
     List<Dao> 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 (file)
index 0000000..8767014
--- /dev/null
@@ -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
+  }
+}
index 10bffed4ced5388e08efdbc43eb6a1ca326e0a9b..6de6ea8288ae926f1895f25e6e658eab3ddebfc7 100644 (file)
@@ -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,
 
index d20232aae200ce830f42d013aa257a1e4e1744d6..26c23e569bdfe2f8924f393256548ee1ef56bce0 100644 (file)
@@ -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();
     }
   }
 
index c72140fd88a6d179c120171c6d15bc9bc771ee41..504249c23cfda69e4fc95a7971b8ad056f2df8de 100644 (file)
@@ -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
index 84c89561028b67c55621c61537e32d66d0e25ea1..cbee45038eda5c64541396f0d63286608cc095ab 100644 (file)
@@ -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)));
   }
index 4ea52d1689080b6c46f8c9224b028fee36016631..79ef9e2d898e2f7dcdedf00805237c2604481055 100644 (file)
@@ -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();