Bug #113413 Connection.changeUser cannot be done after DriverManager.loginTimeout elapses.
Submitted: 14 Dec 2023 6:01 Modified: 15 Dec 2023 5:18
Reporter: kazuhisa kawashima (OCA) Email Updates:
Status: Verified Impact on me:
None 
Category:Connector / J Severity:S3 (Non-critical)
Version:8.0.33, 8.1.0 OS:Any
Assigned to: CPU Architecture:Any
Tags: Contribution

[14 Dec 2023 6:01] kazuhisa kawashima
Description:
If Connection.changeUser is called after DriverManager.loginTimeout has elapsed since the connection was started, the following error occurs and changeUser cannot be performed.

We want to use one connection with switching users to limit the number of connections.

## Error
Exception in thread "main" com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
	at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
	at com.mysql.cj.jdbc.ConnectionImpl.changeUser(ConnectionImpl.java:561)
	at test.TestMainKt.main(TestMain.kt:16)
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
	at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:150)
	at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:166)
	at com.mysql.cj.protocol.a.NativeProtocol.afterHandshake(NativeProtocol.java:451)
	at com.mysql.cj.protocol.a.NativeAuthenticationProvider.proceedHandshakeWithPluggableAuthentication(NativeAuthenticationProvider.java:535)
	at com.mysql.cj.protocol.a.NativeAuthenticationProvider.changeUser(NativeAuthenticationProvider.java:614)
	at com.mysql.cj.protocol.a.NativeProtocol.changeUser(NativeProtocol.java:1385)
	at com.mysql.cj.CoreSession.changeUser(CoreSession.java:103)
	at com.mysql.cj.jdbc.ConnectionImpl.changeUser(ConnectionImpl.java:544)
	... 1 more
Caused by: java.net.SocketException: Connection attempt exceeded defined timeout.
	at com.mysql.cj.protocol.StandardSocketFactory.resetLoginTimeCountdown(StandardSocketFactory.java:212)
	at com.mysql.cj.protocol.StandardSocketFactory.afterHandshake(StandardSocketFactory.java:197)
	at com.mysql.cj.protocol.a.NativeProtocol.afterHandshake(NativeProtocol.java:449)
	... 6 more

How to repeat:
1. Set the loginTimeout of DriverManager.
DriverManager.setLoginTimeout(3) // Set to 3 seconds

2. Establish a connection.
val conn = DriverManager.getConnection(jdbcUrl, "test_user", "")

3. Wait until the loginTimeout of DriverManager is reached.

4. Call Connection.changeUser.
val mysqlConn = conn.unwrap(com.mysql.cj.jdbc.JdbcConnection::class.java)
mysqlConn.changeUser("test_user", "")

You can reproduce it with the following code.

---
import java.sql.Connection;
import java.sql.DriverManager;
import java.time.LocalTime;

public class LoginTimeoutIssue {

    public static void main(String[] args) throws Exception {
        String url = "jdbc:mysql://localhost:3306/test";

        DriverManager.setLoginTimeout(3);

        LocalTime limit = LocalTime.now().plusSeconds(10);
        Connection conn = DriverManager.getConnection(url, "test_user", "");
        com.mysql.cj.jdbc.JdbcConnection mysqlConn = conn.unwrap(com.mysql.cj.jdbc.JdbcConnection.class);
        while (LocalTime.now().isBefore(limit)) {
            mysqlConn.changeUser("test_user", "");
            System.out.println("changeUser success");
            Thread.sleep(1000);
        }
        conn.close();
    }
}
---

Suggested fix:
MySQL is checking the elapsed time since the initialization of the connection in StandardSocketFactory.afterHandshake.
Although afterHandshake is also executed during the changeUser calling, I believe it is unnecessary at that time.
[14 Dec 2023 6:04] kazuhisa kawashima
With this patch, the problem will no longer occur.
However, it is not a complete fix as we do not have an environment that passes the test.
[14 Dec 2023 7:08] MySQL Verification Team
Hello kazuhisa San,

Thank you for the report contribution.
Verified as described.

regards,
Umesh
[14 Dec 2023 7:15] MySQL Verification Team
Hello kazuhisa San,

Please upload the patch via "contribution" tab of this bug page, otherwise we will not be able to use it. Thank you.

regards,
Umesh
[15 Dec 2023 5:18] kazuhisa kawashima
Sorry, I attached the patch.
However, it is not a complete fix as we do not have an environment that passes the test.