Как правильно использовать 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']