mssql-jdbc: Assertion fails when canceling "insert into" statement

Driver version

9.4.1, 10.2.1

SQL Server version

Microsoft SQL Server 2017 (RTM-GDR) (KB4505224) - 14.0.2027.2 (X64) Jun 15 2019 00:26:19 Copyright © 2017 Microsoft Corporation Developer Edition (64-bit) on Windows 10 Enterprise 2016 LTSB 10.0 <X64> (Build 14393: ) (Hypervisor)

Client Operating System

OS Name Microsoft Windows 10 Enterprise 2016 LTSB Version 10.0.14393 Build 14393 System Type x64-based PC

JAVA/JVM version

Eclipse Adoptium jdk-17.0.1.12-hotspot

Table schema

create table test_table (column_name bit)

Problem description

I insert into a table and cancel the statement while it is executing its batch via a concurrent thread.

With Java assertions enabled the thread executing the statement fails frequently with an AssertionError.

Expected behavior

The thread executing the statement should fail with an exception like this:

com.microsoft.sqlserver.jdbc.SQLServerException: The query was canceled.
	at com.microsoft.sqlserver.jdbc.TDSCommand.checkForInterrupt(IOBuffer.java:7605)
	at com.microsoft.sqlserver.jdbc.TDSWriter.writePacket(IOBuffer.java:4219)
	at com.microsoft.sqlserver.jdbc.TDSWriter.endMessage(IOBuffer.java:3269)
	at com.microsoft.sqlserver.jdbc.TDSCommand.startResponse(IOBuffer.java:7779)
	at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatementBatch(SQLServerPreparedStatement.java:2816)
	at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtBatchExecCmd.doExecute(SQLServerPreparedStatement.java:2694)
	at com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:7417)
	at com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:3488)
	at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:262)
	at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:237)
	at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.executeBatch(SQLServerPreparedStatement.java:2092)
	at SqlServerTest.test_cancelStatementWhileInsertInto(SqlServerTest.java:123)
	...

Actual behavior

I frequently get

java.lang.AssertionError
	at com.microsoft.sqlserver.jdbc.TDSCommand.onAttentionAck(IOBuffer.java:7716)
	at com.microsoft.sqlserver.jdbc.StreamDone.setFromTDS(StreamDone.java:159)
	at com.microsoft.sqlserver.jdbc.SQLServerStatement$1NextResult.onDone(SQLServerStatement.java:1472)
	at com.microsoft.sqlserver.jdbc.TDSParser.parse(tdsparser.java:96)
	at com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1658)
	at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatementBatch(SQLServerPreparedStatement.java:2830)
	at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtBatchExecCmd.doExecute(SQLServerPreparedStatement.java:2694)
	at com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:7417)
	at com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:3488)
	at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:262)
	at com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:237)
	at com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.executeBatch(SQLServerPreparedStatement.java:2092)
	at SqlServerTest.test_cancelStatementWhileInsertInto(SqlServerTest.java:123)
	...

Error message/stack trace

See above.

Any other details that can be helpful

Test to reproduce the issue.

import static org.junit.Assert.assertEquals;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.junit.Test;

public class SqlServerTest {

	/**
	 * Repeat a string a given number of times.
	 * 
	 * @param string
	 *            The string
	 * 
	 * @param count
	 *            Number of times to repeat {@code string}
	 * 
	 * @return The repeated string
	 */
	private static String repeat(String string, int count) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < count; i++) {
			sb.append(string);
		}
		return sb.toString();
	}

	/**
	 * Cancel a {@link Statement} after some delay via a concurrent thread.
	 * 
	 * @param statement
	 *            The statement to cancel
	 * 
	 * @param delayInMilliseconds
	 *            Delay to wait until canceling the statement in milliseconds
	 * 
	 * @return The thread used to cancel the statement.
	 * 
	 *             <p>
	 *             The method does not wait until the statement got canceled.
	 *             Use {@link Thread#join()} to wait until the concurrent thread has called {@link Statement#cancel()}.
	 *             </p>
	 */
	private static Thread cancelAsync(Statement statement, long delayInMilliseconds) {
		Thread thread = new Thread(() -> {
			try {
				Thread.sleep(delayInMilliseconds);
				statement.cancel();
			} catch (SQLException | InterruptedException e) {
				// does not/must not happen
				e.printStackTrace();
				throw new IllegalStateException(e);
			}
		});
		thread.start();
		return thread;
	}

	public static void main(String[] args) throws Exception {
		new SqlServerTest().test_cancelStatementWhileInsertInto();
	}

	@Test
	public void test_cancelStatementWhileInsertInto() throws Exception {
		String database = "Test";
		String user = "test";
		String password = "secret";

		Level level = Level.FINE;
		Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc");
		for (Handler handler : logger.getParent().getHandlers()) {
			if (handler instanceof ConsoleHandler consoleHandler) {
				consoleHandler.setLevel(level);
			}
		}
		logger.setLevel(level);

		try (Connection connection =
				DriverManager.getConnection(
						"jdbc:sqlserver://localhost:1433;encrypt=true;trustServerCertificate=true;databaseName=" + database,
						user,
						password)) {
			connection.setAutoCommit(false);

			try (PreparedStatement statement = connection.prepareStatement(
					"if object_id('test_table') is not null drop table test_table")) {
				statement.execute();
			}
			connection.commit();

			try (PreparedStatement statement = connection.prepareStatement(
					"create table test_table (column_name bit)")) {
				statement.execute();
			}
			connection.commit();

			int numberOfCommands = 4;
			int parameterCount = 512;
			try (PreparedStatement statement = connection.prepareStatement(
					"insert into test_table values (?)" + repeat(",(?)", parameterCount - 1))) {

				for (int i = 0; i < numberOfCommands; i++) {
					for (int j = 0; j < parameterCount; j++) {
						statement.setBoolean(j + 1, true);
					}
					statement.addBatch();
				}

				// execute the statement but cancel it after about 20 milliseconds -
				// you may need to try which delay works for you to reproduce the issue
				long delayInMilliseconds = 20;
				Thread cancelThread = cancelAsync(statement, delayInMilliseconds);
				boolean success = false;
				try {
					statement.executeBatch();
					success = true;
				} catch (SQLException e) {
					// this is what should happen
					assertEquals("The query was canceled.", e.getMessage());
				} catch (AssertionError e) {
					// unexpected but happens frequently if Java assertions are enabled
					throw e;
				}
				assertEquals("The statement completed without interruption. This may happen if the chosen delay is too long.", false, success);
				cancelThread.join();
			}
			connection.commit();
		}
	}
}

