All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.cimpress.mcp.streams.sqs.SqsMessageConsumer Maven / Gradle / Ivy

package io.cimpress.mcp.streams.sqs;


import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.ChangeMessageVisibilityRequest;
import com.amazonaws.services.sqs.model.ChangeMessageVisibilityResult;
import com.amazonaws.services.sqs.model.CreateQueueResult;
import com.amazonaws.services.sqs.model.DeleteMessageRequest;
import com.amazonaws.services.sqs.model.DeleteMessageResult;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.MessageSystemAttributeName;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
import com.amazonaws.services.sqs.model.SetQueueAttributesRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.logstash.logback.marker.LogstashMarker;
import net.logstash.logback.marker.Markers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rx.Observable;
import rx.Observer;
import rx.observables.AsyncOnSubscribe;
import rx.schedulers.Schedulers;

import java.util.ArrayList;
import java.util.List;


public class SqsMessageConsumer implements StreamConsumer {

  private static final Logger LOG = LoggerFactory.getLogger(SqsMessageConsumer.class);
  private static final int MAX_VISIBILITY_TIMEOUT = 43200;

  private ExponentialIntervalTransformer exponentialBackOff = new ExponentialIntervalTransformer(new BackoffPolicy() {
  });

  private AmazonSQS client;
  private String queueUrl;

  private String queueName;

  private SqsMessageConsumerOptions options = new SqsMessageConsumerOptions();


  public SqsMessageConsumer(AmazonSQS client, String queueName) {
    this(client, queueName, new SqsMessageConsumerOptions());
  }

  public SqsMessageConsumer(AmazonSQS client, String queueName, SqsMessageConsumerOptions options) {
    this.client = client;
    this.queueName = queueName;
    this.options = options;
    this.queueUrl = createQueue(queueName, options);
    this.options.backOffPolicy.ifPresent(backoffPolicy -> this.exponentialBackOff.setBackoffPolicy(backoffPolicy));
  }

  @Override
  public Observable> start() {

    Observable> stream = Observable.create(new SqsAsyncSubscriber())
        .subscribeOn(Schedulers.io())
        .observeOn(Schedulers.computation())
        .filter(msg -> msg != null)
        .doOnError(e -> {
          LogstashMarker marker = Markers.append("queue", queueName);
          if (e instanceof EmptyQueueException) {
            LOG.info(marker, "No messages to read. SqsMessageConsumer is going to sleep", e);
          } else {
            LOG.error(marker, "Error retrieving messages from SQS", e);
          }
        })
        .retryWhen(o -> o.compose(exponentialBackOff))
        .doOnNext(item -> exponentialBackOff.reset())
        .map(msg -> (Committable) new SqsCommittable(msg, (options) -> delete(msg)));

    return stream;
  }

  private ReceiveMessageResult read(Integer max) {

    final ReceiveMessageRequest readRequest = new ReceiveMessageRequest(this.queueUrl)
        .withAttributeNames(MessageSystemAttributeName.ApproximateReceiveCount.toString(),
            MessageSystemAttributeName.ApproximateFirstReceiveTimestamp.toString())
        .withMaxNumberOfMessages(max)
        .withVisibilityTimeout(options.timeout)
        .withWaitTimeSeconds(options.maxWait);
    final ReceiveMessageResult result = client.receiveMessage(readRequest);
    return result;
  }

  private DeleteMessageResult delete(Message message) {
    final DeleteMessageRequest deleteRequest = new DeleteMessageRequest(this.queueUrl, message.getReceiptHandle());
    final DeleteMessageResult result = client.deleteMessage(deleteRequest);
    return result;
  }

  public ChangeMessageVisibilityResult changeVisibilityTimeout(Message message, int visibilityTimeout) {
    // safeguard to ensure we do not try to set the visibility timeout higher than the maximum allowed
    if (options.timeout + visibilityTimeout > MAX_VISIBILITY_TIMEOUT) {
      visibilityTimeout = MAX_VISIBILITY_TIMEOUT - options.timeout;
    }

    final ChangeMessageVisibilityRequest changeVisibilityResult =
        new ChangeMessageVisibilityRequest(this.queueUrl, message.getReceiptHandle(), visibilityTimeout);
    return client.changeMessageVisibility(changeVisibilityResult);
  }

  public String getQueueUrl() {
    return this.queueUrl;
  }

  private String createQueue(String queueName, SqsMessageConsumerOptions options) {
    CreateQueueResult result = client.createQueue(queueName);
    updateQueueAttributes(result.getQueueUrl(), options);
    return result.getQueueUrl();
  }

  public SqsMessageProducer createProducer(ObjectMapper mapper) {
    return new SqsMessageProducer(client, queueUrl, mapper);
  }

  private void updateQueueAttributes(String queueUrl, SqsMessageConsumerOptions options) {

    try {
      // Set RedrivePolicy for the original queue
      SetQueueAttributesRequest setQueueAttributesRequest =
          new SetQueueAttributesRequest().withQueueUrl(queueUrl)
              .addAttributesEntry("DelaySeconds", options.delaySeconds.toString())
              .addAttributesEntry("MessageRetentionPeriod", "1209600");
      options.dlqName.ifPresent(dlqName -> {
        String dlqUrl = SqsUtils.getQueueUrl(this.client, dlqName);
        String dlqArn = SqsUtils.getQueueArn(this.client, dlqUrl);
        setQueueAttributesRequest.addAttributesEntry("RedrivePolicy", formatRedrivePolicy(options.maxReads.toString(), dlqArn));
      });
      client.setQueueAttributes(setQueueAttributesRequest);

    } catch (Exception e) {
      LOG.error("Failed to update options for " + queueName + ".. moving on", e);
    }
  }

  private String formatRedrivePolicy(String... args) {
    return String.format("{\"maxReceiveCount\":\"%s\", \"deadLetterTargetArn\":\"%s\"}", args);
  }

  public String getQueueName() {
    return queueName;
  }

  private final class SqsAsyncSubscriber extends AsyncOnSubscribe, Message> {

    private int max = options.maxMessages;

    @Override
    protected List generateState() {
      return new ArrayList<>();
    }

    @Override
    protected List next(List state, long requested, Observer> observer) {

      Observable nextBatch = Observable.fromCallable(() -> read(requested > max ? max : (int) requested))
          .subscribeOn(Schedulers.io())
          .observeOn(Schedulers.computation())
          .doOnError(e -> LOG.error("Failed to read from SQS " + queueName, e))
          .flatMap(result -> {
            List msgs = result.getMessages();
            if (msgs.isEmpty()) {
              return Observable.error(new EmptyQueueException("The queue is empty. Go to sleep"));
            }
            return Observable.from(msgs);
          });
      observer.onNext(nextBatch);
      return state;
    }

  }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy