22
33import com .zaxxer .hikari .HikariConfig ;
44import com .zaxxer .hikari .HikariDataSource ;
5+ import io .micrometer .core .instrument .MeterRegistry ;
56import javax .sql .DataSource ;
7+ import org .springframework .beans .factory .ObjectProvider ;
68import org .springframework .beans .factory .annotation .Value ;
79import org .springframework .boot .autoconfigure .condition .ConditionalOnProperty ;
810import org .springframework .context .annotation .Bean ;
@@ -35,36 +37,37 @@ public DataSource dataSource(
3537 @ Value ("${spring.datasource.hikari.max-lifetime:180000}" ) long maxLifetime ,
3638 @ Value ("${spring.datasource.hikari.keepalive-time:60000}" ) long keepaliveTime ,
3739 @ Value ("${spring.datasource.hikari.leak-detection-threshold:60000}" ) long leakDetectionThreshold ,
38- @ Value ("${ilcr.datasource.validation-query:SELECT 1 FROM DUAL}" ) String validationQuery
40+ @ Value ("${ilcr.datasource.validation-query:SELECT 1 FROM DUAL}" ) String validationQuery ,
41+ ObjectProvider <MeterRegistry > meterRegistry
3942 ) {
4043 requireProperty ("spring.datasource.url" , url );
4144 requireProperty ("spring.datasource.username" , username );
4245 requireProperty ("spring.datasource.password" , password );
4346
4447 HikariConfig config = new HikariConfig ();
45- config .setJdbcUrl (url );
46- config .setUsername (username );
47- config .setPassword (password );
48- config .setDriverClassName (driverClassName );
48+ applyCommonHikari (config , url , username , password , driverClassName , idleTimeout , maxLifetime ,
49+ keepaliveTime , validationQuery );
4950 config .setPoolName (poolName );
5051 config .setMaximumPoolSize (maximumPoolSize );
5152 config .setMinimumIdle (minimumIdle );
5253 config .setConnectionTimeout (connectionTimeout );
53- config .setIdleTimeout (idleTimeout );
54- config .setMaxLifetime (maxLifetime );
55- config .setKeepaliveTime (keepaliveTime );
5654 config .setLeakDetectionThreshold (leakDetectionThreshold );
57- config .setConnectionTestQuery (validationQuery );
55+ // Expose hikaricp_connections_* so pool saturation (this story's whole concern) is alertable,
56+ // not inferred from latency. The hand-built pool + DelegatingDataSource wrapper means Boot's
57+ // Hikari metrics binder doesn't pick it up, so bind it here.
58+ meterRegistry .ifAvailable (config ::setMetricRegistry );
5859
5960 return new ValidatingDataSource (new HikariDataSource (config ), validationQuery );
6061 }
6162
6263 /**
63- * A SEPARATE, read-only, small Hikari pool dedicated to the Jasper report-fill path (Story 29.1).
64- * The Schedule 9 SQL-in-template fill borrows a JDBC connection for the ENTIRE render (embedded SQL
65- * + PDF formatting) — the longest-held connection in the app. Drawing it from the {@link Primary}
64+ * A SEPARATE, small Hikari pool dedicated to the Jasper report-fill path (Story 29.1). The Schedule 9
65+ * SQL-in-template fill borrows a JDBC connection for the ENTIRE render (embedded SQL + PDF
66+ * formatting) — the longest-held connection in the app. Drawing it from the {@link Primary}
6667 * transactional pool (default max 5) means a handful of concurrent reports can starve ordinary
67- * schedule requests. This bean isolates report fills so they can only exhaust their own small pool.
68+ * schedule requests. This bean isolates report fills so they can only exhaust their own small pool,
69+ * with a short connection-timeout so the Nth queued render fast-fails instead of parking a Tomcat
70+ * thread for the full 30s.
6871 *
6972 * <p>Deliberately NOT {@link Primary}: the {@code jdbcTemplate}/{@code namedParameterJdbcTemplate}/
7073 * {@code transactionManager} beans keep binding to the transactional pool. Only {@code ReportService}
@@ -79,11 +82,12 @@ public DataSource reportingDataSource(
7982 @ Value ("${spring.datasource.reporting.hikari.pool-name:ILCRReportingPool}" ) String poolName ,
8083 @ Value ("${spring.datasource.reporting.hikari.maximum-pool-size:3}" ) int maximumPoolSize ,
8184 @ Value ("${spring.datasource.reporting.hikari.minimum-idle:0}" ) int minimumIdle ,
82- @ Value ("${spring.datasource.hikari.connection-timeout:30000 }" ) long connectionTimeout ,
85+ @ Value ("${spring.datasource.reporting. hikari.connection-timeout:5000 }" ) long connectionTimeout ,
8386 @ Value ("${spring.datasource.hikari.idle-timeout:60000}" ) long idleTimeout ,
8487 @ Value ("${spring.datasource.hikari.max-lifetime:180000}" ) long maxLifetime ,
8588 @ Value ("${spring.datasource.hikari.keepalive-time:60000}" ) long keepaliveTime ,
86- @ Value ("${ilcr.datasource.validation-query:SELECT 1 FROM DUAL}" ) String validationQuery
89+ @ Value ("${ilcr.datasource.validation-query:SELECT 1 FROM DUAL}" ) String validationQuery ,
90+ ObjectProvider <MeterRegistry > meterRegistry
8791 ) {
8892 requireProperty ("spring.datasource.url" , url );
8993 requireProperty ("spring.datasource.username" , username );
@@ -92,37 +96,53 @@ public DataSource reportingDataSource(
9296 HikariConfig config = reportingHikariConfig (url , username , password , driverClassName , poolName ,
9397 maximumPoolSize , minimumIdle , connectionTimeout , idleTimeout , maxLifetime , keepaliveTime ,
9498 validationQuery );
99+ meterRegistry .ifAvailable (config ::setMetricRegistry );
95100 return new ValidatingDataSource (new HikariDataSource (config ), validationQuery );
96101 }
97102
98103 /**
99- * Build the read-only reporting {@link HikariConfig}. Extracted (package-private) so its
100- * isolation-critical invariants — read-only, its own pool name, a small dedicated ceiling, and NO
101- * leak-detection (a render legitimately holds its connection for the whole fill) — are unit-testable
102- * without opening a real database connection.
104+ * Build the reporting {@link HikariConfig}: the connection settings shared with the primary pool
105+ * (via {@link #applyCommonHikari}) plus the reporting-specific overrides — its own pool name + small
106+ * ceiling, a short connection-timeout (fast-fail rather than parking a Tomcat thread on the Nth
107+ * queued render), NO leak-detection (a render legitimately holds its connection for the whole fill),
108+ * and the read-only HINT. Extracted (package-private) so those invariants are unit-testable without
109+ * opening a database connection.
110+ *
111+ * <p>NOTE: {@code setReadOnly(true)} is a Hikari/JDBC <em>hint</em>, not an enforced privilege — on
112+ * Oracle it does not by itself prevent writes. It is intent-signalling + defence-in-depth only; true
113+ * write-prevention would be a read-only database account for the reporting connection.
103114 */
104115 static HikariConfig reportingHikariConfig (
105116 String url , String username , String password , String driverClassName , String poolName ,
106117 int maximumPoolSize , int minimumIdle , long connectionTimeout , long idleTimeout ,
107118 long maxLifetime , long keepaliveTime , String validationQuery ) {
108119 HikariConfig config = new HikariConfig ();
109- config .setJdbcUrl (url );
110- config .setUsername (username );
111- config .setPassword (password );
112- config .setDriverClassName (driverClassName );
120+ applyCommonHikari (config , url , username , password , driverClassName , idleTimeout , maxLifetime ,
121+ keepaliveTime , validationQuery );
113122 config .setPoolName (poolName );
114123 config .setMaximumPoolSize (maximumPoolSize );
115124 config .setMinimumIdle (minimumIdle );
116125 config .setConnectionTimeout (connectionTimeout );
126+ config .setReadOnly (true );
127+ return config ;
128+ }
129+
130+ /**
131+ * The connection settings both pools share (URL / credentials / driver / lifetimes / validation),
132+ * factored out so a change to one pool's shared setting can't silently drift from the other.
133+ * Pool-specific settings (name, sizes, timeout, leak-detection, read-only) are applied by the caller.
134+ */
135+ private static void applyCommonHikari (HikariConfig config , String url , String username ,
136+ String password , String driverClassName , long idleTimeout , long maxLifetime ,
137+ long keepaliveTime , String validationQuery ) {
138+ config .setJdbcUrl (url );
139+ config .setUsername (username );
140+ config .setPassword (password );
141+ config .setDriverClassName (driverClassName );
117142 config .setIdleTimeout (idleTimeout );
118143 config .setMaxLifetime (maxLifetime );
119144 config .setKeepaliveTime (keepaliveTime );
120145 config .setConnectionTestQuery (validationQuery );
121- // Read-only: report fills never write. Leak-detection intentionally left unset (0/off): a Jasper
122- // render holds its connection for the full fill+format, which would trip a leak warning; the
123- // dedicated small pool bounds the exposure instead.
124- config .setReadOnly (true );
125- return config ;
126146 }
127147
128148 @ Bean
0 commit comments