2014-10-21 18:28:58 +00:00
|
|
|
/*
|
|
|
|
|
* Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Amazon Software License (the "License").
|
|
|
|
|
* You may not use this file except in compliance with the License.
|
|
|
|
|
* A copy of the License is located at
|
|
|
|
|
*
|
|
|
|
|
* http://aws.amazon.com/asl/
|
|
|
|
|
*
|
|
|
|
|
* or in the "license" file accompanying this file. This file 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.amazonaws.services.kinesis.multilang;
|
|
|
|
|
|
|
|
|
|
import java.util.concurrent.ExecutionException;
|
|
|
|
|
import java.util.concurrent.Future;
|
|
|
|
|
|
|
|
|
|
import com.amazonaws.services.kinesis.clientlibrary.exceptions.InvalidStateException;
|
|
|
|
|
import com.amazonaws.services.kinesis.clientlibrary.interfaces.IRecordProcessorCheckpointer;
|
2016-10-26 19:57:50 +00:00
|
|
|
import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShutdownReason;
|
2016-11-07 19:38:04 +00:00
|
|
|
import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput;
|
|
|
|
|
import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput;
|
2014-10-21 18:28:58 +00:00
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.CheckpointMessage;
|
|
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.InitializeMessage;
|
|
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.Message;
|
|
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.ProcessRecordsMessage;
|
|
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.ShutdownMessage;
|
2017-06-21 14:52:30 +00:00
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.ShutdownRequestedMessage;
|
2014-10-21 18:28:58 +00:00
|
|
|
import com.amazonaws.services.kinesis.multilang.messages.StatusMessage;
|
|
|
|
|
|
2016-11-07 19:38:04 +00:00
|
|
|
import lombok.extern.apachecommons.CommonsLog;
|
|
|
|
|
|
2014-10-21 18:28:58 +00:00
|
|
|
/**
|
|
|
|
|
* An implementation of the multi language protocol.
|
|
|
|
|
*/
|
2016-11-07 19:38:04 +00:00
|
|
|
@CommonsLog
|
2014-10-21 18:28:58 +00:00
|
|
|
class MultiLangProtocol {
|
|
|
|
|
|
|
|
|
|
private MessageReader messageReader;
|
|
|
|
|
private MessageWriter messageWriter;
|
2016-11-07 19:38:04 +00:00
|
|
|
private final InitializationInput initializationInput;
|
2014-10-21 18:28:58 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor.
|
|
|
|
|
*
|
2016-11-07 19:38:04 +00:00
|
|
|
* @param messageReader
|
|
|
|
|
* A message reader.
|
|
|
|
|
* @param messageWriter
|
|
|
|
|
* A message writer.
|
|
|
|
|
* @param initializationInput
|
|
|
|
|
* information about the shard this processor is starting to process
|
2014-10-21 18:28:58 +00:00
|
|
|
*/
|
2016-11-07 19:38:04 +00:00
|
|
|
MultiLangProtocol(MessageReader messageReader, MessageWriter messageWriter,
|
|
|
|
|
InitializationInput initializationInput) {
|
2014-10-21 18:28:58 +00:00
|
|
|
this.messageReader = messageReader;
|
|
|
|
|
this.messageWriter = messageWriter;
|
2016-11-07 19:38:04 +00:00
|
|
|
this.initializationInput = initializationInput;
|
2014-10-21 18:28:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Writes an {@link InitializeMessage} to the child process's STDIN and waits for the child process to respond with
|
|
|
|
|
* a {@link StatusMessage} on its STDOUT.
|
|
|
|
|
*
|
|
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
|
|
|
|
boolean initialize() {
|
|
|
|
|
/*
|
|
|
|
|
* Call and response to child process.
|
|
|
|
|
*/
|
2016-11-07 19:38:04 +00:00
|
|
|
Future<Boolean> writeFuture = messageWriter.writeInitializeMessage(initializationInput);
|
2014-10-21 18:28:58 +00:00
|
|
|
return waitForStatusMessage(InitializeMessage.ACTION, null, writeFuture);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Writes a {@link ProcessRecordsMessage} to the child process's STDIN and waits for the child process to respond
|
|
|
|
|
* with a {@link StatusMessage} on its STDOUT.
|
|
|
|
|
*
|
2016-11-07 19:38:04 +00:00
|
|
|
* @param processRecordsInput
|
|
|
|
|
* The records, and associated metadata, to process.
|
2014-10-21 18:28:58 +00:00
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
2016-11-07 19:38:04 +00:00
|
|
|
boolean processRecords(ProcessRecordsInput processRecordsInput) {
|
|
|
|
|
Future<Boolean> writeFuture = messageWriter.writeProcessRecordsMessage(processRecordsInput);
|
|
|
|
|
return waitForStatusMessage(ProcessRecordsMessage.ACTION, processRecordsInput.getCheckpointer(), writeFuture);
|
2014-10-21 18:28:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Writes a {@link ShutdownMessage} to the child process's STDIN and waits for the child process to respond with a
|
|
|
|
|
* {@link StatusMessage} on its STDOUT.
|
|
|
|
|
*
|
|
|
|
|
* @param checkpointer A checkpointer.
|
|
|
|
|
* @param reason Why this processor is being shutdown.
|
|
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
|
|
|
|
boolean shutdown(IRecordProcessorCheckpointer checkpointer, ShutdownReason reason) {
|
|
|
|
|
Future<Boolean> writeFuture = messageWriter.writeShutdownMessage(reason);
|
|
|
|
|
return waitForStatusMessage(ShutdownMessage.ACTION, checkpointer, writeFuture);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-21 14:52:30 +00:00
|
|
|
/**
|
|
|
|
|
* Writes a {@link ShutdownRequestedMessage} to the child process's STDIN and waits for the child process to respond with a
|
|
|
|
|
* {@link StatusMessage} on its STDOUT.
|
|
|
|
|
*
|
|
|
|
|
* @param checkpointer A checkpointer.
|
|
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
|
|
|
|
boolean shutdownRequested(IRecordProcessorCheckpointer checkpointer) {
|
|
|
|
|
Future<Boolean> writeFuture = messageWriter.writeShutdownRequestedMessage();
|
|
|
|
|
return waitForStatusMessage(ShutdownRequestedMessage.ACTION, checkpointer, writeFuture);
|
|
|
|
|
}
|
|
|
|
|
|
2014-10-21 18:28:58 +00:00
|
|
|
/**
|
|
|
|
|
* Waits for a {@link StatusMessage} for a particular action. If a {@link CheckpointMessage} is received, then this
|
|
|
|
|
* method will attempt to checkpoint with the provided {@link IRecordProcessorCheckpointer}. This method returns
|
|
|
|
|
* true if writing to the child process succeeds and the status message received back was for the correct action and
|
|
|
|
|
* all communications with the child process regarding checkpointing were successful. Note that whether or not the
|
|
|
|
|
* checkpointing itself was successful is not the concern of this method. This method simply cares whether it was
|
|
|
|
|
* able to successfully communicate the results of its attempts to checkpoint.
|
|
|
|
|
*
|
2016-11-07 19:38:04 +00:00
|
|
|
* @param action
|
|
|
|
|
* What action is being waited on.
|
|
|
|
|
* @param checkpointer
|
|
|
|
|
* the checkpointer from the process records, or shutdown request
|
|
|
|
|
* @param writeFuture
|
|
|
|
|
* The writing task.
|
2014-10-21 18:28:58 +00:00
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
2016-11-07 19:38:04 +00:00
|
|
|
private boolean waitForStatusMessage(String action, IRecordProcessorCheckpointer checkpointer,
|
|
|
|
|
Future<Boolean> writeFuture) {
|
2014-10-21 18:28:58 +00:00
|
|
|
boolean statusWasCorrect = waitForStatusMessage(action, checkpointer);
|
|
|
|
|
|
|
|
|
|
// Examine whether or not we failed somewhere along the line.
|
|
|
|
|
try {
|
2016-11-07 19:38:04 +00:00
|
|
|
boolean writerIsStillOpen = writeFuture.get();
|
2014-10-21 18:28:58 +00:00
|
|
|
return statusWasCorrect && writerIsStillOpen;
|
|
|
|
|
} catch (InterruptedException e) {
|
2016-11-07 19:38:04 +00:00
|
|
|
log.error(String.format("Interrupted while writing %s message for shard %s", action,
|
|
|
|
|
initializationInput.getShardId()));
|
2014-10-21 18:28:58 +00:00
|
|
|
return false;
|
|
|
|
|
} catch (ExecutionException e) {
|
2016-11-07 19:38:04 +00:00
|
|
|
log.error(
|
|
|
|
|
String.format("Failed to write %s message for shard %s", action, initializationInput.getShardId()),
|
|
|
|
|
e);
|
2014-10-21 18:28:58 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-11-07 19:38:04 +00:00
|
|
|
* Waits for status message and verifies it against the expectation
|
|
|
|
|
*
|
|
|
|
|
* @param action
|
|
|
|
|
* What action is being waited on.
|
|
|
|
|
* @param checkpointer
|
|
|
|
|
* the original process records request
|
2014-10-21 18:28:58 +00:00
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
|
|
|
|
private boolean waitForStatusMessage(String action, IRecordProcessorCheckpointer checkpointer) {
|
|
|
|
|
StatusMessage statusMessage = null;
|
|
|
|
|
while (statusMessage == null) {
|
|
|
|
|
Future<Message> future = this.messageReader.getNextMessageFromSTDOUT();
|
|
|
|
|
try {
|
|
|
|
|
Message message = future.get();
|
|
|
|
|
// Note that instanceof doubles as a check against a value being null
|
|
|
|
|
if (message instanceof CheckpointMessage) {
|
2016-11-07 19:38:04 +00:00
|
|
|
boolean checkpointWriteSucceeded = checkpoint((CheckpointMessage) message, checkpointer).get();
|
2014-10-21 18:28:58 +00:00
|
|
|
if (!checkpointWriteSucceeded) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else if (message instanceof StatusMessage) {
|
|
|
|
|
statusMessage = (StatusMessage) message;
|
|
|
|
|
}
|
|
|
|
|
} catch (InterruptedException e) {
|
2016-11-07 19:38:04 +00:00
|
|
|
log.error(String.format("Interrupted while waiting for %s message for shard %s", action,
|
|
|
|
|
initializationInput.getShardId()));
|
2014-10-21 18:28:58 +00:00
|
|
|
return false;
|
|
|
|
|
} catch (ExecutionException e) {
|
2016-11-07 19:38:04 +00:00
|
|
|
log.error(String.format("Failed to get status message for %s action for shard %s", action,
|
|
|
|
|
initializationInput.getShardId()), e);
|
2014-10-21 18:28:58 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this.validateStatusMessage(statusMessage, action);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Utility for confirming that the status message is for the provided action.
|
|
|
|
|
*
|
|
|
|
|
* @param statusMessage The status of the child process.
|
|
|
|
|
* @param action The action that was being waited on.
|
|
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
|
|
|
|
private boolean validateStatusMessage(StatusMessage statusMessage, String action) {
|
2016-11-07 19:38:04 +00:00
|
|
|
log.info("Received response " + statusMessage + " from subprocess while waiting for " + action
|
|
|
|
|
+ " while processing shard " + initializationInput.getShardId());
|
2014-10-21 18:28:58 +00:00
|
|
|
return !(statusMessage == null || statusMessage.getResponseFor() == null || !statusMessage.getResponseFor()
|
|
|
|
|
.equals(action));
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Attempts to checkpoint with the provided {@link IRecordProcessorCheckpointer} at the sequence number in the
|
|
|
|
|
* provided {@link CheckpointMessage}. If no sequence number is provided, i.e. the sequence number is null, then
|
|
|
|
|
* this method will call {@link IRecordProcessorCheckpointer#checkpoint()}. The method returns a future representing
|
|
|
|
|
* the attempt to write the result of this checkpoint attempt to the child process.
|
|
|
|
|
*
|
|
|
|
|
* @param checkpointMessage A checkpoint message.
|
|
|
|
|
* @param checkpointer A checkpointer.
|
|
|
|
|
* @return Whether or not this operation succeeded.
|
|
|
|
|
*/
|
|
|
|
|
private Future<Boolean> checkpoint(CheckpointMessage checkpointMessage, IRecordProcessorCheckpointer checkpointer) {
|
2016-11-07 19:38:04 +00:00
|
|
|
String sequenceNumber = checkpointMessage.getSequenceNumber();
|
|
|
|
|
Long subSequenceNumber = checkpointMessage.getSubSequenceNumber();
|
2014-10-21 18:28:58 +00:00
|
|
|
try {
|
|
|
|
|
if (checkpointer != null) {
|
2016-11-07 19:38:04 +00:00
|
|
|
log.debug(logCheckpointMessage(sequenceNumber, subSequenceNumber));
|
|
|
|
|
if (sequenceNumber != null) {
|
|
|
|
|
if (subSequenceNumber != null) {
|
|
|
|
|
checkpointer.checkpoint(sequenceNumber, subSequenceNumber);
|
|
|
|
|
} else {
|
|
|
|
|
checkpointer.checkpoint(sequenceNumber);
|
|
|
|
|
}
|
2014-10-21 18:28:58 +00:00
|
|
|
} else {
|
2016-11-07 19:38:04 +00:00
|
|
|
checkpointer.checkpoint();
|
2014-10-21 18:28:58 +00:00
|
|
|
}
|
2016-11-07 19:38:04 +00:00
|
|
|
return this.messageWriter.writeCheckpointMessageWithError(sequenceNumber, subSequenceNumber, null);
|
2014-10-21 18:28:58 +00:00
|
|
|
} else {
|
|
|
|
|
String message =
|
|
|
|
|
String.format("Was asked to checkpoint at %s but no checkpointer was provided for shard %s",
|
2016-11-07 19:38:04 +00:00
|
|
|
sequenceNumber, initializationInput.getShardId());
|
|
|
|
|
log.error(message);
|
|
|
|
|
return this.messageWriter.writeCheckpointMessageWithError(sequenceNumber, subSequenceNumber,
|
|
|
|
|
new InvalidStateException(
|
2014-10-21 18:28:58 +00:00
|
|
|
message));
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable t) {
|
2016-11-07 19:38:04 +00:00
|
|
|
return this.messageWriter.writeCheckpointMessageWithError(sequenceNumber, subSequenceNumber, t);
|
2014-10-21 18:28:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2016-11-07 19:38:04 +00:00
|
|
|
|
|
|
|
|
private String logCheckpointMessage(String sequenceNumber, Long subSequenceNumber) {
|
|
|
|
|
return String.format("Attempting to checkpoint shard %s @ sequence number %s, and sub sequence number %s",
|
|
|
|
|
initializationInput.getShardId(), sequenceNumber, subSequenceNumber);
|
|
|
|
|
}
|
|
|
|
|
|
2014-10-21 18:28:58 +00:00
|
|
|
}
|