Skip to content

Erroneously inserted rows with statement pooling enabled and when a Connection gets re-used from the ConnectionPool #2356

@PriyadharshiniP

Description

@PriyadharshiniP

Driver version

com.microsoft.sqlserver mssql-jdbc 12.6.1.jre11

SQL Server version

Microsoft SQL Server 2019 (RTM-CU18) (KB5017593) - 15.0.4261.1 (X64) 
	Sep 12 2022 15:07:06 
	Copyright (C) 2019 Microsoft Corporation
	Developer Edition (64-bit) on Windows 10 Pro 10.0 <X64> (Build 19045: )

Client Operating System

Windows 10 Pro

JAVA/JVM version

Java 17

Problem description

There are some duplicate inserts and some missing entries when a custom connection pool is being utilized and server side statement pooling is enabled (using the setDisableStatementPooling flag). When the connection is re-used from the connection pool and the flag setDisableStatementPooling is set to false this issue seems to happen.

To test this instance, I was able to simulate a very basic stand alone as below :
Please see the following test program, where I intentionally return the previously created connection object from the _userToConnectionsMap cache map for subsequent runs.

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.PooledConnection;

public class SQLConnectionPool {

	 final private ReentrantLock _connLock = new ReentrantLock();
	 final private Map _userToConnectionsMap = new ConcurrentHashMap();
	 static String tableName = "batchTest";
	/**
	 * Crate a new connection
	 * 
	 * @return the new created connection
	 * @throws SQLException When fail to create a new connection.
	 */
	private Connection createNewConnection(final String username, final String password) throws SQLException {

		com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource msDs = new com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource();
		msDs.setUser(username);
		msDs.setApplicationName("SQL Server Duplicate Row");
		msDs.setPassword(password);
		msDs.setServerName("<>");
		msDs.setPortNumber(0);
		msDs.setDatabaseName("<>");
		msDs.setInstanceName("<>");
		msDs.setEncrypt("false");
		msDs.setDisableStatementPooling(false);
		msDs.setStatementPoolingCacheSize(20);
		msDs.setServerPreparedStatementDiscardThreshold(20);
		msDs.setSendTemporalDataTypesAsStringForBulkCopy(false);
		// ... create the ACTUAL new connection...
		PooledConnection actual = msDs.getPooledConnection(username, password);
		// ... cache it...
		Collection conns = (Collection) _userToConnectionsMap.get(username);
		if (conns == null || conns.isEmpty()) {
			conns = new CopyOnWriteArrayList();
			_userToConnectionsMap.put(username, conns);
		}
		
		conns.add(actual);
		return actual.getConnection();
	}

	final public Connection getConnection(final String username, final String password)
			throws SQLException, InterruptedException {

		if (username == null) {
			throw new NullPointerException("The username cannot be null.");
		}
		if (password == null) {
			throw new NullPointerException("The password cannot be null.");
		}

		// Acquire a lock...
		_connLock.lock();
		// Declare the connection to be returned...
		Connection conn = null;
		try {

			conn = getAvailableConnection(username);
			// If none found...
			if (conn == null) {
				conn = createNewConnection(username, password);
			}
//	           
		} finally {
			// Ensure that the lock is released...
			_connLock.unlock();
		}
		// Return the connection...
		return conn;
	}

	private Connection getAvailableConnection(final String username) throws SQLException {
		// Get the connections for the specified user...
		Collection conns = (Collection) _userToConnectionsMap.get(username);
		// If none found, then NO connections are available...
		if (conns == null || conns.isEmpty()) {
			return null;
		}
		// ... otherwise, check to see if any of the connections are available...
		boolean isAvailable = false;
		final Iterator iterator = conns.iterator();
		PooledConnection pconn = null;
		while (iterator.hasNext()) {			
			pconn = (PooledConnection) iterator.next();

			return pconn.getConnection();
			// Is this connection available? ignore this check for this sample test
			
		}
		return null;
	}

	public static void main(String[] args) throws SQLException, ClassNotFoundException, InterruptedException {

		Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.Statement");
		logger.setLevel(Level.FINER);
		Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

		SQLConnectionPool pool = new SQLConnectionPool();

		try {
		
			Connection conn = pool.getConnection("<>", "<>");
			try (Statement stmt = conn.createStatement();) {

				String dropSql = "if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[" + tableName
						+ "]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) DROP TABLE [" + tableName + "]";
				stmt.execute(dropSql);

				String createSql = "create table " + tableName + " (c1 int, c2 varchar(20))";
				stmt.execute(createSql);
			}
			for (int j = 0; j < 5; j++) {

				conn = pool.getConnection("<>", "<>");
				conn.setAutoCommit(false);
				try (PreparedStatement pstmt = conn.prepareStatement("insert into " + tableName + " values (?, ?)");) {
					for (int i = 0; i < 3; i++) {
						pstmt.setInt(1, i);
						pstmt.setString(2, "test" + i);
						pstmt.addBatch();
					}

					long start = System.currentTimeMillis();
					pstmt.executeBatch();
					conn.commit();
					long end = System.currentTimeMillis();

					System.out.println("Finished. Time taken : " + (end - start) + " milliseconds.");
				}
				conn.close();
			}
		} finally {
			// nothing for this test
		}

	}
}

This program results in incorrect entries being inserted to the test table with last item of the batch being repeated and the first item of the batch missed out.

Expected behavior

Caching and connection pool shouldn't lead to incorrect insert to the table. Should see no duplicates and no missing rows.

Actual behavior

At the end of the stand alone program run, could see there are (2*j) - 1 = 9 rows instead of 5 rows for column c1 : 2 and (j-1) = 4 entries missing for column c1 : 0

image

Error message/stack trace

With FINER logging enabled for SQL driver , could see the following exception : SQLException: com.microsoft.sqlserver.jdbc.SQLServerException: Could not find prepared statement with handle

image

Any other details that can be helpful

This issue does not happen when I comment the call to sp_reset_connection in SQLServerPooledConnection.java
image

JDBC trace logs

SQL Profiler data

image

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in the driver. A high priority item that one can expect to be addressed quickly.

    Type

    No type

    Projects

    Status

    Closed Issues

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions