module RSpec
  module Core
    module Formatters
      # @private
      # Provides terminal syntax highlighting of code snippets
      # when coderay is available.
      class SyntaxHighlighter
        def initialize(configuration)
          @configuration = configuration
        end

        def highlight(lines)
          implementation.highlight_syntax(lines)
        end

        # rubocop:disable Lint/RescueException
        # rubocop:disable Lint/HandleExceptions
        def self.attempt_to_add_rspec_terms_to_coderay_keywords
          CodeRay::Scanners::Ruby::Patterns::IDENT_KIND.add(%w[
            describe context
            it specify
            before after around
            let subject
            expect allow
          ], :keyword)
        rescue Exception
          # Mutating CodeRay's contants like this is not a public API
          # and might not always work. If we cannot add our keywords
          # to CodeRay it is not a big deal and not worth raising an
          # error over, so we ignore it.
        end
      # rubocop:enable Lint/HandleExceptions
      # rubocop:enable Lint/RescueException

      private

        if RSpec::Support::OS.windows?
          # :nocov:
          def implementation
            WindowsImplementation
          end
          # :nocov:
        else
          def implementation
            return color_enabled_implementation if @configuration.color_enabled?
            NoSyntaxHighlightingImplementation
          end
        end

        def color_enabled_implementation
          @color_enabled_implementation ||= begin
            require 'coderay'
            self.class.attempt_to_add_rspec_terms_to_coderay_keywords
            CodeRayImplementation
          rescue LoadError
            NoSyntaxHighlightingImplementation
          end
        end

        # @private
        module CodeRayImplementation
          RESET_CODE = "\e[0m"

          def self.highlight_syntax(lines)
            highlighted = begin
              CodeRay.encode(lines.join("\n"), :ruby, :terminal)
            rescue Support::AllExceptionsExceptOnesWeMustNotRescue
              return lines
            end

            highlighted.split("\n").map do |line|
              line.sub(/\S/) { |char| char.insert(0, RESET_CODE) }
            end
          end
        end

        # @private
        module NoSyntaxHighlightingImplementation
          def self.highlight_syntax(lines)
            lines
          end
        end

        # @private
        # Not sure why, but our code above (and/or coderay itself) does not work
        # on Windows, so we disable the feature on Windows.
        WindowsImplementation = NoSyntaxHighlightingImplementation
      end
    end
  end
end


# frozen_string_literal: true

module Shoryuken
  # Parses SQS message bodies according to worker configuration.
  # Supports JSON parsing, text extraction, custom Procs, and
  # any object that responds to parse or load methods.
  class BodyParser
    class << self
      # Parses the body of an SQS message according to the worker's body_parser option
      #
      # @param worker_class [Class] the worker class with shoryuken options
      # @param sqs_msg [Shoryuken::Message] the SQS message to parse
      # @return [Object] the parsed message body
      def parse(worker_class, sqs_msg)
        body_parser = worker_class.get_shoryuken_options['body_parser']

        case body_parser
        when :json
          JSON.parse(sqs_msg.body)
        when Proc
          body_parser.call(sqs_msg)
        when :text, nil
          sqs_msg.body
        else
          if body_parser.respond_to?(:parse)
            # JSON.parse
            body_parser.parse(sqs_msg.body)
          elsif body_parser.respond_to?(:load)
            # see https://github.com/ruby-shoryuken/shoryuken/pull/91
            # JSON.load
            body_parser.load(sqs_msg.body)
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

module Shoryuken
  # Launches and coordinates Shoryuken's message processing managers.
  # Handles the lifecycle of the processing system including startup, shutdown, and health checks.
  class Launcher
    include Util

    # Initializes a new Launcher with managers for each processing group
    def initialize
      @managers = create_managers
      @stopping = false
    end

    # Indicates whether the launcher is in the process of stopping.
    #
    # This flag is set to true when either {#stop} or {#stop!} is called,
    # and is used by ActiveJob adapters to signal jobs that they should
    # checkpoint and prepare for graceful shutdown.
    #
    # @return [Boolean] true if stopping, false otherwise
    def stopping?
      @stopping
    end

    # Starts the message processing system
    #
    # @return [void]
    def start
      logger.info { 'Starting' }

      start_callback
      start_managers
    end

    # Forces an immediate stop of all processing
    #
    # @return [void]
    def stop!
      @stopping = true
      initiate_stop

      # Don't await here so the timeout below is not delayed
      stop_new_dispatching

      executor.shutdown
      executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])

      fire_event(:stopped)
    end

    # Gracefully stops all processing, waiting for in-flight messages
    #
    # @return [void]
    def stop
      @stopping = true
      fire_event(:quiet, true)

      initiate_stop

      stop_new_dispatching
      await_dispatching_in_progress

      executor.shutdown
      executor.wait_for_termination

      fire_event(:stopped)
    end

    # Checks if all processing groups are healthy
    #
    # @return [Boolean] true if all groups are running normally
    def healthy?
      Shoryuken.groups.keys.all? do |group|
        manager = @managers.find { |m| m.group == group }
        manager && manager.running?
      end
    end

    private

    # Stops all managers from dispatching new messages
    #
    # @return [void]
    def stop_new_dispatching
      @managers.each(&:stop_new_dispatching)
    end

    # Waits for any in-progress dispatching to complete
    #
    # @return [void]
    def await_dispatching_in_progress
      @managers.each(&:await_dispatching_in_progress)
    end

    # Returns the executor for running async operations
    #
    # @return [Concurrent::ExecutorService] the executor service
    def executor
      @_executor ||= Shoryuken.launcher_executor || Concurrent.global_io_executor
    end

    # Starts all managers in parallel futures
    #
    # @return [void]
    def start_managers
      @managers.each do |manager|
        Concurrent::Future.execute { manager.start }
      end
    end

    # Initiates the stop sequence
    #
    # @return [void]
    def initiate_stop
      logger.info { 'Shutting down' }

      stop_callback
    end

    # Executes the start callback and fires startup event
    #
    # @return [void]
    def start_callback
      if (callback = Shoryuken.start_callback)
        logger.debug { 'Calling start_callback' }
        callback.call
      end

      fire_event(:startup)
    end

    # Executes the stop callback and fires shutdown event
    #
    # @return [void]
    def stop_callback
      if (callback = Shoryuken.stop_callback)
        logger.debug { 'Calling stop_callback' }
        callback.call
      end

      fire_event(:shutdown, true)
    end

    # Creates managers for each configured processing group
    #
    # @return [Array<Shoryuken::Manager>] the created managers
    def create_managers
      Shoryuken.groups.map do |group, options|
        Shoryuken::Manager.new(
          group,
          Shoryuken::Fetcher.new(group),
          Shoryuken.polling_strategy(group).new(options[:queues], Shoryuken.delay(group)),
          options[:concurrency],
          executor
        )
      end
    end
  end
end

stop

    def fire_event(event, reverse = false, event_options = {})
      logger.debug { "Firing '#{event}' lifecycle event" }
      arr = Shoryuken.options[:lifecycle_events][event]
      arr.reverse! if reverse
      arr.each do |block|
        block.call(event_options)
      rescue => e
        logger.warn(event: event)
        logger.warn "#{e.class.name}: #{e.message}"
      end
    end


def create_managers
      Shoryuken.groups.map do |group, options|
        Shoryuken::Manager.new(
          group,
          Shoryuken::Fetcher.new(group),
          Shoryuken.polling_strategy(group).new(options[:queues], Shoryuken.delay(group)),
          options[:concurrency],
          executor
        )
      end
    end


# frozen_string_literal: true

