//
// Copyright 2010 Cinch Logic Pty Ltd.
//
// http://www.chililog.com
//
// 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 org.chililog.server.pubsub.jsonhttp;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.apache.commons.lang.StringUtils;
import org.chililog.server.common.AppProperties;
import org.chililog.server.common.Log4JLogger;
import org.chililog.server.pubsub.MqProducerSessionPool;
import org.chililog.server.pubsub.jsonhttp.JsonHttpServerPipelineFactory;
import org.hornetq.api.core.TransportConfiguration;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.ChannelGroupFuture;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
/**
* <p>
* The PubSubService controls the embedded Netty web server used to provide publication and sububscription services
* using JSON over HTTP and HTTP web sockets.
* </p>
*
* <pre class="example">
* // Start web server
* JsonHttpService.getInstance().start();
*
* // Stop web server
* JsonHttpService.getInstance().stop();
* </pre>
*
* <p>
* The web server's request handling pipeline is setup by {@link JsonHttpServerPipelineFactory}. Two Netty thread pools
* are used:
* <ul>
* <li>One for the channel bosses (the server channel acceptors). See NioServerSocketChannelFactory javadoc.</li>
* <li>One for the accepted channels (called workers). See NioServerSocketChannelFactory javadoc.</li>
* </ul>
* </p>
* <p>
* Here's a description of how it all works from http://www.jboss.org/netty/community#nabble-td3434933.
* </p>
*
* <pre class="example">
* For posterity, updated notes on Netty's concurrency architecture:
*
* After calling ServerBootstrap.bind(), Netty starts a boss thread that just accepts new connections and registers them
* with one of the workers from the worker pool in round-robin fashion (pool size defaults to CPU count). Each worker
* runs its own select loop over just the set of keys that have been registered with it. Workers start lazily on demand
* and run only so long as there are interested fd's/keys. All selected events are handled in the same thread and sent
* up the pipeline attached to the channel (this association is established by the boss as soon as a new connection is
* accepted).
*
* All workers, and the boss, run via the executor thread pool; hence, the executor must support at least two
* simultaneous threads.
*
* A pipeline implements the intercepting filter pattern. A pipeline is a sequence of handlers. Whenever a packet is
* read from the wire, it travels up the stream, stopping at each handler that can handle upstream events. Vice-versa
* for writes. Between each filter, control flows back through the centralized pipeline, and a linked list of contexts
* keeps track of where we are in the pipeline (one context object per handler).
* </pre>
*
* @author vibul
*
*/
public class JsonHttpService {
private static Log4JLogger _logger = Log4JLogger.getLogger(JsonHttpService.class);
private static final ChannelGroup _allChannels = new DefaultChannelGroup("PubSubJsonHttpWebServerManager");
private ChannelFactory _channelFactory = null;
private MqProducerSessionPool _mqProducerSessionPool = null;
/**
* Returns the singleton instance for this class
*/
public static JsonHttpService getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* SingletonHolder is loaded on the first execution of Singleton.getInstance() or the first access to
* SingletonHolder.INSTANCE, not before.
*
* See http://en.wikipedia.org/wiki/Singleton_pattern
*/
private static class SingletonHolder {
public static final JsonHttpService INSTANCE = new JsonHttpService();
}
/**
* <p>
* Singleton constructor
* </p>
* <p>
* If there is an exception, we log the error and exit because there's no point continuing without MQ client session
* </p>
*
* @throws Exception
*/
private JsonHttpService() {
return;
}
/**
* Start all pubsub services
*/
public synchronized void start() {
// Create producer session pool equivalent to the number of threads for Json HTTP so that each thread does not
// have to wait for a session
_mqProducerSessionPool = new MqProducerSessionPool(AppProperties.getInstance()
.getPubSubJsonHttpNettyHandlerThreadPoolSize());
AppProperties appProperties = AppProperties.getInstance();
if (_channelFactory != null) {
_logger.info("PubSub JSON HTTP Web Sever Already Started.");
return;
}
_logger.info("Starting PubSub JSON HTTP Web Sever on " + appProperties.getPubSubJsonHttpHost() + ":"
+ appProperties.getPubSubJsonHttpPort() + "...");
// Create channel factory
int workerCount = appProperties.getPubSubJsonHttpNettyWorkerThreadPoolSize();
if (workerCount == 0) {
_channelFactory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
} else {
_channelFactory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
Executors.newCachedThreadPool(), workerCount);
}
// Configure the server.
ServerBootstrap bootstrap = new ServerBootstrap(_channelFactory);
// Setup thread pool to run our handler
Executor executor = Executors.newFixedThreadPool(appProperties.getPubSubJsonHttpNettyHandlerThreadPoolSize());
// Set up the event pipeline factory.
bootstrap.setPipelineFactory(new JsonHttpServerPipelineFactory(executor));
// Bind and start to accept incoming connections.
String[] hosts = TransportConfiguration.splitHosts(appProperties.getPubSubJsonHttpHost());
for (String h : hosts) {
if (StringUtils.isBlank(h)) {
if (hosts.length == 1) {
h = "0.0.0.0";
} else {
continue;
}
}
SocketAddress address = h.equals("0.0.0.0") ? new InetSocketAddress(
appProperties.getPubSubJsonHttpPort()) : new InetSocketAddress(h,
appProperties.getPubSubJsonHttpPort());
Channel channel = bootstrap.bind(address);
_allChannels.add(channel);
}
_logger.info("PubSub JSON HTTP Web Sever Started.");
}
/**
* Stop all pubsub services
*/
public synchronized void stop() {
if (_mqProducerSessionPool != null) {
_mqProducerSessionPool.cleanup();
_mqProducerSessionPool = null;
}
_logger.info("Stopping PubSub JSON HTTP Sever ...");
ChannelGroupFuture future = _allChannels.close();
future.awaitUninterruptibly();
_channelFactory.releaseExternalResources();
_channelFactory = null;
_logger.info("PubSub JSON HTTP Sever Stopped.");
}
/**
* Returns the producer session pool
*/
MqProducerSessionPool getMqProducerSessionPool() {
return _mqProducerSessionPool;
}
/**
* Returns the group holding all channels so we can shutdown without hanging
*/
ChannelGroup getAllChannels() {
return _allChannels;
}
}