Bug #63947 Rollback hangs if OutOfMemoryException was thrown when reading the query results
Submitted: 6 Jan 2012 10:01 Modified: 19 Mar 2014 14:10
Reporter: Topi Nieminen Email Updates:
Status: Verified Impact on me:
None 
Category:Connector / J Severity:S3 (Non-critical)
Version:5.1.18 OS:Any
Assigned to: Filipe Silva CPU Architecture:Any
Tags: Contribution, jdbc

[6 Jan 2012 10:01] Topi Nieminen
Description:
If OutOfMemoryException is thrown when reading the results from the server, 
the driver throws an SQLException. If the client code then tries to do rollback, 
the driver waits for the server response indefinitely (or time specified by socketTimeout, if set):

"main" prio=6 tid=0x000000000021b800 nid=0x161c runnable [0x000000000260e000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.read(Unknown Source)
	at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:158)
	at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:189)
	- locked <0x000000000aaee2a0> (a com.mysql.jdbc.util.ReadAheadInputStream)
	at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2549)
	at com.mysql.jdbc.MysqlIO.readPacket(MysqlIO.java:662)
	at com.mysql.jdbc.MysqlIO.getResultSet(MysqlIO.java:416)
	at com.mysql.jdbc.MysqlIO.readResultsForQueryOrUpdate(MysqlIO.java:2631)
	at com.mysql.jdbc.MysqlIO.readAllResults(MysqlIO.java:1800)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2221)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2618)
	- locked <0x000000000aaf2308> (a com.mysql.jdbc.JDBC4Connection)
	at com.mysql.jdbc.ConnectionImpl.rollbackNoChecks(ConnectionImpl.java:4833)
	at com.mysql.jdbc.ConnectionImpl.rollback(ConnectionImpl.java:4719)
	- locked <0x000000000aaf2308> (a com.mysql.jdbc.JDBC4Connection)
	at test.TestOOM.run(TestOOM.java:67)
	at test.TestOOM.main(TestOOM.java:18)

Server thread stays in state 'sleep' at this point:

show full processlist

Id	User	Host	db	Command	Time	State	Info
16	root	localhost:54222	oom_test	Sleep	35		null
18	root	localhost:54226	mysql	Query	0	null	show full processlist

Under these conditions it might be better to close the connection forcibly 
than wait indefinitely.

How to repeat:
Run the test program test.TestOOM with limited heap size (-Xmx8m -Xms8m) and Log4J in classpath:

--------------------------------------------------------------------------------

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import org.apache.log4j.Logger;

public class TestOOM {
	
	private static Logger logger = Logger.getLogger(TestOOM.class);

	public static void main(String[] args) {
		new TestOOM().run();
	}	
	
	public void run() {
		Connection c = null; 
		
		try {
			logger.debug("run - enter");
			
			Class.forName("com.mysql.jdbc.Driver");
			
			String url = "jdbc:mysql://127.0.0.1:3306/mysql";
						
			Properties config = new Properties();
			config.setProperty("user", "test_user");
			config.setProperty("password", "test_password");
						
			c = DriverManager.getConnection(url, config);
			
			c.setAutoCommit(false);
			
			execute(c, "create schema if not exists oom_test");
			
			execute(c, "use oom_test");			
						
			execute(c, "create or replace view test(a, b) as select 0, 1 union all select 1, 0");
			
			StringBuilder qb = new StringBuilder("select * from ");
  
			int rc = 15;
			
			for (int i = 1; i <= rc; i++) {
				qb.append("test r");
				qb.append(i);
				
				if (i < rc) {
					qb.append(", ");
				}
			}			
			
			logger.info("-- " + qb.toString());
			
			logger.info("executing query...");
			execute(c, qb.toString());
			
			logger.info("connection closed ? " + c.isClosed());
			
			logger.info("rolling back...");
			c.rollback();
			
			logger.info("is closed ? " + c.isClosed());
			
			execute(c, "drop schema if exists oom_test");	
		}
		catch (Throwable e) {
			e.printStackTrace();
		}	
		finally {
			doClose(c);
			logger.debug("run - exit");
		}		
	}