module Shoryuken
  # Represents an SQS queue and provides methods for sending and receiving messages.
  # Handles both standard and FIFO queues, automatically adding required FIFO attributes.
  class Queue
    include Util

    # SQS attribute name for FIFO queue identification
    FIFO_ATTR               = 'FifoQueue'

    # Default message group ID used for FIFO queues
    MESSAGE_GROUP_ID        = 'ShoryukenMessage'

    # SQS attribute name for visibility timeout
    VISIBILITY_TIMEOUT_ATTR = 'VisibilityTimeout'

    # @return [String] the queue name
    attr_accessor :name

    # @return [Aws::SQS::Client] the SQS client
    attr_accessor :client

    # @return [String] the queue URL
    attr_accessor :url

    # Initializes a new Queue instance
    #
    # @param client [Aws::SQS::Client] the SQS client
    # @param name_or_url_or_arn [String] the queue name, URL, or ARN
    def initialize(client, name_or_url_or_arn)
      self.client = client
      set_name_and_url(name_or_url_or_arn)
    end

    # Returns the visibility timeout for the queue
    #
    # @return [Integer] the visibility timeout in seconds
    def visibility_timeout
      # Always lookup for the latest visibility when cache is disabled
      # setting it to nil, forces re-lookup
      @_visibility_timeout = nil unless Shoryuken.cache_visibility_timeout?
      @_visibility_timeout ||= queue_attributes.attributes[VISIBILITY_TIMEOUT_ATTR].to_i
    end

    # Deletes messages from the queue in batch
    #
    # @param options [Hash] options for delete_message_batch
    # @option options [Array<Hash>] :entries entries to delete with id and receipt_handle
    # @return [Boolean] true if any messages failed to delete
    def delete_messages(options)
      failed_messages = client.delete_message_batch(
        options.merge(queue_url: url)
      ).failed || []
      failed_messages.any? do |failure|
        logger.error do
          "Could not delete #{failure.id}, code: '#{failure.code}', message: '#{failure.message}', sender_fault: #{failure.sender_fault}"
        end
      end
    end

    # Sends a single message to the queue
    #
    # @param options [Hash, String] message options or message body string
    # @option options [String] :message_body the message body
    # @option options [Integer] :delay_seconds delay before message becomes visible
    # @option options [Hash] :message_attributes custom message attributes
    # @option options [String] :message_group_id FIFO queue message group ID
    # @option options [String] :message_deduplication_id FIFO queue deduplication ID
    # @return [Aws::SQS::Types::SendMessageResult] the send result
    def send_message(options)
      options = sanitize_message!(options).merge(queue_url: url)

      Shoryuken.client_middleware.invoke(options) do
        client.send_message(options)
      end
    end

    # Sends multiple messages to the queue in batch
    #
    # @param options [Hash, Array] batch options or array of message bodies/hashes
    # @option options [Array<Hash>] :entries message entries to send
    # @return [Aws::SQS::Types::SendMessageBatchResult] the batch send result
    def send_messages(options)
      client.send_message_batch(sanitize_messages!(options).merge(queue_url: url))
    end

    # Receives messages from the queue
    #
    # @param options [Hash] options for receive_message
    # @option options [Integer] :max_number_of_messages maximum messages to receive
    # @option options [Integer] :visibility_timeout visibility timeout for received messages
    # @option options [Integer] :wait_time_seconds long polling wait time
    # @option options [Array<String>] :attribute_names SQS attributes to retrieve
    # @option options [Array<String>] :message_attribute_names message attributes to retrieve
    # @return [Array<Shoryuken::Message>] the received messages
    def receive_messages(options)
      messages = client.receive_message(options.merge(queue_url: url)).messages || []
      messages.map { |m| Message.new(client, self, m) }
    end

    # Checks if the queue is a FIFO queue
    #
    # @return [Boolean] true if the queue is a FIFO queue
    def fifo?
      # Make sure the memoization work with boolean to avoid multiple calls to SQS
      # see https://github.com/ruby-shoryuken/shoryuken/pull/529
      return @_fifo if defined?(@_fifo)

      @_fifo = queue_attributes.attributes[FIFO_ATTR] == 'true'
      @_fifo
    end

    private

    # Initializes the FIFO attribute by calling fifo?
    #
    # @return [Boolean] whether the queue is FIFO
    def initialize_fifo_attribute
      # calling fifo? will also initialize it
      fifo?
    end

    # Sets the queue name and URL from a queue name
    #
    # @param name [String] the queue name
    # @return [void]
    def set_by_name(name) # rubocop:disable Naming/AccessorMethodName
      self.name = name
      self.url  = client.get_queue_url(queue_name: name).queue_url
    end

    # Sets the queue name and URL from a queue URL
    #
    # @param url [String] the queue URL
    # @return [void]
    def set_by_url(url) # rubocop:disable Naming/AccessorMethodName
      self.name = url.split('/').last
      self.url  = url
    end

    # Converts an ARN to a queue URL
    #
    # @param arn_str [String] the ARN string
    # @return [String] the queue URL
    def arn_to_url(arn_str)
      *, region, account_id, resource = arn_str.split(':')

      required = [region, account_id, resource].map(&:to_s)
      valid = required.none?(&:empty?)

      raise Errors::InvalidArnError, "Invalid ARN: #{arn_str}. A valid ARN must include: region, account_id and resource." unless valid

      "https://sqs.#{region}.amazonaws.com/#{account_id}/#{resource}"
    end

    # Sets the queue name and URL from a name, URL, or ARN
    #
    # @param name_or_url_or_arn [String] the queue identifier
    # @return [void]
    def set_name_and_url(name_or_url_or_arn) # rubocop:disable Naming/AccessorMethodName
      if name_or_url_or_arn.include?('://')
        set_by_url(name_or_url_or_arn)

        # anticipate the fifo? checker for validating the queue URL
        initialize_fifo_attribute
        return
      end

      if name_or_url_or_arn.start_with?('arn:')
        url = arn_to_url(name_or_url_or_arn)
        set_by_url(url)

        # anticipate the fifo? checker for validating the queue URL
        initialize_fifo_attribute
        return
      end

      set_by_name(name_or_url_or_arn)
    rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue
      raise Errors::QueueNotFoundError, "The specified queue #{name_or_url_or_arn} does not exist."
    end

    # Returns the queue attributes from SQS
    #
    # @return [Aws::SQS::Types::GetQueueAttributesResult] the queue attributes
    def queue_attributes
      # Note: Retrieving all queue attributes as requesting `FifoQueue` on non-FIFO queue raises error.
      # See issue: https://github.com/aws/aws-sdk-ruby/issues/1350
      client.get_queue_attributes(queue_url: url, attribute_names: ['All'])
    end

    # Sanitizes a batch of messages, converting to proper format
    #
    # @param options [Hash, Array] batch options or array of messages
    # @option options [Array<Hash>] :entries message entries
    # @return [Hash] the sanitized options with entries key
    def sanitize_messages!(options)
      if options.is_a?(Array)
        entries = options.map.with_index do |m, index|
          { id: index.to_s }.merge(m.is_a?(Hash) ? m : { message_body: m })
        end

        options = { entries: entries }
      end

      options[:entries].each(&method(:sanitize_message!))

      options
    end

    # Adds FIFO attributes to message options if needed
    #
    # @param options [Hash] the message options
    # @option options [String] :message_body the message body
    # @return [Hash] the options with FIFO attributes added
    def add_fifo_attributes!(options)
      return unless fifo?

      options[:message_group_id]         ||= MESSAGE_GROUP_ID
      options[:message_deduplication_id] ||= Digest::SHA256.hexdigest(options[:message_body].to_s)

      options
    end

    # Sanitizes a single message, converting body to JSON if needed
    #
    # @param options [Hash, String] message options or body string
    # @option options [String, Hash] :message_body the message body
    # @return [Hash] the sanitized message options
    def sanitize_message!(options)
      options = { message_body: options } if options.is_a?(String)

      if (body = options[:message_body]).is_a?(Hash)
        options[:message_body] = JSON.dump(body)
      end

      add_fifo_attributes!(options)

      options
    end
  end
end


def set_name_and_url(name_or_url_or_arn) # rubocop:disable Naming/AccessorMethodName
      if name_or_url_or_arn.include?('://')
        set_by_url(name_or_url_or_arn)

        # anticipate the fifo? checker for validating the queue URL
        initialize_fifo_attribute
        return
      end

      if name_or_url_or_arn.start_with?('arn:')
        url = arn_to_url(name_or_url_or_arn)
        set_by_url(url)

        # anticipate the fifo? checker for validating the queue URL
        initialize_fifo_attribute
        return
      end

      set_by_name(name_or_url_or_arn)
    rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue
      raise Errors::QueueNotFoundError, "The specified queue #{name_or_url_or_arn} does not exist."
    end

    def set_by_url(url) # rubocop:disable Naming/AccessorMethodName
      self.name = url.split('/').last
      self.url  = url
    end

    def arn_to_url(arn_str)
      *, region, account_id, resource = arn_str.split(':')

      required = [region, account_id, resource].map(&:to_s)
      valid = required.none?(&:empty?)

      raise Errors::InvalidArnError, "Invalid ARN: #{arn_str}. A valid ARN must include: region, account_id and resource." unless valid

      "https://sqs.#{region}.amazonaws.com/#{account_id}/#{resource}"
    end

    def set_by_name(name) # rubocop:disable Naming/AccessorMethodName
      self.name = name
      self.url  = client.get_queue_url(queue_name: name).queue_url
    end


def sanitize_messages!(options)
      if options.is_a?(Array)
        entries = options.map.with_index do |m, index|
          { id: index.to_s }.merge(m.is_a?(Hash) ? m : { message_body: m })
        end

        options = { entries: entries }
      end

      options[:entries].each(&method(:sanitize_message!))

      options
    end


# frozen_string_literal: true

module Shoryuken
  # Utility methods shared across Shoryuken classes.
  # Provides logging, event firing, and helper methods.
  module Util
    # Returns the Shoryuken logger
    #
    # @return [Logger] the configured logger
    def logger
      Shoryuken.logger
    end

    # Fires a lifecycle event to all registered handlers
    #
    # @param event [Symbol] the event name to fire
    # @param reverse [Boolean] whether to call handlers in reverse order
    # @param event_options [Hash] options to pass to event handlers
    # @return [void]
    def fire_event(event, reverse = false, event_options = {})
      logger.debug { "Firing '#{event}' lifecycle event" }
      arr = Shoryuken.options[:lifecycle_events][event]
      arr.reverse! if reverse
      arr.each do |block|
        block.call(event_options)
      rescue => e
        logger.warn(event: event)
        logger.warn "#{e.class.name}: #{e.message}"
      end
    end

    # Calculates elapsed time in milliseconds
    #
    # @param started_at [Time] the start time
    # @return [Float] elapsed time in milliseconds
    def elapsed(started_at)
      # elapsed in ms
      (Time.now - started_at) * 1000
    end

    # Converts a queue array to a hash of queue names and weights
    #
    # @param queues [Array<String>] array of queue names with possible duplicates
    # @return [Array<Array>] array of [queue_name, weight] pairs
    def unparse_queues(queues)
      queues.each_with_object({}) do |name, queue_and_weights|
        queue_and_weights[name] = queue_and_weights[name].to_i + 1
      end.to_a
    end

    # Returns a display name for the worker processing a message
    #
    # @param worker_class [Class] the worker class
    # @param sqs_msg [Aws::SQS::Types::Message, Array] the message or batch
    # @param body [Object, nil] the parsed message body
    # @return [String] the worker display name
    def worker_name(worker_class, sqs_msg, body = nil)
      if Shoryuken.active_job? \
          && !sqs_msg.is_a?(Array) \
          && sqs_msg.message_attributes \
          && sqs_msg.message_attributes['shoryuken_class'] \
          && sqs_msg.message_attributes['shoryuken_class'][:string_value] \
          == 'Shoryuken::ActiveJob::JobWrapper' \
          && body

        "ActiveJob/#{body['job_class']}"
      else
        worker_class.to_s
      end
    end
  end
end


def worker_name(worker_class, sqs_msg, body = nil)
      if Shoryuken.active_job? \
          && !sqs_msg.is_a?(Array) \
          && sqs_msg.message_attributes \
          && sqs_msg.message_attributes['shoryuken_class'] \
          && sqs_msg.message_attributes['shoryuken_class'][:string_value] \
          == 'Shoryuken::ActiveJob::JobWrapper' \
          && body

        "ActiveJob/#{body['job_class']}"
      else
        worker_class.to_s
      end
    end

!(Shoryuken.active_job? \
          && !sqs_msg.is_a?(Array) \
          && sqs_msg.message_attributes \
          && sqs_msg.message_attributes['shoryuken_class'] \
          && sqs_msg.message_attributes['shoryuken_class'][:string_value] \
          == 'Shoryuken::ActiveJob::JobWrapper' \
          && body)

    #   @return [Hash] System attributes like SenderId, SentTimestamp, etc.
    #
    # @!method md5_of_message_attributes
    #   Returns the MD5 hash of the message attributes.
    #   @return [String] MD5 hash of message attributes
    #
    # @!method message_attributes
    #   Returns custom message attributes set by the sender.
    #   @return [Hash] Custom message attributes with typed values
    def_delegators(:data,
                   :message_id,
                   :receipt_handle,
                   :md5_of_body,
                   :body,
                   :attributes,
                   :md5_of_message_attributes,
                   :message_attributes)

    # @return [Aws::SQS::Client] The SQS client used for message operations
    attr_accessor :client

    # @return [String] The URL of the SQS queue this message came from
    attr_accessor :queue_url

    # @return [String] The name of the queue this message came from
    attr_accessor :queue_name

    # @return [Aws::SQS::Types::Message] The raw SQS message data
    attr_accessor :data

    # Creates a new Message instance wrapping SQS message data.
    #
    # @param client [Aws::SQS::Client] The SQS client for message operations
    # @param queue [Shoryuken::Queue] The queue this message came from
    # @param data [Aws::SQS::Types::Message] The raw SQS message data
    # @api private
    def initialize(client, queue, data)
      self.client     = client
      self.data       = data
      self.queue_url  = queue.url
      self.queue_name = queue.name
    end


