Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
Enhance connection wrapping: logging, configuration, mariadb
Browse files Browse the repository at this point in the history
- Log information about database and driver.
- Check for supported database/version.
- Add system property 'zanata.connection.use.wrapper' for explicit
  control.
- Disable connection wrapping for mariadb as well as mysql.
- Log what WrapperManager does.
  • Loading branch information
seanf committed Sep 15, 2014
1 parent 2a1597d commit bf1042f
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 48 deletions.
69 changes: 37 additions & 32 deletions zanata-war/src/main/java/org/zanata/database/ConnectionWrapper.java
Expand Up @@ -24,10 +24,7 @@
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Set;

Expand All @@ -36,6 +33,9 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static java.lang.reflect.Proxy.getInvocationHandler;
import static java.lang.reflect.Proxy.isProxyClass;

/**
* @author Sean Flanigan <a
* href="mailto:sflaniga@redhat.com">sflaniga@redhat.com</a>
Expand All @@ -52,42 +52,25 @@ class ConnectionWrapper implements InvocationHandler {
// sets before attempting more queries.
public static final String CONCURRENT_RESULTSET =
"Streaming ResultSet is still open on this Connection";
private final Connection connection;
private final Connection originalConnection;
private Set<Throwable> resultSetsOpened = Sets.newHashSet();
private Throwable streamingResultSetOpened;

public static Connection wrap(Connection connection) {
if (Proxy.isProxyClass(connection.getClass())
&& Proxy.getInvocationHandler(connection) instanceof ConnectionWrapper) {
return connection;
public static Connection wrap(Connection conn) {
// avoid double-wrapping:
if (isProxyClass(conn.getClass())
&& getInvocationHandler(conn) instanceof ConnectionWrapper) {
return conn;
}
return ProxyUtil
.newProxy(connection, new ConnectionWrapper(connection));
}

public static Connection wrapUnlessMysql(Connection connection)
throws SQLException {
DatabaseMetaData metaData = connection.getMetaData();
String databaseName = metaData.getDatabaseProductName();
if ("MySQL".equals(databaseName)) {
return connection;
} else {
return wrap(connection);
}
}

/**
* @return the connection
*/
public Connection getConnection() {
return connection;
.newProxy(conn, new ConnectionWrapper(conn));
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("toString")) {
return "ConnectionWrapper->" + connection.toString();
return "ConnectionWrapper->" + originalConnection.toString();
}
if (method.getName().equals("close")) {
if (streamingResultSetOpened != null) {
Expand All @@ -100,7 +83,7 @@ public Object invoke(Object proxy, Method method, Object[] args)
}
}
try {
Object result = method.invoke(connection, args);
Object result = method.invoke(originalConnection, args);
if (result instanceof Statement) {
Statement statement = (Statement) result;
return StatementWrapper.wrap(statement, (Connection) proxy);
Expand All @@ -111,23 +94,37 @@ public Object invoke(Object proxy, Method method, Object[] args)
}
}

