/**
* Copyright 2015 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.streamsets.pipeline.cluster;
import com.streamsets.pipeline.api.impl.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* Provides a bi-directional control channel between two threads, one producing
* and the other consuming. Both threads can queue multiple control messages
* for the corresponding thread.
*/
public class ControlChannel {
private static final Logger LOG = LoggerFactory.getLogger(ControlChannel.class);
private final BlockingQueue<Message> producerQueue = new ArrayBlockingQueue<>(10);
private final BlockingQueue<Message> consumerQueue = new ArrayBlockingQueue<>(10);
public void producerComplete() {
LOG.info("Producer complete");
try {
consumerQueue.put(new Message(MessageType.PRODUCER_COMPLETE));
} catch (InterruptedException e) {
LOG.info("Interrupted while queuing '{}'", MessageType.PRODUCER_COMPLETE.name());
Thread.currentThread().interrupt();
}
}
public List<Message> getProducerMessages() {
List<Message> result = new ArrayList<>();
producerQueue.drainTo(result);
return result;
}
public List<Message> getConsumerMessages() {
List<Message> result = new ArrayList<>();
consumerQueue.drainTo(result);
return result;
}
public void consumerError(Throwable throwable) {
LOG.trace("Consumer Error: {}", throwable, throwable);
try {
producerQueue.put(new Message(MessageType.CONSUMER_ERROR, throwable));
} catch (InterruptedException e) {
LOG.info("Interrupted while queuing '{}': {}", MessageType.CONSUMER_ERROR.name(), throwable, throwable);
Thread.currentThread().interrupt();
}
}
public void producerError(Throwable throwable) {
LOG.trace("Producer Error: {}", throwable, throwable);
try {
consumerQueue.put(new Message(MessageType.PRODUCER_ERROR, throwable));
} catch (InterruptedException e) {
LOG.info("Interrupted while queuing '{}': {}", MessageType.PRODUCER_ERROR.name(), throwable, throwable);
Thread.currentThread().interrupt();
}
}
/**
* If a null value is passed to this method, it's replaced with
* a dummy due to the fact the payload for each message is wrapped
* in an Optional.
*/
public void consumerCommit(String offset) {
Object offsetValue = offset;
if (offsetValue == null) {
offsetValue = new NullOffset();
}
LOG.trace("Commit Offset: '{}'", offsetValue);
try {
producerQueue.put(new Message(MessageType.CONSUMER_COMMIT, offsetValue));
} catch (InterruptedException e) {
LOG.info("Interrupted while queuing '{}'", MessageType.CONSUMER_COMMIT.name(), offsetValue);
Thread.currentThread().interrupt();
}
}
private static class NullOffset {
@Override
public String toString() {
return "(null offset)";
}
}
public static class Message {
private final MessageType type;
private final Object payload;
public Message(MessageType type) {
this(type, null);
}
public Message(MessageType type, Object payload) {
this.type = type;
this.payload = payload;
if (type.hasPayload && this.payload == null) {
throw new IllegalStateException(Utils.format("Message type '{}' requires payload", type.name()));
}
}
public MessageType getType() {
return type;
}
public Object getPayload() {
return payload;
}
}
public enum MessageType {
PRODUCER_COMPLETE(false), // inform consumer to return null offset, thus shutting down the pipeline
CONSUMER_ERROR(true), // inform producer consumer is dead due to an error condition
PRODUCER_ERROR(true), // inform consumer producer is dead due to an error condition
CONSUMER_COMMIT(true); // inform producer offset has been committed
private boolean hasPayload;
MessageType(boolean hasPayload) {
this.hasPayload = hasPayload;
}
}
}