package tw.com.providers;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.cli.MissingArgumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tw.com.NotificationProvider;
import tw.com.entity.StackNotification;
import tw.com.exceptions.FailedToCreateQueueException;
import tw.com.exceptions.NotReadyException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.CreateTopicRequest;
import com.amazonaws.services.sns.model.CreateTopicResult;
import com.amazonaws.services.sns.model.ListSubscriptionsResult;
import com.amazonaws.services.sns.model.SubscribeRequest;
import com.amazonaws.services.sns.model.SubscribeResult;
import com.amazonaws.services.sns.model.Subscription;
import com.amazonaws.services.sqs.AmazonSQSClient;
import com.amazonaws.services.sqs.model.CreateQueueRequest;
import com.amazonaws.services.sqs.model.CreateQueueResult;
import com.amazonaws.services.sqs.model.DeleteMessageRequest;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.QueueDeletedRecentlyException;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SNSEventSource extends QueuePolicyManager implements NotificationProvider {
private static final Logger logger = LoggerFactory.getLogger(SNSEventSource.class);
private static final int MAX_NUMBER_MSGS_TO_RECEIVE = 10;
private static final int QUEUE_READ_TIMEOUT_SECS = 20; // 20 is max allowed
private static final String SQS_QUEUE_NAME = "CFN_ASSIST_EVENT_QUEUE";
public static final String SNS_TOPIC_NAME = "CFN_ASSIST_EVENTS";
public static final String SQS_PROTO = "sqs";
private static final int QUEUE_CREATE_RETRYS = 3;
private static final long QUEUE_RETRY_INTERNAL_MILLIS = 70 * 1000;
private AmazonSNSClient snsClient;
private String queueURL;
private String topicSnsArn;
private String queueArn;
private boolean init;
public SNSEventSource(AmazonSNSClient snsClient,AmazonSQSClient sqsClient) {
super(sqsClient);
this.snsClient = snsClient;
init = false;
}
public String getQueueURL() {
return queueURL;
}
@Override
public void init() throws MissingArgumentException, FailedToCreateQueueException, InterruptedException {
if (init) {
logger.warn("SNSMonitor init called again");
return;
}
logger.info("Init SNSMonitor");
topicSnsArn = getOrCreateSNSARN();
queueURL = getOrCreateQueue();
Map<String, String> queueAttributes = getQueueAttributes(queueURL);
queueArn = queueAttributes.get(QUEUE_ARN_KEY);
checkOrCreateQueuePermissions(queueAttributes, topicSnsArn, queueArn, queueURL);
createOrGetSQSSubscriptionToSNS();
init = true;
}
private ReceiveMessageResult receiveMessages() {
logger.info("Waiting for messages for queue " + queueURL);
ReceiveMessageRequest receiveMessageRequest = createWaitRequest();
return sqsClient.receiveMessage(receiveMessageRequest);
}
@Override
public List<StackNotification> receiveNotifications() throws NotReadyException {
guardForInit();
List<StackNotification> notifications = new LinkedList<StackNotification>();
ReceiveMessageResult result = receiveMessages();
ObjectMapper objectMapper = new ObjectMapper();
List<Message> messages = result.getMessages();
logger.info(String.format("Received %s messages", messages.size()));
for(Message msg : messages) {
logger.debug(msg.toString());
JsonNode messageNode;
try {
messageNode = extractMessageNode(msg, objectMapper);
StackNotification notification = StackNotification.parseNotificationMessage(messageNode.textValue());
logger.info("Received notification for stackid: " + notification.getStackId());
notifications.add(notification);
} catch (ArrayIndexOutOfBoundsException | IOException e) {
logger.warn("unable to parse message: " +msg);
}
deleteMessage(msg);
}
return notifications;
}
private JsonNode extractMessageNode(Message msg, ObjectMapper objectMapper)
throws IOException, JsonProcessingException {
String json = msg.getBody();
//logger.debug("Body json: " + json);
JsonNode rootNode = objectMapper.readTree(json);
JsonNode messageNode = rootNode.get("Message");
return messageNode;
}
private ReceiveMessageRequest createWaitRequest() {
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(queueURL);
receiveMessageRequest.setWaitTimeSeconds(QUEUE_READ_TIMEOUT_SECS);
receiveMessageRequest.setMaxNumberOfMessages(MAX_NUMBER_MSGS_TO_RECEIVE);
return receiveMessageRequest;
}
private List<Subscription> getSNSSubs() {
ListSubscriptionsResult subResults = snsClient.listSubscriptions();
List<Subscription> subs = subResults.getSubscriptions();
return subs;
}
private String createNewSQSSubscriptionToSNS() {
logger.info("No SQS Subscription to SNS found, creating a new one");
SubscribeRequest subscribeRequest = new SubscribeRequest(topicSnsArn, SQS_PROTO, queueArn);
SubscribeResult result = snsClient.subscribe(subscribeRequest);
String subscriptionArn = result.getSubscriptionArn();
logger.info("Created new SNS subscription, subscription arn is: " + subscriptionArn);
return subscriptionArn;
}
private String getOrCreateQueue() throws InterruptedException, FailedToCreateQueueException {
CreateQueueRequest createQueueRequest = new CreateQueueRequest(SQS_QUEUE_NAME);
int attemptsLefts = QUEUE_CREATE_RETRYS;
while (attemptsLefts>0) {
try {
logger.info("Attempt to create queue with name " + SQS_QUEUE_NAME);
CreateQueueResult result = sqsClient.createQueue(createQueueRequest); // creates, or returns existing queue
String queueUrl = result.getQueueUrl();
logger.info("Found sqs queue URL:" +queueUrl);
return queueUrl;
}
catch(QueueDeletedRecentlyException exception) {
logger.warn("Queue recently deleted, must pause before retry. " + exception.toString());
// aws docs say have to wait >60 seconds before trying again
Thread.sleep(QUEUE_RETRY_INTERNAL_MILLIS);
}
catch(AmazonServiceException serviceException) {
logger.error("Caught service exception during queue creation: " +serviceException.getErrorMessage());
Thread.sleep(QUEUE_RETRY_INTERNAL_MILLIS);
}
}
throw new FailedToCreateQueueException(SQS_QUEUE_NAME);
}
public String getSNSArn() throws NotReadyException {
guardForInit();
return topicSnsArn;
}
private void guardForInit() throws NotReadyException {
if (!init) {
logger.error("Not initialised");
throw new NotReadyException("SNSMonitor not initialised");
}
}
private String getOrCreateSNSARN() {
CreateTopicRequest createTopicRequest = new CreateTopicRequest(SNS_TOPIC_NAME);
CreateTopicResult result = snsClient.createTopic(createTopicRequest); // returns arn if topic already exists
String topicArn = result.getTopicArn();
logger.info("Using arn :" + topicArn);
return topicArn;
}
private String createOrGetSQSSubscriptionToSNS() {
String existing = getARNofSQSSubscriptionToSNS();
if (existing!=null) {
return existing;
}
return createNewSQSSubscriptionToSNS();
}
public String getARNofSQSSubscriptionToSNS() {
List<Subscription> subs = getSNSSubs();
for(Subscription sub : subs) {
if (sub.getProtocol().equals(SQS_PROTO)) {
if (sub.getEndpoint().equals(queueArn)) {
String subscriptionArn = sub.getSubscriptionArn();
logger.info("Found existing SNS subscription, subscription arn is: " + subscriptionArn);
return subscriptionArn;
}
}
}
return null;
}
private void deleteMessage(Message msg) {
logger.info("Deleting message " + msg.getReceiptHandle());
sqsClient.deleteMessage(new DeleteMessageRequest()
.withQueueUrl(queueURL)
.withReceiptHandle(msg.getReceiptHandle()));
}
@Override
public boolean isInit() {
return init;
}
}