Bug #113413 Connection.changeUser cannot be done after DriverManager.loginTimeout elapses.
Submitted: 14 Dec 2023 6:01 Modified: 14 Jan 21:12
Reporter: kazuhisa kawashima (OCA) Email Updates:
Status: Closed 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.
[14 Jan 21:12] Edward Gilmore
Posted by developer:
 
 Added the following note to the MySQL Connector/J 9.6.0 release notes:		
 
The login timeout set with DriverManager.setLoginTimeout()
        incorrectly affected JdbcConnection.changeUser() calls. The
        handling of login timeouts was refactored to apply only during
        initial authentication, not when changing users on an existing
        connection. Post-authentication code was moved higher in the
        call stack to ensure timeouts are only considered during the
        initial login, addressing the improper timeout behavior
        changeUser() calls.