Skip to content

JDBC user-store connection-pool properties (maxAge, validationQueryTimeout, initialSize, numTestsPerEvictionRun, removeAbandonedTimeout, abandonWhenPercentageFull, suspectTimeout) are silently ignored #28182

Description

@vfraga

Description

For a JDBC user store that defines its database connection inline, seven connection-pool properties that are exposed to administrators as Advanced Properties in the Management Console are silently discarded when the datasource is built — they never reach the underlying Tomcat JDBC pool. The most impactful is maxAge: administrators set it to cap connection lifetime when the database sits behind a connection-reaping layer (e.g. AWS RDS Proxy, or an NLB/firewall with a max-session timeout). The property looks accepted (it appears in the Console with a description) but has no effect, so connections are never age-recycled and the pool keeps handing out connections that the proxy/network already closed, surfacing as PSQLException: An I/O error occurred while sending to the backend / java.io.EOFException on the first use of the borrowed connection.

Root cause

In DatabaseUtil.createUserStoreDataSource(...), the affected setters are guarded by StringUtils.isNumeric(JDBCRealmConstants.<CONST>) — which tests the constant's string value (the property name, e.g. "maxAge"), not the configured value. isNumeric("maxAge") is always false, so the branch is never entered and the setter is never called. Correctly-wired guards on the same method (e.g. validationInterval) wrap the value: StringUtils.isNumeric(realmConfig.getUserStoreProperty(...)).

Affected code (public wso2/carbon-kernel at tag v4.6.0)

The headline case, maxAge in createUserStoreDataSourceDatabaseUtil.java L290–L292:

if (StringUtils.isNotEmpty(realmConfig.getUserStoreProperty(JDBCRealmConstants.MAX_AGE)) &&
        StringUtils.isNumeric(JDBCRealmConstants.MAX_AGE)) {          // L291: constant name -> always false
    poolProperties.setMaxAge(Integer.parseInt(realmConfig.getUserStoreProperty(JDBCRealmConstants.MAX_AGE)));
}

The same defect affects seven properties in createUserStoreDataSource, each on its isNumeric(<constant>) line: initialSize (L203), numTestsPerEvictionRun (L227), removeAbandonedTimeout (L246), abandonWhenPercentageFull (L285), maxAge (L291), suspectTimeout (L302), and validationQueryTimeout (L308). For contrast, the correct value-guarded pattern is used a few lines above for validationInterval (L177–L182).

createRealmDataSource carries the identical defect — e.g. maxAge (L522–L524) and validationQueryTimeout (L539–L541), plus L435, L459, L478, L517, L534.

These properties are genuinely exposed to administrators as Advanced Properties in JDBCUserStoreConstantsmaxAge at L162 and validationQueryTimeout at L174 — so administrators are shown the knob but it has no effect. The equivalent mapping for JNDI datasources (master-datasources.xml) is value-guarded and correct, which is why maxAge works there but not for an inline user store — RDBMSDataSourceUtils.createPoolConfiguration L292–L293.

Steps to Reproduce

  1. Save this diagnostic Tomcat JDBC interceptor. It only reads and logs the pool's effective configuration when the pool starts — it changes no behaviour, and it is attached via the jdbcInterceptors user-store property, which is applied correctly:
package org.wso2.support.repro;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
import org.apache.tomcat.jdbc.pool.PoolConfiguration;
import org.apache.tomcat.jdbc.pool.PooledConnection;

public class PoolConfigProbeInterceptor extends JdbcInterceptor {

    private static final Log log = LogFactory.getLog(PoolConfigProbeInterceptor.class);

    @Override
    public void poolStarted(ConnectionPool pool) {
        PoolConfiguration p = pool.getPoolProperties();
        log.info("[PoolConfigProbe] effective pool config"
                + " | maxAge=" + p.getMaxAge()
                + " | validationQueryTimeout=" + p.getValidationQueryTimeout()
                + " | validationInterval=" + p.getValidationInterval()
                + " | testOnBorrow=" + p.isTestOnBorrow()
                + " | validationQuery=" + p.getValidationQuery()
                + " | maxActive=" + p.getMaxActive());
        super.poolStarted(pool);
    }

