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 */
017
018package org.apache.activemq.jms.pool;
019
020import java.util.List;
021import java.util.concurrent.CopyOnWriteArrayList;
022import java.util.concurrent.atomic.AtomicBoolean;
023
024import javax.jms.Connection;
025import javax.jms.ExceptionListener;
026import javax.jms.IllegalStateException;
027import javax.jms.JMSException;
028import javax.jms.Session;
029import javax.jms.TemporaryQueue;
030import javax.jms.TemporaryTopic;
031
032import org.apache.commons.pool.KeyedPoolableObjectFactory;
033import org.apache.commons.pool.impl.GenericKeyedObjectPool;
034import org.apache.commons.pool.impl.GenericObjectPool;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * Holds a real JMS connection along with the session pools associated with it.
040 * <p/>
041 * Instances of this class are shared amongst one or more PooledConnection object and must
042 * track the session objects that are loaned out for cleanup on close as well as ensuring
043 * that the temporary destinations of the managed Connection are purged when all references
044 * to this ConnectionPool are released.
045 */
046public class ConnectionPool implements ExceptionListener {
047    private static final transient Logger LOG = LoggerFactory.getLogger(ConnectionPool.class);
048
049    protected Connection connection;
050    private int referenceCount;
051    private long lastUsed = System.currentTimeMillis();
052    private final long firstUsed = lastUsed;
053    private boolean hasExpired;
054    private int idleTimeout = 30 * 1000;
055    private long expiryTimeout = 0l;
056    private boolean useAnonymousProducers = true;
057
058    private final AtomicBoolean started = new AtomicBoolean(false);
059    private final GenericKeyedObjectPool<SessionKey, SessionHolder> sessionPool;
060    private final List<PooledSession> loanedSessions = new CopyOnWriteArrayList<PooledSession>();
061    private boolean reconnectOnException;
062    private ExceptionListener parentExceptionListener;
063
064    public ConnectionPool(Connection connection) {
065
066        this.connection = wrap(connection);
067        try {
068            this.connection.setExceptionListener(this);
069        } catch (JMSException ex) {
070            LOG.warn("Could not set exception listener on create of ConnectionPool");
071        }
072
073        // Create our internal Pool of session instances.
074        this.sessionPool = new GenericKeyedObjectPool<SessionKey, SessionHolder>(
075            new KeyedPoolableObjectFactory<SessionKey, SessionHolder>() {
076
077                @Override
078                public void activateObject(SessionKey key, SessionHolder session) throws Exception {
079                }
080
081                @Override
082                public void destroyObject(SessionKey key, SessionHolder session) throws Exception {
083                    session.close();
084                }
085
086                @Override
087                public SessionHolder makeObject(SessionKey key) throws Exception {
088                    return new SessionHolder(makeSession(key));
089                }
090
091                @Override
092                public void passivateObject(SessionKey key, SessionHolder session) throws Exception {
093                }
094
095                @Override
096                public boolean validateObject(SessionKey key, SessionHolder session) {
097                    return true;
098                }
099            }
100        );
101    }
102
103    // useful when external failure needs to force expiry
104    public void setHasExpired(boolean val) {
105        hasExpired = val;
106    }
107
108    protected Session makeSession(SessionKey key) throws JMSException {
109        return connection.createSession(key.isTransacted(), key.getAckMode());
110    }
111
112    protected Connection wrap(Connection connection) {
113        return connection;
114    }
115
116    protected void unWrap(Connection connection) {
117    }
118
119    public void start() throws JMSException {
120        if (started.compareAndSet(false, true)) {
121            try {
122                connection.start();
123            } catch (JMSException e) {
124                started.set(false);
125                throw(e);
126            }
127        }
128    }
129
130    public synchronized Connection getConnection() {
131        return connection;
132    }
133
134    public Session createSession(boolean transacted, int ackMode) throws JMSException {
135        SessionKey key = new SessionKey(transacted, ackMode);
136        PooledSession session;
137        try {
138            session = new PooledSession(key, sessionPool.borrowObject(key), sessionPool, key.isTransacted(), useAnonymousProducers);
139            session.addSessionEventListener(new PooledSessionEventListener() {
140
141                @Override
142                public void onTemporaryTopicCreate(TemporaryTopic tempTopic) {
143                }
144
145                @Override
146                public void onTemporaryQueueCreate(TemporaryQueue tempQueue) {
147                }
148
149                @Override
150                public void onSessionClosed(PooledSession session) {
151                    ConnectionPool.this.loanedSessions.remove(session);
152                }
153            });
154            this.loanedSessions.add(session);
155        } catch (Exception e) {
156            IllegalStateException illegalStateException = new IllegalStateException(e.toString());
157            illegalStateException.initCause(e);
158            throw illegalStateException;
159        }
160        return session;
161    }
162
163    public synchronized void close() {
164        if (connection != null) {
165            try {
166                sessionPool.close();
167            } catch (Exception e) {
168            } finally {
169                try {
170                    connection.close();
171                } catch (Exception e) {
172                } finally {
173                    connection = null;
174                }
175            }
176        }
177    }
178
179    public synchronized void incrementReferenceCount() {
180        referenceCount++;
181        lastUsed = System.currentTimeMillis();
182    }
183
184    public synchronized void decrementReferenceCount() {
185        referenceCount--;
186        lastUsed = System.currentTimeMillis();
187        if (referenceCount == 0) {
188            // Loaned sessions are those that are active in the sessionPool and
189            // have not been closed by the client before closing the connection.
190            // These need to be closed so that all session's reflect the fact
191            // that the parent Connection is closed.
192            for (PooledSession session : this.loanedSessions) {
193                try {
194                    session.close();
195                } catch (Exception e) {
196                }
197            }
198            this.loanedSessions.clear();
199
200            unWrap(getConnection());
201
202            expiredCheck();
203        }
204    }
205
206    /**
207     * Determines if this Connection has expired.
208     * <p/>
209     * A ConnectionPool is considered expired when all references to it are released AND either
210     * the configured idleTimeout has elapsed OR the configured expiryTimeout has elapsed.
211     * Once a ConnectionPool is determined to have expired its underlying Connection is closed.
212     *
213     * @return true if this connection has expired.
214     */
215    public synchronized boolean expiredCheck() {
216
217        boolean expired = false;
218
219        if (connection == null) {
220            return true;
221        }
222
223        if (hasExpired) {
224            if (referenceCount == 0) {
225                close();
226                expired = true;
227            }
228        }
229
230        if (expiryTimeout > 0 && System.currentTimeMillis() > firstUsed + expiryTimeout) {
231            hasExpired = true;
232            if (referenceCount == 0) {
233                close();
234                expired = true;
235            }
236        }
237
238        // Only set hasExpired here is no references, as a Connection with references is by
239        // definition not idle at this time.
240        if (referenceCount == 0 && idleTimeout > 0 && System.currentTimeMillis() > lastUsed + idleTimeout) {
241            hasExpired = true;
242            close();
243            expired = true;
244        }
245
246        return expired;
247    }
248
249    public int getIdleTimeout() {
250        return idleTimeout;
251    }
252
253    public void setIdleTimeout(int idleTimeout) {
254        this.idleTimeout = idleTimeout;
255    }
256
257    public void setExpiryTimeout(long expiryTimeout) {
258        this.expiryTimeout = expiryTimeout;
259    }
260
261    public long getExpiryTimeout() {
262        return expiryTimeout;
263    }
264
265    public int getMaximumActiveSessionPerConnection() {
266        return this.sessionPool.getMaxActive();
267    }
268
269    public void setMaximumActiveSessionPerConnection(int maximumActiveSessionPerConnection) {
270        this.sessionPool.setMaxActive(maximumActiveSessionPerConnection);
271    }
272
273    public boolean isUseAnonymousProducers() {
274        return this.useAnonymousProducers;
275    }
276
277    public void setUseAnonymousProducers(boolean value) {
278        this.useAnonymousProducers = value;
279    }
280
281    /**
282     * @return the total number of Pooled session including idle sessions that are not
283     *          currently loaned out to any client.
284     */
285    public int getNumSessions() {
286        return this.sessionPool.getNumIdle() + this.sessionPool.getNumActive();
287    }
288
289    /**
290     * @return the total number of Sessions that are in the Session pool but not loaned out.
291     */
292    public int getNumIdleSessions() {
293        return this.sessionPool.getNumIdle();
294    }
295
296    /**
297     * @return the total number of Session's that have been loaned to PooledConnection instances.
298     */
299    public int getNumActiveSessions() {
300        return this.sessionPool.getNumActive();
301    }
302
303    /**
304     * Configure whether the createSession method should block when there are no more idle sessions and the
305     * pool already contains the maximum number of active sessions.  If false the create method will fail
306     * and throw an exception.
307     *
308     * @param block
309     *          Indicates whether blocking should be used to wait for more space to create a session.
310     */
311    public void setBlockIfSessionPoolIsFull(boolean block) {
312        this.sessionPool.setWhenExhaustedAction(
313                (block ? GenericObjectPool.WHEN_EXHAUSTED_BLOCK : GenericObjectPool.WHEN_EXHAUSTED_FAIL));
314    }
315
316    public boolean isBlockIfSessionPoolIsFull() {
317        return this.sessionPool.getWhenExhaustedAction() == GenericObjectPool.WHEN_EXHAUSTED_BLOCK;
318    }
319
320    /**
321     * Returns the timeout to use for blocking creating new sessions
322     *
323     * @return true if the pooled Connection createSession method will block when the limit is hit.
324     * @see #setBlockIfSessionPoolIsFull(boolean)
325     */
326    public long getBlockIfSessionPoolIsFullTimeout() {
327        return this.sessionPool.getMaxWait();
328    }
329
330    /**
331     * Controls the behavior of the internal session pool. By default the call to
332     * Connection.getSession() will block if the session pool is full.  This setting
333     * will affect how long it blocks and throws an exception after the timeout.
334     *
335     * The size of the session pool is controlled by the @see #maximumActive
336     * property.
337     *
338     * Whether or not the call to create session blocks is controlled by the @see #blockIfSessionPoolIsFull
339     * property
340     *
341     * @param blockIfSessionPoolIsFullTimeout - if blockIfSessionPoolIsFullTimeout is true,
342     *                                        then use this setting to configure how long to block before retry
343     */
344    public void setBlockIfSessionPoolIsFullTimeout(long blockIfSessionPoolIsFullTimeout) {
345        this.sessionPool.setMaxWait(blockIfSessionPoolIsFullTimeout);
346    }
347
348    /**
349     * @return true if the underlying connection will be renewed on JMSException, false otherwise
350     */
351    public boolean isReconnectOnException() {
352        return reconnectOnException;
353    }
354
355    /**
356     * Controls weather the underlying connection should be reset (and renewed) on JMSException
357     *
358     * @param reconnectOnException
359     *          Boolean value that configures whether reconnect on exception should happen
360     */
361    public void setReconnectOnException(boolean reconnectOnException) {
362        this.reconnectOnException = reconnectOnException;
363    }
364
365    ExceptionListener getParentExceptionListener() {
366        return parentExceptionListener;
367    }
368
369    void setParentExceptionListener(ExceptionListener parentExceptionListener) {
370        this.parentExceptionListener = parentExceptionListener;
371    }
372
373    @Override
374    public void onException(JMSException exception) {
375        if (isReconnectOnException()) {
376            close();
377        }
378        if (parentExceptionListener != null) {
379            parentExceptionListener.onException(exception);
380        }
381    }
382
383    @Override
384    public String toString() {
385        return "ConnectionPool[" + connection + "]";
386    }
387}