# frozen_string_literal: true

module Shoryuken
  # Processes SQS messages by invoking the appropriate worker.
  # Handles middleware chain execution and exception handling.
  class Processor
    include Util

    # @return [String] the queue name
    attr_reader :queue

    # @return [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
    attr_reader :sqs_msg

    # Processes a message from a queue
    #
    # @param queue [String] the queue name
    # @param sqs_msg [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
    # @return [Object] the result of the worker's perform method
    def self.process(queue, sqs_msg)
      new(queue, sqs_msg).process
    end

    # Initializes a new Processor
    #
    # @param queue [String] the queue name
    # @param sqs_msg [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
    def initialize(queue, sqs_msg)
      @queue   = queue
      @sqs_msg = sqs_msg
    end

    # Processes the message through the middleware chain and worker
    #
    # @return [Object] the result of the worker's perform method
    def process
      worker_perform = proc do
        return logger.error { "No worker found for #{queue}" } unless worker

        Shoryuken::Logging.with_context("#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id}") do
          worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
            worker.perform(sqs_msg, body)
          end
        end
      end

      if Shoryuken.enable_reloading
        Shoryuken.reloader.call do
          worker_perform.call
        end
      else
        worker_perform.call
      end
    rescue Exception => e
      Array(Shoryuken.exception_handlers).each { |handler| handler.call(e, queue, sqs_msg) }

      raise
    end

    private

    # Fetches the worker instance for processing
    #
    # @return [Object, nil] the worker instance or nil if not found
    def worker
      @_worker ||= Shoryuken.worker_registry.fetch_worker(queue, sqs_msg)
    end

    # Returns the worker class
    #
    # @return [Class] the worker class
    def worker_class
      worker.class
    end

    # Parses the message body or bodies for batch processing
    #
    # @return [Object, Array<Object>] the parsed body or array of bodies
    def body
      @_body ||= sqs_msg.is_a?(Array) ? sqs_msg.map(&method(:parse_body)) : parse_body(sqs_msg)
    end

    # Parses a single message body
    #
    # @param sqs_msg [Aws::SQS::Types::Message] the message to parse
    # @return [Object] the parsed message body
    def parse_body(sqs_msg)
      BodyParser.parse(worker_class, sqs_msg)
    end
  end
end

process

    def self.with_context(msg)
      previous = context_storage[:shoryuken_context]
      context_storage[:shoryuken_context] = msg
      yield
    ensure
      context_storage[:shoryuken_context] = previous
    end

    def worker_name(worker_class, sqs_msg, body = nil)
      if Shoryuken.active_job? \
          && !sqs_msg.is_a?(Array) \
          && sqs_msg.message_attributes \
          && sqs_msg.message_attributes['shoryuken_class'] \
          && sqs_msg.message_attributes['shoryuken_class'][:string_value] \
          == 'Shoryuken::ActiveJob::JobWrapper' \
          && body

        "ActiveJob/#{body['job_class']}"
      else
        worker_class.to_s
      end
    end


# frozen_string_literal: true

module Shoryuken
  # Worker module provides the core functionality for creating Shoryuken workers
  # that process messages from Amazon SQS queues.
  #
  # Including this module in a class provides methods for configuring queue processing,
  # enqueueing jobs, and setting up middleware. Workers can be configured for different
  # processing patterns including single message processing, batch processing, and
  # various retry and visibility timeout strategies.
  #
  # @example Basic worker implementation
  #   class EmailWorker
  #     include Shoryuken::Worker
  #     shoryuken_options queue: 'emails'
  #
  #     def perform(sqs_msg, body)
  #       send_email(body['recipient'], body['subject'], body['content'])
  #     end
  #   end
  #
  # @example Advanced worker with all options
  #   class AdvancedWorker
  #     include Shoryuken::Worker
  #
  #     shoryuken_options queue: 'advanced_queue',
  #                       batch: false,
  #                       auto_delete: true,
  #                       auto_visibility_timeout: true,
  #                       retry_intervals: [1, 5, 25, 125, 625]
  #
  #     server_middleware do |chain|
  #       chain.add MyCustomMiddleware
  #     end
  #
  #     def perform(sqs_msg, body)
  #       # Worker implementation
  #     end
  #   end
  #
  # @see ClassMethods#shoryuken_options Primary configuration method
  # @see ClassMethods#perform_async For enqueueing jobs
  # @see https://github.com/ruby-shoryuken/shoryuken/wiki/Workers Comprehensive worker documentation
  module Worker
    # Sets up the including class with Shoryuken worker functionality
    #
    # @param base [Class] the class including this module
    # @return [void]
    def self.included(base)
      base.extend(ClassMethods)
      base.shoryuken_class_attribute :shoryuken_options_hash
    end

    # Class methods added to classes that include Shoryuken::Worker.
    # Provides methods for configuring the worker, enqueueing jobs, and managing middleware.
    module ClassMethods
      # Enqueues a job to be processed asynchronously by a Shoryuken worker.
      #
      # @param body [Object] The job payload that will be passed to the worker's perform method
      # @param options [Hash] Additional options for job enqueueing
      # @option options [String] :message_group_id FIFO queue group ID for message ordering
      # @option options [String] :message_deduplication_id FIFO queue deduplication ID
      # @option options [Hash] :message_attributes Custom SQS message attributes
      # @return [String] The message ID of the enqueued job
      #
      # @example Basic job enqueueing
      #   MyWorker.perform_async({ user_id: 123, action: 'send_email' })
      #
      # @example FIFO queue with ordering
      #   MyWorker.perform_async(data, message_group_id: 'user_123')
      def perform_async(body, options = {})
        Shoryuken.worker_executor.perform_async(self, body, options)
      end

      # Enqueues a job to be processed after a specified time interval.
      #
      # @param interval [Integer, ActiveSupport::Duration] Delay in seconds, or duration object
      # @param body [Object] The job payload that will be passed to the worker's perform method
      # @param options [Hash] SQS message options for the delayed job
      # @option options [String] :message_group_id FIFO queue group ID for message ordering
      # @option options [String] :message_deduplication_id FIFO queue deduplication ID
      # @option options [Hash] :message_attributes Custom SQS message attributes
      # @return [String] The message ID of the enqueued job
      #
      # @example Delay job by 5 minutes
      #   MyWorker.perform_in(5.minutes, { user_id: 123 })
      #
      # @example Delay job by specific number of seconds
      #   MyWorker.perform_in(300, { user_id: 123 })
      def perform_in(interval, body, options = {})
        Shoryuken.worker_executor.perform_in(self, interval, body, options)
      end

      alias_method :perform_at, :perform_in

      # Configures server-side middleware chain for this worker class.
      # Middleware runs before and after job processing, similar to Rack middleware.
      #
      # @yield [Shoryuken::Middleware::Chain] The middleware chain for configuration
      # @return [Shoryuken::Middleware::Chain] The configured middleware chain
      #
      # @example Adding custom middleware
      #   class MyWorker
      #     include Shoryuken::Worker
      #
      #     server_middleware do |chain|
      #       chain.add MyCustomMiddleware
      #       chain.remove Shoryuken::Middleware::Server::ActiveRecord
      #     end
      #   end
      def server_middleware
        @_server_chain ||= Shoryuken.server_middleware.dup
        yield @_server_chain if block_given?
        @_server_chain
      end

      # Configures worker options including queue assignment, processing behavior,
      # and SQS-specific settings. This is the main configuration method for workers.
      #
      # @param opts [Hash] Configuration options for the worker
      # @option opts [String, Array<String>] :queue Queue name(s) this worker processes
      # @option opts [Boolean] :batch (false) Process messages in batches of up to 10
      # @option opts [Boolean] :auto_delete (false) Automatically delete messages after processing
      # @option opts [Boolean] :auto_visibility_timeout (false) Automatically extend message visibility
      # @option opts [Array<Integer>] :retry_intervals Exponential backoff retry intervals in seconds
      # @option opts [Array<Class>, Proc] :non_retryable_exceptions Exception classes or lambda that should skip retries and delete message immediately
      # @option opts [Hash] :sqs Additional SQS client options
      #
      # @example Basic worker configuration
      #   class MyWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: 'my_queue'
      #
      #     def perform(sqs_msg, body)
      #       # Process the message
      #     end
      #   end
      #
      # @example Worker with auto-delete and retries
      #   class ReliableWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: 'important_queue',
      #                       auto_delete: true,
      #                       retry_intervals: [1, 5, 25, 125]
      #   end
      #
      # @example Batch processing worker
      #   class BatchWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: 'batch_queue', batch: true
      #
      #     def perform(sqs_msgs, bodies)
      #       # Process array of up to 10 messages
      #       bodies.each { |body| process_item(body) }
      #     end
      #   end
      #
      # @example Multiple queues with priorities
      #   class MultiQueueWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: ['high_priority', 'low_priority']
      #   end
      #
      # @example Auto-extending visibility timeout for long-running jobs
      #   class LongRunningWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: 'slow_queue',
      #                       auto_visibility_timeout: true
      #
      #     def perform(sqs_msg, body)
      #       # Long processing that might exceed visibility timeout
      #       complex_processing(body)
      #     end
      #   end
      #
      # @example Worker with non-retryable exceptions
      #   class ValidationWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: 'validation_queue',
      #                       non_retryable_exceptions: [InvalidInputError, RecordNotFoundError]
      #
      #     def perform(sqs_msg, body)
      #       # If InvalidInputError or RecordNotFoundError is raised,
      #       # the message will be deleted immediately instead of retrying
      #       validate_and_process(body)
      #     end
      #   end
      #
      # @example Worker with lambda for dynamic exception classification
      #   class SmartWorker
      #     include Shoryuken::Worker
      #     shoryuken_options queue: 'smart_queue',
      #                       non_retryable_exceptions: ->(error) {
      #                         error.is_a?(ValidationError) ||
      #                         (error.is_a?(NetworkError) && error.message.include?('permanent'))
      #                       }
      #
      #     def perform(sqs_msg, body)
      #       # Lambda receives the exception and returns true if non-retryable
      #       process_with_validation(body)
      #     end
      #   end
      def shoryuken_options(opts = {})
        self.shoryuken_options_hash = get_shoryuken_options.merge(stringify_keys(opts || {}))
        normalize_worker_queue!
      end

      # Checks if automatic visibility timeout extension is enabled for this worker.
      # When enabled, Shoryuken automatically extends the message visibility timeout
      # during processing to prevent the message from becoming visible to other consumers.
      #
      # @return [Boolean] true if auto visibility timeout is enabled
      #
      # @see #shoryuken_options Documentation for enabling auto_visibility_timeout
      def auto_visibility_timeout?
        !!get_shoryuken_options['auto_visibility_timeout']
      end

      # Checks if exponential backoff retry is configured for this worker.
      # When retry intervals are specified, failed jobs will be retried with
      # increasing delays between attempts.
      #
      # @return [Boolean] true if retry intervals are configured
      #
      # @example Configuring exponential backoff
      #   shoryuken_options retry_intervals: [1, 5, 25, 125, 625]
      #   # Will retry after 1s, 5s, 25s, 125s, then 625s before giving up
      #
      # @see #shoryuken_options Documentation for configuring retry_intervals
      def exponential_backoff?
        !!get_shoryuken_options['retry_intervals']
      end

      # Checks if automatic message deletion is enabled for this worker.
      # When enabled, successfully processed messages are automatically deleted
      # from the SQS queue. When disabled, you must manually delete messages
      # or they will become visible again after the visibility timeout.
      #
      # @return [Boolean] true if auto delete is enabled
      #
      # @example Manual message deletion when auto_delete is false
      #   def perform(sqs_msg, body)
      #     process_message(body)
      #     # Manually delete the message after successful processing
      #     sqs_msg.delete
      #   end
      #
      # @see #shoryuken_options Documentation for enabling auto_delete
      def auto_delete?
        !!(get_shoryuken_options['delete'] || get_shoryuken_options['auto_delete'])
      end

      # Returns the shoryuken options for this worker class
      # @return [Hash] the options hash
      def get_shoryuken_options # :nodoc:
        shoryuken_options_hash || Shoryuken.default_worker_options
      end

      # Converts hash keys to strings
      # @param hash [Hash] the hash to convert
      # @return [Hash] hash with string keys
      def stringify_keys(hash) # :nodoc:
        new_hash = {}
        hash.each { |key, value| new_hash[key.to_s] = value }
        new_hash
      end

      # Defines inheritable class attributes for workers
      # @param attrs [Array<Symbol>] attribute names to define
      # @return [void]
      def shoryuken_class_attribute(*attrs) # :nodoc:
        attrs.each do |name|
          singleton_class.instance_eval do
            undef_method(name) if method_defined?(name) || private_method_defined?(name)
          end
          define_singleton_method(name) { nil }

          ivar = "@#{name}"

          singleton_class.instance_eval do
            m = "#{name}="
            undef_method(m) if method_defined?(m) || private_method_defined?(m)
          end

          define_singleton_method("#{name}=") do |val|
            singleton_class.class_eval do
              undef_method(name) if method_defined?(name) || private_method_defined?(name)
              define_method(name) { val }
            end

            # singleton? backwards compatibility for ruby < 2.1
            singleton_klass = respond_to?(:singleton?) ? singleton? : self != ancestors.first

            if singleton_klass
              class_eval do
                undef_method(name) if method_defined?(name) || private_method_defined?(name)
                define_method(name) do
                  if instance_variable_defined? ivar
                    instance_variable_get ivar
                  else
                    singleton_class.send name
                  end
                end
              end
            end
            val
          end

          # instance reader
          undef_method(name) if method_defined?(name) || private_method_defined?(name)
          define_method(name) do
            if instance_variable_defined?(ivar)
              instance_variable_get ivar
            else
              self.class.public_send name
            end
          end

          # instance writer
          m = "#{name}="
          undef_method(m) if method_defined?(m) || private_method_defined?(m)
          attr_writer name
        end
      end

      private

      # Normalizes the queue option and registers the worker
      # @return [void]
      def normalize_worker_queue!
        queue = shoryuken_options_hash['queue']
        if queue.respond_to?(:call)
          queue = queue.call
          shoryuken_options_hash['queue'] = queue
        end

        case shoryuken_options_hash['queue']
        when Array
          shoryuken_options_hash['queue'].map!(&:to_s)
        when Symbol
          shoryuken_options_hash['queue'] = shoryuken_options_hash['queue'].to_s
        end

        [shoryuken_options_hash['queue']].flatten.compact.each(&method(:register_worker))
      end

      # Registers this worker class for a queue
      # @param queue [String] the queue name
      # @return [void]
      def register_worker(queue)
        Shoryuken.register_worker(queue, self)
      end
    end
  end