	private void execute(Connection c, String q) throws SQLException {		
		Statement st = null; 
			
		try {
			st = c.createStatement();		
			boolean hasResultSet = st.execute(q);
			
			ResultSet rs = null;
			
			if (hasResultSet) {
				rs = st.getResultSet();				
				rs.close();
			}
			else {
				int uc = st.getUpdateCount();
				logger.info("update count: " + uc);
			}
			
			while(true) {
				if (st.getMoreResults()) {
					rs = st.getResultSet();
					rs.close();
					continue;
				}								
								
				int uc = st.getUpdateCount();
				
				if (uc == -1) {
					break;
				}
				
				logger.info("update count: " + uc);
			}
		}
		catch (SQLException e) {
			e.printStackTrace();
		}
		catch (OutOfMemoryError e) {
			e.printStackTrace();		
		}		
		finally {
			doClose(st);
		}		
	}

	private void doClose(Connection c) {		
		try {
			if (c != null) {
				c.close();
			}
		} 
		catch (SQLException e) {
			logger.error(e.getMessage());
		}
	}
	
	private void doClose(Statement s) {		
		try {
			if (s != null) {
				s.close();
			}
		} 
		catch (SQLException e) {
			logger.error(e.getMessage());
		}
	}
}

Suggested fix:
Real fix would be the one that allowed user code to keep connection alive and execute rollback.

The patch proposed here just closes the connection forcibly:

diff --git a/src/com/mysql/jdbc/ConnectionImpl.java b/src/com/mysql/jdbc/ConnectionImpl.java
index 6278022..27b8deb 100644
--- a/src/com/mysql/jdbc/ConnectionImpl.java
+++ b/src/com/mysql/jdbc/ConnectionImpl.java
@@ -2667,7 +2667,18 @@ public class ConnectionImpl extends ConnectionPropertiesImpl implements
 			sqlEx.initCause(ex);
 			
 			throw sqlEx;
-		} finally {
+		} catch (OutOfMemoryError ex) {  
+			cleanup(ex);
+			
+			SQLException sqlEx = SQLError.createSQLException(
+					Messages.getString("Connection.OutOfMemoryError"),
+					SQLError.SQL_STATE_COMMUNICATION_LINK_FAILURE, getExceptionInterceptor());
+			
+			sqlEx.initCause(ex);
+			
+			throw sqlEx;
+		}
+		finally {
 			if (getMaintainTimeStats()) {
 				this.lastQueryFinishedTime = System.currentTimeMillis();
 			}
diff --git a/src/com/mysql/jdbc/LocalizedErrorMessages.properties b/src/com/mysql/jdbc/LocalizedErrorMessages.properties
index eb74f8d..c0d4ff3 100644
--- a/src/com/mysql/jdbc/LocalizedErrorMessages.properties
+++ b/src/com/mysql/jdbc/LocalizedErrorMessages.properties
@@ -433,6 +433,7 @@ Connection.UnableToConnectWithRetries=Could not create connection to database se
 Attempted reconnect {0} times. Giving up.
 Connection.UnexpectedException=Unexpected exception encountered during query.
 Connection.UnhandledExceptionDuringShutdown=Unexpected exception during server shutdown.
+Connection.OutOfMemoryError=Connection closed forcibly due to memory allocation failure. Transaction resolution unknown.
 
 #
 # ConnectionProperty Categories
[26 Jan 2012 19:21] Sveta Smirnova
Thank you for the report.

Problem is repeatable for me with 1 exception: connection still exists:

[2012-01-26 21:15:01,084] INFO   681[main] - bug63947.run(bug63947.java:62) - connection closed ? false
  [2012-01-26 21:15:01,085] INFO   682[main] - bug63947.run(bug63947.java:64) - rolling back...
...
*************************** 13. row ***************************
  event_time: 2012-01-26 21:15:00
   user_host: root[root] @ localhost [127.0.0.1]
   thread_id: 3
   server_id: 1
command_type: Query
    argument: select * from test r1, test r2, test r3, test r4, test r5, test r6, test r7, test r8, test r9, test r10, test r11, test r12, test r13, test r14, test r15
*************************** 14. row ***************************
  event_time: 2012-01-26 21:15:01
   user_host: root[root] @ localhost [127.0.0.1]
   thread_id: 3
   server_id: 1
command_type: Query
    argument: rollback

So it is surely not closed and c/J confirms it.

Please turn on general query log and check if this is your case too.
[26 Jan 2012 20:36] Topi Nieminen
I'm curious to know how the connection goes through cleanup() and stays open in your case.

Which location does OutOfMemoryError originate from in your test case?

According my test program output connection is closed:

2012-01-26 22:19:59,896 [main] INFO  test.TestOOM - -- select * from test r1, test r2, test r3, test r4, test r5, test r6, test r7, test r8, test r9, test r10, test r11, test r12, test r13, test r14, test r15
2012-01-26 22:19:59,896 [main] INFO  test.TestOOM - executing query...
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Connection closed forcibly due to memory allocation failure. Transaction resolution unknown.
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
	at com.mysql.jdbc.Util.getInstance(Util.java:386)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1013)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:987)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:982)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:927)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2674)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2568)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:842)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:681)
	at test.TestOOM.execute(TestOOM.java:94)
	at test.TestOOM.run(TestOOM.java:69)
	at test.TestOOM.main(TestOOM.java:20)
