lib/eventq/eventq_aws/aws_calculate_visibility_timeout.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

module EventQ
  module Amazon
    # Class responsible to know how to calculate message Visibility Timeout for Amazon SQS
    class CalculateVisibilityTimeout
      def initialize(max_timeout:, logger: EventQ.logger)
        @max_timeout = seconds_to_ms(max_timeout)
        @logger      = logger
      end

      # Calculate Visibility Timeout
      #
      # @param retry_attempts [Integer] Current retry
      # @param queue_settings [Hash] Queue settings
      # @option allow_retry_back_off [Bool] Enables/Disables backoff strategy
      # @option allow_exponential_back_off [Bool] Enables/Disables exponential backoff strategy
      # @option max_retry_delay [Integer] Maximum amount of time a retry will take in ms
      # @option retry_back_off_grace [Integer] Amount of retries to wait before starting to backoff
      # @option retry_back_off_weight [Integer] Multiplier for the backoff retry
      # @option retry_jitter_ratio [Integer] Ratio of how much jitter to apply to the backoff retry
      # @option retry_delay [Integer] Amount of time to wait until retry in ms
      # @return [Integer] the calculated visibility timeout in seconds
      def call(retry_attempts:, queue_settings:)
        @retry_attempts = retry_attempts

        @allow_retry_back_off       = queue_settings.fetch(:allow_retry_back_off)
        @allow_exponential_back_off = queue_settings.fetch(:allow_exponential_back_off)
        @max_retry_delay            = queue_settings.fetch(:max_retry_delay)
        @retry_back_off_grace       = queue_settings.fetch(:retry_back_off_grace)
        @retry_back_off_weight      = queue_settings.fetch(:retry_back_off_weight)
        @retry_jitter_ratio         = queue_settings.fetch(:retry_jitter_ratio)
        @retry_delay                = queue_settings.fetch(:retry_delay)

        visibility_timeout = if @allow_retry_back_off && retry_past_grace_period?
          timeout_with_back_off
        else
          timeout_without_back_off
        end

        visibility_timeout = apply_jitter(visibility_timeout) if @retry_jitter_ratio > 0

        ms_to_seconds(visibility_timeout)
      end

      private

      attr_reader :logger

      def retry_past_grace_period?
        @retry_attempts > @retry_back_off_grace
      end

      def timeout_without_back_off
        @retry_delay
      end

      def timeout_with_back_off
        factor = @retry_attempts - @retry_back_off_grace
        weighted_retry_delay = @retry_delay * @retry_back_off_weight

        visibility_timeout = if @allow_exponential_back_off
          weighted_retry_delay * 2 ** (factor - 1)
        else
          weighted_retry_delay * factor
        end

        visibility_timeout = check_for_max_retry_delay(visibility_timeout)
        check_for_max_timeout(visibility_timeout)
      end

      def apply_jitter(visibility_timeout)
        ratio = @retry_jitter_ratio / 100.0
        min_visibility_timeout = (visibility_timeout * (1 - ratio)).to_i
        rand(min_visibility_timeout..visibility_timeout)
      end

      def ms_to_seconds(value)
        value / 1000
      end

      def seconds_to_ms(value)
        value * 1000
      end

      def check_for_max_retry_delay(visibility_timeout)
        return visibility_timeout if visibility_timeout <= @max_retry_delay

        logger.debug do
          "[#{self.class}] - Max message back off retry delay reached: #{ms_to_seconds(@max_retry_delay)}"
        end

        @max_retry_delay
      end

      def check_for_max_timeout(visibility_timeout)
        return visibility_timeout if visibility_timeout <= @max_timeout

        logger.debug do
          "[#{self.class}] - AWS max visibility timeout of 12 hours has been exceeded. "\
          "Setting message retry delay to 12 hours."
        end

        @max_timeout
      end
    end
  end
end