end


def normalize_worker_queue!
        queue = shoryuken_options_hash['queue']
        if queue.respond_to?(:call)
          queue = queue.call
          shoryuken_options_hash['queue'] = queue
        end

        case shoryuken_options_hash['queue']
        when Array
          shoryuken_options_hash['queue'].map!(&:to_s)
        when Symbol
          shoryuken_options_hash['queue'] = shoryuken_options_hash['queue'].to_s
        end

        [shoryuken_options_hash['queue']].flatten.compact.each(&method(:register_worker))
      end


# frozen_string_literal: true

module Shoryuken
  # Fetches messages from SQS queues.
  # Handles message retrieval with automatic retry on connectivity errors.
  class Fetcher
    include Util

    # Maximum number of messages that can be fetched in a single SQS request
    FETCH_LIMIT = 10

    # Initializes a new Fetcher for a processing group
    #
    # @param group [String] the processing group name
    def initialize(group)
      @group = group
    end

    # Fetches messages from a queue with automatic retry
    #
    # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
    # @param limit [Integer] the maximum number of messages to fetch
    # @return [Array<Aws::SQS::Types::Message>] the fetched messages
    def fetch(queue, limit)
      fetch_with_auto_retry(3) do
        started_at = Time.now

        logger.debug { "Looking for new messages in #{queue}" }

        sqs_msgs = Array(receive_messages(queue, limit))

        logger.debug { "Found #{sqs_msgs.size} messages for #{queue.name}" } unless sqs_msgs.empty?
        logger.debug { "Fetcher for #{queue} completed in #{elapsed(started_at)} ms" }

        sqs_msgs
      end
    end

    private

    # Fetches with automatic retry on errors
    #
    # @param max_attempts [Integer] maximum number of retry attempts
    # @yield the fetch operation to retry
    # @return [Object] the result of the block
    def fetch_with_auto_retry(max_attempts)
      attempts = 0

      begin
        yield
      rescue => e
        # Tries to auto retry connectivity errors
        raise if attempts >= max_attempts

        attempts += 1

        logger.debug { "Retrying fetch attempt #{attempts} for #{e.message}" }

        sleep((1..5).to_a.sample)

        retry
      end
    end

    # Receives messages from an SQS queue
    #
    # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
    # @param limit [Integer] the maximum number of messages to receive
    # @return [Array<Aws::SQS::Types::Message>, nil] the received messages
    def receive_messages(queue, limit)
      options = receive_options(queue)

      shoryuken_queue = Shoryuken::Client.queues(queue.name)

      options[:max_number_of_messages]  = max_number_of_messages(shoryuken_queue, limit, options)
      options[:message_attribute_names] = %w[All]
      options[:attribute_names]         = %w[All]

      options.merge!(queue.options)

      shoryuken_queue.receive_messages(options)
    end

    # Determines the maximum number of messages to fetch
    #
    # @param shoryuken_queue [Shoryuken::Queue] the queue instance
    # @param limit [Integer] the requested limit
    # @param options [Hash] receive options that may contain max_number_of_messages
    # @option options [Integer] :max_number_of_messages optional override for max messages
    # @return [Integer] the maximum number of messages to fetch
    def max_number_of_messages(shoryuken_queue, limit, options)
      # For FIFO queues we want to make sure we process one message per group at a time
      # if we set max_number_of_messages greater than 1,
      # SQS may return more than one message for the same message group
      # since Shoryuken uses threads, it will try to process more than one message at once
      # > The message group ID is the tag that specifies that a message belongs to a specific message group.
      # > Messages that belong to the same message group are always processed one by one,
      # > in a strict order relative to the message group
      # > (however, messages that belong to different message groups might be processed out of order).
      # > https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagegroupid-property.html
      limit = 1 if shoryuken_queue.fifo? && !batched_queue?(shoryuken_queue)

      [limit, FETCH_LIMIT, options[:max_number_of_messages]].compact.min
    end

    # Returns the receive options for a queue
    #
    # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
    # @return [Hash] the receive options hash
    def receive_options(queue)
      options = Shoryuken.sqs_client_receive_message_opts[queue.name]
      options ||= Shoryuken.sqs_client_receive_message_opts[@group]

      options.to_h.dup
    end

    # Checks if a queue uses batch message processing
    #
    # @param queue [Shoryuken::Queue] the queue to check
    # @return [Boolean] true if the queue is configured for batch processing
    def batched_queue?(queue)
      Shoryuken.worker_registry.batch_receive_messages?(queue.name)
    end
  end
end

fetch

elapsed

    def elapsed(started_at)
      # elapsed in ms
      (Time.now - started_at) * 1000
    end


def fetch_with_auto_retry(max_attempts)
      attempts = 0

      begin
        yield
      rescue => e
        # Tries to auto retry connectivity errors
        raise if attempts >= max_attempts

        attempts += 1

        logger.debug { "Retrying fetch attempt #{attempts} for #{e.message}" }

        sleep((1..5).to_a.sample)

        retry
      end
    end


# frozen_string_literal: true

