Styles

Friday, October 31, 2025

Spring @Transactional causing database connections to remain open from HTTP calls

We faced an interesting issue at Tyro this week. 

There was a sudden spike in errors in our customer application.

The errors were related to the following exception:

JDBCConnectionException: Unable to acquire JDBC Connection

We could so easily conclude that a database is either down, or the driver has failed to connect.

The application had no code changes or database updates and there were no network problems and yet inbound HTTP calls made to the customer application were hanging for more than 20 seconds.

After digging deeper into the problem we discovered that there was no database issue at all.

Coincidentally another completely separate API server was having its own issues with the CPU maxing at 100%.

We initially ignored this as we were having "database issues" rather than "HTTP endpoint issues".

After further discovery, we found that the customer application was indeed calling endpoints to the API server, and although we received a couple of errors relating to I/O issues when that API server was rebooted, it didn't concern us that it would be the cause of any of the database connection issues we were encountering.

We had a look at the code and found the following:

@Transactional(readOnly = true)
public CustomerData getCustomerData(String id) {
    // Run a SELECT query against the DB
    Customer customer = customerRepository.findById(Id);

    // Run a SELECT query against the DB
    CustomerDetails details = customerDetailsRepository.findById(Id);

    // Call the API server to retrieve other information
    CustomerAddress address = apiClient.getAddressByCustomerId(mids)

    // Run a SELECT query against the DB
    Location location = locationRepository.findByCustomerId(Id);
    
    return new CustomerData(
        customer,
        details,
        address,
        location
    );
}

At first glance this seems vaild as any of the calls to the repositories run relevant queries in the database and return results, then seemingly closes the database connection and moves onto the next line of code.

The key factor here however, is the @Transactional annotation.

ChatGPT states:

If your method is inside an open Spring @Transactional and you've already touched the database, a hanging HTTP call can keep a JDBC connection checked out of the Hikari pool until the transaction ends. That can starve the pool and cause timeouts for other requests.

In a Spring @Transactional, the persistence context is created and a JDBC connection is obtained the first time you hit the DB.

Once obtained, that same connection is held for the entire transaction until commit/rollback (i.e. the function completes its invocation and is returned).

If your code then makes an external HTTP call that hangs before the transaction completes, the connection remains checked out, which means Hikari pool pressure and a SQLTransientConnectionException (pool exhausted) could happen for other threads.

If the HTTP call happens before any DB interaction and nothing triggers a flush, the connection might not yet be acquired so although the code may hang, there may not be any Hikari pool impact.

If the remote server spikes at 100% CPU usage and stops responding, this does not return an error response to the calling HttpClient.

The client opens a TCP connection to the target server and sends the request bytes, then waits for the server’s response (status line, headers, then body).

If the server’s CPU is fully utilized, it may accept the TCP connection but never process the request, which in turn does not return any response bytes, which means the socket remains open but idle.

The client would be waiting for data and does not technically fail, so the client thread can sit blocked indefinitely unless you define limits.

By default, most HTTP libraries do not time out idle sockets unless you configure explicit timeouts.

This fact helped us understand one simple rule of thumb:

Never make HTTP calls within @Transactional contexts to avoid keeping unnecessary database connections open which can starve your connection pool with unrelated HTTP issues. 




Tuesday, June 18, 2024

Hibernate JPA mapping using @Query done right

Hibernate is a very powerful ORM. Its been around for a while, but sometimes the magic under the hood can leave you for hours scratching your head trying to figure out why a perfectly normal sql query isn't mapping correctly to your POJO.

Consider the following query.

@Query(nativeQuery = false, value = """
    select
        t.abn,
        t.accountNumber,
        count(1) transactionCount,
        sum(t.balance) totalAmount
    from AccountTransaction t
    where t.reportDate = :reportDate
    group by t.abn, t.accountNumber
    """
)
fun findTotalAmountByDate(
    @Param("reportDate") reportDate: LocalDate
): List<AccountTransactionEntity>
The Entity that should be mapped to the results is as follows.
@Entity
data class AccountTransactionEntity(
    val abn: String,
    val accountNumber: String,
    val transactionCount: Int,
    val totalAmount: BigDecimal
)
At first glance it seems to be perfectly fine. However looking at the logs, the following error occurs.

org.springframework.core.convert.ConversionFailedException:
  Failed to convert from type [java.lang.Object[]]
  to type [AccountTransactionEntity] for value '{67220345566, 200300809, 8, -2459.25}';
nested exception is org.springframework.core.convert.ConverterNotFoundException:
  No converter found capable of converting from type [String]
  to type [AccountTransactionEntity]

The reason for this is that it doesn't understand how to bind the query result set implicitly with a strongly typed object using inference. You'll need to help hibernate a little by making an explicit instantiation within the non-native query as follows.
@Query(nativeQuery = false, value = """
    select new AccountTransactionEntity(
        t.abn,
        t.accountNumber,
        count(1) transactionCount,
        sum(t.balance) totalAmount
    ) from AccountTransaction t
    where t.reportDate = :reportDate
    group by t.abn, t.accountNumber
    """
)
fun findTotalAmountByDate(
    @Param("reportDate") reportDate: LocalDate
): List<AccountTransactionEntity>
That will compile fine and won't need any further transformation downstream.

Monday, August 14, 2023

Importing Trusted Certificate to all your JVM cacerts

Sometimes when you work in a corporate company there are proxies that require certificates to validate Java applications. Usually your company will provide a valid certificate that will validate any requests coming in and going out from your local machine. In order to use your Java applications successfully with your corporation's certificate you need to import it using keytool to the cacerts file for your installed JVM. 

The problem is however you may have multiple JVMs installed and the location may differ from machine to machine. The following bash script locates all the cacerts on your machine and adds the appropriate certificate to them.
#!/usr/bin/env sh
 
PROXY_CERT="${HOME}/Your_Company_Proxy_CA.cer"
KEYSTORE_ALIAS="proxy-root"

echo "Finding cacerts..."
KEYSTORES=$(find / -name cacerts -type f -print 2>/dev/null)
 
while IFS= read -r KEYSTORE
do
  echo "Finding alias ${KEYSTORE_ALIAS} from JDK Keystore ${KEYSTORE}"
  sudo keytool -list -alias ${KEYSTORE_ALIAS} -keystore "${KEYSTORE}" -storepass changeit -v && {
    echo "Deleting alias ${KEYSTORE_ALIAS} from JDK Keystore ${KEYSTORE}"
    sudo keytool -delete -alias ${KEYSTORE_ALIAS} -storepass changeit -noprompt -keystore "${KEYSTORE}"
  } || echo "Adding cert to JDK Keystore ${KEYSTORE}"
  
  sudo keytool -import -trustcacerts -storepass changeit -noprompt -alias ${KEYSTORE_ALIAS} -keystore "${KEYSTORE}" -file "${PROXY_CERT}"
done <<< "${KEYSTORES}"