001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.jms.pool;
018
019import java.util.NoSuchElementException;
020import java.util.Properties;
021import java.util.concurrent.atomic.AtomicBoolean;
022import java.util.concurrent.atomic.AtomicReference;
023
024import javax.jms.Connection;
025import javax.jms.ConnectionFactory;
026import javax.jms.JMSException;
027import javax.jms.QueueConnection;
028import javax.jms.QueueConnectionFactory;
029import javax.jms.TopicConnection;
030import javax.jms.TopicConnectionFactory;
031
032import org.apache.commons.pool2.KeyedPooledObjectFactory;
033import org.apache.commons.pool2.PooledObject;
034import org.apache.commons.pool2.impl.DefaultPooledObject;
035import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
036import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * A JMS provider which pools Connection, Session and MessageProducer instances
042 * so it can be used with tools like <a href="http://camel.apache.org/activemq.html">Camel</a> and Spring's
043 * <a href="http://activemq.apache.org/spring-support.html">JmsTemplate and MessagListenerContainer</a>.
044 * Connections, sessions and producers are returned to a pool after use so that they can be reused later
045 * without having to undergo the cost of creating them again.
046 *
047 * b>NOTE:</b> while this implementation does allow the creation of a collection of active consumers,
048 * it does not 'pool' consumers. Pooling makes sense for connections, sessions and producers, which
049 * are expensive to create and can remain idle a minimal cost. Consumers, on the other hand, are usually
050 * just created at startup and left active, handling incoming messages as they come. When a consumer is
051 * complete, it is best to close it rather than return it to a pool for later reuse: this is because,
052 * even if a consumer is idle, ActiveMQ will keep delivering messages to the consumer's prefetch buffer,
053 * where they'll get held until the consumer is active again.
054 *
055 * If you are creating a collection of consumers (for example, for multi-threaded message consumption), you
056 * might want to consider using a lower prefetch value for each consumer (e.g. 10 or 20), to ensure that
057 * all messages don't end up going to just one of the consumers. See this FAQ entry for more detail:
058 * http://activemq.apache.org/i-do-not-receive-messages-in-my-second-consumer.html
059 *
060 * Optionally, one may configure the pool to examine and possibly evict objects as they sit idle in the
061 * pool. This is performed by an "idle object eviction" thread, which runs asynchronously. Caution should
062 * be used when configuring this optional feature. Eviction runs contend with client threads for access
063 * to objects in the pool, so if they run too frequently performance issues may result. The idle object
064 * eviction thread may be configured using the {@link org.apache.activemq.jms.pool.PooledConnectionFactory#setTimeBetweenExpirationCheckMillis} method.  By
065 * default the value is -1 which means no eviction thread will be run.  Set to a non-negative value to
066 * configure the idle eviction thread to run.
067 */
068public class PooledConnectionFactory implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory {
069    private static final transient Logger LOG = LoggerFactory.getLogger(PooledConnectionFactory.class);
070
071    protected final AtomicBoolean stopped = new AtomicBoolean(false);
072    private GenericKeyedObjectPool<ConnectionKey, ConnectionPool> connectionsPool;
073
074    protected Object connectionFactory;
075
076    private int maximumActiveSessionPerConnection = 500;
077    private int idleTimeout = 30 * 1000;
078    private int connectionTimeout = 30 * 1000;
079    private boolean blockIfSessionPoolIsFull = true;
080    private long blockIfSessionPoolIsFullTimeout = -1L;
081    private long expiryTimeout = 0l;
082    private boolean createConnectionOnStartup = true;
083    private boolean useAnonymousProducers = true;
084    private boolean reconnectOnException = true;
085
086    // Temporary value used to always fetch the result of makeObject.
087    private final AtomicReference<ConnectionPool> mostRecentlyCreated = new AtomicReference<ConnectionPool>(null);
088
089    public void initConnectionsPool() {
090        if (this.connectionsPool == null) {
091            final GenericKeyedObjectPoolConfig poolConfig = new GenericKeyedObjectPoolConfig();
092            poolConfig.setJmxEnabled(false);
093            this.connectionsPool = new GenericKeyedObjectPool<ConnectionKey, ConnectionPool>(
094                new KeyedPooledObjectFactory<ConnectionKey, ConnectionPool>() {
095                    @Override
096                    public PooledObject<ConnectionPool> makeObject(ConnectionKey connectionKey) throws Exception {
097                        Connection delegate = createConnection(connectionKey);
098
099                        ConnectionPool connection = createConnectionPool(delegate);
100                        connection.setIdleTimeout(getIdleTimeout());
101                        connection.setExpiryTimeout(getExpiryTimeout());
102                        connection.setMaximumActiveSessionPerConnection(getMaximumActiveSessionPerConnection());
103                        connection.setBlockIfSessionPoolIsFull(isBlockIfSessionPoolIsFull());
104                        if (isBlockIfSessionPoolIsFull() && getBlockIfSessionPoolIsFullTimeout() > 0) {
105                            connection.setBlockIfSessionPoolIsFullTimeout(getBlockIfSessionPoolIsFullTimeout());
106                        }
107                        connection.setUseAnonymousProducers(isUseAnonymousProducers());
108                        connection.setReconnectOnException(isReconnectOnException());
109
110                        LOG.trace("Created new connection: {}", connection);
111
112                        PooledConnectionFactory.this.mostRecentlyCreated.set(connection);
113
114                        return new DefaultPooledObject<ConnectionPool>(connection);
115                    }
116
117                    @Override
118                    public void destroyObject(ConnectionKey connectionKey, PooledObject<ConnectionPool> pooledObject) throws Exception {
119                        ConnectionPool connection = pooledObject.getObject();
120                        try {
121                            LOG.trace("Destroying connection: {}", connection);
122                            connection.close();
123                        } catch (Exception e) {
124                            LOG.warn("Close connection failed for connection: " + connection + ". This exception will be ignored.",e);
125                        }
126                    }
127
128                    @Override
129                    public boolean validateObject(ConnectionKey connectionKey, PooledObject<ConnectionPool> pooledObject) {
130                        ConnectionPool connection = pooledObject.getObject();
131                        if (connection != null && connection.expiredCheck()) {
132                            LOG.trace("Connection has expired: {} and will be destroyed", connection);
133                            return false;
134                        }
135
136                        return true;
137                    }
138
139                    @Override
140                    public void activateObject(ConnectionKey connectionKey, PooledObject<ConnectionPool> pooledObject) throws Exception {
141                    }
142
143                    @Override
144                    public void passivateObject(ConnectionKey connectionKey, PooledObject<ConnectionPool> pooledObject) throws Exception {
145                    }
146
147                }, poolConfig);
148
149            // Set max wait time to control borrow from pool.
150            this.connectionsPool.setMaxWaitMillis(getConnectionTimeout());
151
152            // Set max idle (not max active) since our connections always idle in the pool.
153            this.connectionsPool.setMaxIdlePerKey(1);
154            this.connectionsPool.setLifo(false);
155
156            // We always want our validate method to control when idle objects are evicted.
157            this.connectionsPool.setTestOnBorrow(true);
158            this.connectionsPool.setTestWhileIdle(true);
159        }
160    }
161
162    /**
163     * @return the currently configured ConnectionFactory used to create the pooled Connections.
164     */
165    public Object getConnectionFactory() {
166        return connectionFactory;
167    }
168
169    /**
170     * Sets the ConnectionFactory used to create new pooled Connections.
171     * <p/>
172     * Updates to this value do not affect Connections that were previously created and placed
173     * into the pool.  In order to allocate new Connections based off this new ConnectionFactory
174     * it is first necessary to {@link #clear} the pooled Connections.
175     *
176     * @param toUse
177     *      The factory to use to create pooled Connections.
178     */
179    public void setConnectionFactory(final Object toUse) {
180        if (toUse instanceof ConnectionFactory) {
181            this.connectionFactory = toUse;
182        } else {
183            throw new IllegalArgumentException("connectionFactory should implement javax.jms.ConnectionFactory");
184        }
185    }
186
187    @Override
188    public QueueConnection createQueueConnection() throws JMSException {
189        return (QueueConnection) createConnection();
190    }
191
192    @Override
193    public QueueConnection createQueueConnection(String userName, String password) throws JMSException {
194        return (QueueConnection) createConnection(userName, password);
195    }
196
197    @Override
198    public TopicConnection createTopicConnection() throws JMSException {
199        return (TopicConnection) createConnection();
200    }
201
202    @Override
203    public TopicConnection createTopicConnection(String userName, String password) throws JMSException {
204        return (TopicConnection) createConnection(userName, password);
205    }
206
207    @Override
208    public Connection createConnection() throws JMSException {
209        return createConnection(null, null);
210    }
211
212    @Override
213    public synchronized Connection createConnection(String userName, String password) throws JMSException {
214        if (stopped.get()) {
215            LOG.debug("PooledConnectionFactory is stopped, skip create new connection.");
216            return null;
217        }
218
219        ConnectionPool connection = null;
220        ConnectionKey key = new ConnectionKey(userName, password);
221
222        // This will either return an existing non-expired ConnectionPool or it
223        // will create a new one to meet the demand.
224        if (getConnectionsPool().getNumIdle(key) < getMaxConnections()) {
225            try {
226                connectionsPool.addObject(key);
227                connection = mostRecentlyCreated.getAndSet(null);
228                connection.incrementReferenceCount();
229            } catch (Exception e) {
230                throw createJmsException("Error while attempting to add new Connection to the pool", e);
231            }
232        } else {
233            try {
234                // We can race against other threads returning the connection when there is an
235                // expiration or idle timeout.  We keep pulling out ConnectionPool instances until
236                // we win and get a non-closed instance and then increment the reference count
237                // under lock to prevent another thread from triggering an expiration check and
238                // pulling the rug out from under us.
239                while (connection == null) {
240                    try {
241                        connection = connectionsPool.borrowObject(key);
242                    } catch (NoSuchElementException ex) {
243                        if (!"Timeout waiting for idle object".equals(ex.getMessage())) {
244                            throw ex;
245                        }
246                    }
247                    if (connection != null) {
248                        synchronized (connection) {
249                            if (connection.getConnection() != null) {
250                                connection.incrementReferenceCount();
251                                break;
252                            }
253
254                            // Return the bad one to the pool and let if get destroyed as normal.
255                            connectionsPool.returnObject(key, connection);
256                            connection = null;
257                        }
258                    }
259                }
260            } catch (Exception e) {
261                throw createJmsException("Error while attempting to retrieve a connection from the pool", e);
262            }
263
264            try {
265                connectionsPool.returnObject(key, connection);
266            } catch (Exception e) {
267                throw createJmsException("Error when returning connection to the pool", e);
268            }
269        }
270
271        return newPooledConnection(connection);
272    }
273
274    protected Connection newPooledConnection(ConnectionPool connection) {
275        return new PooledConnection(connection);
276    }
277
278    private JMSException createJmsException(String msg, Exception cause) {
279        JMSException exception = new JMSException(msg);
280        exception.setLinkedException(cause);
281        exception.initCause(cause);
282        return exception;
283    }
284
285    protected Connection createConnection(ConnectionKey key) throws JMSException {
286        if (connectionFactory instanceof ConnectionFactory) {
287            if (key.getUserName() == null && key.getPassword() == null) {
288                return ((ConnectionFactory) connectionFactory).createConnection();
289            } else {
290                return ((ConnectionFactory) connectionFactory).createConnection(key.getUserName(), key.getPassword());
291            }
292        } else {
293            throw new IllegalStateException("connectionFactory should implement javax.jms.ConnectionFactory");
294        }
295    }
296
297    public void start() {
298        LOG.debug("Staring the PooledConnectionFactory: create on start = {}", isCreateConnectionOnStartup());
299        stopped.set(false);
300        if (isCreateConnectionOnStartup()) {
301            try {
302                // warm the pool by creating a connection during startup
303                createConnection().close();
304            } catch (JMSException e) {
305                LOG.warn("Create pooled connection during start failed. This exception will be ignored.", e);
306            }
307        }
308    }
309
310    public void stop() {
311        if (stopped.compareAndSet(false, true)) {
312            LOG.debug("Stopping the PooledConnectionFactory, number of connections in cache: {}",
313                      connectionsPool != null ? connectionsPool.getNumActive() : 0);
314            try {
315                if (connectionsPool != null) {
316                    connectionsPool.close();
317                    connectionsPool = null;
318                }
319            } catch (Exception e) {
320            }
321        }
322    }
323
324    /**
325     * Clears all connections from the pool.  Each connection that is currently in the pool is
326     * closed and removed from the pool.  A new connection will be created on the next call to
327     * {@link #createConnection}.  Care should be taken when using this method as Connections that
328     * are in use be client's will be closed.
329     */
330    public void clear() {
331        if (stopped.get()) {
332            return;
333        }
334
335        getConnectionsPool().clear();
336    }
337
338    /**
339     * Returns the currently configured maximum number of sessions a pooled Connection will
340     * create before it either blocks or throws an exception when a new session is requested,
341     * depending on configuration.
342     *
343     * @return the number of session instances that can be taken from a pooled connection.
344     */
345    public int getMaximumActiveSessionPerConnection() {
346        return maximumActiveSessionPerConnection;
347    }
348
349    /**
350     * Sets the maximum number of active sessions per connection
351     *
352     * @param maximumActiveSessionPerConnection
353     *      The maximum number of active session per connection in the pool.
354     */
355    public void setMaximumActiveSessionPerConnection(int maximumActiveSessionPerConnection) {
356        this.maximumActiveSessionPerConnection = maximumActiveSessionPerConnection;
357    }
358
359    /**
360     * Controls the behavior of the internal session pool. By default the call to
361     * Connection.getSession() will block if the session pool is full.  If the
362     * argument false is given, it will change the default behavior and instead the
363     * call to getSession() will throw a JMSException.
364     *
365     * The size of the session pool is controlled by the @see #maximumActive
366     * property.
367     *
368     * @param block - if true, the call to getSession() blocks if the pool is full
369     * until a session object is available.  defaults to true.
370     */
371    public void setBlockIfSessionPoolIsFull(boolean block) {
372        this.blockIfSessionPoolIsFull = block;
373    }
374
375    /**
376     * Returns whether a pooled Connection will enter a blocked state or will throw an Exception
377     * once the maximum number of sessions has been borrowed from the the Session Pool.
378     *
379     * @return true if the pooled Connection createSession method will block when the limit is hit.
380     * @see #setBlockIfSessionPoolIsFull(boolean)
381     */
382    public boolean isBlockIfSessionPoolIsFull() {
383        return this.blockIfSessionPoolIsFull;
384    }
385
386    /**
387     * Returns the maximum number to pooled Connections that this factory will allow before it
388     * begins to return connections from the pool on calls to ({@link #createConnection}.
389     *
390     * @return the maxConnections that will be created for this pool.
391     */
392    public int getMaxConnections() {
393        return getConnectionsPool().getMaxIdlePerKey();
394    }
395
396    /**
397     * Sets the maximum number of pooled Connections (defaults to one).  Each call to
398     * {@link #createConnection} will result in a new Connection being create up to the max
399     * connections value.
400     *
401     * @param maxConnections the maxConnections to set
402     */
403    public void setMaxConnections(int maxConnections) {
404        getConnectionsPool().setMaxIdlePerKey(maxConnections);
405        getConnectionsPool().setMaxTotalPerKey(maxConnections);
406    }
407
408    /**
409     * Gets the Idle timeout value applied to new Connection's that are created by this pool.
410     * <p/>
411     * The idle timeout is used determine if a Connection instance has sat to long in the pool unused
412     * and if so is closed and removed from the pool.  The default value is 30 seconds.
413     *
414     * @return idle timeout value (milliseconds)
415     */
416    public int getIdleTimeout() {
417        return idleTimeout;
418    }
419
420    /**
421     * Sets the idle timeout  value for Connection's that are created by this pool in Milliseconds,
422     * defaults to 30 seconds.
423     * <p/>
424     * For a Connection that is in the pool but has no current users the idle timeout determines how
425     * long the Connection can live before it is eligible for removal from the pool.  Normally the
426     * connections are tested when an attempt to check one out occurs so a Connection instance can sit
427     * in the pool much longer than its idle timeout if connections are used infrequently.
428     *
429     * @param idleTimeout
430     *      The maximum time a pooled Connection can sit unused before it is eligible for removal.
431     */
432    public void setIdleTimeout(int idleTimeout) {
433        this.idleTimeout = idleTimeout;
434    }
435
436    /**
437     * Gets the connection timeout value. The maximum time waited to get a Connection from the pool.
438     * The default value is 30 seconds.
439     *
440     * @return connection timeout value (milliseconds)
441     */
442    public int getConnectionTimeout() {
443        return connectionTimeout;
444    }
445
446    /**
447     * Sets the connection timeout value for getting Connections from this pool in Milliseconds,
448     * defaults to 30 seconds.
449     *
450     * @param connectionTimeout
451     *      The maximum time to wait for getting a pooled Connection.
452     */
453    public void setConnectionTimeout(int connectionTimeout) {
454        this.connectionTimeout = connectionTimeout;
455    }
456
457    /**
458     * allow connections to expire, irrespective of load or idle time. This is useful with failover
459     * to force a reconnect from the pool, to reestablish load balancing or use of the master post recovery
460     *
461     * @param expiryTimeout non zero in milliseconds
462     */
463    public void setExpiryTimeout(long expiryTimeout) {
464        this.expiryTimeout = expiryTimeout;
465    }
466
467    /**
468     * @return the configured expiration timeout for connections in the pool.
469     */
470    public long getExpiryTimeout() {
471        return expiryTimeout;
472    }
473
474    /**
475     * @return true if a Connection is created immediately on a call to {@link start}.
476     */
477    public boolean isCreateConnectionOnStartup() {
478        return createConnectionOnStartup;
479    }
480
481    /**
482     * Whether to create a connection on starting this {@link PooledConnectionFactory}.
483     * <p/>
484     * This can be used to warm-up the pool on startup. Notice that any kind of exception
485     * happens during startup is logged at WARN level and ignored.
486     *
487     * @param createConnectionOnStartup <tt>true</tt> to create a connection on startup
488     */
489    public void setCreateConnectionOnStartup(boolean createConnectionOnStartup) {
490        this.createConnectionOnStartup = createConnectionOnStartup;
491    }
492
493    /**
494     * Should Sessions use one anonymous producer for all producer requests or should a new
495     * MessageProducer be created for each request to create a producer object, default is true.
496     *
497     * When enabled the session only needs to allocate one MessageProducer for all requests and
498     * the MessageProducer#send(destination, message) method can be used.  Normally this is the
499     * right thing to do however it does result in the Broker not showing the producers per
500     * destination.
501     *
502     * @return true if a PooledSession will use only a single anonymous message producer instance.
503     */
504    public boolean isUseAnonymousProducers() {
505        return this.useAnonymousProducers;
506    }
507
508    /**
509     * Sets whether a PooledSession uses only one anonymous MessageProducer instance or creates
510     * a new MessageProducer for each call the create a MessageProducer.
511     *
512     * @param value
513     *      Boolean value that configures whether anonymous producers are used.
514     */
515    public void setUseAnonymousProducers(boolean value) {
516        this.useAnonymousProducers = value;
517    }
518
519    /**
520     * Gets the Pool of ConnectionPool instances which are keyed by different ConnectionKeys.
521     *
522     * @return this factories pool of ConnectionPool instances.
523     */
524    protected GenericKeyedObjectPool<ConnectionKey, ConnectionPool> getConnectionsPool() {
525        initConnectionsPool();
526        return this.connectionsPool;
527    }
528
529    /**
530     * Sets the number of milliseconds to sleep between runs of the idle Connection eviction thread.
531     * When non-positive, no idle object eviction thread will be run, and Connections will only be
532     * checked on borrow to determine if they have sat idle for too long or have failed for some
533     * other reason.
534     * <p/>
535     * By default this value is set to -1 and no expiration thread ever runs.
536     *
537     * @param timeBetweenExpirationCheckMillis
538     *      The time to wait between runs of the idle Connection eviction thread.
539     */
540    public void setTimeBetweenExpirationCheckMillis(long timeBetweenExpirationCheckMillis) {
541        getConnectionsPool().setTimeBetweenEvictionRunsMillis(timeBetweenExpirationCheckMillis);
542    }
543
544    /**
545     * @return the number of milliseconds to sleep between runs of the idle connection eviction thread.
546     */
547    public long getTimeBetweenExpirationCheckMillis() {
548        return getConnectionsPool().getTimeBetweenEvictionRunsMillis();
549    }
550
551    /**
552     * @return the number of Connections currently in the Pool
553     */
554    public int getNumConnections() {
555        return getConnectionsPool().getNumIdle();
556    }
557
558    /**
559     * Delegate that creates each instance of an ConnectionPool object.  Subclasses can override
560     * this method to customize the type of connection pool returned.
561     *
562     * @param connection
563     *
564     * @return instance of a new ConnectionPool.
565     */
566    protected ConnectionPool createConnectionPool(Connection connection) {
567        return new ConnectionPool(connection);
568    }
569
570    /**
571     * Returns the timeout to use for blocking creating new sessions
572     *
573     * @return true if the pooled Connection createSession method will block when the limit is hit.
574     * @see #setBlockIfSessionPoolIsFull(boolean)
575     */
576    public long getBlockIfSessionPoolIsFullTimeout() {
577        return blockIfSessionPoolIsFullTimeout;
578    }
579
580    /**
581     * Controls the behavior of the internal session pool. By default the call to
582     * Connection.getSession() will block if the session pool is full.  This setting
583     * will affect how long it blocks and throws an exception after the timeout.
584     *
585     * The size of the session pool is controlled by the @see #maximumActive
586     * property.
587     *
588     * Whether or not the call to create session blocks is controlled by the @see #blockIfSessionPoolIsFull
589     * property
590     *
591     * @param blockIfSessionPoolIsFullTimeout - if blockIfSessionPoolIsFullTimeout is true,
592     *                                        then use this setting to configure how long to block before retry
593     */
594    public void setBlockIfSessionPoolIsFullTimeout(long blockIfSessionPoolIsFullTimeout) {
595        this.blockIfSessionPoolIsFullTimeout = blockIfSessionPoolIsFullTimeout;
596    }
597
598    /**
599     * @return true if the underlying connection will be renewed on JMSException, false otherwise
600     */
601    public boolean isReconnectOnException() {
602        return reconnectOnException;
603    }
604
605    /**
606     * Controls weather the underlying connection should be reset (and renewed) on JMSException
607     *
608     * @param reconnectOnException
609     *          Boolean value that configures whether reconnect on exception should happen
610     */
611    public void setReconnectOnException(boolean reconnectOnException) {
612        this.reconnectOnException = reconnectOnException;
613    }
614
615    /**
616     * Called by any superclass that implements a JNDIReferencable or similar that needs to collect
617     * the properties of this class for storage etc.
618     *
619     * This method should be updated any time there is a new property added.
620     *
621     * @param props
622     *        a properties object that should be filled in with this objects property values.
623     */
624    protected void populateProperties(Properties props) {
625        props.setProperty("maximumActiveSessionPerConnection", Integer.toString(getMaximumActiveSessionPerConnection()));
626        props.setProperty("maxConnections", Integer.toString(getMaxConnections()));
627        props.setProperty("idleTimeout", Integer.toString(getIdleTimeout()));
628        props.setProperty("expiryTimeout", Long.toString(getExpiryTimeout()));
629        props.setProperty("timeBetweenExpirationCheckMillis", Long.toString(getTimeBetweenExpirationCheckMillis()));
630        props.setProperty("createConnectionOnStartup", Boolean.toString(isCreateConnectionOnStartup()));
631        props.setProperty("useAnonymousProducers", Boolean.toString(isUseAnonymousProducers()));
632        props.setProperty("blockIfSessionPoolIsFullTimeout", Long.toString(getBlockIfSessionPoolIsFullTimeout()));
633        props.setProperty("reconnectOnException", Boolean.toString(isReconnectOnException()));
634    }
635}