/*
* Copyright MapR Technologies, 2013
*
* 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 com.mapr.franz.hazel;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.googlecode.protobuf.pro.duplex.PeerInfo;
import com.googlecode.protobuf.pro.duplex.execute.ThreadPoolCallExecutor;
import com.googlecode.protobuf.pro.duplex.server.DuplexTcpServerBootstrap;
import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.mapr.franz.Server;
import com.mapr.franz.catcher.Client;
import com.mapr.franz.catcher.wire.Catcher;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.SecureRandom;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
/**
* Server process for catching log messages.
* <p/>
* A server is required to do a number of things:
* <p/>
* a) catch messages for topics it is handling.
* <p/>
* b) catch and forward messages for servers it is not handling
* <p/>
* c) respond to hello messages with a list of the catchers in service
* <p/>
* d) report traffic statistics on topics every few seconds
* <p/>
* e) clean up old queue files when starting a new file.
* <p/>
* Tasks (a), (b) and (c) are handled by the server implementation.
* <p/>
* Task (d) by the statistics reporter.
* <p/>
* Task (e) is handled as part of the message appender.
*/
public class HazelCatcher {
private static Logger logger = LoggerFactory.getLogger(HazelCatcher.class);
public static void main(String[] args) throws FileNotFoundException, SocketException {
run(parseOptions(args));
}
public static void run(Options opts) throws SocketException, FileNotFoundException {
logger.warn("Starting server {}", opts);
// start this first so that Hazel has to take second pickings
DuplexTcpServerBootstrap bootstrap = startProtoServer(opts.port);
HazelcastInstance instance = setupHazelCast(opts.hosts);
Server us = recordServerInstance(opts, instance);
bootstrap.getRpcServiceRegistry().registerBlockingService(
Catcher.CatcherService.newReflectiveBlockingService(new CatcherImpl(us, instance, opts.basePath)));
bootstrap.bind();
}
private static DuplexTcpServerBootstrap startProtoServer(int port) {
PeerInfo serverInfo = new PeerInfo("0.0.0.0", port);
DuplexTcpServerBootstrap bootstrap = new DuplexTcpServerBootstrap(
serverInfo,
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool())
);
bootstrap.setRpcServerCallExecutor(new ThreadPoolCallExecutor(10, 10));
return bootstrap;
}
private static Server recordServerInstance(Options opts, HazelcastInstance instance) throws SocketException {
List<Client.HostPort> addresses = Lists.newArrayList();
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface ifc = networkInterfaces.nextElement();
if (!ifc.isLoopback()) {
for (InterfaceAddress address : ifc.getInterfaceAddresses()) {
addresses.add(new Client.HostPort(address.getAddress().getHostAddress(), opts.port));
}
}
}
long serverId = new SecureRandom().nextLong();
Set<Server> servers = instance.getSet("servers");
Server r = new Server(serverId, addresses);
servers.add(r);
logger.warn("Currently have these servers: {}", servers);
return r;
}
private static HazelcastInstance setupHazelCast(String hosts) {
Config config = new ClasspathXmlConfig("cluster.xml");
Splitter onCommas = Splitter.on(",").omitEmptyStrings().trimResults();
for (String host : onCommas.split(hosts)) {
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(host);
}
logger.warn("config = {}", config);
return Hazelcast.newHazelcastInstance(config);
}
private static Options parseOptions(String[] args) {
Options opts = new Options();
CmdLineParser parser = new CmdLineParser(opts);
try {
parser.parseArgument(args);
} catch (CmdLineException e) {
e.printStackTrace(System.err);
parser.printUsage(System.err);
System.exit(1);
}
if (opts.hosts == null) {
parser.printUsage(System.err);
System.exit(1);
}
return opts;
}
public static class Options {
public Options host(String hosts) {
this.hosts = hosts;
return this;
}
public Options base(String basePath) {
this.basePath = basePath;
return this;
}
public Options port(int port) {
this.port = port;
return this;
}
@Option(name = "-cluster", usage = "Comma separated list of at least one node's hostname or IP address in the cluster. IP and port ranges are acceptable here.")
String hosts;
@Option(name = "-port", usage = "Port number for the catcher server to listen to")
int port = 5900;
@Option(name = "-base", usage = "Home directory for recording topics")
String basePath = "/tmp/mapr-spout";
@Override
public String toString() {
return "Options{" +
"basePath='" + basePath + '\'' +
", hosts='" + hosts + '\'' +
", port=" + port +
'}';
}
}
}