INT-3047 SFTP Multiplex Over Single Connection

Enable the use of multiple JSch channels over a
single connection.

JIRA: https://jira.springsource.org/browse/INT-3047

INT-3047: Polishing locks logic

INT-3047 SFTP Reset Cache; Quiesce Shared Sessions

Support resetCache() on the CachingSessionFactory
- reset a shared JSch session so it is reestablished on next use
- immediately close idle sessions
- close in-use sessions as they are returned
- close the channel when an SftpSession is closed and a shared JSch is being used
- physically close a shared JSch session only when the last channel is closed

Polishing

Forgot to save the ftp.xml.

Change CachedSession.epoch to created.

INT-3047 Polishing - PR Comments

- Also add missing docs for `preserve-timestamp`

INT-3047 Polish - PR Comments

Also change epoch to nanoseconds and use a simple != comparison with the
epoch in which a session was created.
This commit is contained in:
Gary Russell
2013-11-07 21:09:33 +02:00
committed by Artem Bilan
parent e128c80c19
commit 7bc4fe2d0b
12 changed files with 649 additions and 19 deletions

View File

@@ -45,6 +45,10 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
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.
@@ -80,6 +84,7 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
session.close();
}
});
this.isSharedSessionCapable = sessionFactory instanceof SharedSessionCapable;
}
@@ -105,7 +110,7 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
*/
@Override
public Session<F> getSession() {
return new CachedSession(this.pool.getItem());
return new CachedSession(this.pool.getItem(), this.sharedSessionEpoch);
}
/**
@@ -116,27 +121,64 @@ public class CachingSessionFactory<F> implements SessionFactory<F>, DisposableBe
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();
}
private class CachedSession implements Session<F> {
private final Session<F> targetSession;
private boolean released;
private volatile boolean released;
private CachedSession(Session<F> targetSession) {
/**
* 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 (released) {
if (logger.isDebugEnabled()){
logger.debug("Session already released.");
logger.debug("Session " + targetSession + " already released.");
}
}
else {
if (logger.isDebugEnabled()){
logger.debug("Releasing Session back to the pool.");
logger.debug("Releasing Session " + targetSession + " back to the pool.");
}
if (this.sharedSessionEpoch != CachingSessionFactory.this.sharedSessionEpoch) {
if (logger.isDebugEnabled()){
logger.debug("Closing session " + targetSession + " after reset.");
}
this.targetSession.close();
}
pool.releaseItem(targetSession);
released = true;

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2013 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;
/**
* A {@link SessionFactory} that implements this interface is capable of supporting a shared session.
*
* @author Gary Russell
* @since 3.0
*
*/
public interface SharedSessionCapable {
/**
* @return true if this factory uses a shared session.
*/
public abstract boolean isSharedSession();
/**
* Resets the shared session so the next {@code #getSession()} will return a session
* using a new connection.
*/
public abstract void resetSharedSession();
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2013 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.junit.Test;
import org.springframework.integration.test.util.TestUtils;
/**
* @author Gary Russell
* @since 3.0
*
*/
public class CachingSessionFactoryTests {
@Test
public void testCacheAndReset() {
TestSessionFactory factory = new TestSessionFactory();
CachingSessionFactory<String> cache = new CachingSessionFactory<String>(factory);
Session<String> sess1 = cache.getSession();
assertEquals("session:1", TestUtils.getPropertyValue(sess1, "targetSession.id"));
Session<String> sess2 = cache.getSession();
assertEquals("session:2", TestUtils.getPropertyValue(sess2, "targetSession.id"));
sess1.close();
// session back to pool; should be open and reused.
assertTrue(sess1.isOpen());
sess1 = cache.getSession();
assertEquals("session:1", TestUtils.getPropertyValue(sess1, "targetSession.id"));
sess1.close();
assertTrue(sess1.isOpen());
// reset the cache; should close idle (sess1); sess2 should closed later
cache.resetCache();
assertFalse(sess1.isOpen());
sess1 = cache.getSession();
assertEquals("session:3", TestUtils.getPropertyValue(sess1, "targetSession.id"));
sess1.close();
assertTrue(sess1.isOpen());
// session from previous epoch is closed on return
sess2.close();
assertFalse(sess2.isOpen());
cache.resetCache();
assertFalse(sess1.isOpen());
}
private class TestSessionFactory implements SessionFactory<String> {
private int n;
@Override
public Session<String> getSession() {
return new TestSession("session:" + ++n);
}
}
private class TestSession implements Session<String> {
@SuppressWarnings("unused")
private final String id;
private volatile boolean open = true;
private TestSession(String id) {
this.id = id;
}
@Override
public boolean remove(String path) throws IOException {
return false;
}
@Override
public String[] list(String path) throws IOException {
return null;
}
@Override
public void read(String source, OutputStream outputStream) throws IOException {
}
@Override
public void write(InputStream inputStream, String destination) throws IOException {
}
@Override
public boolean mkdir(String directory) throws IOException {
return false;
}
@Override
public void rename(String pathFrom, String pathTo) throws IOException {
}
@Override
public void close() {
this.open = false;
}
@Override
public boolean isOpen() {
return this.open;
}
@Override
public boolean exists(String path) throws IOException {
return false;
}
@Override
public String[] listNames(String path) throws IOException {
return null;
}
@Override
public InputStream readRaw(String source) throws IOException {
return null;
}
@Override
public boolean finalizeRaw() throws IOException {
return false;
}
}
}