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:
Norman Maurer 2020-02-06 09:02:31 +01:00 committed by GitHub
parent 38b5607c6d
commit 56055f4404
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 18 deletions

View File

@ -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() {

View File

@ -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));
}

View File

@ -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() {

View File

@ -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) {