module Shoryuken
  # Loads and configures the Shoryuken environment from configuration files
  # and command line options. Handles Rails integration and queue setup.
  class EnvironmentLoader
    # @return [Hash] the configuration options
    attr_reader :options

    # Sets up a new EnvironmentLoader with the given options
    #
    # @param options [Hash] configuration options
    # @option options [String] :config_file path to the configuration file
    # @option options [Boolean] :rails whether to initialize Rails
    # @option options [String] :logfile path to the log file
    # @option options [Boolean] :verbose whether to enable verbose logging
    # @option options [String] :require path to require workers from
    # @option options [Integer] :concurrency number of concurrent workers
    # @return [Shoryuken::EnvironmentLoader] the configured instance
    def self.setup_options(options)
      instance = new(options)
      instance.setup_options
      instance
    end

    # Loads the environment for Rails console usage
    #
    # @return [void]
    def self.load_for_rails_console
      instance = setup_options(config_file: (Rails.root + 'config' + 'shoryuken.yml'))
      instance.load
    end

    # Initializes a new EnvironmentLoader with the given options
    #
    # @param options [Hash] configuration options
    # @option options [String] :config_file path to the configuration file
    # @option options [Boolean] :rails whether to initialize Rails
    # @option options [String] :logfile path to the log file
    # @option options [Boolean] :verbose whether to enable verbose logging
    # @option options [String] :require path to require workers from
    # @option options [Integer] :concurrency number of concurrent workers
    def initialize(options)
      @options = options
    end

    # Sets up configuration options from file and initializes components
    #
    # @return [void]
    def setup_options
      initialize_rails if load_rails?
      initialize_options
      initialize_logger
    end

    # Loads the environment including queues and workers
    #
    # @return [void]
    def load
      prefix_active_job_queue_names
      parse_queues
      require_workers
      validate_queues
      validate_workers
    end

    private

    # Merges configuration file options with runtime options
    #
    # @return [void]
    def initialize_options
      Shoryuken.options.merge!(config_file_options)
      Shoryuken.options.merge!(options)
    end

    # Reads and parses the configuration file
    #
    # @return [Hash] the parsed configuration options
    def config_file_options
      return {} unless (path = options[:config_file])

      raise Errors::InvalidConfigurationError, "The supplied config file #{path} does not exist" unless File.exist?(path)

      if (result = YAML.load(ERB.new(IO.read(path)).result))
        Shoryuken::Helpers::HashUtils.deep_symbolize_keys(result)
      else
        {}
      end
    end

    # Initializes the logger with file output and verbosity settings
    #
    # @return [void]
    def initialize_logger
      Shoryuken::Logging.initialize_logger(Shoryuken.options[:logfile]) if Shoryuken.options[:logfile]
      Shoryuken.logger.level = Logger::DEBUG if Shoryuken.options[:verbose]
    end

    # Initializes the Rails environment
    #
    # @return [void]
    def initialize_rails
      # Adapted from: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb

      require 'rails'
      if ::Rails::VERSION::MAJOR < 4
        require File.expand_path('config/environment.rb')
        ::Rails.application.eager_load!
      else
        # Painful contortions, see 1791 for discussion
        require File.expand_path('config/application.rb')
        if ::Rails::VERSION::MAJOR == 4
          ::Rails::Application.initializer 'shoryuken.eager_load' do
            ::Rails.application.config.eager_load = true
          end
        end
        ::Rails::Application.initializer 'shoryuken.set_reloader_hook' do |app|
          Shoryuken.reloader = proc do |&block|
            app.reloader.wrap do
              block.call
            end
          end
        end
        if Shoryuken.active_job?
          require 'active_job/extensions'
          require 'active_job/queue_adapters/shoryuken_adapter'
          require 'active_job/queue_adapters/shoryuken_concurrent_send_adapter'
        end
        require File.expand_path('config/environment.rb')
      end
    end

    # Checks if Rails should be loaded
    #
    # @return [Boolean] true if Rails should be initialized
    def load_rails?
      options[:rails]
    end

    # Prefixes a queue name with ActiveJob queue name prefix
    #
    # @param queue_name [String] the queue name to prefix
    # @param weight [Integer] the queue weight
    # @return [Array<String, Integer>] the prefixed queue name and weight
    def prefix_active_job_queue_name(queue_name, weight)
      return [queue_name, weight] if queue_name.start_with?('https://', 'arn:')

      queue_name_prefix = ::ActiveJob::Base.queue_name_prefix
      queue_name_delimiter = ::ActiveJob::Base.queue_name_delimiter

      # See https://github.com/rails/rails/blob/master/activejob/lib/active_job/queue_name.rb#L27
      name_parts = [queue_name_prefix.presence, queue_name]
      prefixed_queue_name = name_parts.compact.join(queue_name_delimiter)
      [prefixed_queue_name, weight]
    end

    # Prefixes all queue names with ActiveJob prefix if enabled
    #
    # @return [void]
    def prefix_active_job_queue_names
      return unless Shoryuken.active_job?
      return unless Shoryuken.active_job_queue_name_prefixing?

      Shoryuken.options[:queues].to_a.map! do |queue_name, weight|
        prefix_active_job_queue_name(queue_name, weight)
      end

      Shoryuken.options[:groups].to_a.map! do |group, options|
        if options[:queues]
          options[:queues].map! do |queue_name, weight|
            prefix_active_job_queue_name(queue_name, weight)
          end
        end

        [group, options]
      end
    end

    # Parses a single queue and adds it to a group
    #
    # @param queue [String] the queue name
    # @param weight [Integer] the queue weight
    # @param group [String] the group name
    # @return [void]
    def parse_queue(queue, weight, group)
      Shoryuken.add_queue(queue, [weight.to_i, 1].max, group)
    end

    # Parses all queues from configuration and adds them to groups
    #
    # @return [void]
    def parse_queues
      if Shoryuken.options[:queues].to_a.any?
        Shoryuken.add_group('default', Shoryuken.options[:concurrency])

        Shoryuken.options[:queues].to_a.each do |queue, weight|
          parse_queue(queue, weight, 'default')
        end
      end

      Shoryuken.options[:groups].to_a.each do |group, options|
        Shoryuken.add_group(group, options[:concurrency], delay: options[:delay], polling_strategy: options[:polling_strategy])

        options[:queues].to_a.each do |queue, weight|
          parse_queue(queue, weight, group)
        end
      end
    end

    # Requires worker files from the configured path
    #
    # @return [void]
    def require_workers
      required = Shoryuken.options[:require]

      return unless required

      if File.directory?(required)
        Dir[File.join(required, '**', '*.rb')].each(&method(:require))
      else
        require required
      end
    end

    # Validates that all configured queues exist in SQS
    #
    # @return [void]
    # @raise [ArgumentError] if any queues do not exist
    def validate_queues
      return Shoryuken.logger.warn { 'No queues supplied' } if Shoryuken.ungrouped_queues.empty?

      non_existent_queues = []

      Shoryuken.ungrouped_queues.uniq.each do |queue|
        Shoryuken::Client.queues(queue)
      rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue
        non_existent_queues << queue
      end

      return if non_existent_queues.none?

      # NOTE: HEREDOC's ~ operator removes indents, but is only available Ruby 2.3+
      # See github PR: https://github.com/ruby-shoryuken/shoryuken/pull/691#issuecomment-1007653595
      error_msg = <<-MSG.gsub(/^\s+/, '')
        The specified queue(s) #{non_existent_queues.join(', ')} do not exist.
        Try 'shoryuken sqs create QUEUE-NAME' for creating a queue with default settings.
        It's also possible that you don't have permission to access the specified queues.
      MSG

      raise Errors::QueueNotFoundError, error_msg
    end

    # Validates that all queues have registered workers
    #
    # @return [void]
    def validate_workers
      return if Shoryuken.active_job?

      all_queues = Shoryuken.ungrouped_queues
      queues_with_workers = Shoryuken.worker_registry.queues

      (all_queues - queues_with_workers).each do |queue|
        Shoryuken.logger.warn { "No worker supplied for #{queue}" }
      end
    end
  end
end

Shoryuken.active_job?
::Rails::VERSION::MAJOR < 4
::Rails::VERSION::MAJOR == 4
!(Shoryuken.active_job?)
!(::Rails::VERSION::MAJOR < 4)
!(::Rails::VERSION::MAJOR == 4)


def prefix_active_job_queue_names
      return unless Shoryuken.active_job?
      return unless Shoryuken.active_job_queue_name_prefixing?

      Shoryuken.options[:queues].to_a.map! do |queue_name, weight|
        prefix_active_job_queue_name(queue_name, weight)
      end

      Shoryuken.options[:groups].to_a.map! do |group, options|
        if options[:queues]
          options[:queues].map! do |queue_name, weight|
            prefix_active_job_queue_name(queue_name, weight)
          end
        end

        [group, options]
      end
    end

    def prefix_active_job_queue_name(queue_name, weight)
      return [queue_name, weight] if queue_name.start_with?('https://', 'arn:')

      queue_name_prefix = ::ActiveJob::Base.queue_name_prefix
      queue_name_delimiter = ::ActiveJob::Base.queue_name_delimiter

      # See https://github.com/rails/rails/blob/master/activejob/lib/active_job/queue_name.rb#L27
      name_parts = [queue_name_prefix.presence, queue_name]
      prefixed_queue_name = name_parts.compact.join(queue_name_delimiter)
      [prefixed_queue_name, weight]
    end


def parse_queues
      if Shoryuken.options[:queues].to_a.any?
        Shoryuken.add_group('default', Shoryuken.options[:concurrency])

        Shoryuken.options[:queues].to_a.each do |queue, weight|
          parse_queue(queue, weight, 'default')
        end
      end

      Shoryuken.options[:groups].to_a.each do |group, options|
        Shoryuken.add_group(group, options[:concurrency], delay: options[:delay], polling_strategy: options[:polling_strategy])

        options[:queues].to_a.each do |queue, weight|
          parse_queue(queue, weight, group)
        end
      end
    end

    def parse_queue(queue, weight, group)
      Shoryuken.add_queue(queue, [weight.to_i, 1].max, group)
    end


def validate_queues
      return Shoryuken.logger.warn { 'No queues supplied' } if Shoryuken.ungrouped_queues.empty?

      non_existent_queues = []

      Shoryuken.ungrouped_queues.uniq.each do |queue|
        Shoryuken::Client.queues(queue)
      rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue
        non_existent_queues << queue
      end

      return if non_existent_queues.none?

      # NOTE: HEREDOC's ~ operator removes indents, but is only available Ruby 2.3+
      # See github PR: https://github.com/ruby-shoryuken/shoryuken/pull/691#issuecomment-1007653595
      error_msg = <<-MSG.gsub(/^\s+/, '')
        The specified queue(s) #{non_existent_queues.join(', ')} do not exist.
        Try 'shoryuken sqs create QUEUE-NAME' for creating a queue with default settings.
        It's also possible that you don't have permission to access the specified queues.
      MSG

      raise Errors::QueueNotFoundError, error_msg
    end

queues

      def queues(name)
        @@queues[name.to_s] ||= Shoryuken::Queue.new(sqs, name)
      end


# frozen_string_literal: true

module Shoryuken
  # Default implementation of the worker registry.
  # Stores and retrieves worker classes mapped to queue names.
  class DefaultWorkerRegistry < WorkerRegistry
    # Initializes a new DefaultWorkerRegistry with an empty workers hash
    def initialize
      @workers = Shoryuken::Helpers::AtomicHash.new
    end

    # Checks if a queue is configured for batch message receiving
    #
    # @param queue [String] the queue name
    # @return [Boolean] true if the queue's worker has batch mode enabled
    def batch_receive_messages?(queue)
      !!(@workers[queue] && @workers[queue].get_shoryuken_options['batch'])
    end

    # Clears all registered workers
    #
    # @return [void]
    def clear
      @workers.clear
    end

    # Fetches a worker instance for processing a message
    #
    # @param queue [String] the queue name
    # @param message [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
    # @return [Object, nil] a new worker instance or nil if not found
    def fetch_worker(queue, message)
      worker_class = !message.is_a?(Array) &&
                     message.message_attributes &&
                     message.message_attributes['shoryuken_class'] &&
                     message.message_attributes['shoryuken_class'][:string_value]

      worker_class = begin
        Shoryuken::Helpers::StringUtils.constantize(worker_class)
      rescue
        @workers[queue]
      end

      worker_class.new if worker_class
    end

    # Returns all registered queue names
    #
    # @return [Array<String>] the queue names with registered workers
    def queues
      @workers.keys
    end

    # Registers a worker class for a queue
    #
    # @param queue [String] the queue name
    # @param clazz [Class] the worker class to register
    # @return [Class] the registered worker class
    # @raise [Errors::InvalidWorkerRegistrationError] if a batchable worker is already registered for the queue
    def register_worker(queue, clazz)
      if (worker_class = @workers[queue]) && (worker_class.get_shoryuken_options['batch'] == true || clazz.get_shoryuken_options['batch'] == true)
        raise Errors::InvalidWorkerRegistrationError, "Could not register #{clazz} for #{queue}, "\
          "because #{worker_class} is already registered for this queue, "\
          "and Shoryuken doesn't support a batchable worker for a queue with multiple workers"
      end

      @workers[queue] = clazz
    end

    # Returns all worker classes for a queue
    #
    # @param queue [String] the queue name
    # @return [Array<Class>] the registered worker classes
    def workers(queue)
      [@workers.fetch(queue, [])].flatten
    end
  end
