Как правильно использовать Sidekiq для обработки фоновых задач в Rails

Итак, я создал приложение rails, используя https://github.com/Shopify/shopify_app - и по большей части приложение работает так, как задумано - его цель - получить количество продукта из внешнего API управления запасами, а затем обновите вариантные количества в Shopify, указав последние количества из этой системы управления запасами.

Моя проблема в том, что начальный POST На запрос внешнего API отвечает большое количество продуктов - иногда это занимает более 15 секунд. В дополнение к этому другая часть моего приложения получает этот ответ, и для каждого продукта в ответе, который также существует в Shopify, он создает PUT просьба к Shopify обновить количество вариантов. Как и в случае первоначального запроса, это также занимает более 10-15 секунд.

Моя проблема в том, что я размещаю приложение на Heroku, и в результате я достиг 30-секундного лимита времени ожидания запроса. В результате мне нужно использовать фоновый рабочий, чтобы сместить по крайней мере один из указанных выше запросов (возможно, оба) в рабочую очередь. Я пошел с широко рекомендуемым гемом Sidekiq - https://github.com/mperham/sidekiq - который достаточно легко настроить.

Моя проблема в том, что я не знаю, как получить результаты от законченного рабочего задания Sidekiq, а затем снова использовать его в контроллере - я также не знаю, является ли это лучшей практикой (я немного новичок в Rails/ Разработка приложений).

Я включил свой контроллер (до того, как разбить его на рабочих), который в настоящее время запускает приложение ниже - я думаю, мне просто нужен какой-то совет - правильно ли я делаю - должна ли часть этой логики быть внутри Модели, и если да, то как будет ли эта модель взаимодействовать с контроллером, и как тогда Sidekiq вписывается во все это?

Спасибо за любые советы или помощь, спасибо.

class StockManagementController < ShopifyApp::AuthenticatedController

require 'uri'
require 'net/http'
require 'json'
require 'nokogiri'
require 'open-uri'
require 'rexml/document'

def new
    @token = StockManagementController.new
end

def get_token

    url = URI('https://external.api.endpoint/api/v1/AuthToken')
    http = Net::HTTP.new(url.host, url.port)
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE

    @HEROKU_ENV_USERNAME = ENV['HEROKU_ENV_USERNAME']
    @HEROKU_ENV_PASSWORD = ENV['HEROKU_ENV_PASSWORD']

    request = Net::HTTP::Post.new(url)
    request['content-type'] = 'application/x-www-form-urlencoded'
    request['cache-control'] = 'no-cache'
    request.body = 'username=' + @HEROKU_ENV_USERNAME + '&password=' + @HEROKU_ENV_PASSWORD + '&grant_type=password'
    response = http.request(request)
    responseJSON = JSON.parse(response.read_body)
    session[:accessToken] = responseJSON['access_token']

    if session[:accessToken]
        flash[:notice] = 'StockManagement token generation was successful.'
        redirect_to '/StockManagement/product_quantity'
    else
        flash[:alert] = 'StockManagement token generation was unsuccessful.'
    end
end

