/** * Copyright 2015 Otto (GmbH & Co KG) * * 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.ottogroup.bi.spqr.websocket.kafka; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import org.apache.log4j.Logger; import uk.co.real_logic.queues.BlockingWaitStrategy; import uk.co.real_logic.queues.MessageWaitStrategy; import uk.co.real_logic.queues.OneToOneConcurrentArrayQueue3; import com.ottogroup.bi.spqr.exception.RequiredInputMissingException; /** * Receives a web socket connection and starts to emit incoming messages received from configured * kafka topic * @author mnxfst * @since Apr 17, 2015 */ public class KafkaTopicWebSocketEmitter implements Runnable { /** our faithful logging facility ... ;-) */ private static final Logger logger = Logger.getLogger(KafkaTopicWebSocketEmitter.class); /** web socket channel to write content to */ private final Channel websocketChannel; /** queue used for receiving kafka content */ private final OneToOneConcurrentArrayQueue3<byte[]> messages; /** strategy to apply when waiting for messages from queue */ private final MessageWaitStrategy<byte[]> messageWaitStrategy; /** runtime environment for topic stream consumers */ private final ExecutorService executorService; /** consumer client establishing a connection with the Kafka server and spawning stream readers */ private KafkaTopicConsumer consumer; /** connection string used for establishing a connection with zookeeper */ private final String zookeeperConnect; /** group identifier assigned to kafka client when establishing a connection with kafka */ private final String groupId; /** topic to consume data from */ private final String topicId; /** indicates whether the emitter is running or halted */ private boolean running = false; /** * Initializes the socket emitter using the provided input * @param websocketChannel channel to write incoming messages to * @param messageQueueCapacity max. number of elements to be contained inside the queue used for internal data exchange * @param executorService runtime environment stream consumers will live in * @param zookeeperConnect zookeeper connection string, eg. localhost:2181 * @param groupId group identifier used by client when connecting with Kafka * @param topicId topic to consume data from */ public KafkaTopicWebSocketEmitter(final Channel websocketChannel, final int messageQueueCapacity, final ExecutorService executorService, final String zookeeperConnect, final String groupId, final String topicId) { this.websocketChannel = websocketChannel; this.messages = new OneToOneConcurrentArrayQueue3<byte[]>(messageQueueCapacity); this.messageWaitStrategy = new BlockingWaitStrategy(); this.executorService = executorService; this.zookeeperConnect = zookeeperConnect; this.groupId = groupId; this.topicId = topicId; if(logger.isDebugEnabled()) logger.debug("kafka topic to websocket emitter initialized [topic="+topicId+", group="+groupId+", zkConnect="+zookeeperConnect+"]"); } /** * @see java.lang.Runnable#run() */ public void run() { // prepare topic consumer settings and initialize the client Map<String, String> settings = new HashMap<>(); settings.put(KafkaTopicConsumer.CFG_OPT_KAFKA_GROUP_ID, groupId); settings.put(KafkaTopicConsumer.CFG_OPT_KAFKA_ZOOKEEPER_CONNECT, zookeeperConnect); settings.put(KafkaTopicConsumer.CFG_OPT_KAFKA_TOPIC, topicId); this.consumer = new KafkaTopicConsumer(this.messages, this.messageWaitStrategy, this.executorService); try { this.consumer.initialize(settings); } catch (RequiredInputMissingException e) { throw new RuntimeException(e); } // keep on running until externally halted (set running to 'false') this.running = true; while(this.running) { // fetch message from queue via provided wait strategy byte[] message = null; try { message = this.messageWaitStrategy.waitFor(messages); } catch(InterruptedException e) { // } // if the byte array contains anything, forward it to web socket, otherwise try to fetch a new message from queue if(message != null) { try { websocketChannel.writeAndFlush(new TextWebSocketFrame(Unpooled.copiedBuffer(message))); } catch(Exception e) { logger.error("Failed to write message to websocket. Error: " + e.getMessage()); } } } // shut down consumer and websocket channel this.consumer.shutdown(); this.websocketChannel.close(); if(logger.isDebugEnabled()) logger.debug("kafka topic to websocket emitter shut down"); } /** * Signle the emitter to halt */ public void shutdown() { this.running = false; } }