/*
* Licensed to Crate under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial
* agreement.
*/
package io.crate.protocols.postgres;
import com.carrotsearch.hppc.IntHashSet;
import com.carrotsearch.hppc.IntSet;
import com.google.common.annotations.VisibleForTesting;
import io.crate.action.sql.SQLOperations;
import io.crate.operation.auth.Authentication;
import io.crate.operation.auth.AuthenticationProvider;
import io.crate.settings.CrateSetting;
import io.crate.types.DataTypes;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Singleton;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.common.network.NetworkService;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.BoundTransportAddress;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.transport.PortsRange;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.http.BindHttpException;
import org.elasticsearch.transport.BindTransportException;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import static org.elasticsearch.common.util.concurrent.EsExecutors.daemonThreadFactory;
@Singleton
public class PostgresNetty extends AbstractLifecycleComponent {
public static final CrateSetting<Boolean> PSQL_ENABLED_SETTING = CrateSetting.of(Setting.boolSetting(
"psql.enabled", true,
Setting.Property.NodeScope), DataTypes.BOOLEAN);
public static final CrateSetting<String> PSQL_PORT_SETTING = CrateSetting.of(new Setting<>(
"psql.port", "5432-5532",
Function.identity(), Setting.Property.NodeScope), DataTypes.STRING);
private final SQLOperations sqlOperations;
private final NetworkService networkService;
private final boolean enabled;
private final String port;
private final AuthenticationProvider authProvider;
private final Logger namedLogger;
private ServerBootstrap bootstrap;
private final List<Channel> serverChannels = new ArrayList<>();
private final List<InetSocketTransportAddress> boundAddresses = new ArrayList<>();
@Nullable
private BoundTransportAddress boundAddress = null;
@Inject
public PostgresNetty(Settings settings,
SQLOperations sqlOperations,
NetworkService networkService,
AuthenticationProvider authProvider) {
super(settings);
namedLogger = Loggers.getLogger("psql", settings);
this.sqlOperations = sqlOperations;
this.networkService = networkService;
this.authProvider = authProvider;
enabled = PSQL_ENABLED_SETTING.setting().get(settings);
port = PSQL_PORT_SETTING.setting().get(settings);
}
@Nullable
public BoundTransportAddress boundAddress() {
return boundAddress;
}
@Override
protected void doStart() {
if (!enabled) {
return;
}
Authentication authService = authProvider.authService();
bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(daemonThreadFactory(settings, "postgres-netty-boss")),
Executors.newCachedThreadPool(daemonThreadFactory(settings, "postgres-netty-worker"))
));
bootstrap.setOption("child.tcpNoDelay", settings.getAsBoolean("tcp_no_delay", true));
bootstrap.setOption("child.keepAlive", settings.getAsBoolean("tcp_keep_alive", true));
bootstrap.setPipelineFactory(() -> {
ChannelPipeline pipeline = Channels.pipeline();
ConnectionContext connectionContext = new ConnectionContext(sqlOperations, authService);
pipeline.addLast("frame-decoder", connectionContext.decoder);
pipeline.addLast("handler", connectionContext.handler);
return pipeline;
});
boolean success = false;
try {
boundAddress = resolveBindAddress();
namedLogger.info("{}", boundAddress);
success = true;
} finally {
if (!success) {
doStop(); // stop boss/worker threads to avoid leaks
}
}
}
static int resolvePublishPort(List<InetSocketTransportAddress> boundAddresses, InetAddress publishInetAddress) {
for (InetSocketTransportAddress boundAddress : boundAddresses) {
InetAddress boundInetAddress = boundAddress.address().getAddress();
if (boundInetAddress.isAnyLocalAddress() || boundInetAddress.equals(publishInetAddress)) {
return boundAddress.getPort();
}
}
// if no matching boundAddress found, check if there is a unique port for all bound addresses
final IntSet ports = new IntHashSet();
for (InetSocketTransportAddress boundAddress : boundAddresses) {
ports.add(boundAddress.getPort());
}
if (ports.size() == 1) {
return ports.iterator().next().value;
}
throw new BindHttpException("Failed to auto-resolve psql publish port, multiple bound addresses " + boundAddresses +
" with distinct ports and none of them matched the publish address (" + publishInetAddress + "). ");
}
private BoundTransportAddress resolveBindAddress() {
// Bind and start to accept incoming connections.
try {
InetAddress[] hostAddresses = networkService.resolveBindHostAddresses(null);
for (InetAddress address : hostAddresses) {
if (address instanceof Inet4Address) {
boundAddresses.add(bindAddress(address));
}
}
} catch (IOException e) {
throw new BindPostgresException("Failed to resolve binding network host", e);
}
final InetAddress publishInetAddress;
try {
publishInetAddress = networkService.resolvePublishHostAddresses(null);
} catch (Exception e) {
throw new BindTransportException("Failed to resolve publish address", e);
}
final int publishPort = resolvePublishPort(boundAddresses, publishInetAddress);
final InetSocketAddress publishAddress = new InetSocketAddress(publishInetAddress, publishPort);
return new BoundTransportAddress(boundAddresses.toArray(new TransportAddress[boundAddresses.size()]), new InetSocketTransportAddress(publishAddress));
}
private InetSocketTransportAddress bindAddress(final InetAddress hostAddress) {
PortsRange portsRange = new PortsRange(port);
final AtomicReference<Exception> lastException = new AtomicReference<>();
final AtomicReference<InetSocketAddress> boundSocket = new AtomicReference<>();
boolean success = portsRange.iterate(portNumber -> {
try {
Channel channel = bootstrap.bind(new InetSocketAddress(hostAddress, portNumber));
serverChannels.add(channel);
boundSocket.set((InetSocketAddress) channel.getLocalAddress());
} catch (Exception e) {
lastException.set(e);
return false;
}
return true;
});
if (!success) {
throw new BindPostgresException("Failed to bind to [" + port + "]", lastException.get());
}
if (logger.isDebugEnabled()) {
logger.debug("Bound psql to address {{}}", NetworkAddress.format(boundSocket.get()));
}
return new InetSocketTransportAddress(boundSocket.get());
}
@Override
protected void doStop() {
for (Channel channel : serverChannels) {
channel.close().awaitUninterruptibly();
}
if (bootstrap != null) {
bootstrap.releaseExternalResources();
bootstrap = null;
}
}
@Override
protected void doClose() {
}
@VisibleForTesting
public Collection<InetSocketTransportAddress> boundAddresses() {
return Collections.unmodifiableCollection(boundAddresses);
}
}