end

constantize

        def constantize(string)
          names = string.split('::')
          names.shift if names.empty? || names.first.empty?

          constant = Object

          names.each do |name|
            constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
          end

          constant
        end

class DefaultWorkerRegistry < < WorkerRegistry


# frozen_string_literal: true

module Shoryuken
  # Manages message dispatching and processing for a single processing group.
  # Coordinates between the fetcher, polling strategy, and processor.
  class Manager
    include Util

    # Maximum number of messages to fetch in a single batch request
    BATCH_LIMIT = 10

    # Minimum interval between dispatch cycles
    # @see https://github.com/ruby-shoryuken/shoryuken/issues/348#issuecomment-292847028
    MIN_DISPATCH_INTERVAL = 0.1

    # @return [String] the processing group name
    attr_reader :group

    # Initializes a new Manager for a processing group
    #
    # @param group [String] the processing group name
    # @param fetcher [Shoryuken::Fetcher] the message fetcher
    # @param polling_strategy [Shoryuken::Polling::BaseStrategy] the polling strategy
    # @param concurrency [Integer] the maximum number of concurrent processors
    # @param executor [Concurrent::ExecutorService] the executor for async operations
    def initialize(group, fetcher, polling_strategy, concurrency, executor)
      @group                      = group
      @fetcher                    = fetcher
      @polling_strategy           = polling_strategy
      @max_processors             = concurrency
      @busy_processors            = Shoryuken::Helpers::AtomicCounter.new(0)
      @executor                   = executor
      @running                    = Shoryuken::Helpers::AtomicBoolean.new(true)
      @stop_new_dispatching       = Shoryuken::Helpers::AtomicBoolean.new(false)
      @dispatching_release_signal = ::Queue.new
    end

    # Starts the dispatch loop
    #
    # @return [void]
    def start
      fire_utilization_update_event
      dispatch_loop
    end

    # Signals the manager to stop dispatching new messages
    #
    # @return [void]
    def stop_new_dispatching
      @stop_new_dispatching.make_true
    end

    # Waits for any in-progress dispatching to complete
    #
    # @return [void]
    def await_dispatching_in_progress
      # There might still be a dispatching on-going, as the response from SQS could take some time
      # We don't want to stop the process before processing incoming messages, as they would stay "in-flight" for some time on SQS
      # We use a queue, as the dispatch_loop is running on another thread, and this is a efficient way of communicating between threads.
      @dispatching_release_signal.pop
    end

    # Checks if the manager is still running
    #
    # @return [Boolean] true if the manager is running
    def running?
      @running.true? && @executor.running?
    end

    private

    # The main dispatch loop
    #
    # @return [void]
    def dispatch_loop
      if @stop_new_dispatching.true? || !running?
        @dispatching_release_signal << 1
        return
      end

      @executor.post { dispatch }
    end

    # Dispatches messages from a queue
    #
    # @return [void]
    def dispatch
      return unless running?

      if ready <= 0 || (queue = @polling_strategy.next_queue).nil?
        return sleep(MIN_DISPATCH_INTERVAL)
      end

      fire_event(:dispatch, false, queue_name: queue.name)

      logger.debug { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }

      batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
    rescue => e
      handle_dispatch_error(e)
    ensure
      dispatch_loop
    end

    # Returns the count of busy processors
    #
    # @return [Integer] the number of busy processors
    def busy
      @busy_processors.value
    end

    # Returns the count of ready processors
    #
    # @return [Integer] the number of available processors
    def ready
      @max_processors - busy
    end

    # Handles completion of processor work
    #
    # @param queue [String] the queue name
    # @return [void]
    def processor_done(queue)
      @busy_processors.decrement
      fire_utilization_update_event

      client_queue = Shoryuken::Client.queues(queue)
      return unless client_queue.fifo?
      return unless @polling_strategy.respond_to?(:message_processed)

      @polling_strategy.message_processed(queue)
    end

    # Assigns a message to a processor
    #
    # @param queue_name [String] the queue name
    # @param sqs_msg [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
    # @return [Concurrent::Promise, nil] the processing promise or nil if not running
    def assign(queue_name, sqs_msg)
      return unless running?

      logger.debug { "Assigning #{sqs_msg.message_id}" }

      @busy_processors.increment
      fire_utilization_update_event

      Concurrent::Promise
        .execute(executor: @executor) do
          original_priority = Thread.current.priority
          begin
            Thread.current.priority = Shoryuken.thread_priority
            Processor.process(queue_name, sqs_msg)
          ensure
            Thread.current.priority = original_priority
          end
        end
        .then { processor_done(queue_name) }
        .rescue { processor_done(queue_name) }
    end

    # Dispatches a batch of messages from a queue
    #
    # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
    # @return [void]
    def dispatch_batch(queue)
      batch = @fetcher.fetch(queue, BATCH_LIMIT)
      @polling_strategy.messages_found(queue.name, batch.size)
      assign(queue.name, patch_batch!(batch)) if batch.any?
    end

    # Dispatches individual messages from a queue
    #
    # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
    # @return [void]
    def dispatch_single_messages(queue)
      messages = @fetcher.fetch(queue, ready)
      @polling_strategy.messages_found(queue.name, messages.size)
      messages.each { |message| assign(queue.name, message) }
    end

    # Checks if a queue uses batch message processing
    #
    # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
    # @return [Boolean] true if the queue is configured for batch processing
    def batched_queue?(queue)
      Shoryuken.worker_registry.batch_receive_messages?(queue.name)
    end

    # Patches a batch array with a message_id method
    #
    # @param sqs_msgs [Array<Aws::SQS::Types::Message>] the batch of messages
    # @return [Array<Aws::SQS::Types::Message>] the patched batch
    def patch_batch!(sqs_msgs)
      sqs_msgs.instance_eval do
        def message_id
          "batch-with-#{size}-messages"
        end
      end

      sqs_msgs
    end

    # Handles errors during dispatch
    #
    # @param ex [Exception] the exception that occurred
    # @return [void]
    def handle_dispatch_error(ex)
      logger.error { "Manager failed: #{ex.message}" }
      logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?

      Process.kill('USR1', Process.pid)

      @running.make_false
    end

    # Fires a utilization update event
    #
    # @return [void]
    def fire_utilization_update_event
      fire_event :utilization_update, false, {
        group: @group,
        max_processors: @max_processors,
        busy_processors: busy
      }
    end
  end
end


def dispatch
      return unless running?

      if ready <= 0 || (queue = @polling_strategy.next_queue).nil?
        return sleep(MIN_DISPATCH_INTERVAL)
      end

      fire_event(:dispatch, false, queue_name: queue.name)

      logger.debug { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }

      batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
    rescue => e
      handle_dispatch_error(e)
    ensure
      dispatch_loop
    end

dispatch

!(ready <= 0 || (queue = @polling_strategy.next_queue).nil?)

      def next_queue
        fail NotImplementedError
      end

    def batched_queue?(queue)
      Shoryuken.worker_registry.batch_receive_messages?(queue.name)
    end

    def dispatch_batch(queue)
      batch = @fetcher.fetch(queue, BATCH_LIMIT)
      @polling_strategy.messages_found(queue.name, batch.size)
      assign(queue.name, patch_batch!(batch)) if batch.any?
    end

    def dispatch_single_messages(queue)
      messages = @fetcher.fetch(queue, ready)
      @polling_strategy.messages_found(queue.name, messages.size)
      messages.each { |message| assign(queue.name, message) }
    end

    def handle_dispatch_error(ex)
      logger.error { "Manager failed: #{ex.message}" }
      logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?

      Process.kill('USR1', Process.pid)

      @running.make_false
    end


def assign(queue_name, sqs_msg)
      return unless running?

      logger.debug { "Assigning #{sqs_msg.message_id}" }

      @busy_processors.increment
      fire_utilization_update_event

      Concurrent::Promise
        .execute(executor: @executor) do
          original_priority = Thread.current.priority
          begin
            Thread.current.priority = Shoryuken.thread_priority
            Processor.process(queue_name, sqs_msg)
          ensure
            Thread.current.priority = original_priority
          end
        end
        .then { processor_done(queue_name) }
        .rescue { processor_done(queue_name) }
    end

assign

process

    def self.process(queue, sqs_msg)
      new(queue, sqs_msg).process
    end

    def processor_done(queue)
      @busy_processors.decrement
      fire_utilization_update_event

      client_queue = Shoryuken::Client.queues(queue)
      return unless client_queue.fifo?
      return unless @polling_strategy.respond_to?(:message_processed)

      @polling_strategy.message_processed(queue)
    end


# frozen_string_literal: true