public void executed() throws SQLException {
/**
* Notify ConnectionWrapper that Statement.execute() has been called.
* @throws StreamingResultSetSQLException
*/
public void executed() throws StreamingResultSetSQLException {
if (streamingResultSetOpened != null) {
throw new StreamingResultSetSQLException(CONCURRENT_RESULTSET,
streamingResultSetOpened);
}
}

public void resultSetOpened(Throwable throwable) throws SQLException {
/**
* Notify ConnectionWrapper that a Statement has opened a non-streaming
* ResultSet.
* @throws StreamingResultSetSQLException
*/
public void resultSetOpened(Throwable throwable) throws StreamingResultSetSQLException {
if (streamingResultSetOpened != null) {
throw new StreamingResultSetSQLException(CONCURRENT_RESULTSET,
streamingResultSetOpened);
}
resultSetsOpened.add(throwable);
}

/**
* Notify ConnectionWrapper that a Statement has opened a streaming
* ResultSet.
* @throws StreamingResultSetSQLException
*/
public void streamingResultSetOpened(Throwable throwable)
throws SQLException {
throws StreamingResultSetSQLException {
if (streamingResultSetOpened != null) {
throw new StreamingResultSetSQLException(CONCURRENT_RESULTSET,
streamingResultSetOpened);
Expand All @@ -138,10 +135,18 @@ public void streamingResultSetOpened(Throwable throwable)
streamingResultSetOpened = throwable;
}

/**
* Notify ConnectionWrapper that a non-streaming ResultSet has been closed.
* @throws StreamingResultSetSQLException
*/
public void resultSetClosed(Throwable throwable) {
resultSetsOpened.remove(throwable);
}

/**
* Notify ConnectionWrapper that a streaming ResultSet has been closed.
* @throws StreamingResultSetSQLException
*/
public void streamingResultSetClosed() {
streamingResultSetOpened = null;
}
Expand Down
Expand Up @@ -27,23 +27,17 @@
import org.hibernate.service.jdbc.connections.internal.DatasourceConnectionProviderImpl;

/**
* This class wraps JDBC Connections/Statements/ResultSets to detect attempts to
* use mysql's streaming ResultSet feature. It then watches for any usage which
* would exceed the limitations of mysql's streaming ResultSets, and throws an
* SQLException. This enables us to catch these problems without having to test
* against mysql in our unit tests.
*
* @author Sean Flanigan <a
* href="mailto:sflaniga@redhat.com">sflaniga@redhat.com</a>
*
*/
public class WrappedDatasourceConnectionProvider extends
DatasourceConnectionProviderImpl {
private static final long serialVersionUID = 1L;
private final WrapperManager wrapperManager = new WrapperManager();

@Override
public Connection getConnection() throws SQLException {
return ConnectionWrapper.wrapUnlessMysql(super.getConnection());
return wrapperManager.wrapIfNeeded(super.getConnection());
}

}
Expand Up @@ -27,23 +27,17 @@
import org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl;

/**
* This class wraps JDBC Connections/Statements/ResultSets to detect attempts to
* use mysql's streaming ResultSet feature. It then watches for any usage which
* would exceed the limitations of mysql's streaming ResultSets, and throws an
* SQLException. This enables us to catch these problems without having to test
* against mysql in our unit tests.
*
* @author Sean Flanigan <a
* href="mailto:sflaniga@redhat.com">sflaniga@redhat.com</a>
*
*/
public class WrappedDriverManagerConnectionProvider extends
DriverManagerConnectionProviderImpl {
private static final long serialVersionUID = 1L;
private final WrapperManager wrapperManager = new WrapperManager();

@Override
public Connection getConnection() throws SQLException {
return ConnectionWrapper.wrapUnlessMysql(super.getConnection());
return wrapperManager.wrapIfNeeded(super.getConnection());
}

}
146 changes: 146 additions & 0 deletions zanata-war/src/main/java/org/zanata/database/WrapperManager.java
@@ -0,0 +1,146 @@
/*
* Copyright 2013, Red Hat, Inc. and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.zanata.database;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;

import lombok.extern.slf4j.Slf4j;
import org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl;

/**
* This class wraps JDBC Connections/Statements/ResultSets to detect attempts to
* use mysql's streaming ResultSet feature. It then watches for any usage which
* would exceed the limitations of mysql's streaming ResultSets, and throws an
* SQLException. This enables us to catch these problems without having to test
* against mysql in our unit tests.
*
* @author Sean Flanigan <a
* href="mailto:sflaniga@redhat.com">sflaniga@redhat.com</a>
*
*/
@Slf4j
public class WrapperManager {
public static final String PROPERTY_USE_WRAPPER =
"zanata.connection.use.wrapper";
private static final String USE_WRAPPER =
System.getProperty(PROPERTY_USE_WRAPPER);

private boolean checkedFirstConnection = false;
private boolean wrappingEnabled = false;

public Connection wrapIfNeeded(Connection conn) throws SQLException {
if (!checkedFirstConnection) {
DatabaseMetaData metaData = conn.getMetaData();
checkSupported(metaData);
wrappingEnabled = shouldWrap(metaData);
checkedFirstConnection = true;
}
if (wrappingEnabled) {
Connection wrapped = ConnectionWrapper.wrap(conn);
log.debug("Connection {} is wrapped by {}",
conn, wrapped);
return wrapped;
} else {
return conn;
}
}

/**
* Log warnings or errors if the database or driver is not supported.
* @param metaData
* @throws SQLException
*/
private static void checkSupported(DatabaseMetaData metaData)
throws SQLException {
String dbName = metaData.getDatabaseProductName();
String dbVer = metaData.getDatabaseProductVersion();
int dbMaj = metaData.getDatabaseMajorVersion();
int dbMin = metaData.getDatabaseMinorVersion();
log.info("Database product: {} version: {} ({}.{})",
dbName, dbVer, dbMaj, dbMin);
String lcName = dbName.toLowerCase();
if (lcName.contains("mysql")) {
log.info("Using MySQL database");
if (dbMaj != 5) {
log.warn("Unsupported MySQL major version: {}", dbMaj);
}
if (dbMin > 5) {
log.warn("Unsupported MySQL minor version: {}", dbMin);
}
} else if (lcName.contains("h2")) {
log.info("Using H2 database (not for production)");
} else {
log.warn("Unsupported database");
}
String drvName = metaData.getDriverName();
String drvVer = metaData.getDriverVersion();
int drvMaj = metaData.getDriverMajorVersion();
int drvMin = metaData.getDriverMinorVersion();
log.info("JDBC driver: {} version: {} ({}.{})",
drvName, drvVer, drvMaj, drvMin);
}

private static boolean shouldWrap(DatabaseMetaData metaData)
throws SQLException {
if (USE_WRAPPER != null) {
if ("false".equals(USE_WRAPPER)) {
log.info("Not wrapping JDBC connection (disabled by system " +
"property {})", PROPERTY_USE_WRAPPER);
return false;
}
if ("true".equals(USE_WRAPPER)) {
log.info("Wrapping JDBC connection (forced by system " +
"property {})", PROPERTY_USE_WRAPPER);
return true;
}
if (!("auto".equals(USE_WRAPPER))) {
log.warn("Unknown value for system property {}: {}",
PROPERTY_USE_WRAPPER, USE_WRAPPER);
}
}
String driverName = metaData.getDriverName();
if (driverName.equals("MySQL Connector Java") ||
driverName.equals("MySQL-AB JDBC Driver") ||
driverName.equals("mariadb-jdbc")) {
// these drivers are known to use streaming result sets
// when fetchSize == Integer.MIN_VALUE
log.info("No need to wrap JDBC connection: driver: {}", driverName);
return false;
} else if (driverName.toLowerCase().contains("mysql") ||
driverName.toLowerCase().contains("mariadb")) {
// NB: if a future mysql/mariadb driver does away with the
// fetchSize trick for streaming, please add a special case and
// return true, or remove the Zanata code which calls
// setFetchSize(Integer.MIN_VALUE). See StreamingEntityIterator.
log.warn("Unrecognised mysql/mariadb driver: {}", driverName);
log.warn("Streaming results may not work");
return false;
} else {
log.info("Wrapping JDBC connection: found non-mysql/mariadb " +
"driver: {}", driverName);
return true;
}
}

}

0 comments on commit bf1042f

Please sign in to comment.