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