module Shoryuken
  # Stores and manages all Shoryuken configuration options.
  # This class is used internally to hold settings for workers, queues,
  # middleware, and other runtime configurations.
  class Options
    # Default configuration values for Shoryuken
    DEFAULTS = {
      thread_priority: -1,
      concurrency: 25,
      queues: [],
      aws: {},
      delay: 0.0,
      timeout: 8,
      lifecycle_events: {
        startup: [],
        dispatch: [],
        utilization_update: [],
        quiet: [],
        shutdown: [],
        stopped: []
      }
    }.freeze

    # @return [Boolean] whether to enable ActiveJob queue name prefixing
    # @return [Boolean] whether to cache SQS visibility timeout
    # @return [Hash{String => Hash}] the configured processing groups
    # @return [Object] the executor used to launch workers
    # @return [Proc] the code reloader proc for development environments
    # @return [Boolean] whether code reloading is enabled
    # @return [Proc, nil] callback to execute when server starts
    # @return [Proc, nil] callback to execute when server stops
    # @return [Class] the executor class for running workers
    # @return [Shoryuken::WorkerRegistry] the registry for worker classes
    # @return [Array<#call>] handlers for processing exceptions
    attr_accessor :active_job_queue_name_prefixing, :cache_visibility_timeout,
                  :groups, :launcher_executor, :reloader, :enable_reloading,
                  :start_callback, :stop_callback, :worker_executor, :worker_registry,
                  :exception_handlers

    # @return [Hash] the default options for workers
    # @return [Aws::SQS::Client] the SQS client instance
    # @return [Logger] the logger instance
    attr_writer :default_worker_options, :sqs_client, :logger

    # @return [Hash] options passed to SQS receive_message calls
    attr_reader :sqs_client_receive_message_opts

    # Initializes a new Options instance with default values
    def initialize
      self.groups = {}
      self.worker_registry = DefaultWorkerRegistry.new
      self.exception_handlers = [DefaultExceptionHandler]
      self.active_job_queue_name_prefixing = false
      self.worker_executor = Worker::DefaultExecutor
      self.cache_visibility_timeout = false
      self.reloader = proc { |&block| block.call }
      self.enable_reloading ||= false
      # this is needed for keeping backward compatibility
      @sqs_client_receive_message_opts ||= {}
    end

    # Checks if ActiveJob is available
    #
    # @return [Boolean] true if ActiveJob is defined
    def active_job?
      defined?(::ActiveJob)
    end

    # Adds a processing group with the specified concurrency and delay
    #
    # @param group [String] the name of the group
    # @param concurrency [Integer, nil] the number of concurrent workers for the group
    # @param delay [Float, nil] the delay between polling cycles
    # @param polling_strategy [Class, String, nil] the polling strategy class for the group
    # @return [Hash] the group configuration
    # @raise [Errors::InvalidPollingStrategyError] if polling_strategy is not a Class, String, or nil
    def add_group(group, concurrency = nil, delay: nil, polling_strategy: nil)
      unless polling_strategy.nil? || polling_strategy.is_a?(Class) || polling_strategy.is_a?(String)
        raise Errors::InvalidPollingStrategyError, "#{polling_strategy} is not a valid polling_strategy"
      end

      concurrency ||= options[:concurrency]
      delay ||= options[:delay]

      groups[group] ||= {
        concurrency: concurrency,
        delay: delay,
        polling_strategy: polling_strategy,
        queues: []
      }
    end

    # Adds a queue to a processing group with the specified weight
    #
    # @param queue [String] the name of the queue
    # @param weight [Integer] the weight (priority) of the queue
    # @param group [String] the name of the group to add the queue to
    # @return [void]
    def add_queue(queue, weight, group)
      weight.times do
        groups[group][:queues] << queue
      end
    end

    # Returns all queues from all groups
    #
    # @return [Array<String>] flat array of all queue names
    def ungrouped_queues
      groups.values.flat_map { |options| options[:queues] }
    end

    # Returns the polling strategy class for a group
    #
    # @param group [String] the name of the group
    # @return [Class] the polling strategy class to use
    def polling_strategy(group)
      strategy = groups[group].to_h[:polling_strategy] ||
                 (group == 'default' ? options : options[:groups].to_h[group]).to_h[:polling_strategy]
      case strategy
      when 'WeightedRoundRobin', nil # Default case
        Polling::WeightedRoundRobin
      when 'StrictPriority'
        Polling::StrictPriority
      when String
        begin
          Object.const_get(strategy)
        rescue NameError
          raise Errors::InvalidPollingStrategyError, "#{strategy} is not a valid polling_strategy"
        end
      when Class
        strategy
      end
    end

    # Returns the polling delay for a group
    #
    # @param group [String] the name of the group
    # @return [Float] the delay in seconds
    def delay(group)
      groups[group].to_h.fetch(:delay, options[:delay]).to_f
    end

    # Returns the SQS client, initializing a default one if needed.
    # Uses AWS configuration from options[:aws] if available.
    #
    # @return [Aws::SQS::Client] the SQS client
    def sqs_client
      @sqs_client ||= Aws::SQS::Client.new(options[:aws])
    end

    # Sets the SQS client receive message options for the default group
    #
    # @param sqs_client_receive_message_opts [Hash] the options hash
    # @return [Hash] the options hash
    def sqs_client_receive_message_opts=(sqs_client_receive_message_opts)
      @sqs_client_receive_message_opts['default'] = sqs_client_receive_message_opts
    end

    # Returns the global options hash
    #
    # @return [Hash] the options hash
    def options
      @options ||= DEFAULTS.dup
    end

    # Returns the logger instance
    #
    # @return [Logger] the logger
    def logger
      @logger ||= Shoryuken::Logging.logger
    end

    # Returns the thread priority setting
    #
    # @return [Integer] the thread priority
    def thread_priority
      @thread_priority ||= options[:thread_priority]
    end

    # Sets the thread priority
    #
    # @param value [Integer] the thread priority value
    # @return [Integer] the thread priority
    attr_writer :thread_priority

    # Registers a worker class with the worker registry
    #
    # @param args [Array] arguments to pass to the registry
    # @return [void]
    def register_worker(*args)
      worker_registry.register_worker(*args)
    end

    # Yields self if running as a server for server-specific configuration
    #
    # @yield [Shoryuken::Options] the options instance
    # @return [void]
    def configure_server
      yield self if server?
    end

    # Returns the server middleware chain
    #
    # @yield [Shoryuken::Middleware::Chain] the middleware chain for configuration
    # @return [Shoryuken::Middleware::Chain] the server middleware chain
    def server_middleware
      @_server_chain ||= default_server_middleware
      yield @_server_chain if block_given?
      @_server_chain
    end

    # Yields self unless running as a server for client-specific configuration
    #
    # @yield [Shoryuken::Options] the options instance
    # @return [void]
    def configure_client
      yield self unless server?
    end

    # Returns the client middleware chain
    #
    # @yield [Shoryuken::Middleware::Chain] the middleware chain for configuration
    # @return [Shoryuken::Middleware::Chain] the client middleware chain
    def client_middleware
      @_client_chain ||= default_client_middleware
      yield @_client_chain if block_given?
      @_client_chain
    end

    # Returns the default worker options hash
    #
    # @return [Hash{String => Object}] the default worker options
    def default_worker_options
      @default_worker_options ||= {
        'queue' => 'default',
        'delete' => false,
        'auto_delete' => false,
        'auto_visibility_timeout' => false,
        'retry_intervals' => nil,
        'batch' => false
      }
    end

    # Registers a callback to run when the server starts
    #
    # @param block [Proc] the block to execute on start
    # @return [void]
    # @yield the block to execute on start
    def on_start(&block)
      self.start_callback = block
    end

    # Registers a callback to run when the server stops
    #
    # @param block [Proc] the block to execute on stop
    # @return [void]
    # @yield the block to execute on stop
    def on_stop(&block)
      self.stop_callback = block
    end

    # Registers a block to run at a point in the Shoryuken lifecycle.
    #
    # @param event [Symbol] the lifecycle event (:startup, :quiet, :shutdown, or :stopped)
    # @param block [Proc] the block to execute for the event
    # @return [void]
    # @raise [ArgumentError] if event is not a Symbol or not a valid event name
    # @yield the block to execute for the event
    # @example
    #   Shoryuken.configure_server do |config|
    #     config.on(:shutdown) do
    #       puts "Goodbye cruel world!"
    #     end
    #   end
    def on(event, &block)
      raise Errors::InvalidEventError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
      raise Errors::InvalidEventError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)

      options[:lifecycle_events][event] << block
    end

    # Checks if running as a server (CLI mode)
    #
    # @return [Boolean] true if Shoryuken::CLI is defined
    def server?
      defined?(Shoryuken::CLI)
    end

    # Checks if visibility timeout caching is enabled
    #
    # @return [Boolean] true if caching is enabled
    def cache_visibility_timeout?
      @cache_visibility_timeout
    end

    # Checks if ActiveJob queue name prefixing is enabled
    #
    # @return [Boolean] true if prefixing is enabled
    def active_job_queue_name_prefixing?
      @active_job_queue_name_prefixing
    end

    private

    # Creates the default server middleware chain
    #
    # @return [Shoryuken::Middleware::Chain] the default middleware chain
    def default_server_middleware
      Middleware::Chain.new do |m|
        m.add Middleware::Server::Timing
        m.add Middleware::Server::NonRetryableException
        m.add Middleware::Server::ExponentialBackoffRetry
        m.add Middleware::Server::AutoDelete
        m.add Middleware::Server::AutoExtendVisibility
        if defined?(::ActiveRecord::Base)
          require 'shoryuken/middleware/server/active_record'
          m.add Middleware::Server::ActiveRecord
        end
      end
    end

    # Creates the default client middleware chain
    #
    # @return [Shoryuken::Middleware::Chain] an empty middleware chain
    def default_client_middleware
      Middleware::Chain.new
    end
  end
end


def add_group(group, concurrency = nil, delay: nil, polling_strategy: nil)
      unless polling_strategy.nil? || polling_strategy.is_a?(Class) || polling_strategy.is_a?(String)
        raise Errors::InvalidPollingStrategyError, "#{polling_strategy} is not a valid polling_strategy"
      end

      concurrency ||= options[:concurrency]
      delay ||= options[:delay]

      groups[group] ||= {
        concurrency: concurrency,
        delay: delay,
        polling_strategy: polling_strategy,
        queues: []
      }
    end

!(polling_strategy.nil? || polling_strategy.is_a?(Class) || polling_strategy.is_a?(String))


def polling_strategy(group)
      strategy = groups[group].to_h[:polling_strategy] ||
                 (group == 'default' ? options : options[:groups].to_h[group]).to_h[:polling_strategy]
      case strategy
      when 'WeightedRoundRobin', nil # Default case
        Polling::WeightedRoundRobin
      when 'StrictPriority'
        Polling::StrictPriority
      when String
        begin
          Object.const_get(strategy)
        rescue NameError
          raise Errors::InvalidPollingStrategyError, "#{strategy} is not a valid polling_strategy"
        end
      when Class
        strategy
      end
    end


def default_worker_options
      @default_worker_options ||= {
        'queue' => 'default',
        'delete' => false,
        'auto_delete' => false,
        'auto_visibility_timeout' => false,
        'retry_intervals' => nil,
        'batch' => false
      }
    end


def default_server_middleware
      Middleware::Chain.new do |m|
        m.add Middleware::Server::Timing
        m.add Middleware::Server::NonRetryableException
        m.add Middleware::Server::ExponentialBackoffRetry
        m.add Middleware::Server::AutoDelete
        m.add Middleware::Server::AutoExtendVisibility
        if defined?(::ActiveRecord::Base)
          require 'shoryuken/middleware/server/active_record'
          m.add Middleware::Server::ActiveRecord
        end
      end
    end


# frozen_string_literal: true

$stdout.sync = true

require 'singleton'
require 'optparse'
require 'erb'

require 'shoryuken'

