This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 364d4ba58b79d4267a68a0e25f2cb2b286964266 Author: opencode <[email protected]> AuthorDate: Tue Nov 3 16:15:35 2026 +0100 Guard the pool against throwables that out-type its catches The guards around a pool validation - the custom Validator call, the driver's isValid() check and the validation statement - converted exceptions into a failed check but let Errors through. An Error is not far from routine behind a Validator or a driver wrapper loaded through a web application class loader: a LinkageError, most often the NoClassDefFoundError of a context that has since stopped, is that loader's ordinary failure. Such an Error unwound the borrow, return or idle path after the pooled connection had left one queue and before it reached the other, stranding it outside both - invisible to the abandoned check, to the eviction sweep and to the pool's close alike, and still counted against maxActive for the lifetime of the JVM. The scheduled maintenance task was the worse arrival. Its guard caught only exceptions, and a throwable escaping a timer task cancels java.util.Timer for good: every pool shares one cleaner timer, so one pool's Error silently ended eviction and abandoned-check for all of them, and the pool-side field still held the dead timer as if it were live, so the next pool that needed a cleaner failed to start, with "IllegalStateException: Timer already cancelled" for the rest of the JVM's life. The validation guards now apply the module's fatal policy through ExceptionUtils.handleThrowable and report everything short of it as the failed check that the throw says the session is. The maintenance task runs its body's arrivals through the same policy, so nothing except a fatal error can interrupt it, and reports the rest under the new connectionPool.poolCleanerFailed message. A shared timer that arrives dead is replaced rather than obeyed, so even a timer lost to a fatal error costs nothing after the registration that followed it. The pins: the LinkageError validator now fails borrows, returns and sweeps exactly like the throwing one - a checked exception at the application, no stranding, the slot whole - while a scheduled sweep broken by an Error leaves the same live shared timer that a new pool joins; a dead timer is replaced on the next registration; and a fatal error still propagates through a borrow and through the task unaltered, so the containment cannot silently grow into swallowing what no recovery is written for. --- .../apache/tomcat/jdbc/pool/ConnectionPool.java | 52 ++++-- .../apache/tomcat/jdbc/pool/PooledConnection.java | 37 +++- .../tomcat/jdbc/pool/util/LocalStrings.properties | 1 + .../tomcat/jdbc/pool/TestSharedCleanerTimer.java | 187 +++++++++++++++++++++ .../tomcat/jdbc/pool/TestThrowingValidator.java | 95 ++++++++++- 5 files changed, 353 insertions(+), 19 deletions(-) diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java index 41dcd9cd51..19ac544293 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java @@ -51,6 +51,7 @@ import org.apache.juli.logging.LogFactory; import org.apache.tomcat.jdbc.pool.interceptor.QueryTimeoutInterceptor; import org.apache.tomcat.jdbc.pool.interceptor.StatementCache; import org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer; +import org.apache.tomcat.jdbc.pool.util.ExceptionUtils; import org.apache.tomcat.jdbc.pool.util.MessageFactory; /** @@ -2779,22 +2780,35 @@ public class ConnectionPool { private static synchronized void registerCleaner(PoolCleaner cleaner) { unregisterCleaner(cleaner); cleaners.add(cleaner); - if (poolCleanTimer == null) { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); + // A timer cancels itself when a task throws an uncaught throwable, + // and scheduling onto such a dead timer fails - which would make + // every pool that needs a cleaner afterwards fail the same way for + // the rest of the JVM's life. PoolCleaner.run no longer lets + // anything escape; this is the safety net for a timer that is + // already dead when it arrives: replace it rather than refuse. + while (true) { + if (poolCleanTimer == null) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(ConnectionPool.class.getClassLoader()); + // Create the timer thread in a PrivilegedAction so that a + // reference to the web application class loader is not created + // via Thread.inheritedAccessControlContext + poolCleanTimer = new Timer( + "Tomcat JDBC Pool Cleaner[" + System.identityHashCode(ConnectionPool.class.getClassLoader()) + + ":" + System.currentTimeMillis() + "]", + true); + } finally { + Thread.currentThread().setContextClassLoader(loader); + } + } try { - Thread.currentThread().setContextClassLoader(ConnectionPool.class.getClassLoader()); - // Create the timer thread in a PrivilegedAction so that a - // reference to the web application class loader is not created - // via Thread.inheritedAccessControlContext - poolCleanTimer = new Timer( - "Tomcat JDBC Pool Cleaner[" + System.identityHashCode(ConnectionPool.class.getClassLoader()) + - ":" + System.currentTimeMillis() + "]", - true); - } finally { - Thread.currentThread().setContextClassLoader(loader); + poolCleanTimer.schedule(cleaner, cleaner.sleepTime, cleaner.sleepTime); + return; + } catch (IllegalStateException x) { + poolCleanTimer = null; } } - poolCleanTimer.schedule(cleaner, cleaner.sleepTime, cleaner.sleepTime); } private static synchronized void unregisterCleaner(PoolCleaner cleaner) { @@ -2896,8 +2910,16 @@ public class ConnectionPool { } else if (pool.getPoolProperties().getMaxAge() > 0) { pool.testAllIdle(true); } - } catch (Exception x) { - log.error(x.toString(), x); + } catch (Throwable x) { + // The guards on the maintenance paths convert exceptions + // and - since handleThrowable joined them - non-fatal + // Errors, but a throwable can still reach here from a + // path without a guard of its own. The module's fatal + // policy runs first, as everywhere else; losing this + // task's next runs to a timer that dies on a fatal Error + // is borne by the replacement in registerCleaner. + ExceptionUtils.handleThrowable(x); + log.error(MessageFactory.get("connectionPool.poolCleanerFailed"), x); } } } diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java index 46ea99ddb2..d6746f9cb2 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java @@ -33,6 +33,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.jdbc.pool.interceptor.ConnectionState; import org.apache.tomcat.jdbc.pool.jmx.JmxUtil; +import org.apache.tomcat.jdbc.pool.util.ExceptionUtils; import org.apache.tomcat.jdbc.pool.util.MessageFactory; /** @@ -943,13 +944,20 @@ public class PooledConnection implements PooledConnectionMBean { boolean valid; try { valid = poolProperties.getValidator().validate(connection, validateAction); - } catch (RuntimeException x) { + } catch (Throwable x) { + ExceptionUtils.handleThrowable(x); // The SPI declares no checked exception, so throwing is the // only way an implementation can report its own failure; the // pool must treat that as a failed validation, because a // throw escaping from here unwinds the borrow or return path // between the idle and busy queues and strands the pooled - // connection - permanently shrinking the pool + // connection - permanently shrinking the pool. Errors belong + // in this guard too: an implementation loaded through a web + // application class loader fails with LinkageError variants - + // a NoClassDefFoundError once the context has stopped is the + // common one - and that is a check that cannot vouch for the + // session, nothing else. Only the fatal conditions that + // handleThrowable re-throws keep propagating. if (getPoolProperties().getLogValidationErrors()) { log.error(MessageFactory.get("pooledConnection.customValidationThrew", poolProperties.getValidator()), x); @@ -1034,6 +1042,23 @@ public class PooledConnection implements PooledConnectionMBean { log.debug("isValid() failed.", e); } return false; + } catch (Throwable x) { + // Same reasoning one step further: a driver or wrapper that + // fails the check with an Error - a LinkageError surfacing + // through a wrapper whose class loader has lost a class is + // the realistic arrival - has answered the question the + // check asks, "can this session be vouched for", with + // nothing better than an exception would; handleThrowable + // keeps the fatal conditions it keeps everywhere else. + ExceptionUtils.handleThrowable(x); + if (getPoolProperties().getLogValidationErrors()) { + log.error(MessageFactory.get("pooledConnection.isValidFailed"), x); + } else { + // Logged like the unchecked case above: this is the + // same anomaly one type further out + log.warn(MessageFactory.get("pooledConnection.isValidFailed"), x); + } + return false; } finally { if (!transactionTerminated) { silentlyRollbackTransactionIfNeeded(); @@ -1075,7 +1100,13 @@ public class PooledConnection implements PooledConnectionMBean { transactionTerminated = silentlyRollbackTransactionIfNeeded(); } return transactionTerminated; - } catch (Exception ex) { + } catch (Throwable ex) { + // The checked and unchecked failures of the driver calls above + // are failed validations, and so is an Error short of the fatal + // conditions handleThrowable keeps: an escaping throw unwinds + // the borrow path between the idle and busy queues and strands + // the pooled connection, whatever the throw's type. + ExceptionUtils.handleThrowable(ex); if (getPoolProperties().getLogValidationErrors()) { log.error(MessageFactory.get("pooledConnection.sqlValidationError"), ex); } else if (log.isDebugEnabled()) { diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties index 29c076edea..047bcb2315 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties @@ -53,6 +53,7 @@ connectionPool.maxActiveTooSmall=maxActive is smaller than 1, setting maxActive connectionPool.maxIdleTooLarge=maxIdle is larger than maxActive, setting maxIdle to: {0} connectionPool.maxIdleTooSmall=maxIdle is smaller than minIdle, setting maxIdle to: {0} connectionPool.minIdleTooLarge=minIdle is larger than maxActive, setting minIdle to: {0} +connectionPool.poolCleanerFailed=The pool maintenance task failed, it will run again at the next scheduled interval. connectionPool.poolClosed=Connection pool closed. connectionPool.poolEmptyNoWait=[{0}] NoWait: Pool empty. Unable to fetch a connection, none available[{1} in use]. connectionPool.poolEmptyTimeout=[{0}] Timeout: Pool empty. Unable to fetch a connection in {1} seconds, none available[size:{2}; busy:{3}; idle:{4}; lastwait:{5}]. diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestSharedCleanerTimer.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestSharedCleanerTimer.java new file mode 100644 index 0000000000..a70990cb42 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestSharedCleanerTimer.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.jdbc.pool; + +import java.sql.Connection; +import java.util.Timer; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.apache.tomcat.jdbc.JdbcPoolBaseTest; + +/** + * One {@link java.util.Timer} serves the maintenance of every pool the + * class loader holds, and a timer cancels itself for good the moment a + * task lets a throwable escape - so an escaping {@code Error} from the + * cleaner's body is not one pool's accident but the end of eviction and + * abandoned-check for every pool in the JVM, later pool starts included: + * scheduling onto the dead timer fails with {@code IllegalStateException} + * and the pool-side field still holds it as if it were live. These tests + * pin the three parts of that contract: an ordinary {@code Error} inside + * the swept work is contained and the shared timer goes on serving, a + * timer that arrives dead is replaced rather than obeyed, and - so the + * containment cannot silently grow into swallowing the unrecoverable - + * the fatal conditions ExceptionUtils keeps fatal still propagate, from + * a borrow and from the task alike. + */ +class TestSharedCleanerTimer extends JdbcPoolBaseTest { + + /** + * Fails the idle sweep's check the way a stopped web-application class + * loader fails an interceptor: a LinkageError the maintenance work can + * neither predict nor need. Every other action answers normally, so + * the sweep is the only place the error can arrive. + */ + public static class SweepErrorValidator implements Validator { + private final AtomicInteger idleCalls = new AtomicInteger(); + + @Override + public boolean validate(Connection connection, int validateAction) { + if (validateAction == PooledConnection.VALIDATE_IDLE) { + idleCalls.incrementAndGet(); + throw new NoClassDefFoundError("the context this validator was loaded from is stopped"); + } + return true; + } + } + + /** Reports every check fatal, whatever asked for it. */ + public static class FatalValidator implements Validator { + private final AtomicInteger calls = new AtomicInteger(); + + @Override + public boolean validate(Connection connection, int validateAction) { + calls.incrementAndGet(); + throw new OutOfMemoryError("the validator reports a dying JVM"); + } + } + + @Test + void anErrorInsideTheSweptWorkLeavesTheSharedTimerServing() throws Exception { + SweepErrorValidator validator = new SweepErrorValidator(); + DataSource first = newDataSource(); + first.getPoolProperties().setInitialSize(0); + // the task's checkIdle step evicts idle connections down to min + // idle; park the floor at the size this test keeps so the sweep is + // the only thing the task can do to the connection + first.getPoolProperties().setMinIdle(1); + first.getPoolProperties().setTestWhileIdle(true); + first.getPoolProperties().setTimeBetweenEvictionRunsMillis(200); + first.getPoolProperties().setValidationInterval(0); + first.getPoolProperties().setValidator(validator); + first.createPool(); + Timer timer = ConnectionPool.getPoolTimer(); + Assertions.assertNotNull(timer); + + Connection firstBorrowed = first.getConnection(); + firstBorrowed.close(); + // let the scheduled sweep really run into the error + awaitUntil(() -> validator.idleCalls.get() >= 1); + + // the swept connection was read as a failed check and taken out of + // service - not stranded mid-sweep + Assertions.assertEquals(0, first.getPool().getIdle()); + Assertions.assertEquals(0, first.getPool().getSize()); + + // the timer outlived the sweep: a second pool starts on the very + // same timer a dying task would have cancelled + DataSource next = newDataSource(); + next.getPoolProperties().setTimeBetweenEvictionRunsMillis(200); + next.createPool(); + Assertions.assertSame(timer, ConnectionPool.getPoolTimer(), + "the shared timer must still be the live one the second pool joined"); + try (Connection c = next.getConnection()) { + Assertions.assertNotNull(c); + } + + next.close(); + first.close(); + } + + @Test + void afatalErrorInsideTheSweptWorkStillPropagatesFromTheTask() throws Exception { + FatalValidator validator = new FatalValidator(); + DataSource ds = newDataSource(); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMinIdle(1); + ds.getPoolProperties().setTestWhileIdle(true); + // the sweep runs here, on this thread, exactly as the shared timer + // would run it - and no background competitor for the idle queue + ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0); + ds.getPoolProperties().setValidationInterval(0); + ds.getPoolProperties().setValidator(validator); + ds.createPool(); + Connection first = ds.getConnection(); + first.close(); + Assertions.assertEquals(1, ds.getPool().getIdle()); + + ConnectionPool.PoolCleaner cleaner = new ConnectionPool.PoolCleaner(ds.getPool(), 100); + + // the task-wide guard reports and keeps the module's fatal policy: + // a VirtualMachineError is beyond any per-call recovery and keeps + // propagating from the pool, the task and the borrow alike + Assertions.assertThrows(OutOfMemoryError.class, cleaner::run); + Assertions.assertTrue(validator.calls.get() >= 1, + "the sweep must have run into the error the guard is tested against"); + } + + @Test + void aDeadSharedTimerIsReplacedWhenANewPoolNeedsOne() throws Exception { + DataSource first = newDataSource(); + first.getPoolProperties().setTimeBetweenEvictionRunsMillis(500); + first.createPool(); + Timer dead = ConnectionPool.getPoolTimer(); + Assertions.assertNotNull(dead, "a pool with a sweeper must hold the shared timer"); + + // The state a task that died on a fatal error leaves behind: the + // timer cancelled for good while the field still points at it as + // if live. + dead.cancel(); + + DataSource next = newDataSource(); + next.getPoolProperties().setTimeBetweenEvictionRunsMillis(500); + Assertions.assertDoesNotThrow(next::createPool, + "a dead shared timer must not make every later pool fail to start"); + Assertions.assertNotSame(dead, ConnectionPool.getPoolTimer(), + "the refusal must have been read as a dead timer and answered with a fresh one"); + try (Connection c = next.getConnection()) { + Assertions.assertNotNull(c); + } + + next.close(); + first.close(); + } + + @Test + void afatalErrorStillPropagatesThroughABorrow() throws Exception { + DataSource ds = newDataSource(); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMaxActive(1); + ds.getPoolProperties().setTestOnBorrow(true); + ds.getPoolProperties().setValidationInterval(0); + ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0); + ds.getPoolProperties().setValidator(new FatalValidator()); + // the creating borrow is not validated; the re-borrow is + Connection first = ds.getConnection(); + first.close(); + + Assertions.assertThrows(OutOfMemoryError.class, ds::getConnection); + ds.close(); + } +} diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java index db922762bc..91edd4b710 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java @@ -31,6 +31,12 @@ import org.apache.tomcat.jdbc.JdbcPoolBaseTest; * escape {@code getConnection()}/{@code close()}, must not strand the * pooled connection between the idle and busy queues (a permanently lost * slot), and must not silently disable idle eviction. + * + * <p>An {@code Error} thrown from behind an implementation counts the + * same way: an implementation loaded through a web-application class + * loader fails with {@code LinkageError} variants long after any + * deliberate throw, and the guard must be as wide as the stranding it + * prevents - fatal JVM errors excepted.</p> */ class TestThrowingValidator extends JdbcPoolBaseTest { @@ -42,6 +48,18 @@ class TestThrowingValidator extends JdbcPoolBaseTest { } } + /** + * The broken implementation of the stopped-web-application case: a + * class the implementation can no longer reach fails every call with + * a LinkageError, which no implementation author throws on purpose. + */ + public static class ErrorThrowingValidator implements Validator { + @Override + public boolean validate(Connection connection, int validateAction) { + throw new NoClassDefFoundError("the context this validator was loaded from is stopped"); + } + } + /** A validator that only fails, the way the throwing one now fails. */ public static class RejectingValidator implements Validator { @Override @@ -52,6 +70,11 @@ class TestThrowingValidator extends JdbcPoolBaseTest { /* One-slot pool with a throwing validator, gated on by the caller's test flag. */ private DataSource throwingPool() { + return throwingPool(new ThrowingValidator()); + } + + /* As above, with the implementation of the caller's choosing. */ + private DataSource throwingPool(Validator validator) { DataSource ds = newDataSource(); ds.getPoolProperties().setInitialSize(0); // exactly one slot: any leaked slot exhausts the pool immediately @@ -61,7 +84,7 @@ class TestThrowingValidator extends JdbcPoolBaseTest { // background sweep would now be a working competitor for the idle // assertions below ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0); - ds.getPoolProperties().setValidator(new ThrowingValidator()); + ds.getPoolProperties().setValidator(validator); return ds; } @@ -147,6 +170,76 @@ class TestThrowingValidator extends JdbcPoolBaseTest { Assertions.assertEquals(0, ds.getPool().getSize()); } + @Test + void errorThrowingValidatorOnBorrowFailsAsCheckedException() throws Exception { + DataSource ds = throwingPool(new ErrorThrowingValidator()); + ds.getPoolProperties().setTestOnBorrow(true); + // The stopped-class-loader failure mode: the LinkageError must be + // read as a check that cannot vouch for the session, not as a + // condition beyond the pool's handling - a throw escaping the + // borrow would strand the session outside both queues, where no + // drain, sweep or abandoned check can ever reach it. + Connection first = ds.getConnection(); + awaitSessions(1); + first.close(); + Assertions.assertEquals(1, ds.getPool().getIdle()); + + SQLException sqlX = Assertions.assertThrows(SQLException.class, ds::getConnection); + Assertions.assertTrue(sqlX.getMessage().contains("Failed to validate"), + "expected a validation failure, got: " + sqlX.getMessage()); + awaitSessions(0); + Assertions.assertEquals(0, ds.getPool().getSize()); + + // and the slot survived its own validator: the pool is whole + ds.getPoolProperties().setValidator(null); + try (Connection again = ds.getConnection()) { + Assertions.assertNotNull(again); + } + // the returned session parks in idle, where it is whole and + // reusable - the failed borrow cost the pool nothing + Assertions.assertEquals(1, ds.getPool().getIdle()); + Assertions.assertEquals(1, ds.getPool().getSize()); + ds.close(); + awaitSessions(0); + } + + @Test + void errorThrowingValidatorOnReturnDoesNotEscapeOrStrand() throws Exception { + DataSource ds = throwingPool(new ErrorThrowingValidator()); + ds.getPoolProperties().setTestOnReturn(true); + Connection first = ds.getConnection(); + awaitSessions(1); + + Assertions.assertDoesNotThrow(first::close); + awaitSessions(0); + Assertions.assertEquals(0, ds.getPool().getIdle()); + Assertions.assertEquals(0, ds.getPool().getActive()); + Assertions.assertEquals(0, ds.getPool().getSize()); + + ds.getPoolProperties().setValidator(null); + try (Connection again = ds.getConnection()) { + Assertions.assertNotNull(again); + } + ds.close(); + awaitSessions(0); + } + + @Test + void errorThrowingValidatorOnIdleValidationDoesNotAbortTheSweep() throws Exception { + DataSource ds = throwingPool(new ErrorThrowingValidator()); + ds.getPoolProperties().setTestWhileIdle(true); + Connection first = ds.getConnection(); + first.close(); + awaitSessions(1); + Assertions.assertEquals(1, ds.getPool().getIdle()); + + // the Error must not escape the sweep with the tested connection + // claimed out of idle and stranded, nor leave it parked untested + Assertions.assertDoesNotThrow(ds::testIdle); + awaitSessions(0); + Assertions.assertEquals(0, ds.getPool().getIdle()); + } + @Test void rejectingValidatorOnBorrowFailsAndReleasesTheSlot() throws Exception { DataSource ds = throwingPool(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
