// 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.network;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.twitter.heron.common.basics.Communicator;
import com.twitter.heron.common.basics.NIOLooper;
import com.twitter.heron.common.basics.SingletonRegistry;
import com.twitter.heron.common.config.SystemConfig;
import com.twitter.heron.common.network.HeronClient;
import com.twitter.heron.common.network.HeronSocketOptions;
import com.twitter.heron.common.network.StatusCode;
import com.twitter.heron.common.utils.misc.PhysicalPlanHelper;
import com.twitter.heron.instance.InstanceControlMsg;
import com.twitter.heron.metrics.GatewayMetrics;
import com.twitter.heron.proto.stmgr.StreamManager;
import com.twitter.heron.proto.system.Common;
import com.twitter.heron.proto.system.HeronTuples;
import com.twitter.heron.proto.system.PhysicalPlans;
/**
* StreamClient implements SocketClient and communicate with Stream Manager, it will:
* 1. Register the message of NewInstanceAssignmentMessage and TupleMessage.
* 2. Send Register Request when it is onConnect()
* 3. Handle relative response for requests
* 4. if onIncomingMessage(message) is called, it will see whether it is NewAssignment or NewTuples.
* 5. If it is a new assignment, it will pass the PhysicalPlan to Slave,
* which will new a corresponding instance.
*/
public class StreamManagerClient extends HeronClient {
private static final Logger LOG = Logger.getLogger(StreamManagerClient.class.getName());
private final String topologyName;
private final String topologyId;
private final PhysicalPlans.Instance instance;
// For spout, it will buffer Control tuple, while for bolt, it will buffer data tuple.
private final Communicator<HeronTuples.HeronTupleSet> inStreamQueue;
private final Communicator<HeronTuples.HeronTupleSet> outStreamQueue;
private final Communicator<InstanceControlMsg> inControlQueue;
private final GatewayMetrics gatewayMetrics;
private final SystemConfig systemConfig;
private PhysicalPlanHelper helper;
private long lastNotConnectedLogTime = 0;
public StreamManagerClient(NIOLooper s, String streamManagerHost, int streamManagerPort,
String topologyName, String topologyId,
PhysicalPlans.Instance instance,
Communicator<HeronTuples.HeronTupleSet> inStreamQueue,
Communicator<HeronTuples.HeronTupleSet> outStreamQueue,
Communicator<InstanceControlMsg> inControlQueue,
HeronSocketOptions options,
GatewayMetrics gatewayMetrics) {
super(s, streamManagerHost, streamManagerPort, options);
this.topologyName = topologyName;
this.topologyId = topologyId;
this.instance = instance;
this.inStreamQueue = inStreamQueue;
this.outStreamQueue = outStreamQueue;
this.inControlQueue = inControlQueue;
this.systemConfig =
(SystemConfig) SingletonRegistry.INSTANCE.getSingleton(SystemConfig.HERON_SYSTEM_CONFIG);
this.gatewayMetrics = gatewayMetrics;
addStreamManagerClientTasksOnWakeUp();
}
private void addStreamManagerClientTasksOnWakeUp() {
Runnable task = new Runnable() {
@Override
public void run() {
sendStreamMessageIfNeeded();
readStreamMessageIfNeeded();
}
};
getNIOLooper().addTasksOnWakeup(task);
}
private void registerMessagesToHandle() {
registerOnMessage(StreamManager.NewInstanceAssignmentMessage.newBuilder());
registerOnMessage(StreamManager.TupleMessage.newBuilder());
registerOnMessage(HeronTuples.HeronTupleSet2.newBuilder());
}
@Override
public void onError() {
LOG.severe("Disconnected from Stream Manager.");
// We would set PhysicalPlanHelper to null onError(),
// since we would re-connect to stream manager and wait for new PhysicalPlan
// the stream manager publishes
LOG.info("Clean the old PhysicalPlanHelper in StreamManagerClient.");
helper = null;
// Dispatch to onConnect(...)
onConnect(StatusCode.CONNECT_ERROR);
}
@Override
public void onConnect(StatusCode status) {
if (status != StatusCode.OK) {
LOG.log(Level.WARNING,
"Error connecting to Stream Manager with status: {0}, Retrying...", status);
Runnable r = new Runnable() {
public void run() {
start();
}
};
getNIOLooper().registerTimerEvent(
systemConfig.getInstanceReconnectStreammgrInterval(), r);
return;
}
// Initialize the register: determine what messages we would like to handle
registerMessagesToHandle();
// Build the request and send it.
LOG.info("Connected to Stream Manager. Ready to send register request");
sendRegisterRequest();
}
// Build register request and send to stream mgr
private void sendRegisterRequest() {
StreamManager.RegisterInstanceRequest request =
StreamManager.RegisterInstanceRequest.newBuilder().
setInstance(instance).setTopologyName(topologyName).setTopologyId(topologyId).
build();
// The timeout would be the reconnect-interval-seconds
sendRequest(request, null,
StreamManager.RegisterInstanceResponse.newBuilder(),
systemConfig.getInstanceReconnectStreammgrInterval());
}
@Override
public void onResponse(StatusCode status, Object ctx, Message response) {
if (status != StatusCode.OK) {
//TODO:- is this a good thing?
throw new RuntimeException("Response from Stream Manager not ok");
}
if (response instanceof StreamManager.RegisterInstanceResponse) {
handleRegisterResponse((StreamManager.RegisterInstanceResponse) response);
} else {
throw new RuntimeException("Unknown kind of response received from Stream Manager");
}
}
@Override
public void onIncomingMessage(Message message) {
gatewayMetrics.updateReceivedPacketsCount(1);
gatewayMetrics.updateReceivedPacketsSize(message.getSerializedSize());
if (message instanceof StreamManager.NewInstanceAssignmentMessage) {
StreamManager.NewInstanceAssignmentMessage m =
(StreamManager.NewInstanceAssignmentMessage) message;
LOG.info("Handling assignment message from direct NewInstanceAssignmentMessage");
handleAssignmentMessage(m.getPplan());
} else if (message instanceof StreamManager.TupleMessage) {
handleNewTuples((StreamManager.TupleMessage) message);
} else if (message instanceof HeronTuples.HeronTupleSet2) {
handleNewTuples2((HeronTuples.HeronTupleSet2) message);
} else {
throw new RuntimeException("Unknown kind of message received from Stream Manager");
}
}
@Override
public void onClose() {
LOG.info("StreamManagerClient exits.");
}
// Send out all the data
public void sendAllMessage() {
if (!isConnected()) {
return;
}
LOG.info("Flushing all pending data in StreamManagerClient");
// Collect all tuples in queue
int size = outStreamQueue.size();
for (int i = 0; i < size; i++) {
HeronTuples.HeronTupleSet tupleSet = outStreamQueue.poll();
StreamManager.TupleMessage msg = StreamManager.TupleMessage.newBuilder()
.setSet(tupleSet).build();
sendMessage(msg);
}
}
private void sendStreamMessageIfNeeded() {
if (isStreamMgrReadyReceiveTuples()) {
if (getOutstandingPackets() <= 0) {
// In order to avoid packets back up in Client side,
// We would poll message from queue and send them only when there are no outstanding packets
while (!outStreamQueue.isEmpty()) {
HeronTuples.HeronTupleSet tupleSet = outStreamQueue.poll();
gatewayMetrics.updateSentPacketsCount(1);
gatewayMetrics.updateSentPacketsSize(tupleSet.getSerializedSize());
sendMessage(tupleSet);
}
}
if (!outStreamQueue.isEmpty()) {
// We still have messages to send
startWriting();
}
} else {
LOG.info("Stop writing due to not yet connected to Stream Manager.");
}
}
private void readStreamMessageIfNeeded() {
final long lastNotConnectedLogThrottleSeconds = 5;
// If client is not connected, just return
if (isConnected()) {
if (isInQueuesAvailable() || helper == null) {
startReading();
} else {
gatewayMetrics.updateInQueueFullCount();
stopReading();
}
} else {
long now = System.currentTimeMillis();
if (now - lastNotConnectedLogTime > lastNotConnectedLogThrottleSeconds * 1000) {
LOG.info(String.format("Stop reading due to not yet connected to Stream Manager. This "
+ "message is throttled to emit no more than once every %d seconds.",
lastNotConnectedLogThrottleSeconds));
lastNotConnectedLogTime = now;
}
}
}
private void handleRegisterResponse(StreamManager.RegisterInstanceResponse response) {
if (response.getStatus().getStatus() != Common.StatusCode.OK) {
throw new RuntimeException("Stream Manager returned a not ok response for register");
}
LOG.info("We registered ourselves to the Stream Manager");
if (response.hasPplan()) {
LOG.info("Handling assignment message from response");
handleAssignmentMessage(response.getPplan());
}
}
private void handleNewTuples(StreamManager.TupleMessage message) {
inStreamQueue.offer(message.getSet());
}
private void handleNewTuples2(HeronTuples.HeronTupleSet2 set) {
HeronTuples.HeronTupleSet.Builder toFeed = HeronTuples.HeronTupleSet.newBuilder();
if (set.hasControl()) {
toFeed.setControl(set.getControl());
} else {
// Either control or data
HeronTuples.HeronDataTupleSet.Builder builder = HeronTuples.HeronDataTupleSet.newBuilder();
builder.setStream(set.getData().getStream());
try {
for (ByteString bs : set.getData().getTuplesList()) {
builder.addTuples(HeronTuples.HeronDataTuple.parseFrom(bs));
}
} catch (InvalidProtocolBufferException e) {
LOG.log(Level.SEVERE, "Failed to parse protobuf", e);
}
toFeed.setData(builder);
}
HeronTuples.HeronTupleSet s = toFeed.build();
inStreamQueue.offer(s);
}
private void handleAssignmentMessage(PhysicalPlans.PhysicalPlan pplan) {
LOG.fine("Physical Plan: " + pplan);
PhysicalPlanHelper newHelper = new PhysicalPlanHelper(pplan, instance.getInstanceId());
if (helper != null && (!helper.getMyComponent().equals(newHelper.getMyComponent())
|| helper.getMyTaskId() != newHelper.getMyTaskId())) {
// Right now if we already are something, and the stmgr tell us to
// change our role, we just exit. When we come back up again
// we will get the new assignment
throw new RuntimeException("Our Assignment has changed. We will die to pick it");
}
if (helper == null) {
LOG.info("We received a new Physical Plan.");
} else {
LOG.info("We received a new Physical Plan with same assignment. Should be state changes.");
LOG.info(String.format("Old state: %s; new sate: %s.",
helper.getTopologyState(), newHelper.getTopologyState()));
}
helper = newHelper;
LOG.info("Push to Slave");
InstanceControlMsg instanceControlMsg = InstanceControlMsg.newBuilder().
setNewPhysicalPlanHelper(helper).
build();
inControlQueue.offer(instanceControlMsg);
}
private boolean isStreamMgrReadyReceiveTuples() {
// The Stream Manager is ready only when:
// 1. We could connect to it
// 2. We receive the PhysicalPlan published by Stream Manager
return isConnected() && helper != null;
}
// Return true if we could offer item to the inStreamQueue
private boolean isInQueuesAvailable() {
return inStreamQueue.size() < inStreamQueue.getExpectedAvailableCapacity();
}
}