module Shoryuken
  # Runs the Shoryuken server process.
  # Handles signal trapping, daemonization, and lifecycle management.
  class Runner
    include Util
    include Singleton

    # @return [Shoryuken::Launcher, nil] the launcher instance, or nil if not yet initialized
    attr_reader :launcher

    # Runs the Shoryuken server with the given options
    #
    # @param options [Hash] runtime configuration options
    # @option options [Boolean] :daemon whether to daemonize the process
    # @option options [String] :pidfile path to the PID file
    # @option options [String] :logfile path to the log file
    # @option options [String] :config_file path to the configuration file
    # @return [void]
    def run(options)
      self_read, self_write = IO.pipe

      %w[INT TERM USR1 TSTP TTIN].each do |sig|
        trap sig do
          self_write.puts(sig)
        end
      rescue ArgumentError
        puts "Signal #{sig} not supported"
      end

      loader = EnvironmentLoader.setup_options(options)

      daemonize(Shoryuken.options)
      write_pid(Shoryuken.options)

      loader.load

      initialize_concurrent_logger

      @launcher = Shoryuken::Launcher.new

      begin
        @launcher.start

        while (readable_io = IO.select([self_read]))
          signal = readable_io.first[0].gets.strip
          handle_signal(signal)
        end
      rescue Interrupt
        @launcher.stop!
        exit 0
      end
    end

    # Checks if the server is healthy
    #
    # @return [Boolean] true if the launcher is running and healthy
    def healthy?
      (@launcher && @launcher.healthy?) || false
    end

    private

    # Initializes the Concurrent Ruby logger
    #
    # @return [void]
    def initialize_concurrent_logger
      return unless Shoryuken.logger

      Concurrent.global_logger = lambda do |level, progname, msg = nil, &block|
        Shoryuken.logger.log(level, msg, progname, &block)
      end
    end

    # Daemonizes the process
    #
    # @param options [Hash] options containing daemon and logfile settings
    # @option options [Boolean] :daemon whether to daemonize
    # @option options [String] :logfile path to the log file for daemon output
    # @return [void]
    def daemonize(options)
      return unless options[:daemon]

      files_to_reopen = []
      ObjectSpace.each_object(File) do |file|
        files_to_reopen << file unless file.closed?
      end

      Process.daemon(true, true)

      files_to_reopen.each do |file|
        file.reopen file.path, 'a+'
        file.sync = true
      rescue ::Exception
      end

      [$stdout, $stderr].each do |io|
        File.open(options[:logfile], 'ab') do |f|
          io.reopen(f)
        end
        io.sync = true
      end
      $stdin.reopen('/dev/null')
    end

    # Writes the process ID to a file
    #
    # @param options [Hash] options containing the pidfile path
    # @option options [String] :pidfile path to write the PID file
    # @return [void]
    def write_pid(options)
      return unless (path = options[:pidfile])

      File.open(path, 'w') { |f| f.puts(Process.pid) }
    end

    # Executes a soft shutdown on USR1 signal
    #
    # @return [void]
    def execute_soft_shutdown
      logger.info { 'Received USR1, will soft shutdown' }

      @launcher.stop
      exit 0
    end

    # Executes a terminal stop on TSTP signal
    #
    # @return [void]
    def execute_terminal_stop
      logger.info { 'Received TSTP, will stop accepting new work' }

      @launcher.stop
    end

    # Prints backtraces of all threads
    #
    # @return [void]
    def print_threads_backtrace
      Thread.list.each do |thread|
        logger.info { "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}" }
        if thread.backtrace
          logger.info { thread.backtrace.join("\n") }
        else
          logger.info { '<no backtrace available>' }
        end
      end
    end

    # Handles incoming signals
    #
    # @param sig [String] the signal name
    # @return [void]
    # @raise [Interrupt] on TERM or INT signals
    def handle_signal(sig)
      logger.debug "Got #{sig} signal"

      case sig
      when 'USR1' then execute_soft_shutdown
      when 'TTIN' then print_threads_backtrace
      when 'TSTP' then execute_terminal_stop
      when 'TERM', 'INT'
        logger.info { "Received #{sig}, will shutdown" }

        raise Interrupt
      end
    end
  end
end


def print_threads_backtrace
      Thread.list.each do |thread|
        logger.info { "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}" }
        if thread.backtrace
          logger.info { thread.backtrace.join("\n") }
        else
          logger.info { '<no backtrace available>' }
        end
      end
    end


def handle_signal(sig)
      logger.debug "Got #{sig} signal"

      case sig
      when 'USR1' then execute_soft_shutdown
      when 'TTIN' then print_threads_backtrace
      when 'TSTP' then execute_terminal_stop
      when 'TERM', 'INT'
        logger.info { "Received #{sig}, will shutdown" }

        raise Interrupt
      end
    end


# frozen_string_literal: true

module Shoryuken
  module Polling
    # Configuration object representing a queue and its associated options.
    #
    # This class encapsulates a queue name along with any polling-specific
    # options or metadata. It provides a structured way to pass queue
    # information between polling strategies and the message fetching system.
    #
    # The class extends Struct to provide attribute accessors for name and options
    # while adding custom behavior for equality comparison and string representation.
    #
    # @example Creating a basic queue configuration
    #   config = QueueConfiguration.new('my_queue', {})
    #   config.name     # => 'my_queue'
    #   config.options  # => {}
    #
    # @example Creating a queue configuration with options
    #   config = QueueConfiguration.new('priority_queue', { priority: :high })
    #   config.name     # => 'priority_queue'
    #   config.options  # => { priority: :high }
    #
    # @example Comparing configurations
    #   config1 = QueueConfiguration.new('queue', {})
    #   config2 = QueueConfiguration.new('queue', {})
    #   config1 == config2  # => true
    #   config1 == 'queue'  # => true (when options are empty)
    #
    # @attr_reader [String] name The name of the queue
    # @attr_reader [Hash] options Additional options or metadata for the queue
    QueueConfiguration = Struct.new(:name, :options) do
      # Generates a hash value based on the queue name.
      #
      # This method ensures that QueueConfiguration objects can be used
      # as hash keys and that configurations with the same queue name
      # will have the same hash value regardless of their options.
      #
      # @return [Integer] Hash value based on the queue name
      def hash
        name.hash
      end

      # Compares this configuration with another object for equality.
      #
      # Two QueueConfiguration objects are equal if they have the same name
      # and options. For convenience, a configuration with empty options can
      # also be compared directly with a string queue name.
      #
      # @param other [Object] The object to compare with
      # @return [Boolean] true if the objects are considered equal
      #
      # @example Comparing with another QueueConfiguration
      #   config1 = QueueConfiguration.new('queue', {})
      #   config2 = QueueConfiguration.new('queue', {})
      #   config1 == config2  # => true
      #
      # @example Comparing with a string (only when options are empty)
      #   config = QueueConfiguration.new('queue', {})
      #   config == 'queue'  # => true
      #
      #   config_with_options = QueueConfiguration.new('queue', { weight: 5 })
      #   config_with_options == 'queue'  # => false
      def ==(other)
        case other
        when String
          if options.empty?
            name == other
          else
            false
          end
        else
          super
        end
      end

      alias_method :eql?, :==

      # Returns a string representation of the queue configuration.
      #
      # For configurations with empty options, returns just the queue name.
      # For configurations with options, returns a detailed representation
      # showing both the name and the options hash.
      #
      # @return [String] String representation of the configuration
      #
      # @example Simple queue without options
      #   config = QueueConfiguration.new('simple_queue', {})
      #   config.to_s  # => 'simple_queue'
      #
      # @example Queue with options
      #   config = QueueConfiguration.new('complex_queue', { priority: :high })
      #   config.to_s  # => '#<QueueConfiguration complex_queue options={:priority=>:high}>'
      def to_s
        if options&.empty?
          name
        else
          "#<QueueConfiguration #{name} options=#{options.inspect}>"
        end
      end
    end
  end
end


# frozen_string_literal: true

module Shoryuken
  # Polling strategies for determining which queue to fetch messages from next
  module Polling
    # Abstract base class for queue polling strategies.
    #
    # This class defines the interface that all polling strategies must implement
    # to manage queue selection and message flow control in Shoryuken workers.
    # Polling strategies determine which queue to fetch messages from next and
    # how to handle scenarios where queues have no messages available.
    #
    # @abstract Subclass and override {#next_queue}, {#messages_found}, and {#active_queues}
    #   to implement a custom polling strategy.
    #
    # @example Implementing a custom polling strategy
    #   class CustomStrategy < BaseStrategy
    #     def initialize(queues)
    #       @queues = queues
    #     end
    #
    #     def next_queue
    #       # Return next queue to poll
    #       @queues.sample
    #     end
    #
    #     def messages_found(queue, count)
    #       # Handle result of polling
    #       logger.info "Found #{count} messages in #{queue}"
    #     end
    #
    #     def active_queues
    #       # Return list of active queues
    #       @queues
    #     end
    #   end
    #
    # @see WeightedRoundRobin
    # @see StrictPriority
    class BaseStrategy
      include Util

      # Returns the next queue to poll for messages.
      #
      # This method should return a QueueConfiguration object representing
      # the next queue that should be polled for messages, or nil if no
      # queues are currently available for polling.
      #
      # @abstract Subclasses must implement this method
      # @return [QueueConfiguration, nil] Next queue to poll, or nil if none available
      # @raise [NotImplementedError] if not implemented by subclass
      def next_queue
        fail NotImplementedError
      end

      # Called when messages are found (or not found) in a queue.
      #
      # This method is invoked after polling a queue to inform the strategy
      # about the number of messages that were retrieved. Strategies can use
      # this information to make decisions about future polling behavior,
      # such as pausing empty queues or adjusting queue weights.
      #
      # @abstract Subclasses must implement this method
      # @param _queue [String] The name of the queue that was polled
      # @param _messages_found [Integer] The number of messages retrieved
      # @raise [NotImplementedError] if not implemented by subclass
      def messages_found(_queue, _messages_found)
        fail NotImplementedError
      end

      # Called when a message from a queue has been processed.
      #
      # This optional callback is invoked after a message has been successfully
      # processed by a worker. Strategies can use this information for cleanup
      # or to adjust their polling behavior.
      #
      # @param _queue [String] The name of the queue whose message was processed
      # @return [void]
      def message_processed(_queue); end

      # Returns the list of currently active queues.
      #
      # This method should return an array representing the queues that are
      # currently active and available for polling. The format may vary by
      # strategy implementation.
      #
      # @abstract Subclasses must implement this method
      # @return [Array] List of active queues
      # @raise [NotImplementedError] if not implemented by subclass
      def active_queues
        fail NotImplementedError
      end

      # Compares this strategy with another object for equality.
      #
      # Two strategies are considered equal if they have the same active queues.
      # This method also supports comparison with Array objects for backward
      # compatibility.
      #
      # @param other [Object] Object to compare with
      # @return [Boolean] true if strategies are equivalent
      def ==(other)
        case other
        when Array
          @queues == other
        else
          if other.respond_to?(:active_queues)
            active_queues == other.active_queues
          else
            false
          end
        end
      end

      # Returns the delay time for pausing empty queues.
      #
      # This method returns the amount of time (in seconds) that empty queues
      # should be paused before being polled again. The delay can be set at
      # the strategy level or falls back to the global Shoryuken delay setting.
      #
      # @return [Float] Delay time in seconds
      def delay
        @delay || Shoryuken.options[:delay].to_f
      end
    end
  end
end