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