JDBC trace logs

With log level FINE enabled. Please note that I have shortend the output regarding the parameter list by using ellipsis.

Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.Util parseUrl
FINE: Property:serverName Value:localhost
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.Util parseUrl
FINE: Property:portNumber Value:1433
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.Util parseUrl
FINE: Property:encrypt Value:true
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.Util parseUrl
FINE: Property:trustServerCertificate Value:true
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.Util parseUrl
FINE: Property:databaseName Value:Test
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.SQLServerConnection <init>
FINE: ConnectionID:1 created by (SQLServerDriver:1)
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.SQLServerConnection login
FINE: ConnectionID:1 This attempt server name: localhost port: 1433 InstanceName: null useParallel: false
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.SQLServerConnection login
FINE: ConnectionID:1 This attempt endtime: 1655899063673
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.SQLServerConnection login
FINE: ConnectionID:1 This attempt No: 0
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.SQLServerConnection connectHelper
FINE: ConnectionID:1 Connecting with server: localhost port: 1433 Timeout slice: 1850 Timeout Full: 15
Jun 22, 2022 1:57:41 PM com.microsoft.sqlserver.jdbc.SQLServerConnection Prelogin
FINE: ConnectionID:1 ClientConnectionId: df4b14e5-8985-4487-a848-0c8553a60520 Server returned major version:14
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerStatement <init>
FINE: SQLServerPreparedStatement:1 created by (ConnectionID:1 ClientConnectionId: df4b14e5-8985-4487-a848-0c8553a60520)
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement buildExecSQLParams
FINE: SQLServerPreparedStatement:1: calling sp_executesql: SQL:if object_id('test_table') is not null drop table test_table
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerStatement <init>
FINE: SQLServerPreparedStatement:2 created by (ConnectionID:1 ClientConnectionId: df4b14e5-8985-4487-a848-0c8553a60520)
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement buildExecSQLParams
FINE: SQLServerPreparedStatement:2: calling sp_executesql: SQL:create table test_table (column_name bit)
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerStatement <init>
FINE: SQLServerPreparedStatement:3 created by (ConnectionID:1 ClientConnectionId: df4b14e5-8985-4487-a848-0c8553a60520)
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement buildExecSQLParams
FINE: SQLServerPreparedStatement:3: calling sp_executesql: SQL:insert into test_table values (@P0),...,(@P511)
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.TDSWriter sendAttention
FINE: TDSWriter@60dcc9fe (ConnectionID:1): sending attention...
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerException logException
FINE: *** SQLException: com.microsoft.sqlserver.jdbc.SQLServerException: The query was canceled. The query was canceled.
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerException logException
FINE: com.microsoft.sqlserver.jdbc.TDSCommand.checkForInterrupt(IOBuffer.java:7605)com.microsoft.sqlserver.jdbc.TDSParser.parse(tdsparser.java:95)com.microsoft.sqlserver.jdbc.SQLServerStatement.getNextResult(SQLServerStatement.java:1658)com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.doExecutePreparedStatementBatch(SQLServerPreparedStatement.java:2830)com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement$PrepStmtBatchExecCmd.doExecute(SQLServerPreparedStatement.java:2694)com.microsoft.sqlserver.jdbc.TDSCommand.execute(IOBuffer.java:7417)com.microsoft.sqlserver.jdbc.SQLServerConnection.executeCommand(SQLServerConnection.java:3488)com.microsoft.sqlserver.jdbc.SQLServerStatement.executeCommand(SQLServerStatement.java:262)com.microsoft.sqlserver.jdbc.SQLServerStatement.executeStatement(SQLServerStatement.java:237)com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement.executeBatch(SQLServerPreparedStatement.java:2092)SqlServerTest.test_cancelStatementWhileInsertInto(SqlServerTest.java:116)java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)java.base/java.lang.reflect.Method.invoke(Method.java:568)org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)org.junit.runners.ParentRunner.run(ParentRunner.java:413)org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:93)org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Jun 22, 2022 1:57:42 PM com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement buildPrepExecParams
FINE: SQLServerPreparedStatement:3: calling sp_prepexec: PreparedHandle:0, SQL:insert into test_table values (@P0),...,(@P511)

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 15 (7 by maintainers)

Most upvoted comments

Sure, @Jeffery-Wasty, this is the new issue: #1896.