// Copyright 2016 Twitter. All rights reserved.
//
// 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.twitter.heron.simulator.executors;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.twitter.heron.api.generated.TopologyAPI;
import com.twitter.heron.common.basics.SlaveLooper;
import com.twitter.heron.common.basics.WakeableLooper;
import com.twitter.heron.proto.stmgr.StreamManager;
import com.twitter.heron.proto.system.HeronTuples;
import com.twitter.heron.proto.system.PhysicalPlans;
import com.twitter.heron.simulator.utils.PhysicalPlanUtil;
import com.twitter.heron.simulator.utils.StreamConsumers;
import com.twitter.heron.simulator.utils.TupleCache;
import com.twitter.heron.simulator.utils.XORManager;
public class StreamExecutor implements Runnable {
public static final int NUM_BUCKETS = 3;
private static final Logger LOG = Logger.getLogger(InstanceExecutor.class.getName());
// TaskId -> InstanceExecutor
private final Map<Integer, InstanceExecutor> taskIdToInstanceExecutor;
private final Map<TopologyAPI.StreamId, StreamConsumers> streamIdStreamConsumersMap;
private final Set<String> spoutSets;
private final XORManager xorManager;
private final TupleCache tupleCache;
private final WakeableLooper looper;
public StreamExecutor(PhysicalPlans.PhysicalPlan pPlan) {
this.taskIdToInstanceExecutor = new HashMap<>();
this.looper = createWakeableLooper();
this.spoutSets = createSpoutsSet(pPlan);
Map<String, List<Integer>> componentToTaskIds =
PhysicalPlanUtil.getComponentToTaskIds(pPlan);
this.streamIdStreamConsumersMap =
StreamConsumers.populateStreamConsumers(pPlan.getTopology(), componentToTaskIds);
this.xorManager =
XORManager.populateXORManager(looper, pPlan.getTopology(), NUM_BUCKETS, componentToTaskIds);
this.tupleCache = new TupleCache();
}
public void addInstanceExecutor(InstanceExecutor instanceExecutor) {
// Set the InstanceExecutor's streamOutQueue's consumer
instanceExecutor.getStreamOutQueue().setConsumer(looper);
// Set the InstanceExecutor's streamInQueue's producer
instanceExecutor.getStreamInQueue().setProducer(looper);
taskIdToInstanceExecutor.put(instanceExecutor.getTaskId(), instanceExecutor);
}
@Override
public void run() {
Thread.currentThread().setName("Simulator_Stream_Executor");
LOG.info("Stream_Executor starts");
addStreamExecutorTasks();
looper.loop();
}
public void stop() {
looper.exitLoop();
}
protected void addStreamExecutorTasks() {
// 1. Insert data tuples to cache
// 2. If acking is enabled, do the acking management
// 3. If needed, send back acking tuples
// 4. Eventually route the tuples to correct places
Runnable streamExecutorsTasks = new Runnable() {
@Override
public void run() {
// When Wakelooper is running, if it is notified by other WakeLooper,
// It would wake up immediately in next time's wait().
// So it would not enter dead-lock condition
if (tupleCache.isEmpty()) {
// We would read stream from instance executor only when the cache is drained
// We do this to trigger back-pressure to avoid too many live objects in memory
handleInstanceExecutor();
}
drainCache();
}
};
looper.addTasksOnWakeup(streamExecutorsTasks);
}
/**
* Handle the execution of the instance
*/
public void handleInstanceExecutor() {
for (InstanceExecutor executor : taskIdToInstanceExecutor.values()) {
boolean isLocalSpout = spoutSets.contains(executor.getComponentName());
int taskId = executor.getTaskId();
int items = executor.getStreamOutQueue().size();
for (int i = 0; i < items; i++) {
HeronTuples.HeronTupleSet tupleSet = executor.getStreamOutQueue().poll();
if (tupleSet == null) {
// No stream from this queue
break;
}
if (tupleSet.hasData()) {
HeronTuples.HeronDataTupleSet d = tupleSet.getData();
TopologyAPI.StreamId streamId = d.getStream();
StreamConsumers consumers = streamIdStreamConsumersMap.get(streamId);
if (consumers != null) {
for (HeronTuples.HeronDataTuple tuple : d.getTuplesList()) {
List<Integer> outTasks = consumers.getListToSend(tuple);
outTasks.addAll(tuple.getDestTaskIdsList());
if (outTasks.isEmpty()) {
LOG.severe("Nobody to sent the tuple to");
}
copyDataOutBound(taskId, isLocalSpout, streamId, tuple, outTasks);
}
} else {
LOG.severe("Nobody consumes stream: " + streamId);
}
}
if (tupleSet.hasControl()) {
HeronTuples.HeronControlTupleSet c = tupleSet.getControl();
for (HeronTuples.AckTuple ack : c.getAcksList()) {
copyControlOutBound(ack, true);
}
for (HeronTuples.AckTuple fail : c.getFailsList()) {
copyControlOutBound(fail, false);
}
}
}
}
}
// Check whether target destination task has free room to receive more tuples
protected boolean isSendTuplesToInstance(List<Integer> taskIds) {
for (Integer taskId : taskIds) {
if (taskIdToInstanceExecutor.get(taskId).getStreamInQueue().remainingCapacity() <= 0) {
return false;
}
}
return true;
}
// Process HeronDataTuple and insert it into cache
protected void copyDataOutBound(int sourceTaskId,
boolean isLocalSpout,
TopologyAPI.StreamId streamId,
HeronTuples.HeronDataTuple tuple,
List<Integer> outTasks) {
boolean firstIteration = true;
boolean isAnchored = tuple.getRootsCount() > 0;
for (Integer outTask : outTasks) {
long tupleKey = tupleCache.addDataTuple(outTask, streamId, tuple, isAnchored);
if (isAnchored) {
// Anchored tuple
if (isLocalSpout) {
// This is from a local spout. We need to maintain xors
if (firstIteration) {
xorManager.create(sourceTaskId, tuple.getRoots(0).getKey(), tupleKey);
} else {
xorManager.anchor(sourceTaskId, tuple.getRoots(0).getKey(), tupleKey);
}
} else {
// Anchored emits from local bolt
for (HeronTuples.RootId rootId : tuple.getRootsList()) {
HeronTuples.AckTuple t =
HeronTuples.AckTuple.newBuilder().
addRoots(rootId).
setAckedtuple(tupleKey).
build();
tupleCache.addEmitTuple(rootId.getTaskid(), t);
}
}
}
firstIteration = false;
}
}
// Process HeronAckTuple and insert it into cache
protected void copyControlOutBound(HeronTuples.AckTuple control,
boolean isSuccess) {
for (HeronTuples.RootId rootId : control.getRootsList()) {
HeronTuples.AckTuple t =
HeronTuples.AckTuple.newBuilder().
addRoots(rootId).
setAckedtuple(control.getAckedtuple()).
build();
if (isSuccess) {
tupleCache.addAckTuple(rootId.getTaskid(), t);
} else {
tupleCache.addFailTuple(rootId.getTaskid(), t);
}
}
}
// Do the XOR control and send the ack tuples to instance if necessary
protected void processAcksAndFails(int taskId,
HeronTuples.HeronControlTupleSet controlTupleSet) {
StreamManager.TupleMessage.Builder out = StreamManager.TupleMessage.newBuilder();
// First go over emits. This makes sure that new emits makes
// a tuples stay alive before we process its acks
for (HeronTuples.AckTuple emitTuple : controlTupleSet.getEmitsList()) {
for (HeronTuples.RootId rootId : emitTuple.getRootsList()) {
xorManager.anchor(taskId, rootId.getKey(), emitTuple.getAckedtuple());
}
}
// Then go over acks
for (HeronTuples.AckTuple ackTuple : controlTupleSet.getAcksList()) {
for (HeronTuples.RootId rootId : ackTuple.getRootsList()) {
if (xorManager.anchor(taskId, rootId.getKey(), ackTuple.getAckedtuple())) {
// This tuple tree is all over
HeronTuples.AckTuple.Builder a = out.getSetBuilder().getControlBuilder().addAcksBuilder();
HeronTuples.RootId.Builder r = a.addRootsBuilder();
r.setKey(rootId.getKey());
r.setTaskid(taskId);
a.setAckedtuple(0); // This is ignored
xorManager.remove(taskId, rootId.getKey());
}
}
}
// Now go over the fails
for (HeronTuples.AckTuple failTuple : controlTupleSet.getFailsList()) {
for (HeronTuples.RootId rootId : failTuple.getRootsList()) {
if (xorManager.remove(taskId, rootId.getKey())) {
// This tuple tree is failed
HeronTuples.AckTuple.Builder f =
out.getSetBuilder().getControlBuilder().addFailsBuilder();
HeronTuples.RootId.Builder r = f.addRootsBuilder();
r.setKey(rootId.getKey());
r.setTaskid(taskId);
f.setAckedtuple(0); // This is ignored
}
}
}
// Check if we need to send ack tuples to spout task
if (out.hasSet()) {
sendMessageToInstance(taskId, out.build());
}
}
// Drain the TupleCache if there are room in destination tasks
protected void drainCache() {
// Route the tuples to correct places
Map<Integer, List<HeronTuples.HeronTupleSet>> cache = tupleCache.getCache();
if (!isSendTuplesToInstance(new LinkedList<Integer>(cache.keySet()))) {
// Check whether we could send tuples
return;
}
for (Map.Entry<Integer, List<HeronTuples.HeronTupleSet>> entry : cache.entrySet()) {
int taskId = entry.getKey();
for (HeronTuples.HeronTupleSet message : entry.getValue()) {
sendInBound(taskId, message);
}
}
// Reset the tupleCache
tupleCache.clear();
}
// Send Stream to instance
protected void sendInBound(int taskId, HeronTuples.HeronTupleSet message) {
if (message.hasData()) {
StreamManager.TupleMessage.Builder out = StreamManager.TupleMessage.newBuilder();
out.getSetBuilder().setData(message.getData());
sendMessageToInstance(taskId, out.build());
}
if (message.hasControl()) {
processAcksAndFails(taskId, message.getControl());
}
}
// Send one message to target task
protected void sendMessageToInstance(int taskId, StreamManager.TupleMessage message) {
taskIdToInstanceExecutor.get(taskId).getStreamInQueue().offer(message.getSet());
}
protected WakeableLooper createWakeableLooper() {
return new SlaveLooper();
}
protected Set<String> createSpoutsSet(PhysicalPlans.PhysicalPlan physicalPlan) {
Set<String> spoutsSet = new HashSet<>();
for (TopologyAPI.Spout spout : physicalPlan.getTopology().getSpoutsList()) {
spoutsSet.add(spout.getComp().getName());
}
return spoutsSet;
}
}