def product_quantity

    REXML::Document.entity_expansion_text_limit = 1_000_000

    @theToken = session[:accessToken]

    if @theToken

        url = URI('https://external.api.endpoint/api/v1/ProductQuantity')
        http = Net::HTTP.new(url.host, url.port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE

        request = Net::HTTP::Post.new(url)
        request['authorization'] = 'bearer ' + @theToken + ''
        request['content-type'] = 'application/xml'
        request['cache-control'] = 'no-cache'

        response = http.request(request)
        responseBody = response.read_body
        finalResponse = Hash.from_xml(responseBody).to_json
        resultQuantity = JSON.parse finalResponse

        @connectionType = resultQuantity['AutomatorResponse']['Type']
        @successResponse = resultQuantity['AutomatorResponse']['Success']
        @errorResponse = resultQuantity['AutomatorResponse']['ErrorMsg']

        productQuantityResponse = resultQuantity['AutomatorResponse']['ResponseString']
        xmlResponse = Hash.from_xml(productQuantityResponse).to_json
        jsonResponse = JSON.parse xmlResponse

        @fullResponse = jsonResponse['StockManagement']['Company']['InventoryQuantitiesByLocation']['InventoryQuantity']

        # This hash is used to store the final list of items that we need in order to display the item's we've synced, and to show the number of items we've sycned successfully.
        @finalList = Hash.new

        # This array is used to contain the available products - this is used later on as a way of only rendering
        @availableProducts = Array.new

        # Here we get all of the variant data from Shopify.
        @variants = ShopifyAPI::Variant.find(:all, params: {})

        # For each peace of variant data, we push all of the available SKUs in the store to the @availableProducts Array for use later
        @variants.each do |variant|
            @availableProducts << variant.sku
        end

        #Our final list of products which will contain details from both the Stock Management company and Shopify - we will use this list to run api calls against each item
        @finalProductList = Array.new

        puts "Final product list has #{@fullResponse.length} items."
        puts @fullResponse.inspect

        # We look through every item in the response from Company
        @fullResponse.each_with_index do |p, index|

            # We get the Quantity and Product Code
            @productQTY = p["QtyOnHand"].to_f.round
            @productCode = p["Code"].upcase

            # If the product code is found in the list of available products in the Shopify store...
            if @availableProducts.include? @productCode
                @variants.each do |variant|
                    if @productCode === variant.sku
                        if @productQTY != 0
                            @finalProductList << {
                                "sku" => variant.sku,
                                "inventory_quantity" => variant.inventory_quantity,
                                "old_inventory_quantity" => variant.old_inventory_quantity,
                                "id" => variant.id,
                                "company_sku" => @productCode,
                                "company_qty" => @productQTY
                            }
                        end
                    end
                end
            end
        end

        # If we get a successful response from StockManagement, proceed...
        if @finalProductList
            flash[:notice] = 'StockManagement product quantity check was successful.'

            puts "Final product list has #{@finalProductList.length} items."
            puts @finalProductList

            @finalProductList.each do |item|

                @productSKU = item["sku"]
                @productInventoryQuantity = item["inventory_quantity"]
                @productOldInventoryQuantity = item["old_inventory_quantity"]
                @productID = item["id"]
                @companySKU = item["company_sku"]
                @companyQTY = item["company_qty"]

                url = URI("https://example.myshopify.com/admin/variants/#{@productID}.json")

                http = Net::HTTP.new(url.host, url.port)
                http.use_ssl = true
                http.verify_mode = OpenSSL::SSL::VERIFY_NONE
                request = Net::HTTP::Put.new(url)
                request["content-type"] = 'application/json'
                request["authorization"] = 'Basic KJSHDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDFKJHSDF'
                request["cache-control"] = 'no-cache'
                request.body = "{\n\t\"variant\": {\n\t\t\"id\": #{@productID},\n\t\t\"inventory_quantity\": #{@companyQTY},\n\t\t\"old_inventory_quantity\": #{@productOldInventoryQuantity}\n\t}\n}"

                # This is the line that actually runs the put request to update the quantity.
                response = http.request(request)

                # Finally, we populate the finalList has with response information.
                @finalList[@companySKU] = ["","You had #{@productOldInventoryQuantity} in stock, now you have #{@companyQTY} in stock."]

            end

        else
            # If the overall sync failed, we flash an alert.
            flash[:alert] = 'Quantity synchronisation was unsuccessful.'

        end

        # Lastly we get the final number of items that were synchronised.
        @synchronisedItems = @finalList.length

        # We flash this notification, letting the user known how many products were successfully synchronised.
        flash[:notice] = "#{@synchronisedItems} product quantities were synchronised successfully."

        # We then pretty print this to the console for debugging purposes.
        pp @finalList

    else

        flash[:alert] = @errorResponse

    end
end
end

1 ответ

Решение

Прежде всего, ваш product_quantity метод слишком длинный. Вы должны разбить его на более мелкие части. второй, http.verify_mode = OpenSSL::SSL::VERIFY_NONE не должно быть сделано в производстве. Пример, который вы приводите вместе со своим вопросом, слишком сложен, и на него сложно ответить. Похоже, вам нужно базовое понимание шаблонов проектирования, и это не конкретный вопрос рубина.

Если ваше приложение должно выполнять вызовы API в реальном времени внутри контроллера, это плохой дизайн. Вы не хотите, чтобы запросы любого типа ожидали не более пары секунд. Вы должны подумать, ПОЧЕМУ вам нужно сделать эти запросы в первую очередь. Если это данные, к которым вам нужен быстрый доступ, вы должны написать фоновые задания, чтобы очистить данные по расписанию и сохранить их в своей собственной базе данных.

Если пользователь вашего приложения делает запрос, который должен дождаться ответа API, вы можете написать работника, который будет обрабатывать выборку данных API и в конечном итоге отправить ответ в браузер пользователя, возможно, с помощью actioncable.

Для ваших постоянных определений вы, вероятно, должны сделать это в инициализаторе, который вы бы сохранили в my_app_root/config/initializers/constants.rb которые загружаются в ваше приложение во время выполнения. Вы можете просто позвонить им, где нужно, используя те ENV[] синтаксис, но если вы предпочитаете более простые константы, отбросьте @ так как это соглашение об именах в ruby, например, объекты.

#app_root/config/initializers/constants.rb
HEROKU_ENV_USERNAME = ENV['HEROKU_ENV_USERNAME']
HEROKU_ENV_PASSWORD = ENV['HEROKU_ENV_PASSWORD']
Другие вопросы по тегам