/* * Copyright 2002-2016 the original author or authors. * * Licensed 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.springframework.integration.file.remote.session; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.integration.util.SimplePool; import org.springframework.util.Assert; /** * A {@link SessionFactory} implementation that caches Sessions for reuse without * requiring reconnection each time the Session is retrieved from the factory. * This implementation wraps and delegates to a target SessionFactory instance. * * @author Josh Long * @author Oleg Zhurakousky * @author Mark Fisher * @author Gary Russell * @since 2.0 */ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBean { private static final Log logger = LogFactory.getLog(CachingSessionFactory.class); private final SessionFactory<F> sessionFactory; private final SimplePool<Session<F>> pool; private final boolean isSharedSessionCapable; private volatile long sharedSessionEpoch; /** * Create a CachingSessionFactory with an unlimited number of sessions. * * @param sessionFactory the underlying session factory. */ public CachingSessionFactory(SessionFactory<F> sessionFactory) { this(sessionFactory, 0); } /** * Create a CachingSessionFactory with the specified session limit. By default, if * no sessions are available in the cache, and the size limit has been reached, * calling threads will block until a session is available. * <p> * Do not cache a {@link DelegatingSessionFactory}, cache each delegate therein instead. * @see #setSessionWaitTimeout(long) * @see #setPoolSize(int) * * @param sessionFactory The underlying session factory. * @param sessionCacheSize The maximum cache size. */ public CachingSessionFactory(SessionFactory<F> sessionFactory, int sessionCacheSize) { Assert.isTrue(!(sessionFactory instanceof DelegatingSessionFactory), "'sessionFactory' cannot be a 'DelegatingSessionFactory'; cache each delegate instead"); this.sessionFactory = sessionFactory; this.pool = new SimplePool<Session<F>>(sessionCacheSize, new SimplePool.PoolItemCallback<Session<F>>() { @Override public Session<F> createForPool() { return CachingSessionFactory.this.sessionFactory.getSession(); } @Override public boolean isStale(Session<F> session) { return !session.isOpen(); } @Override public void removedFromPool(Session<F> session) { session.close(); } }); this.isSharedSessionCapable = sessionFactory instanceof SharedSessionCapable; } /** * Sets the limit of how long to wait for a session to become available. * * @param sessionWaitTimeout the session wait timeout. * @throws IllegalStateException if the wait expires prior to a Session becoming available. */ public void setSessionWaitTimeout(long sessionWaitTimeout) { this.pool.setWaitTimeout(sessionWaitTimeout); } /** * Modify the target session pool size; the actual pool size will adjust up/down * to this size as and when sessions are requested or retrieved. * * @param poolSize The pool size. */ public void setPoolSize(int poolSize) { this.pool.setPoolSize(poolSize); } /** * Get a session from the pool (or block if none available). */ @Override public Session<F> getSession() { return new CachedSession(this.pool.getItem(), this.sharedSessionEpoch); } /** * Remove (close) any unused sessions in the pool. */ @Override public void destroy() { this.pool.removeAllIdleItems(); } /** * Clear the cache of sessions; also any in-use sessions will be closed when * returned to the cache. */ public synchronized void resetCache() { if (logger.isDebugEnabled()) { logger.debug("Cache reset; idle sessions will be removed, in-use sessions will be closed when returned"); } if (this.isSharedSessionCapable && ((SharedSessionCapable) this.sessionFactory).isSharedSession()) { ((SharedSessionCapable) this.sessionFactory).resetSharedSession(); } long sharedSessionEpoch = System.nanoTime(); /* * Spin until we get a new value - nano precision but may be lower resolution. * We reset the epoch AFTER resetting the shared session so there is no possibility * of an "old" session being created in the new epoch. There is a slight possibility * that a "new" session might appear in the old epoch and thus be closed when returned to * the cache. */ while (sharedSessionEpoch == this.sharedSessionEpoch) { sharedSessionEpoch = System.nanoTime(); } this.sharedSessionEpoch = sharedSessionEpoch; this.pool.removeAllIdleItems(); } public class CachedSession implements Session<F> { //NOSONAR (final) private final Session<F> targetSession; private boolean released; private boolean dirty; /** * The epoch in which this session was created. */ private final long sharedSessionEpoch; private CachedSession(Session<F> targetSession, long sharedSessionEpoch) { this.targetSession = targetSession; this.sharedSessionEpoch = sharedSessionEpoch; } @Override public synchronized void close() { if (this.released) { if (logger.isDebugEnabled()) { logger.debug("Session " + this.targetSession + " already released."); } } else { if (logger.isDebugEnabled()) { logger.debug("Releasing Session " + this.targetSession + " back to the pool."); } if (this.sharedSessionEpoch != CachingSessionFactory.this.sharedSessionEpoch) { if (logger.isDebugEnabled()) { logger.debug("Closing session " + this.targetSession + " after reset."); } this.targetSession.close(); } else if (this.dirty) { this.targetSession.close(); } if (this.targetSession.isOpen()) { try { this.targetSession.finalizeRaw(); } catch (IOException e) { //No-op in this context } } CachingSessionFactory.this.pool.releaseItem(this.targetSession); this.released = true; } } @Override public boolean remove(String path) throws IOException { return this.targetSession.remove(path); } @Override public F[] list(String path) throws IOException { return this.targetSession.list(path); } @Override public void read(String source, OutputStream os) throws IOException { this.targetSession.read(source, os); } @Override public void write(InputStream inputStream, String destination) throws IOException { this.targetSession.write(inputStream, destination); } @Override public void append(InputStream inputStream, String destination) throws IOException { this.targetSession.append(inputStream, destination); } @Override public boolean isOpen() { return this.targetSession.isOpen(); } @Override public void rename(String pathFrom, String pathTo) throws IOException { this.targetSession.rename(pathFrom, pathTo); } @Override public boolean mkdir(String directory) throws IOException { return this.targetSession.mkdir(directory); } @Override public boolean rmdir(String directory) throws IOException { return this.targetSession.rmdir(directory); } @Override public boolean exists(String path) throws IOException { return this.targetSession.exists(path); } @Override public String[] listNames(String path) throws IOException { return this.targetSession.listNames(path); } @Override public InputStream readRaw(String source) throws IOException { return this.targetSession.readRaw(source); } @Override public boolean finalizeRaw() throws IOException { return this.targetSession.finalizeRaw(); } public void dirty() { this.dirty = true; } @Override public Object getClientInstance() { return this.targetSession.getClientInstance(); } } }