/**
* 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.google.common.base.Throwables;
import com.streamsets.pipeline.impl.OffsetAndResult;
import com.streamsets.pipeline.api.impl.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Implements the consumer side of the cluster producer consumer pattern.
* Ensures that each take is followed by a subsequent commit and stops
* attempting to consume messages when the producer indicates it has
* completed.
*/
public class Consumer {
private static final Logger LOG = LoggerFactory.getLogger(Consumer.class);
private final ControlChannel controlChannel;
private final DataChannel dataChannel;
private boolean running;
private boolean batchCommitted;
private volatile Throwable producerError;
private volatile Throwable consumerError;
private String lastCommittedOffset;
public Consumer(ControlChannel controlChannel, DataChannel dataChannel) {
this.controlChannel = controlChannel;
this.dataChannel = dataChannel;
this.running = true;
this.batchCommitted = true;
this.lastCommittedOffset = "";
}
/**
* Consumes messages off the queue. Returns null when the producer
* has indicated it is complete and throws an exception
* when the consumer producer has indicated it is in error.
*/
public OffsetAndResult<Map.Entry> take() {
if (producerError != null) {
throw new RuntimeException(Utils.format("Producer encountered error: {}", producerError), producerError);
}
if (consumerError != null) {
throw new RuntimeException(Utils.format("Consumer encountered error: {}", consumerError), consumerError);
}
try {
Utils.checkState(batchCommitted, "Cannot take messages when last batch is uncommitted");
while (running) {
for (ControlChannel.Message controlMessage : controlChannel.getConsumerMessages()) {
switch (controlMessage.getType()) {
case PRODUCER_COMPLETE:
// producer is complete, empty channel and afterwards return null
running = false;
break;
case PRODUCER_ERROR:
running = false;
Throwable throwable = (Throwable) controlMessage.getPayload();
producerError = throwable;
throw new ProducerRuntimeException(Utils.format("Producer encountered error: {}", throwable), throwable);
default:
String msg = Utils.format("Illegal control message type: '{}'", controlMessage.getType());
throw new IllegalStateException(msg);
}
}
OffsetAndResult<Map.Entry> batch = dataChannel.take(10, TimeUnit.MILLISECONDS);
LOG.trace("Received batch: {}", batch);
if (batch != null) {
batchCommitted = false; // got a new batch
return batch;
}
}
LOG.trace("Returning null");
return null;
} catch (Throwable throwable) {
if (!(throwable instanceof ProducerRuntimeException)) {
String msg = "Error caught in consumer: " + throwable;
LOG.error(msg, throwable);
error(throwable);
}
throw Throwables.propagate(throwable);
}
}
/**
* Commit the offset. Required after take has returned a non-null value.
*/
public void commit(String offset) {
batchCommitted = true;
LOG.trace("Last committed offset '{}', attempting to commit '{}'", lastCommittedOffset, offset);
Utils.checkState(null != lastCommittedOffset, "Last committed offset cannot be null");
controlChannel.consumerCommit(offset);
lastCommittedOffset = offset;
}
/**
* Send a control message indicating the consumer has encountered an error.
*/
public void error(Throwable throwable) {
if (consumerError == null) {
consumerError = throwable;
controlChannel.consumerError(throwable);
}
}
public boolean inErrorState() {
return consumerError != null || producerError != null;
}
}