/*
* Copyright 2016 ANI Technologies Pvt. Ltd.
*
* 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.olacabs.fabric.compute.pipeline;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.olacabs.fabric.compute.comms.ChannelFactory;
import com.olacabs.fabric.compute.comms.CommsChannel;
import com.olacabs.fabric.compute.source.PipelineStreamSource;
import com.olacabs.fabric.compute.tracking.SimpleBitSet;
import com.olacabs.fabric.model.event.EventSet;
import lombok.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
*TODO Add more.
*/
public class NotificationBus {
private static final Logger LOGGER = LoggerFactory.getLogger(NotificationBus.class);
private final Map<Long, SimpleBitSet> tracker = Maps.newConcurrentMap();
private Properties properties;
private Map<Integer, Connection> connections = Maps.newHashMap();
private Map<Integer, Communicator> comms = Maps.newHashMap();
private Map<Integer, PipelineStreamSource> sources = Maps.newHashMap();
public NotificationBus(final Properties properties) {
this.properties = properties;
LOGGER.info("Notification bus created...");
}
public NotificationBus source(PipelineStreamSource streamSource) {
sources.put(streamSource.communicationId(), streamSource);
return this;
}
public NotificationBus connect(MessageSource to, PipelineStage... pipelineStages) {
if (!connections.containsKey(to.communicationId())) {
connections.put(to.communicationId(), new Connection());
}
for (PipelineStage pipelineStage : pipelineStages) {
connections.get(to.communicationId()).addConnection(pipelineStage.communicationId());
if (!comms.containsKey(pipelineStage.communicationId())) {
comms.put(pipelineStage.communicationId(),
Communicator.builder()
.commsChannel(ChannelFactory.create(properties, pipelineStage.name(), false, pipelineStage))
.pipelineStage(pipelineStage)
.build());
}
}
return this;
}
public synchronized void publish(PipelineMessage message, int from) {
publish(message, from, true);
}
public synchronized void publish(PipelineMessage message, int from, boolean forward) {
switch (message.getMessageType()) {
case TIMER:
// It is a timer message, ACKing is disabled
comms.get(from).commsChannel.publish(message);
break;
case USERSPACE:
// It is a userspace message, ACKing is enabled
PipelineMessage actionableMessage = message;
ImmutableSet.Builder<EventSet> ackCandidatesBuilder = ImmutableSet.builder();
if (!message.getMessages().isAggregate()) {
// Find out the oldest ancestor of this event set
while (true) {
if (null == actionableMessage.getParent()) {
break;
}
actionableMessage = actionableMessage.getParent();
}
if (!tracker.containsKey(actionableMessage.getMessages().getId())) {
// Set up the tracker for this event set since it is sent from a source for the first time
// Add this event set to the tracker with an empty bitset
tracker.put(actionableMessage.getMessages().getId(), new SimpleBitSet(64));
}
SimpleBitSet msgBitSet = tracker.get(actionableMessage.getMessages().getId());
try {
// If the event set is being sent forward and the sender sends normal message, set bits
// for each of the receivers based on the auto incremented id assigned to them
if (forward
&& connections.containsKey(from)
&& ((comms.containsKey(from) && comms.get(from).pipelineStage.sendsNormalMessage())
|| !comms.containsKey(from))) {
connections.get(from).pipelineStages.stream().forEach(msgBitSet::set);
}
} catch (Exception e) {
LOGGER.error("Error setting tracking bits for generator: {}", from, e);
}
// unset the bit corresponding to the sender
msgBitSet.unset(from);
// if all the bits are unset and if this event set is generated by a source, mark the
// event set as a candidate for ACKing
if (!msgBitSet.hasSetBits() && actionableMessage.getMessages().isSourceGenerated()) {
ackCandidatesBuilder.add(actionableMessage.getMessages());
}
}
try {
// publish the message to each of the receivers
if (forward && connections.containsKey(from)) {
connections.get(from).pipelineStages.stream()
.forEach(pipeLineStage -> comms.get(pipeLineStage).commsChannel.publish(message));
}
} catch (Throwable t) {
LOGGER.error("Error sending event to children for {}", from, t);
}
// event sets which are eligible for ACKing
ImmutableSet<EventSet> ackSet = ackCandidatesBuilder.build();
// ACK all the event sets
ackSet.stream()
.forEach(eventSet -> sources.get(eventSet.getSourceId()).ackMessage(eventSet));
// No event set to ACK, hence we can remove the tracker entry for this event set
if (!ackSet.isEmpty()) {
tracker.remove(actionableMessage.getMessages().getId());
}
break;
default: break;
}
}
public void start() {
connections = ImmutableMap.copyOf(connections);
comms.values().forEach(communicator -> communicator.commsChannel.start());
}
public void stop() {
comms.values().forEach(communicator -> communicator.commsChannel.stop());
}
private static class Connection {
private final Set<Integer> pipelineStages;
Connection() {
pipelineStages = Sets.newHashSet();
}
Connection addConnection(int pipelineStage) {
pipelineStages.add(pipelineStage);
return this;
}
}
/**
* TODO javadoc.
*/
@Builder
public static class Communicator {
private MessageSource pipelineStage;
private CommsChannel<PipelineMessage> commsChannel;
}
}