浏览代码

SONAR-8785 cache DbSession in ThreadLocals for HTTP requests

tags/6.5-RC1
Sébastien Lesaint 7 年前
父节点
当前提交
81a6eef465

+ 2
- 0
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

+ 1
- 1
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

+ 28
- 0
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();
}

+ 151
- 0
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<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;
}
}

}

+ 4
- 2
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<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() {

+ 1
- 0
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();
}

+ 5
- 0
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;
}
}

+ 194
- 0
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> 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();
}
}

+ 101
- 0
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;
}
}

+ 522
- 0
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<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());
}
}

+ 1
- 1
server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java 查看文件

@@ -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) {

+ 43
- 0
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
}
}

+ 2
- 0
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,


+ 16
- 5
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();
}
}


+ 2
- 1
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

+ 2
- 1
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)));
}

+ 29
- 0
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();

正在加载...
取消
保存