<commit_before>require_relative 'apn_connection'
require 'houston'

module NotifyUser
  class Houston < Apns

    APN_POOL = ConnectionPool.new(
      size: NotifyUser.connection_pool_size,
      timeout: NotifyUser.connection_pool_timeout) do
      APNConnection.new
    end

    NO_ERROR = -42

    def push
      space_allowance = PAYLOAD_LIMIT - used_space

      push_options = {
        alert: @notification.mobile_message(space_allowance),
        badge: @notification.count_for_target,
        category: @notification.params[:category] || @notification.type,
        custom_data: @notification.params
      }

      Houston.notify(push_options, @notification.target.devices)
    end

    class << self

      def notify(push_options = {}, devices)
        APN_POOL.with do |connection|
          connection = APNConnection.new

          ssl = connection.ssl
          error_index = NO_ERROR

          devices.each_with_index do |device, index|
            notification = ::Houston::Notification.new(push_options.dup.merge({ token: device.token, id: index }))

            connection.write(notification.message)
          end

          read_socket, write_socket = IO.select([ssl], [], [ssl], 1)
          if (read_socket && read_socket[0])
            if error = connection.connection.read(6)
              command, status, error_index = error.unpack("ccN")

              if error_index != NO_ERROR
                device = devices[error_index]

                if device.failures + 1 > NotifyUser.failure_tolerance
                  # Delete the device token if it has failed consecutively too many times:
                  device.delete
                else
                  # Up the failure rate for the device that failed to send:
                  device.update_attributes(failures: device.failures + 1)
                end
              end
            end
          end

          # Set the failure rate for all successfully sent notifications back to 0:
          Device.where(id: devices[0...error_index].map(&:id)).update_all(failures: 0 )

          # Resend all notifications after the once that produced the error:
          notify(push_options, devices[(error_index+1)..-1]) if error_index != NO_ERROR
        end
      end

    end

  end
end<commit_msg>Restructured a lot of how the Houston notifications are working right now, the now retry sending the broken notification a number of times until it either succeeds or fails completely and is removed from the database. Biggest change though is that the connections are only re-spawned if an error is encountered (we close the connection manually), instead of a connection being opened every run through.<commit_after>require_relative 'apn_connection'
require 'houston'

module NotifyUser
  class Houston < Apns

    NO_ERROR = -42
    APN_POOL = ConnectionPool.new(
      size: NotifyUser.connection_pool_size,
      timeout: NotifyUser.connection_pool_timeout) do
      APNConnection.new
    end

    def initialize(notification, options)
      super(notification, options)

      @devices = @notification.target.devices.order(:id).reverse
    end

    def push
      space_allowance = PAYLOAD_LIMIT - used_space

      @push_options = {
        alert: @notification.mobile_message(space_allowance),
        badge: @notification.count_for_target,
        category: @notification.params[:category] || @notification.type,
        custom_data: @notification.params
      }

      send_notifications
    end

    def send_notifications
      APN_POOL.with do |connection|
        if !connection.connection.open?
          connection = APNConnection.new
        end

        ssl = connection.ssl
        error_index = NO_ERROR

        @devices.each_with_index do |device, index|
          notification = ::Houston::Notification.new(@push_options.dup.merge({ token: device.token, id: index }))

          connection.write(notification.message)
        end

        read_socket, write_socket = IO.select([ssl], [], [ssl], 1)
        if (read_socket && read_socket[0])
          if error = connection.connection.read(6)
            command, status, error_index = error.unpack("ccN")

            if error_index != NO_ERROR
              device = @devices[error_index]
              range = 0...error_index

              if device.failures + 1 >= NotifyUser.failure_tolerance
                # Delete the device token if it has failed consecutively too many times:
                device.delete
                range = 0..error_index
              else
                # Up the failure rate for the device that failed to send:
                device.update_attributes(failures: device.failures + 1)
              end

              # Close the connection:
              connection.connection.close

              # Set the failure rate for all successfully sent notifications back to 0:
              Device.where(id: @devices.slice!(range).map(&:id)).update_all(failures: 0 )
            end
          end
        end

        # Resend all notifications after the once that produced the error:
        send_notifications if error_index != NO_ERROR
      end
    end
  end
end
{'fraction_non_alphanumeric': 0.07553398058252427, 'fraction_numerical': 0.004466019417475728, 'mean_word_length': 2.26425855513308, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1535689', 'n_tokens_mistral': 1384, 'n_tokens_neox': 1310, 'n_words': 386}