Caused by: java.lang.OutOfMemoryError: Java heap space
	at com.mysql.jdbc.MysqlIO.nextRowFast(MysqlIO.java:1657)
	at com.mysql.jdbc.MysqlIO.nextRow(MysqlIO.java:1434)
	at com.mysql.jdbc.MysqlIO.readSingleRowSet(MysqlIO.java:2936)
	at com.mysql.jdbc.MysqlIO.getResultSet(MysqlIO.java:477)
	at com.mysql.jdbc.MysqlIO.readResultsForQueryOrUpdate(MysqlIO.java:2631)
	at com.mysql.jdbc.MysqlIO.readAllResults(MysqlIO.java:1800)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2221)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2618)
	... 6 more
2012-01-26 22:20:00,460 [main] INFO  test.TestOOM - connection closed ? true
2012-01-26 22:20:00,460 [main] INFO  test.TestOOM - rolling back...
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
	at com.mysql.jdbc.Util.getInstance(Util.java:386)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1013)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:987)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:982)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:927)
	at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1205)
	at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1197)
	at com.mysql.jdbc.ConnectionImpl.rollback(ConnectionImpl.java:4705)
	at test.TestOOM.run(TestOOM.java:74)
	at test.TestOOM.main(TestOOM.java:20)
Caused by: java.lang.OutOfMemoryError: Java heap space
	at com.mysql.jdbc.MysqlIO.nextRowFast(MysqlIO.java:1657)
	at com.mysql.jdbc.MysqlIO.nextRow(MysqlIO.java:1434)
	at com.mysql.jdbc.MysqlIO.readSingleRowSet(MysqlIO.java:2936)
	at com.mysql.jdbc.MysqlIO.getResultSet(MysqlIO.java:477)
	at com.mysql.jdbc.MysqlIO.readResultsForQueryOrUpdate(MysqlIO.java:2631)
	at com.mysql.jdbc.MysqlIO.readAllResults(MysqlIO.java:1800)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2221)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2618)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2568)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:842)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:681)
	at test.TestOOM.execute(TestOOM.java:94)
	at test.TestOOM.run(TestOOM.java:69)
	... 1 more
[28 Jan 2012 0:40] Sveta Smirnova
Thank you for the feedback.

Verified as described.

Error in my environment:

There was 1 error:
1) testBug63947(bug63947)com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 120 milliseconds ago.  The last packet sent successfully to the server was 120 milliseconds ago.
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
	at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:1116)
	at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3102)
	at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2991)
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3532)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2002)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2163)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2618)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2568)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:842)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:681)
	at bug63947.testBug63947(bug63947.java:17)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at bug63947.main(bug63947.java:12)
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.read(Unknown Source)
	at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:114)
	at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:161)
	at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:189)
	at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:2549)
	at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3002)
	... 26 more

This is from test case made for c/J testsuite. Your code printed data similar to yours
[28 Jan 2012 0:40] Sveta Smirnova
test case for c/J testsuite

Attachment: bug63947.java (text/x-java), 889 bytes.

[28 Jan 2012 1:13] Mark Matthews
Given how Java handles OOM, there's not a whole lot that can be done when this condition happens. We can maybe have the driver try to forcibly close the socket, which will cause MySQL to rollback any pending transaction, but there is no guarantee that this can happen in a normal way because any allocation of memory required to close the socket may fail (because the VM is out of memory), and it may fail in code that is not under the driver's control (i.e. java.net, java.lang). If anything the solution proposed here should *not* throw SQLException, it should not try and allocate any memory, and it should re-throw the OOM error as errors should be handled differently than checked exceptions.

Java applications should generally treat OOM as a fatal error, and terminate the JVM, because no *known* progress can be made once this condition happens.
[30 Jan 2012 8:36] Tonci Grgin
Thank you Sveta and Mark.
[12 Mar 2014 13:21] Alexander Soklakov
Marked as Won't fix until we realize that something could be done.
[19 Mar 2014 14:10] Alexander Soklakov
It's decided to leave it in Verified state.