Ensure ChannelOptions are applied in the same order as configured in *Bootstrap (#9998)
Motivation: https://github.com/netty/netty/pull/9458 changed how we handled ChannelOptions internally to use a ConcurrentHashMap. This unfortunally had the side-effect that the ordering may be affected and not stable anymore. Here the problem is that sometimes we do validation based on two different ChannelOptions (for example we validate high and low watermarks against each other). Thus even if the user specified the options in the same order we may fail to configure them. Modifications: - Use again a LinkedHashMap to preserve order - Add unit test Result: Apply ChannelOptions in correct and expected order
This commit is contained in:
parent
38b5607c6d
commit
56055f4404
@ -39,6 +39,7 @@ import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@ -59,7 +60,10 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
|
||||
@SuppressWarnings("deprecation")
|
||||
private volatile ChannelFactory<? extends C> channelFactory;
|
||||
private volatile SocketAddress localAddress;
|
||||
private final Map<ChannelOption<?>, Object> options = new ConcurrentHashMap<ChannelOption<?>, Object>();
|
||||
|
||||
// The order in which ChannelOptions are applied is important they may depend on each other for validation
|
||||
// purposes.
|
||||
private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
|
||||
private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
|
||||
private volatile ChannelHandler handler;
|
||||
|
||||
@ -72,7 +76,9 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
|
||||
channelFactory = bootstrap.channelFactory;
|
||||
handler = bootstrap.handler;
|
||||
localAddress = bootstrap.localAddress;
|
||||
options.putAll(bootstrap.options);
|
||||
synchronized (bootstrap.options) {
|
||||
options.putAll(bootstrap.options);
|
||||
}
|
||||
attrs.putAll(bootstrap.attrs);
|
||||
}
|
||||
|
||||
@ -166,10 +172,12 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
|
||||
*/
|
||||
public <T> B option(ChannelOption<T> option, T value) {
|
||||
ObjectUtil.checkNotNull(option, "option");
|
||||
if (value == null) {
|
||||
options.remove(option);
|
||||
} else {
|
||||
options.put(option, value);
|
||||
synchronized (options) {
|
||||
if (value == null) {
|
||||
options.remove(option);
|
||||
} else {
|
||||
options.put(option, value);
|
||||
}
|
||||
}
|
||||
return self();
|
||||
}
|
||||
@ -377,6 +385,12 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
|
||||
*/
|
||||
public abstract AbstractBootstrapConfig<B, C> config();
|
||||
|
||||
final Map.Entry<ChannelOption<?>, Object>[] newOptionsArray() {
|
||||
synchronized (options) {
|
||||
return options.entrySet().toArray(EMPTY_OPTION_ARRAY);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<ChannelOption<?>, Object> options0() {
|
||||
return options;
|
||||
}
|
||||
@ -399,7 +413,9 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
|
||||
}
|
||||
|
||||
final Map<ChannelOption<?>, Object> options() {
|
||||
return copiedMap(options);
|
||||
synchronized (options) {
|
||||
return copiedMap(options);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<AttributeKey<?>, Object> attrs() {
|
||||
|
@ -256,7 +256,7 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
|
||||
ChannelPipeline p = channel.pipeline();
|
||||
p.addLast(config.handler());
|
||||
|
||||
setChannelOptions(channel, options0().entrySet().toArray(EMPTY_OPTION_ARRAY), logger);
|
||||
setChannelOptions(channel, newOptionsArray(), logger);
|
||||
setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ import io.netty.util.internal.ObjectUtil;
|
||||
import io.netty.util.internal.logging.InternalLogger;
|
||||
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -45,7 +46,9 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
|
||||
|
||||
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class);
|
||||
|
||||
private final Map<ChannelOption<?>, Object> childOptions = new ConcurrentHashMap<ChannelOption<?>, Object>();
|
||||
// The order in which child ChannelOptions are applied is important they may depend on each other for validation
|
||||
// purposes.
|
||||
private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
|
||||
private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
|
||||
private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
|
||||
private volatile EventLoopGroup childGroup;
|
||||
@ -57,7 +60,9 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
|
||||
super(bootstrap);
|
||||
childGroup = bootstrap.childGroup;
|
||||
childHandler = bootstrap.childHandler;
|
||||
childOptions.putAll(bootstrap.childOptions);
|
||||
synchronized (bootstrap.childOptions) {
|
||||
childOptions.putAll(bootstrap.childOptions);
|
||||
}
|
||||
childAttrs.putAll(bootstrap.childAttrs);
|
||||
}
|
||||
|
||||
@ -90,10 +95,12 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
|
||||
*/
|
||||
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {
|
||||
ObjectUtil.checkNotNull(childOption, "childOption");
|
||||
if (value == null) {
|
||||
childOptions.remove(childOption);
|
||||
} else {
|
||||
childOptions.put(childOption, value);
|
||||
synchronized (childOptions) {
|
||||
if (value == null) {
|
||||
childOptions.remove(childOption);
|
||||
} else {
|
||||
childOptions.put(childOption, value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@ -122,15 +129,17 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
|
||||
|
||||
@Override
|
||||
void init(Channel channel) {
|
||||
setChannelOptions(channel, options0().entrySet().toArray(EMPTY_OPTION_ARRAY), logger);
|
||||
setChannelOptions(channel, newOptionsArray(), logger);
|
||||
setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
|
||||
|
||||
ChannelPipeline p = channel.pipeline();
|
||||
|
||||
final EventLoopGroup currentChildGroup = childGroup;
|
||||
final ChannelHandler currentChildHandler = childHandler;
|
||||
final Entry<ChannelOption<?>, Object>[] currentChildOptions =
|
||||
childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
|
||||
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
|
||||
synchronized (childOptions) {
|
||||
currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
|
||||
}
|
||||
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);
|
||||
|
||||
p.addLast(new ChannelInitializer<Channel>() {
|
||||
@ -261,7 +270,9 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
|
||||
}
|
||||
|
||||
final Map<ChannelOption<?>, Object> childOptions() {
|
||||
return copiedMap(childOptions);
|
||||
synchronized (childOptions) {
|
||||
return copiedMap(childOptions);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<AttributeKey<?>, Object> childAttrs() {
|
||||
|
@ -17,15 +17,20 @@
|
||||
package io.netty.bootstrap;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelConfig;
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelInboundHandler;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.DefaultChannelConfig;
|
||||
import io.netty.channel.DefaultEventLoop;
|
||||
import io.netty.channel.DefaultEventLoopGroup;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.ServerChannel;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
@ -47,7 +52,9 @@ import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
@ -293,6 +300,56 @@ public class BootstrapTest {
|
||||
assertThat(connectFuture.channel(), is(not(nullValue())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannelOptionOrderPreserve() throws InterruptedException {
|
||||
final BlockingQueue<ChannelOption<?>> options = new LinkedBlockingQueue<ChannelOption<?>>();
|
||||
class ChannelConfigValidator extends DefaultChannelConfig {
|
||||
ChannelConfigValidator(Channel channel) {
|
||||
super(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> boolean setOption(ChannelOption<T> option, T value) {
|
||||
options.add(option);
|
||||
return super.setOption(option, value);
|
||||
}
|
||||
}
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final Bootstrap bootstrap = new Bootstrap()
|
||||
.handler(new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
protected void initChannel(Channel ch) {
|
||||
latch.countDown();
|
||||
}
|
||||
})
|
||||
.group(groupA)
|
||||
.channelFactory(new ChannelFactory<Channel>() {
|
||||
@Override
|
||||
public Channel newChannel() {
|
||||
return new LocalChannel() {
|
||||
private ChannelConfigValidator config;
|
||||
@Override
|
||||
public synchronized ChannelConfig config() {
|
||||
if (config == null) {
|
||||
config = new ChannelConfigValidator(this);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 1)
|
||||
.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 2);
|
||||
|
||||
bootstrap.register().syncUninterruptibly();
|
||||
|
||||
latch.await();
|
||||
|
||||
// Check the order is the same as what we defined before.
|
||||
assertSame(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, options.take());
|
||||
assertSame(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, options.take());
|
||||
}
|
||||
|
||||
private static final class DelayedEventLoopGroup extends DefaultEventLoop {
|
||||
@Override
|
||||
public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
|
||||
|
Loading…
Reference in New Issue
Block a user