/*
* Copyright 2016 Netflix, Inc.
*
* 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 io.reactivex.netty.client.pool;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.FileRegion;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutorGroup;
import io.reactivex.netty.channel.AllocatingTransformer;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.client.ClientConnectionToChannelBridge;
import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observable.Transformer;
import rx.Subscriber;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Actions;
import rx.functions.Func1;
/**
* An implementation of {@link Connection} which is pooled and reused.
*
* It is required to call {@link #reuse(Subscriber)} for reusing this connection.
*
* @param <R> Type of object that is read from this connection.
* @param <W> Type of object that is written to this connection.
*/
public class PooledConnection<R, W> extends Connection<R, W> {
private static final Logger logger = LoggerFactory.getLogger(PooledConnection.class);
public static final AttributeKey<Long> DYNAMIC_CONN_KEEP_ALIVE_TIMEOUT_MS =
AttributeKey.valueOf("rxnetty_conn_keep_alive_timeout_millis");
private final Owner owner;
private final Connection<R, W> unpooledDelegate;
private volatile long lastReturnToPoolTimeMillis;
private volatile boolean releasedAtLeastOnce;
private volatile long maxIdleTimeMillis;
private final Observable<Void> releaseObservable;
private PooledConnection(Owner owner, long maxIdleTimeMillis, Connection<R, W> unpooledDelegate) {
super(unpooledDelegate);
if (null == owner) {
throw new IllegalArgumentException("Pooled connection owner can not be null");
}
if (null == unpooledDelegate) {
throw new IllegalArgumentException("Connection delegate can not be null");
}
this.owner = owner;
this.unpooledDelegate = unpooledDelegate;
this.maxIdleTimeMillis = maxIdleTimeMillis;
lastReturnToPoolTimeMillis = System.currentTimeMillis();
releaseObservable = Observable.create(new OnSubscribe<Void>() {
@Override
public void call(Subscriber<? super Void> subscriber) {
if (!isUsable()) {
PooledConnection.this.owner.discard(PooledConnection.this)
.unsafeSubscribe(subscriber);
} else {
Long keepAliveTimeout = unsafeNettyChannel().attr(DYNAMIC_CONN_KEEP_ALIVE_TIMEOUT_MS).get();
if (null != keepAliveTimeout) {
PooledConnection.this.maxIdleTimeMillis = keepAliveTimeout;
}
markAwarePipeline.reset(); // Reset pipeline state, if changed, on release.
PooledConnection.this.owner.release(PooledConnection.this)
.doOnCompleted(new Action0() {
@Override
public void call() {
releasedAtLeastOnce = true;
lastReturnToPoolTimeMillis = System.currentTimeMillis();
}
})
.unsafeSubscribe(subscriber);
}
}
}).onErrorResumeNext(discard());
}
private PooledConnection(PooledConnection<?, ?> toCopy, Connection<R, W> unpooledDelegate) {
super(unpooledDelegate);
owner = toCopy.owner;
this.unpooledDelegate = unpooledDelegate;
lastReturnToPoolTimeMillis = toCopy.lastReturnToPoolTimeMillis;
releasedAtLeastOnce = toCopy.releasedAtLeastOnce;
maxIdleTimeMillis = toCopy.maxIdleTimeMillis;
releaseObservable = toCopy.releaseObservable;
}
@Override
public Observable<Void> write(Observable<W> msgs) {
return unpooledDelegate.write(msgs);
}
@Override
public Observable<Void> write(Observable<W> msgs, Func1<W, Boolean> flushSelector) {
return unpooledDelegate.write(msgs, flushSelector);
}
@Override
public Observable<Void> writeAndFlushOnEach(Observable<W> msgs) {
return unpooledDelegate.writeAndFlushOnEach(msgs);
}
@Override
public Observable<Void> writeString(Observable<String> msgs) {
return unpooledDelegate.writeString(msgs);
}
@Override
public Observable<Void> writeString(Observable<String> msgs,
Func1<String, Boolean> flushSelector) {
return unpooledDelegate.writeString(msgs, flushSelector);
}
@Override
public Observable<Void> writeStringAndFlushOnEach(Observable<String> msgs) {
return unpooledDelegate.writeStringAndFlushOnEach(msgs);
}
@Override
public Observable<Void> writeBytes(Observable<byte[]> msgs) {
return unpooledDelegate.writeBytes(msgs);
}
@Override
public Observable<Void> writeBytes(Observable<byte[]> msgs,
Func1<byte[], Boolean> flushSelector) {
return unpooledDelegate.writeBytes(msgs, flushSelector);
}
@Override
public Observable<Void> writeBytesAndFlushOnEach(Observable<byte[]> msgs) {
return unpooledDelegate.writeBytesAndFlushOnEach(msgs);
}
@Override
public Observable<Void> writeFileRegion(Observable<FileRegion> msgs) {
return unpooledDelegate.writeFileRegion(msgs);
}
@Override
public Observable<Void> writeFileRegion(Observable<FileRegion> msgs,
Func1<FileRegion, Boolean> flushSelector) {
return unpooledDelegate.writeFileRegion(msgs, flushSelector);
}
@Override
public Observable<Void> writeFileRegionAndFlushOnEach(Observable<FileRegion> msgs) {
return unpooledDelegate.writeFileRegionAndFlushOnEach(msgs);
}
@Override
public void flush() {
unpooledDelegate.flush();
}
@Override
public Observable<Void> close() {
return close(true);
}
@Override
public Observable<Void> close(boolean flush) {
if (flush) {
return releaseObservable.doOnSubscribe(new Action0() {
@Override
public void call() {
unpooledDelegate.flush();
}
});
} else {
return releaseObservable;
}
}
@Override
public void closeNow() {
close().subscribe(Actions.empty(), new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
logger.error("Error closing connection.", throwable);
}
});
}
@Override
public Observable<Void> closeListener() {
return unpooledDelegate.closeListener();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerAfter(String baseName, String name,
ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerAfter(baseName, name, handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerAfter(EventExecutorGroup group,
String baseName, String name,
ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerAfter(group, baseName, name,
handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerBefore(String baseName, String name,
ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerBefore(baseName, name, handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerBefore(EventExecutorGroup group,
String baseName, String name,
ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerBefore(group, baseName, name,
handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerFirst(EventExecutorGroup group,
String name, ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerFirst(group, name, handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerFirst(String name, ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerFirst(name, handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerLast(EventExecutorGroup group,
String name, ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerLast(group, name, handler));
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerLast(String name, ChannelHandler handler) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>addChannelHandlerLast(name, handler));
}
@Override
public <RR, WW> Connection<RR, WW> pipelineConfigurator(Action1<ChannelPipeline> pipelineConfigurator) {
return new PooledConnection<>(this, unpooledDelegate.<RR, WW>pipelineConfigurator(pipelineConfigurator));
}
@Override
public <RR> Connection<RR, W> transformRead(Transformer<R, RR> transformer) {
return new PooledConnection<>(this, unpooledDelegate.transformRead(transformer));
}
@Override
public <WW> Connection<R, WW> transformWrite(AllocatingTransformer<WW, W> transformer) {
return new PooledConnection<>(this, unpooledDelegate.transformWrite(transformer));
}
/**
* Discards this connection, to be called when this connection will never be used again.
*
* @return {@link Observable} representing the result of the discard, this will typically be resulting in a close
* on the underlying {@link Connection}.
*/
/*package private, externally shouldn't be discardable.*/Observable<Void> discard() {
return unpooledDelegate.close();
}
/**
* Returns whether this connection is safe to be used at this moment. <br/>
* This makes sure that the underlying netty's channel is active as returned by
* {@link Channel#isActive()} and it has not passed the maximum idle time in the pool.
*
* @return {@code true} if the connection is usable.
*/
public boolean isUsable() {
final Channel nettyChannel = unsafeNettyChannel();
Boolean discardConn = nettyChannel.attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).get();
if (!nettyChannel.isActive() || Boolean.TRUE == discardConn) {
return false;
}
long nowMillis = System.currentTimeMillis();
long idleTime = nowMillis - lastReturnToPoolTimeMillis;
return idleTime < maxIdleTimeMillis;
}
/**
* This method must be called for reusing the connection i.e. for sending this connection to the passed subscriber.
*
* @param connectionSubscriber Subscriber for the pooled connection for reuse.
*/
public void reuse(Subscriber<? super PooledConnection<R, W>> connectionSubscriber) {
unsafeNettyChannel().pipeline().fireUserEventTriggered(new ConnectionReuseEvent<>(connectionSubscriber, this));
}
public static <R, W> PooledConnection<R, W> create(Owner owner, long maxIdleTimeMillis,
Connection<R, W> unpooledDelegate) {
final PooledConnection<R, W> toReturn = new PooledConnection<>(owner, maxIdleTimeMillis, unpooledDelegate
);
toReturn.connectCloseToChannelClose();
return toReturn;
}
/**
* Returns {@code true} if this connection is reused at least once.
*
* @return {@code true} if this connection is reused at least once.
*/
public boolean isReused() {
return releasedAtLeastOnce;
}
@Override
public ChannelPipeline getChannelPipeline() {
return markAwarePipeline; // Always return mark aware as, we always have to reset state on release to pool.
}
/*Visible for testin*/ void setLastReturnToPoolTimeMillis(long lastReturnToPoolTimeMillis) {
this.lastReturnToPoolTimeMillis = lastReturnToPoolTimeMillis;
}
/**
* A contract for the owner of the {@link PooledConnection} to which any instance of {@link PooledConnection} must
* be returned after use.
*/
public interface Owner {
/**
* Releases the passed connection back to the owner, for reuse.
*
* @param connection Connection to be released.
*
* @return {@link Observable} representing result of the release. Every subscription to this, releases the
* connection.
*/
Observable<Void> release(PooledConnection<?, ?> connection);
/**
* Discards the passed connection from the pool. This is usually called due to an external event like closing of
* a connection that the pool may not know. <br/>
* <b> This operation is idempotent and hence can be called multiple times with no side effects</b>
*
* @param connection The connection to discard.
*
* @return {@link Observable} indicating the result of the discard (which usually results in a close()).
* Every subscription to this {@link Observable} will discard the connection.
*/
Observable<Void> discard(PooledConnection<?, ?> connection);
}
}