/*
* Copyright 2017 Async-IO.org
*
* 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.atmosphere.kafka;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.Deserializer;
import org.apache.kafka.common.serialization.Serializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.atmosphere.cpr.AtmosphereConfig;
import org.atmosphere.cpr.Broadcaster;
import org.atmosphere.util.AbstractBroadcasterProxy;
import org.atmosphere.util.ExecutorsFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Kafka Support via a {@link Broadcaster}
*
* @author Jeanfrancois Arcand.
*/
public class KafkaBroadcaster extends AbstractBroadcasterProxy {
private final Logger logger = LoggerFactory.getLogger(KafkaBroadcaster.class);
public final static String PROPERTIES_FILE = "org.atmosphere.kafka.propertiesFile";
private String topic;
// using kafka 0.9+ API
private KafkaProducer producer;
private KafkaConsumer consumer;
private final Serializer stringSerializer = new StringSerializer();
private final Deserializer stringDeserializer = new StringDeserializer();
private final AtomicBoolean closed = new AtomicBoolean(false);
@Override
public Broadcaster initialize(String id, URI uri, final AtmosphereConfig config) {
super.initialize(id, uri, config);
topic = id.equals(ROOT_MASTER) ? "atmosphere" : id.replaceAll("[^a-zA-Z0-9\\s]", "");
// We are thread-safe
producer = (KafkaProducer) config.properties().get("producer");
Set<String> topics = (Set<String>) config.properties().get("topics");
if (topics == null) {
topics = new HashSet<String>();
config.properties().put("topics", topics);
}
// create a new producer and consumer when the topic changes
if (topics.isEmpty() || !topics.contains(topic)) {
String load = config.getInitParameter(PROPERTIES_FILE, null);
Properties props = new Properties();
// let each consumer use its own group by default so that each can receive messages
UUID uuid = UUID.randomUUID();
String defaultGroupId = "atmosphere-consumer-" + Long.toHexString(uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits());
props.put("group.id", defaultGroupId);
props.put("bootstrap.servers", "127.0.0.1:9092");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
if (load != null) {
try {
props.load(config.getServletContext().getResourceAsStream(load));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (topics.isEmpty()) {
// producer can be reused, so it is instantiated only once
producer = new KafkaProducer<String, String>(props, stringSerializer, stringSerializer);
config.properties().put("producer", producer);
}
// consumer needs to be created for each topic subscription
consumer = new KafkaConsumer<String, String>(props, stringDeserializer, stringDeserializer);
topics.add(topic);
startConsumer();
}
return this;
}
@Override
public synchronized void destroy() {
closed.set(true);
super.destroy();
}
void startConsumer() {
consumer.subscribe(Arrays.asList(topic));
ExecutorsFactory.getMessageDispatcher(config, "kafka").execute(new Runnable() {
@Override
public void run() {
while (!closed.get()) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
broadcastReceivedMessage(record.value());
}
}
consumer.close();
((Set<String>)config.properties().get("topics")).remove(topic);
}
});
}
@Override
public void incomingBroadcast() {
}
@Override
public void outgoingBroadcast(Object message) {
logger.trace("{} outgoingBroadcast {}", topic, message);
// TODO: Prevent message round trip.
producer.send(new ProducerRecord<String, String>(topic, message.toString()));
}
}