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 createUserStoreDataSource — DatabaseUtil.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 JDBCUserStoreConstants — maxAge 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
- 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.
}
}
- 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/
- 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 -->
- 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
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 asPSQLException: An I/O error occurred while sending to the backend/java.io.EOFExceptionon the first use of the borrowed connection.Root cause
In
DatabaseUtil.createUserStoreDataSource(...), the affected setters are guarded byStringUtils.isNumeric(JDBCRealmConstants.<CONST>)— which tests the constant's string value (the property name, e.g."maxAge"), not the configured value.isNumeric("maxAge")is alwaysfalse, 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-kernelat tagv4.6.0)The headline case,
maxAgeincreateUserStoreDataSource—DatabaseUtil.javaL290–L292:The same defect affects seven properties in
createUserStoreDataSource, each on itsisNumeric(<constant>)line:initialSize(L203),numTestsPerEvictionRun(L227),removeAbandonedTimeout(L246),abandonWhenPercentageFull(L285),maxAge(L291),suspectTimeout(L302), andvalidationQueryTimeout(L308). For contrast, the correct value-guarded pattern is used a few lines above forvalidationInterval(L177–L182).createRealmDataSourcecarries the identical defect — e.g.maxAge(L522–L524) andvalidationQueryTimeout(L539–L541), plus L435, L459, L478, L517, L534.These properties are genuinely exposed to administrators as Advanced Properties in
JDBCUserStoreConstants—maxAgeat L162 andvalidationQueryTimeoutat 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 whymaxAgeworks there but not for an inline user store —RDBMSDataSourceUtils.createPoolConfigurationL292–L293.Steps to Reproduce
jdbcInterceptorsuser-store property, which is applied correctly:jdbc-poolhost bundle — a plainlibjar 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/:<IS_HOME>/repository/deployment/server/userstores/PROBE.xml, or via the Console) with an inline connection plus:PROBEdomain 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=300000andvalidationQueryTimeout=30are applied.Actual behaviour
They fall back to Tomcat defaults (
0/-1) while every control property in the same pool is honored. Realwso2carbon.logline observed on WSO2 IS 5.10.0:The store was configured with
maxAge=300000andvalidationQueryTimeout=30, yet the pool reportsmaxAge=0andvalidationQueryTimeout=-1; the controlsvalidationInterval=7777,testOnBorrow=true,validationQuery=SELECT 1,maxActive=99are all applied — confirming the interceptor reads the correct pool and that only the affected properties are dropped.Suggested Fix
In both
createUserStoreDataSourceandcreateRealmDataSource, change the second condition of each affected guard fromStringUtils.isNumeric(JDBCRealmConstants.<CONST>)toStringUtils.isNumeric(realmConfig.getUserStoreProperty(JDBCRealmConstants.<CONST>))(andgetRealmProperty(...)for the realm method) — matching the correct pattern already used forvalidationIntervalat 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
impact/behavioral-changeadded7.2.0-migration)configadded