Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Jdbi3-core - connection object can be stuck in a connection pool forever if connection was closed. #2446

Closed
kristoffSC opened this issue Jul 21, 2023 · 8 comments · Fixed by #2458

Comments

@kristoffSC
Copy link

kristoffSC commented Jul 21, 2023

Hi,
it seems that jdbi3-core might have a code path that can make broken connection objects stuck in the connection pool forever in "ALLOCATED" state. This can lead to max out the connection pool capacity. This is what most likely have happen in our system.

I have created a reproducer for this issue with detailed instructions - here

In Handle's constructor there is a check, that verifies if used connection for this handle is live. The Cleanable is added to the created Handle instance ONLY if connection is Live.

addCleanable(() -> {
            // shut down statement builder
            if (checkConnectionIsLive()) {
                statementBuilder.close(connection);
            }
        });

If the connection was closed or had any connection issue (client or server side) after it was borrowed from the pool and before this check, then no Cleanable will be registered making this connection object never be returned to the pool.

###### UPDATE ######
After deeper analysis it seems that problem is more complex.

The generalization is that if there will be an exception thrown anywhere in org.jdbi.v3.core.Jdbi::open() method, after the connection was acquired from pool, then the connection object will not be returned to the pool.

I would say that org.jdbi.v3.core.Jdbi::open() should have exception handling logic which will guaranty that connection object will be properly returned to the pool in case of exception. Currently it is not the case.

Specific use cases:

  1. The connection is closed from the client side, as described in this bug report.
    In this case it turns out that exception will be thrown by this line in Handle's constructor.
    this.transactionHandler = transactionHandler.specialize(this); so even before registering any Cleanable objects

In this case, since exception was thrown from Handle constructor, there is no Handle instance to close/clean at the end.

  1. The second use case is where the connection is closed from server side, for example Data Base restart.
    In our system we are using Jdbi's PostgresPlugin. In org.jdbi.v3.core.Jdbi::open() method, after Handle object is created there is this call:
for (JdbiPlugin p : plugins) {
        h = p.customizeHandle(h);
}

This will throw an exception for PostgresPlugin.
In result the open() method will not return any Handle reference so no Handle will be closed/clean up hence connection object will not be returned to the pool. The stack trace for this case -
JdbiTrace.txt

To reproduce this, please pull my repository (I added PostgresPlugin) and simply restart Postgresql container on a break point just after where connection object is acquired from the pool.

The PoC patch for this could be something like this:

public Handle open() {
    Connection conn = null;
    Handle h = null;
    try {
      final long start = System.nanoTime();
      conn = Objects.requireNonNull(connectionFactory.openConnection(),
          () -> "Connection factory " + connectionFactory + " returned a null connection");
      final long stop = System.nanoTime();

      for (JdbiPlugin p : plugins) {
        conn = p.customizeConnection(conn);
      }

      StatementBuilder cache = statementBuilderFactory.get().createStatementBuilder(conn);

      h = Handle.createHandle(this,
          connectionFactory.getCleanableFor(conn), // don't use conn::close, the cleanup must be done by the connection factory!
          transactionhandler.get(),
          cache,
          conn);

      for (JdbiPlugin p : plugins) {
        h = p.customizeHandle(h);
      }
      LOG.trace("Jdbi [{}] obtain handle [{}] in {}ms", this, h, MILLISECONDS.convert(stop - start, NANOSECONDS));
      return h;
    } catch (Exception e) {
     // NEW EXCEPTION HANDLING TO GUARANTEE THAT CONNECTION OBJECT IS WILL BE RETURNED TO THE POOL.
      if (h != null) {
        // close Handle if we have one in case of Exception, connection will be clean up by this.
        h.close();
      } else if (conn != null) {
        try {
         // clean up a connection if we have one since exception must have been thrown before Handle instance was created
          this.connectionFactory.closeConnection(conn);
        } catch (SQLException ex) {
          throw new ConnectionException(e);
        }
      } if (e instanceof SQLException) { // not sure about this part, whatever Exception type we would like to have here
        throw new ConnectionException(e);
      }
      throw new RuntimeException(e);
    }
  }
@kristoffSC kristoffSC changed the title Jdbi3-core - connection object can be stuck in a connection pool forever. [Bug] Jdbi3-core - connection object can be stuck in a connection pool forever. Jul 21, 2023
@kristoffSC kristoffSC changed the title [Bug] Jdbi3-core - connection object can be stuck in a connection pool forever. [Bug] Jdbi3-core - connection object can be stuck in a connection pool forever if connection was closed. Jul 21, 2023
@stevenschlansker
Copy link
Member

Thank you for the very detailed bug report. We'll take a look at this as soon as we can.

stevenschlansker added a commit that referenced this issue Aug 3, 2023
Right now, we check if a connection is closed before closing it.

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
stevenschlansker added a commit that referenced this issue Aug 3, 2023
Right now, we check if a connection is closed before closing it.

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
@stevenschlansker
Copy link
Member

Hi @kristoffSC , I have proposed a change that closes the connection without first checking to see if it is already closed in #2451. Could you see if this fixes the problem for you?

@kristoffSC
Copy link
Author

Hi @kristoffSC , I have proposed a change that closes the connection without first checking to see if it is already closed in #2451. Could you see if this fixes the problem for you?

Hi Thanks for looking at this issue.
I will re-test it as soon as I can. I'm on vac to the end of this week with limited access to the Internet.

@kristoffSC
Copy link
Author

kristoffSC commented Aug 7, 2023

Hi @stevenschlansker
I've test your patch and it seems that issue is still there. I did further analysis and it seems that problem is more complex.

The generalization is that if there will be an exception thrown anywhere in org.jdbi.v3.core.Jdbi::open() method, after the connection was acquired from pool, then the connection object will not be returned to the pool.

I would say that org.jdbi.v3.core.Jdbi::open() should have exception handling logic which will guaranty that connection object will be properly returned to the pool in case of exception. Currently it is not the case.

Specific use cases:

  1. The connection is closed from the client side, as described in this bug report.
    In this case it turns out that exception will be thrown by this line in Handle's constructor.
    this.transactionHandler = transactionHandler.specialize(this); so even before registering any Cleanable objects

In this case, since exception was thrown from Handle constructor, there is no Handle instance to close/clean at the end.

  1. The second use case is where the connection is closed from server side, for example Data Base restart.
    In our system we are using Jdbi's PostgresPlugin. In org.jdbi.v3.core.Jdbi::open() method, after Handle object is created there is this call:
for (JdbiPlugin p : plugins) {
        h = p.customizeHandle(h);
}

This will throw an exception for PostgresPlugin.
In result the open() method will not return any Handle reference so no Handle will be closed/clean up hence connection object will not be returned to the pool. The stack trace for this case -
JdbiTrace.txt

To reproduce this, please pull my repository (I added PostgresPlugin) and simply restart Postgresql container on a break point just after where connection object is acquired from the pool.

The PoC patch for this could be something like this:

public Handle open() {
    Connection conn = null;
    Handle h = null;
    try {
      final long start = System.nanoTime();
      conn = Objects.requireNonNull(connectionFactory.openConnection(),
          () -> "Connection factory " + connectionFactory + " returned a null connection");
      final long stop = System.nanoTime();

      for (JdbiPlugin p : plugins) {
        conn = p.customizeConnection(conn);
      }

      StatementBuilder cache = statementBuilderFactory.get().createStatementBuilder(conn);

      h = Handle.createHandle(this,
          connectionFactory.getCleanableFor(conn), // don't use conn::close, the cleanup must be done by the connection factory!
          transactionhandler.get(),
          cache,
          conn);

      for (JdbiPlugin p : plugins) {
        h = p.customizeHandle(h);
      }
      LOG.trace("Jdbi [{}] obtain handle [{}] in {}ms", this, h, MILLISECONDS.convert(stop - start, NANOSECONDS));
      return h;
    } catch (Exception e) {
     // NEW EXCEPTION HANDLING TO GUARANTEE THAT CONNECTION OBJECT WILL BE RETURNED TO THE POOL.
      if (h != null) {
        // close Handle if we have one in case of Exception, connection will be clean up by this.
        h.close();
      } else if (conn != null) {
        try {
         // clean up a connection if we have one since exception must have been thrown before Handle instance was created
          this.connectionFactory.closeConnection(conn);
        } catch (SQLException ex) {
          throw new ConnectionException(e);
        }
      } if (e instanceof SQLException) { // not sure about this part, whatever Exception type we would like to have here
        throw new ConnectionException(e);
      }
      throw new RuntimeException(e);
    }
  }

stevenschlansker added a commit that referenced this issue Aug 7, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
stevenschlansker added a commit that referenced this issue Aug 7, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
@stevenschlansker
Copy link
Member

OK, I agree if a customizeHandle call or other point throws, we have to close resources. I updated the PR. Would you mind trying again @kristoffSC ?

stevenschlansker added a commit that referenced this issue Aug 7, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
stevenschlansker added a commit that referenced this issue Aug 7, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
hgschmie pushed a commit that referenced this issue Aug 8, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
stevenschlansker added a commit that referenced this issue Aug 8, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
stevenschlansker added a commit that referenced this issue Aug 8, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
stevenschlansker added a commit that referenced this issue Aug 8, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes #2446
hgschmie added a commit to hgschmie/jdbi that referenced this issue Aug 8, 2023
If the c'tor of a handle throws an exception, ensure that the underlying
connection is closed as the caller will not have a chance to do so with
try-with-resources

This fixes the cause of jdbi#2446
hgschmie added a commit to hgschmie/jdbi that referenced this issue Aug 8, 2023
If the c'tor of a handle throws an exception, ensure that the underlying
connection is closed as the caller will not have a chance to do so with
try-with-resources

This fixes the cause of jdbi#2446
hgschmie added a commit to hgschmie/jdbi that referenced this issue Aug 8, 2023
If the c'tor of a handle throws an exception, ensure that the underlying
connection is closed as the caller will not have a chance to do so with
try-with-resources

This fixes the cause of jdbi#2446
hgschmie added a commit to hgschmie/jdbi that referenced this issue Aug 8, 2023
If the c'tor of a handle throws an exception, ensure that the underlying
connection is closed as the caller will not have a chance to do so with
try-with-resources

This fixes the cause of jdbi#2446
hgschmie pushed a commit to hgschmie/jdbi that referenced this issue Aug 8, 2023
Right now, we check if a connection is closed before closing it. Also, if a plugin
fails to customize a Handle, the connection can leak

However, this isn't good enough: even if the server closed the connection,
we might have borrowed this from a pool, which needs to know we've returned it.

Connection.close is specified to no-op if called more than once, so it's safe to just do it regardless.

Fixes jdbi#2446
hgschmie added a commit to hgschmie/jdbi that referenced this issue Aug 8, 2023
If the c'tor of a handle throws an exception, ensure that the underlying
connection is closed as the caller will not have a chance to do so with
try-with-resources

This fixes the cause of jdbi#2446
@kristoffSC
Copy link
Author

The issue has been fixed by #2458

Tested using reproducer from this bug report.
Thanks.

@hgschmie
Copy link
Contributor

I cut a 3.41.0-rc1 release which should hit central soon. As this bug fix is a major part of this release, please test with the 3.41.0-rc1 and lmk if that works for you.

@kristoffSC
Copy link
Author

kristoffSC commented Aug 14, 2023

I cut a 3.41.0-rc1 release which should hit central soon. As this bug fix is a major part of this release, please test with the 3.41.0-rc1 and lmk if that works for you.

Hi,
I've check with 3.41.0-rc1, looking good, thx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
3 participants