c784271620
Motivation: JCTools supports both non-unsafe, unsafe versions of queues and JDK6 which allows us to shade the library in netty-common allowing it to stay "zero dependency". Modifications: - Remove copy paste JCTools code and shade the library (dependencies that are shaded should be removed from the <dependencies> section of the generated POM). - Remove usage of OneTimeTask and remove it all together. Result: Less code to maintain and easier to update JCTools and less GC pressure as the queue implementation nt creates so much garbage
1381 lines
45 KiB
Java
1381 lines
45 KiB
Java
/*
|
|
* Copyright 2012 The Netty Project
|
|
*
|
|
* The Netty Project licenses this file to you 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.netty.channel;
|
|
|
|
import io.netty.channel.Channel.Unsafe;
|
|
import io.netty.util.ReferenceCountUtil;
|
|
import io.netty.util.concurrent.EventExecutor;
|
|
import io.netty.util.concurrent.EventExecutorGroup;
|
|
import io.netty.util.concurrent.FastThreadLocal;
|
|
import io.netty.util.internal.ObjectUtil;
|
|
import io.netty.util.internal.StringUtil;
|
|
import io.netty.util.internal.logging.InternalLogger;
|
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
|
|
|
import java.net.SocketAddress;
|
|
import java.util.ArrayList;
|
|
import java.util.IdentityHashMap;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.NoSuchElementException;
|
|
import java.util.WeakHashMap;
|
|
import java.util.concurrent.RejectedExecutionException;
|
|
|
|
/**
|
|
* The default {@link ChannelPipeline} implementation. It is usually created
|
|
* by a {@link Channel} implementation when the {@link Channel} is created.
|
|
*/
|
|
public class DefaultChannelPipeline implements ChannelPipeline {
|
|
|
|
static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class);
|
|
|
|
private static final String HEAD_NAME = generateName0(HeadContext.class);
|
|
private static final String TAIL_NAME = generateName0(TailContext.class);
|
|
|
|
private static final FastThreadLocal<Map<Class<?>, String>> nameCaches =
|
|
new FastThreadLocal<Map<Class<?>, String>>() {
|
|
@Override
|
|
protected Map<Class<?>, String> initialValue() throws Exception {
|
|
return new WeakHashMap<Class<?>, String>();
|
|
}
|
|
};
|
|
|
|
final AbstractChannelHandlerContext head;
|
|
final AbstractChannelHandlerContext tail;
|
|
|
|
private final Channel channel;
|
|
private Map<EventExecutorGroup, EventExecutor> childExecutors;
|
|
private MessageSizeEstimator.Handle estimatorHandle;
|
|
|
|
/**
|
|
* This is the head of a linked list that is processed by {@link #callHandlerAddedForAllHandlers()} and so process
|
|
* all the pending {@link #callHandlerAdded0(AbstractChannelHandlerContext)}.
|
|
*
|
|
* We only keep the head because it is expected that the list is used infrequently and its size is small.
|
|
* Thus full iterations to do insertions is assumed to be a good compromised to saving memory and tail management
|
|
* complexity.
|
|
*/
|
|
private PendingHandlerCallback pendingHandlerCallbackHead;
|
|
|
|
/**
|
|
* Set to {@code true} once the {@link AbstractChannel} is registered.Once set to {@code true} the value will never
|
|
* change.
|
|
*/
|
|
private boolean registered;
|
|
|
|
protected DefaultChannelPipeline(Channel channel) {
|
|
this.channel = ObjectUtil.checkNotNull(channel, "channel");
|
|
|
|
tail = new TailContext(this);
|
|
head = new HeadContext(this);
|
|
|
|
head.next = tail;
|
|
tail.prev = head;
|
|
}
|
|
|
|
final MessageSizeEstimator.Handle estimatorHandle() {
|
|
if (estimatorHandle == null) {
|
|
estimatorHandle = channel.config().getMessageSizeEstimator().newHandle();
|
|
}
|
|
return estimatorHandle;
|
|
}
|
|
|
|
private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
|
|
return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
|
|
}
|
|
|
|
private EventExecutor childExecutor(EventExecutorGroup group) {
|
|
if (group == null) {
|
|
return null;
|
|
}
|
|
Map<EventExecutorGroup, EventExecutor> childExecutors = this.childExecutors;
|
|
if (childExecutors == null) {
|
|
// Use size of 4 as most people only use one extra EventExecutor.
|
|
childExecutors = this.childExecutors = new IdentityHashMap<EventExecutorGroup, EventExecutor>(4);
|
|
}
|
|
// Pin one of the child executors once and remember it so that the same child executor
|
|
// is used to fire events for the same channel.
|
|
EventExecutor childExecutor = childExecutors.get(group);
|
|
if (childExecutor == null) {
|
|
childExecutor = group.next();
|
|
childExecutors.put(group, childExecutor);
|
|
}
|
|
return childExecutor;
|
|
}
|
|
|
|
@Override
|
|
public final Channel channel() {
|
|
return channel;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addFirst(String name, ChannelHandler handler) {
|
|
return addFirst(null, name, handler);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) {
|
|
final AbstractChannelHandlerContext newCtx;
|
|
synchronized (this) {
|
|
checkMultiplicity(handler);
|
|
name = filterName(name, handler);
|
|
|
|
newCtx = newContext(group, name, handler);
|
|
|
|
addFirst0(newCtx);
|
|
|
|
// If the registered is false it means that the channel was not registered on an eventloop yet.
|
|
// In this case we add the context to the pipeline and add a task that will call
|
|
// ChannelHandler.handlerAdded(...) once the channel is registered.
|
|
if (!registered) {
|
|
callHandlerCallbackLater(newCtx, true);
|
|
return this;
|
|
}
|
|
|
|
EventExecutor executor = newCtx.executor();
|
|
if (!executor.inEventLoop()) {
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callHandlerAdded0(newCtx);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
}
|
|
callHandlerAdded0(newCtx);
|
|
return this;
|
|
}
|
|
|
|
private void addFirst0(AbstractChannelHandlerContext newCtx) {
|
|
AbstractChannelHandlerContext nextCtx = head.next;
|
|
newCtx.prev = head;
|
|
newCtx.next = nextCtx;
|
|
head.next = newCtx;
|
|
nextCtx.prev = newCtx;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addLast(String name, ChannelHandler handler) {
|
|
return addLast(null, name, handler);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
|
|
final AbstractChannelHandlerContext newCtx;
|
|
synchronized (this) {
|
|
checkMultiplicity(handler);
|
|
|
|
newCtx = newContext(group, filterName(name, handler), handler);
|
|
|
|
addLast0(newCtx);
|
|
|
|
// If the registered is false it means that the channel was not registered on an eventloop yet.
|
|
// In this case we add the context to the pipeline and add a task that will call
|
|
// ChannelHandler.handlerAdded(...) once the channel is registered.
|
|
if (!registered) {
|
|
callHandlerCallbackLater(newCtx, true);
|
|
return this;
|
|
}
|
|
|
|
EventExecutor executor = newCtx.executor();
|
|
if (!executor.inEventLoop()) {
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callHandlerAdded0(newCtx);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
}
|
|
callHandlerAdded0(newCtx);
|
|
return this;
|
|
}
|
|
|
|
private void addLast0(AbstractChannelHandlerContext newCtx) {
|
|
AbstractChannelHandlerContext prev = tail.prev;
|
|
newCtx.prev = prev;
|
|
newCtx.next = tail;
|
|
prev.next = newCtx;
|
|
tail.prev = newCtx;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler) {
|
|
return addBefore(null, baseName, name, handler);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addBefore(
|
|
EventExecutorGroup group, String baseName, String name, ChannelHandler handler) {
|
|
final AbstractChannelHandlerContext newCtx;
|
|
final AbstractChannelHandlerContext ctx;
|
|
synchronized (this) {
|
|
checkMultiplicity(handler);
|
|
name = filterName(name, handler);
|
|
ctx = getContextOrDie(baseName);
|
|
|
|
newCtx = newContext(group, name, handler);
|
|
|
|
addBefore0(ctx, newCtx);
|
|
|
|
// If the registered is false it means that the channel was not registered on an eventloop yet.
|
|
// In this case we add the context to the pipeline and add a task that will call
|
|
// ChannelHandler.handlerAdded(...) once the channel is registered.
|
|
if (!registered) {
|
|
callHandlerCallbackLater(newCtx, true);
|
|
return this;
|
|
}
|
|
|
|
EventExecutor executor = newCtx.executor();
|
|
if (!executor.inEventLoop()) {
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callHandlerAdded0(newCtx);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
}
|
|
callHandlerAdded0(newCtx);
|
|
return this;
|
|
}
|
|
|
|
private static void addBefore0(AbstractChannelHandlerContext ctx, AbstractChannelHandlerContext newCtx) {
|
|
newCtx.prev = ctx.prev;
|
|
newCtx.next = ctx;
|
|
ctx.prev.next = newCtx;
|
|
ctx.prev = newCtx;
|
|
}
|
|
|
|
private String filterName(String name, ChannelHandler handler) {
|
|
if (name == null) {
|
|
return generateName(handler);
|
|
}
|
|
checkDuplicateName(name);
|
|
return name;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler) {
|
|
return addAfter(null, baseName, name, handler);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addAfter(
|
|
EventExecutorGroup group, String baseName, String name, ChannelHandler handler) {
|
|
final AbstractChannelHandlerContext newCtx;
|
|
final AbstractChannelHandlerContext ctx;
|
|
|
|
synchronized (this) {
|
|
checkMultiplicity(handler);
|
|
name = filterName(name, handler);
|
|
ctx = getContextOrDie(baseName);
|
|
|
|
newCtx = newContext(group, name, handler);
|
|
|
|
addAfter0(ctx, newCtx);
|
|
|
|
// If the registered is false it means that the channel was not registered on an eventloop yet.
|
|
// In this case we remove the context from the pipeline and add a task that will call
|
|
// ChannelHandler.handlerRemoved(...) once the channel is registered.
|
|
if (!registered) {
|
|
callHandlerCallbackLater(newCtx, true);
|
|
return this;
|
|
}
|
|
EventExecutor executor = newCtx.executor();
|
|
if (!executor.inEventLoop()) {
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callHandlerAdded0(newCtx);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
}
|
|
callHandlerAdded0(newCtx);
|
|
return this;
|
|
}
|
|
|
|
private static void addAfter0(AbstractChannelHandlerContext ctx, AbstractChannelHandlerContext newCtx) {
|
|
newCtx.prev = ctx;
|
|
newCtx.next = ctx.next;
|
|
ctx.next.prev = newCtx;
|
|
ctx.next = newCtx;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addFirst(ChannelHandler... handlers) {
|
|
return addFirst(null, handlers);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addFirst(EventExecutorGroup executor, ChannelHandler... handlers) {
|
|
if (handlers == null) {
|
|
throw new NullPointerException("handlers");
|
|
}
|
|
if (handlers.length == 0 || handlers[0] == null) {
|
|
return this;
|
|
}
|
|
|
|
int size;
|
|
for (size = 1; size < handlers.length; size ++) {
|
|
if (handlers[size] == null) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int i = size - 1; i >= 0; i --) {
|
|
ChannelHandler h = handlers[i];
|
|
addFirst(executor, null, h);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addLast(ChannelHandler... handlers) {
|
|
return addLast(null, handlers);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
|
|
if (handlers == null) {
|
|
throw new NullPointerException("handlers");
|
|
}
|
|
|
|
for (ChannelHandler h: handlers) {
|
|
if (h == null) {
|
|
break;
|
|
}
|
|
addLast(executor, null, h);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
private String generateName(ChannelHandler handler) {
|
|
Map<Class<?>, String> cache = nameCaches.get();
|
|
Class<?> handlerType = handler.getClass();
|
|
String name = cache.get(handlerType);
|
|
if (name == null) {
|
|
name = generateName0(handlerType);
|
|
cache.put(handlerType, name);
|
|
}
|
|
|
|
// It's not very likely for a user to put more than one handler of the same type, but make sure to avoid
|
|
// any name conflicts. Note that we don't cache the names generated here.
|
|
if (context0(name) != null) {
|
|
String baseName = name.substring(0, name.length() - 1); // Strip the trailing '0'.
|
|
for (int i = 1;; i ++) {
|
|
String newName = baseName + i;
|
|
if (context0(newName) == null) {
|
|
name = newName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
private static String generateName0(Class<?> handlerType) {
|
|
return StringUtil.simpleClassName(handlerType) + "#0";
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline remove(ChannelHandler handler) {
|
|
remove(getContextOrDie(handler));
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler remove(String name) {
|
|
return remove(getContextOrDie(name)).handler();
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
@Override
|
|
public final <T extends ChannelHandler> T remove(Class<T> handlerType) {
|
|
return (T) remove(getContextOrDie(handlerType)).handler();
|
|
}
|
|
|
|
private AbstractChannelHandlerContext remove(final AbstractChannelHandlerContext ctx) {
|
|
assert ctx != head && ctx != tail;
|
|
|
|
synchronized (this) {
|
|
remove0(ctx);
|
|
|
|
// If the registered is false it means that the channel was not registered on an eventloop yet.
|
|
// In this case we remove the context from the pipeline and add a task that will call
|
|
// ChannelHandler.handlerRemoved(...) once the channel is registered.
|
|
if (!registered) {
|
|
callHandlerCallbackLater(ctx, false);
|
|
return ctx;
|
|
}
|
|
|
|
EventExecutor executor = ctx.executor();
|
|
if (!executor.inEventLoop()) {
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callHandlerRemoved0(ctx);
|
|
}
|
|
});
|
|
return ctx;
|
|
}
|
|
}
|
|
callHandlerRemoved0(ctx);
|
|
return ctx;
|
|
}
|
|
|
|
private static void remove0(AbstractChannelHandlerContext ctx) {
|
|
AbstractChannelHandlerContext prev = ctx.prev;
|
|
AbstractChannelHandlerContext next = ctx.next;
|
|
prev.next = next;
|
|
next.prev = prev;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler removeFirst() {
|
|
if (head.next == tail) {
|
|
throw new NoSuchElementException();
|
|
}
|
|
return remove(head.next).handler();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler removeLast() {
|
|
if (head.next == tail) {
|
|
throw new NoSuchElementException();
|
|
}
|
|
return remove(tail.prev).handler();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler) {
|
|
replace(getContextOrDie(oldHandler), newName, newHandler);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler) {
|
|
return replace(getContextOrDie(oldName), newName, newHandler);
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings("unchecked")
|
|
public final <T extends ChannelHandler> T replace(
|
|
Class<T> oldHandlerType, String newName, ChannelHandler newHandler) {
|
|
return (T) replace(getContextOrDie(oldHandlerType), newName, newHandler);
|
|
}
|
|
|
|
private ChannelHandler replace(
|
|
final AbstractChannelHandlerContext ctx, final String newName, ChannelHandler newHandler) {
|
|
assert ctx != head && ctx != tail;
|
|
|
|
final AbstractChannelHandlerContext newCtx;
|
|
synchronized (this) {
|
|
checkMultiplicity(newHandler);
|
|
boolean sameName = ctx.name().equals(newName);
|
|
if (!sameName) {
|
|
checkDuplicateName(newName);
|
|
}
|
|
|
|
newCtx = newContext(ctx.executor, newName, newHandler);
|
|
|
|
replace0(ctx, newCtx);
|
|
|
|
// If the registered is false it means that the channel was not registered on an eventloop yet.
|
|
// In this case we replace the context in the pipeline
|
|
// and add a task that will call ChannelHandler.handlerAdded(...) and
|
|
// ChannelHandler.handlerRemoved(...) once the channel is registered.
|
|
if (!registered) {
|
|
callHandlerCallbackLater(newCtx, true);
|
|
callHandlerCallbackLater(ctx, false);
|
|
return ctx.handler();
|
|
}
|
|
EventExecutor executor = ctx.executor();
|
|
if (!executor.inEventLoop()) {
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
// Invoke newHandler.handlerAdded() first (i.e. before oldHandler.handlerRemoved() is invoked)
|
|
// because callHandlerRemoved() will trigger channelRead() or flush() on newHandler and
|
|
// those event handlers must be called after handlerAdded().
|
|
callHandlerAdded0(newCtx);
|
|
callHandlerRemoved0(ctx);
|
|
}
|
|
});
|
|
return ctx.handler();
|
|
}
|
|
}
|
|
// Invoke newHandler.handlerAdded() first (i.e. before oldHandler.handlerRemoved() is invoked)
|
|
// because callHandlerRemoved() will trigger channelRead() or flush() on newHandler and those
|
|
// event handlers must be called after handlerAdded().
|
|
callHandlerAdded0(newCtx);
|
|
callHandlerRemoved0(ctx);
|
|
return ctx.handler();
|
|
}
|
|
|
|
private static void replace0(AbstractChannelHandlerContext oldCtx, AbstractChannelHandlerContext newCtx) {
|
|
AbstractChannelHandlerContext prev = oldCtx.prev;
|
|
AbstractChannelHandlerContext next = oldCtx.next;
|
|
newCtx.prev = prev;
|
|
newCtx.next = next;
|
|
|
|
// Finish the replacement of oldCtx with newCtx in the linked list.
|
|
// Note that this doesn't mean events will be sent to the new handler immediately
|
|
// because we are currently at the event handler thread and no more than one handler methods can be invoked
|
|
// at the same time (we ensured that in replace().)
|
|
prev.next = newCtx;
|
|
next.prev = newCtx;
|
|
|
|
// update the reference to the replacement so forward of buffered content will work correctly
|
|
oldCtx.prev = newCtx;
|
|
oldCtx.next = newCtx;
|
|
}
|
|
|
|
private static void checkMultiplicity(ChannelHandler handler) {
|
|
if (handler instanceof ChannelHandlerAdapter) {
|
|
ChannelHandlerAdapter h = (ChannelHandlerAdapter) handler;
|
|
if (!h.isSharable() && h.added) {
|
|
throw new ChannelPipelineException(
|
|
h.getClass().getName() +
|
|
" is not a @Sharable handler, so can't be added or removed multiple times.");
|
|
}
|
|
h.added = true;
|
|
}
|
|
}
|
|
|
|
private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
|
|
try {
|
|
ctx.handler().handlerAdded(ctx);
|
|
ctx.setAdded();
|
|
} catch (Throwable t) {
|
|
boolean removed = false;
|
|
try {
|
|
remove0(ctx);
|
|
try {
|
|
ctx.handler().handlerRemoved(ctx);
|
|
} finally {
|
|
ctx.setRemoved();
|
|
}
|
|
removed = true;
|
|
} catch (Throwable t2) {
|
|
if (logger.isWarnEnabled()) {
|
|
logger.warn("Failed to remove a handler: " + ctx.name(), t2);
|
|
}
|
|
}
|
|
|
|
if (removed) {
|
|
fireExceptionCaught(new ChannelPipelineException(
|
|
ctx.handler().getClass().getName() +
|
|
".handlerAdded() has thrown an exception; removed.", t));
|
|
} else {
|
|
fireExceptionCaught(new ChannelPipelineException(
|
|
ctx.handler().getClass().getName() +
|
|
".handlerAdded() has thrown an exception; also failed to remove.", t));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void callHandlerRemoved0(final AbstractChannelHandlerContext ctx) {
|
|
// Notify the complete removal.
|
|
try {
|
|
try {
|
|
ctx.handler().handlerRemoved(ctx);
|
|
} finally {
|
|
ctx.setRemoved();
|
|
}
|
|
} catch (Throwable t) {
|
|
fireExceptionCaught(new ChannelPipelineException(
|
|
ctx.handler().getClass().getName() + ".handlerRemoved() has thrown an exception.", t));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler first() {
|
|
ChannelHandlerContext first = firstContext();
|
|
if (first == null) {
|
|
return null;
|
|
}
|
|
return first.handler();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandlerContext firstContext() {
|
|
AbstractChannelHandlerContext first = head.next;
|
|
if (first == tail) {
|
|
return null;
|
|
}
|
|
return head.next;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler last() {
|
|
AbstractChannelHandlerContext last = tail.prev;
|
|
if (last == head) {
|
|
return null;
|
|
}
|
|
return last.handler();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandlerContext lastContext() {
|
|
AbstractChannelHandlerContext last = tail.prev;
|
|
if (last == head) {
|
|
return null;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandler get(String name) {
|
|
ChannelHandlerContext ctx = context(name);
|
|
if (ctx == null) {
|
|
return null;
|
|
} else {
|
|
return ctx.handler();
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
@Override
|
|
public final <T extends ChannelHandler> T get(Class<T> handlerType) {
|
|
ChannelHandlerContext ctx = context(handlerType);
|
|
if (ctx == null) {
|
|
return null;
|
|
} else {
|
|
return (T) ctx.handler();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandlerContext context(String name) {
|
|
if (name == null) {
|
|
throw new NullPointerException("name");
|
|
}
|
|
|
|
return context0(name);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandlerContext context(ChannelHandler handler) {
|
|
if (handler == null) {
|
|
throw new NullPointerException("handler");
|
|
}
|
|
|
|
AbstractChannelHandlerContext ctx = head.next;
|
|
for (;;) {
|
|
|
|
if (ctx == null) {
|
|
return null;
|
|
}
|
|
|
|
if (ctx.handler() == handler) {
|
|
return ctx;
|
|
}
|
|
|
|
ctx = ctx.next;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final ChannelHandlerContext context(Class<? extends ChannelHandler> handlerType) {
|
|
if (handlerType == null) {
|
|
throw new NullPointerException("handlerType");
|
|
}
|
|
|
|
AbstractChannelHandlerContext ctx = head.next;
|
|
for (;;) {
|
|
if (ctx == null) {
|
|
return null;
|
|
}
|
|
if (handlerType.isAssignableFrom(ctx.handler().getClass())) {
|
|
return ctx;
|
|
}
|
|
ctx = ctx.next;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final List<String> names() {
|
|
List<String> list = new ArrayList<String>();
|
|
AbstractChannelHandlerContext ctx = head.next;
|
|
for (;;) {
|
|
if (ctx == null) {
|
|
return list;
|
|
}
|
|
list.add(ctx.name());
|
|
ctx = ctx.next;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final Map<String, ChannelHandler> toMap() {
|
|
Map<String, ChannelHandler> map = new LinkedHashMap<String, ChannelHandler>();
|
|
AbstractChannelHandlerContext ctx = head.next;
|
|
for (;;) {
|
|
if (ctx == tail) {
|
|
return map;
|
|
}
|
|
map.put(ctx.name(), ctx.handler());
|
|
ctx = ctx.next;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final Iterator<Map.Entry<String, ChannelHandler>> iterator() {
|
|
return toMap().entrySet().iterator();
|
|
}
|
|
|
|
/**
|
|
* Returns the {@link String} representation of this pipeline.
|
|
*/
|
|
@Override
|
|
public final String toString() {
|
|
StringBuilder buf = new StringBuilder()
|
|
.append(StringUtil.simpleClassName(this))
|
|
.append('{');
|
|
AbstractChannelHandlerContext ctx = head.next;
|
|
for (;;) {
|
|
if (ctx == tail) {
|
|
break;
|
|
}
|
|
|
|
buf.append('(')
|
|
.append(ctx.name())
|
|
.append(" = ")
|
|
.append(ctx.handler().getClass().getName())
|
|
.append(')');
|
|
|
|
ctx = ctx.next;
|
|
if (ctx == tail) {
|
|
break;
|
|
}
|
|
|
|
buf.append(", ");
|
|
}
|
|
buf.append('}');
|
|
return buf.toString();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelRegistered() {
|
|
AbstractChannelHandlerContext.invokeChannelRegistered(head);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelUnregistered() {
|
|
AbstractChannelHandlerContext.invokeChannelUnregistered(head);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Removes all handlers from the pipeline one by one from tail (exclusive) to head (exclusive) to trigger
|
|
* handlerRemoved().
|
|
*
|
|
* Note that we traverse up the pipeline ({@link #destroyUp(AbstractChannelHandlerContext, boolean)})
|
|
* before traversing down ({@link #destroyDown(Thread, AbstractChannelHandlerContext, boolean)}) so that
|
|
* the handlers are removed after all events are handled.
|
|
*
|
|
* See: https://github.com/netty/netty/issues/3156
|
|
*/
|
|
private synchronized void destroy() {
|
|
destroyUp(head.next, false);
|
|
}
|
|
|
|
private void destroyUp(AbstractChannelHandlerContext ctx, boolean inEventLoop) {
|
|
final Thread currentThread = Thread.currentThread();
|
|
final AbstractChannelHandlerContext tail = this.tail;
|
|
for (;;) {
|
|
if (ctx == tail) {
|
|
destroyDown(currentThread, tail.prev, inEventLoop);
|
|
break;
|
|
}
|
|
|
|
final EventExecutor executor = ctx.executor();
|
|
if (!inEventLoop && !executor.inEventLoop(currentThread)) {
|
|
final AbstractChannelHandlerContext finalCtx = ctx;
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
destroyUp(finalCtx, true);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
|
|
ctx = ctx.next;
|
|
inEventLoop = false;
|
|
}
|
|
}
|
|
|
|
private void destroyDown(Thread currentThread, AbstractChannelHandlerContext ctx, boolean inEventLoop) {
|
|
// We have reached at tail; now traverse backwards.
|
|
final AbstractChannelHandlerContext head = this.head;
|
|
for (;;) {
|
|
if (ctx == head) {
|
|
break;
|
|
}
|
|
|
|
final EventExecutor executor = ctx.executor();
|
|
if (inEventLoop || executor.inEventLoop(currentThread)) {
|
|
synchronized (this) {
|
|
remove0(ctx);
|
|
}
|
|
callHandlerRemoved0(ctx);
|
|
} else {
|
|
final AbstractChannelHandlerContext finalCtx = ctx;
|
|
executor.execute(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
destroyDown(Thread.currentThread(), finalCtx, true);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
|
|
ctx = ctx.prev;
|
|
inEventLoop = false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelActive() {
|
|
AbstractChannelHandlerContext.invokeChannelActive(head);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelInactive() {
|
|
AbstractChannelHandlerContext.invokeChannelInactive(head);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireExceptionCaught(Throwable cause) {
|
|
AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireUserEventTriggered(Object event) {
|
|
AbstractChannelHandlerContext.invokeUserEventTriggered(head, event);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelRead(Object msg) {
|
|
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelReadComplete() {
|
|
AbstractChannelHandlerContext.invokeChannelReadComplete(head);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline fireChannelWritabilityChanged() {
|
|
AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture bind(SocketAddress localAddress) {
|
|
return tail.bind(localAddress);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture connect(SocketAddress remoteAddress) {
|
|
return tail.connect(remoteAddress);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
|
|
return tail.connect(remoteAddress, localAddress);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture disconnect() {
|
|
return tail.disconnect();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture close() {
|
|
return tail.close();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture deregister() {
|
|
return tail.deregister();
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline flush() {
|
|
tail.flush();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
|
|
return tail.bind(localAddress, promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
|
|
return tail.connect(remoteAddress, promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture connect(
|
|
SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
|
|
return tail.connect(remoteAddress, localAddress, promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture disconnect(ChannelPromise promise) {
|
|
return tail.disconnect(promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture close(ChannelPromise promise) {
|
|
return tail.close(promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture deregister(final ChannelPromise promise) {
|
|
return tail.deregister(promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelPipeline read() {
|
|
tail.read();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture write(Object msg) {
|
|
return tail.write(msg);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture write(Object msg, ChannelPromise promise) {
|
|
return tail.write(msg, promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
|
|
return tail.writeAndFlush(msg, promise);
|
|
}
|
|
|
|
@Override
|
|
public final ChannelFuture writeAndFlush(Object msg) {
|
|
return tail.writeAndFlush(msg);
|
|
}
|
|
|
|
private void checkDuplicateName(String name) {
|
|
if (context0(name) != null) {
|
|
throw new IllegalArgumentException("Duplicate handler name: " + name);
|
|
}
|
|
}
|
|
|
|
private AbstractChannelHandlerContext context0(String name) {
|
|
AbstractChannelHandlerContext context = head.next;
|
|
while (context != tail) {
|
|
if (context.name().equals(name)) {
|
|
return context;
|
|
}
|
|
context = context.next;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private AbstractChannelHandlerContext getContextOrDie(String name) {
|
|
AbstractChannelHandlerContext ctx = (AbstractChannelHandlerContext) context(name);
|
|
if (ctx == null) {
|
|
throw new NoSuchElementException(name);
|
|
} else {
|
|
return ctx;
|
|
}
|
|
}
|
|
|
|
private AbstractChannelHandlerContext getContextOrDie(ChannelHandler handler) {
|
|
AbstractChannelHandlerContext ctx = (AbstractChannelHandlerContext) context(handler);
|
|
if (ctx == null) {
|
|
throw new NoSuchElementException(handler.getClass().getName());
|
|
} else {
|
|
return ctx;
|
|
}
|
|
}
|
|
|
|
private AbstractChannelHandlerContext getContextOrDie(Class<? extends ChannelHandler> handlerType) {
|
|
AbstractChannelHandlerContext ctx = (AbstractChannelHandlerContext) context(handlerType);
|
|
if (ctx == null) {
|
|
throw new NoSuchElementException(handlerType.getName());
|
|
} else {
|
|
return ctx;
|
|
}
|
|
}
|
|
|
|
private void callHandlerAddedForAllHandlers() {
|
|
final PendingHandlerCallback pendingHandlerCallbackHead;
|
|
synchronized (this) {
|
|
assert !registered;
|
|
|
|
// This Channel itself was registered.
|
|
registered = true;
|
|
|
|
pendingHandlerCallbackHead = this.pendingHandlerCallbackHead;
|
|
// Null out so it can be GC'ed.
|
|
this.pendingHandlerCallbackHead = null;
|
|
}
|
|
|
|
// This must happen outside of the synchronized(...) block as otherwise handlerAdded(...) may be called while
|
|
// holding the lock and so produce a deadlock if handlerAdded(...) will try to add another handler from outside
|
|
// the EventLoop.
|
|
PendingHandlerCallback task = pendingHandlerCallbackHead;
|
|
while (task != null) {
|
|
task.execute();
|
|
task = task.next;
|
|
}
|
|
}
|
|
|
|
private void callHandlerCallbackLater(AbstractChannelHandlerContext ctx, boolean added) {
|
|
assert !registered;
|
|
|
|
PendingHandlerCallback task = added ? new PendingHandlerAddedTask(ctx) : new PendingHandlerRemovedTask(ctx);
|
|
PendingHandlerCallback pending = pendingHandlerCallbackHead;
|
|
if (pending == null) {
|
|
pendingHandlerCallbackHead = task;
|
|
} else {
|
|
// Find the tail of the linked-list.
|
|
while (pending.next != null) {
|
|
pending = pending.next;
|
|
}
|
|
pending.next = task;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called once a {@link Throwable} hit the end of the {@link ChannelPipeline} without been handled by the user
|
|
* in {@link ChannelHandler#exceptionCaught(ChannelHandlerContext, Throwable)}.
|
|
*/
|
|
protected void onUnhandledInboundException(Throwable cause) {
|
|
try {
|
|
logger.warn(
|
|
"An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
|
|
"It usually means the last handler in the pipeline did not handle the exception.",
|
|
cause);
|
|
} finally {
|
|
ReferenceCountUtil.release(cause);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
|
|
* in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
|
|
* to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
|
|
*/
|
|
protected void onUnhandledInboundMessage(Object msg) {
|
|
try {
|
|
logger.debug(
|
|
"Discarded inbound message {} that reached at the tail of the pipeline. " +
|
|
"Please check your pipeline configuration.", msg);
|
|
} finally {
|
|
ReferenceCountUtil.release(msg);
|
|
}
|
|
}
|
|
|
|
// A special catch-all handler that handles both bytes and messages.
|
|
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
|
|
|
|
TailContext(DefaultChannelPipeline pipeline) {
|
|
super(pipeline, null, TAIL_NAME, true, false);
|
|
setAdded();
|
|
}
|
|
|
|
@Override
|
|
public ChannelHandler handler() {
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void channelRegistered(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void channelActive(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void channelInactive(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void handlerAdded(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { }
|
|
|
|
@Override
|
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
|
// This may not be a configuration error and so don't log anything.
|
|
// The event may be superfluous for the current pipeline configuration.
|
|
ReferenceCountUtil.release(evt);
|
|
}
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
|
onUnhandledInboundException(cause);
|
|
}
|
|
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
onUnhandledInboundMessage(msg);
|
|
}
|
|
|
|
@Override
|
|
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { }
|
|
}
|
|
|
|
final class HeadContext extends AbstractChannelHandlerContext
|
|
implements ChannelOutboundHandler, ChannelInboundHandler {
|
|
|
|
private final Unsafe unsafe;
|
|
private boolean firstRegistration = true;
|
|
|
|
HeadContext(DefaultChannelPipeline pipeline) {
|
|
super(pipeline, null, HEAD_NAME, false, true);
|
|
unsafe = pipeline.channel().unsafe();
|
|
setAdded();
|
|
}
|
|
|
|
@Override
|
|
public ChannelHandler handler() {
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
|
// NOOP
|
|
}
|
|
|
|
@Override
|
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
|
// NOOP
|
|
}
|
|
|
|
@Override
|
|
public void bind(
|
|
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
|
|
throws Exception {
|
|
unsafe.bind(localAddress, promise);
|
|
}
|
|
|
|
@Override
|
|
public void connect(
|
|
ChannelHandlerContext ctx,
|
|
SocketAddress remoteAddress, SocketAddress localAddress,
|
|
ChannelPromise promise) throws Exception {
|
|
unsafe.connect(remoteAddress, localAddress, promise);
|
|
}
|
|
|
|
@Override
|
|
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
|
|
unsafe.disconnect(promise);
|
|
}
|
|
|
|
@Override
|
|
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
|
|
unsafe.close(promise);
|
|
}
|
|
|
|
@Override
|
|
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
|
|
unsafe.deregister(promise);
|
|
}
|
|
|
|
@Override
|
|
public void read(ChannelHandlerContext ctx) {
|
|
unsafe.beginRead();
|
|
}
|
|
|
|
@Override
|
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
|
unsafe.write(msg, promise);
|
|
}
|
|
|
|
@Override
|
|
public void flush(ChannelHandlerContext ctx) throws Exception {
|
|
unsafe.flush();
|
|
}
|
|
|
|
@Override
|
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
|
ctx.fireExceptionCaught(cause);
|
|
}
|
|
|
|
@Override
|
|
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
|
if (firstRegistration) {
|
|
firstRegistration = false;
|
|
// We are now registered to the EventLoop. It's time to call the callbacks for the ChannelHandlers,
|
|
// that were added before the registration was done.
|
|
callHandlerAddedForAllHandlers();
|
|
}
|
|
|
|
ctx.fireChannelRegistered();
|
|
}
|
|
|
|
@Override
|
|
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
|
|
ctx.fireChannelUnregistered();
|
|
|
|
// Remove all handlers sequentially if channel is closed and unregistered.
|
|
if (!channel.isOpen()) {
|
|
destroy();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
|
ctx.fireChannelActive();
|
|
|
|
readIfIsAutoRead();
|
|
}
|
|
|
|
@Override
|
|
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
|
ctx.fireChannelInactive();
|
|
}
|
|
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
ctx.fireChannelRead(msg);
|
|
}
|
|
|
|
@Override
|
|
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
|
|
ctx.fireChannelReadComplete();
|
|
|
|
readIfIsAutoRead();
|
|
}
|
|
|
|
private void readIfIsAutoRead() {
|
|
if (channel.config().isAutoRead()) {
|
|
channel.read();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
|
ctx.fireUserEventTriggered(evt);
|
|
}
|
|
|
|
@Override
|
|
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
|
|
ctx.fireChannelWritabilityChanged();
|
|
}
|
|
}
|
|
|
|
private abstract static class PendingHandlerCallback implements Runnable {
|
|
final AbstractChannelHandlerContext ctx;
|
|
PendingHandlerCallback next;
|
|
|
|
PendingHandlerCallback(AbstractChannelHandlerContext ctx) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
abstract void execute();
|
|
}
|
|
|
|
private final class PendingHandlerAddedTask extends PendingHandlerCallback {
|
|
|
|
PendingHandlerAddedTask(AbstractChannelHandlerContext ctx) {
|
|
super(ctx);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
callHandlerAdded0(ctx);
|
|
}
|
|
|
|
@Override
|
|
void execute() {
|
|
EventExecutor executor = ctx.executor();
|
|
if (executor.inEventLoop()) {
|
|
callHandlerAdded0(ctx);
|
|
} else {
|
|
try {
|
|
executor.execute(this);
|
|
} catch (RejectedExecutionException e) {
|
|
if (logger.isWarnEnabled()) {
|
|
logger.warn(
|
|
"Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.",
|
|
executor, ctx.name(), e);
|
|
}
|
|
remove0(ctx);
|
|
ctx.setRemoved();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class PendingHandlerRemovedTask extends PendingHandlerCallback {
|
|
|
|
PendingHandlerRemovedTask(AbstractChannelHandlerContext ctx) {
|
|
super(ctx);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
callHandlerRemoved0(ctx);
|
|
}
|
|
|
|
@Override
|
|
void execute() {
|
|
EventExecutor executor = ctx.executor();
|
|
if (executor.inEventLoop()) {
|
|
callHandlerRemoved0(ctx);
|
|
} else {
|
|
try {
|
|
executor.execute(this);
|
|
} catch (RejectedExecutionException e) {
|
|
if (logger.isWarnEnabled()) {
|
|
logger.warn(
|
|
"Can't invoke handlerRemoved() as the EventExecutor {} rejected it," +
|
|
" removing handler {}.", executor, ctx.name(), e);
|
|
}
|
|
// remove0(...) was call before so just call AbstractChannelHandlerContext.setRemoved().
|
|
ctx.setRemoved();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|