    @Override
    public void reset(ConnectionPool parent, PooledConnection con) {
        // Required abstract method. No-op — observes only, at pool start.
    }
}
  1. Compile it and package it as an OSGi fragment of the jdbc-pool host bundle — a plain lib jar is not visible to the (Equinox) classloader that reflectively loads the interceptor; a fragment places the class on the host's classpath. Drop the jar into <IS_HOME>/repository/components/dropins/:
javac -cp "<IS_HOME>/repository/components/plugins/*" -d out PoolConfigProbeInterceptor.java

mkdir -p out/META-INF
cat > out/META-INF/MANIFEST.MF <<'MF'
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-SymbolicName: org.wso2.support.poolconfigprobe
Bundle-Version: 1.0.0
Fragment-Host: jdbc-pool
MF

jar cfm poolconfig-probe-1.0.jar out/META-INF/MANIFEST.MF -C out org
cp poolconfig-probe-1.0.jar <IS_HOME>/repository/components/dropins/
  1. Configure a JDBC user store (<IS_HOME>/repository/deployment/server/userstores/PROBE.xml, or via the Console) with an inline connection plus:
<Property name="jdbcInterceptors">org.wso2.support.repro.PoolConfigProbeInterceptor</Property>
<Property name="maxAge">300000</Property>                <!-- under test -->
<Property name="validationQueryTimeout">30</Property>     <!-- under test -->
<Property name="validationInterval">7777</Property>       <!-- control -->
<Property name="testOnBorrow">true</Property>             <!-- control -->
<Property name="validationQuery">SELECT 1</Property>      <!-- control -->
<Property name="maxActive">99</Property>                  <!-- control -->
  1. Start the server. The pool initializes lazily, so perform one operation against the PROBE domain to trigger it (a failed lookup is fine — the pool logs at start, before the query runs), e.g.:
curl -sk -u admin:admin "https://localhost:9443/scim2/Users?filter=userName+eq+%22PROBE/anyuser%22"

Then inspect <IS_HOME>/repository/logs/wso2carbon.log.

Expected behaviour

maxAge=300000 and validationQueryTimeout=30 are applied.

Actual behaviour

They fall back to Tomcat defaults (0 / -1) while every control property in the same pool is honored. Real wso2carbon.log line observed on WSO2 IS 5.10.0:

TID: [-1234] [scim2] [2026-07-14 23:36:09,186] [1295c000-cbcf-44ad-8605-bc30e7a6c67e]  INFO {org.wso2.support.repro.PoolConfigProbeInterceptor} - [PoolConfigProbe] effective pool config | maxAge=0 | validationQueryTimeout=-1 | validationInterval=7777 | testOnBorrow=true | validationQuery=SELECT 1 | maxActive=99

The store was configured with maxAge=300000 and validationQueryTimeout=30, yet the pool reports maxAge=0 and validationQueryTimeout=-1; the controls validationInterval=7777, testOnBorrow=true, validationQuery=SELECT 1, maxActive=99 are all applied — confirming the interceptor reads the correct pool and that only the affected properties are dropped.

Suggested Fix

In both createUserStoreDataSource and createRealmDataSource, change the second condition of each affected guard from StringUtils.isNumeric(JDBCRealmConstants.<CONST>) to StringUtils.isNumeric(realmConfig.getUserStoreProperty(JDBCRealmConstants.<CONST>)) (and getRealmProperty(...) for the realm method) — matching the correct pattern already used for validationInterval at L177–L182.

Please select the area issue is related to

Identity Server Core

Version

IS 5.10.0, IS 5.11.0 (older versions may also be affected)

Environment Details (with versions)

Userstore: JDBC

Developer Checklist

  • [Behavioural Change] Does this change introduce a behavioral change to the product?
  •  ↳ Approved by team lead
  •  ↳ Label impact/behavioral-change added
  • [Migration Impact] Does this change have a migration impact?
  •  ↳ Migration label added (e.g., 7.2.0-migration)
  •  ↳ Migration issues created and linked
  • [New Configuration] Does this change introduce a new configuration?
  •  ↳ Label config added
  •  ↳ Configuration is properly documented

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions