Как сделать очередь на PHP по работе с CRM системой?
======================================================

 Всем привет!  

В одной из популярных CRM систем, есть публичное приложение, которым пользуются 400+ аккаунтов. При изменении сущностей в CRM (сделка, контакт и .т.д) на наш сервер приходят вебхуки, при обработке которых нам необходимо опрашивать CRM аккаунт клиента через API. Ограничение API - 7 запросов в секунду. При превышении этого лимита, аккаунт блокируется для получения данных по API. Перед блокировкой CRM система конечно отправляет в заголовке ответа, что начинается превышение лимита запросов, но этот ответ ничем не помогает, так как во первых запрос уже сделан, во-вторых запросы от CRM системы все равно обрабатываются параллельно.   

  

Проблема: если клиент импортирует в CRM сразу 10000 контактов, то CRM будет посылать к нам на сервер ~10-20 запросов в секунду о создании контакта. Так как нам нужно каждый запрос обработать, наш сервер тоже делает ~10-20 запросов в секунду по API уже в CRM. Так как ограничение запросов в 7 шт, наше приложение блокируется.  

  

Вопрос: как можно решить проблему? Напрашивается очевидное решение - очереди, но познания в них у меня небольшие. На сколько знаю воркеров может быть фиксированное количество, например 10. А что если 30 аккаунтов CRM системы решат импортировать одновременно по 10000 контактов? А воркеров только 10. Остальные 20 будут ждать?  

  

И есть ли в готовых решениях по работе с очередями динамическое создание воркеров? Например, когда есть процесс, который сканирует очереди. Если в очередях есть сообщения, запускает воркер на эту очередь. Как только воркер справился с очередью, процесс воркера убивается. Новый воркер будет запущен, если в очереди появились новые сообщения. 

 В Yii2 есть [Yii-quque](https://github.com/yiisoft/yii2-queue) 

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

  

В базе данных сделайте таблицу для очереди, в которой одним из столбцов будет номер аккаунта клиента.   

При получении вебхука не обрабатывайте (т.е. не делайте запросы к crm) его сразу, а только сохраняйте в вышеуказанную таблицу.  

Напишите скрипт, который будет запускаться раз в минуту по крону. Этот скрипт должен получить уникальные аккаунты (DISTINCT) из таблицы очереди и запустить все обработчики, каждый из которых будет обрабатывать только 1 аккаунт клиента (т.е. аккаунт передается обработчику как аргумент). Запускать нужно как фоновый процесс (&). Можно перед запуском проверять, не запущены ли они уже, или сделать это на следующем шаге.   

Обработчик при старте проверяет, не запущен ли другой его экземпляр для этого же клиента. Затем до тех пор, пока есть записи в очереди именно для этого аккаунта, скрипт получает по 7 записей для этого аккаунта, и по ним делает 7 одновременных запросов к api crm с помощью guzzle [docs.guzzlephp.org/en/stable/quickstart.html#concu...](http://docs.guzzlephp.org/en/stable/quickstart.html#concurrent-requests) , после чего удаляет записи из очереди.  

  

Альтернативным вариантом будет использование очереди вроде RabbitMQ вместо базы данных. Нужно будет в exchange создать очередь для каждого аккаунта клиента, и также скриптом по крону получать информацию по тому, какие есть очереди и запускать обработчиков. Первый способ, на мой взгляд, проще, да и его будет достаточно, если вы не обрабатываете миллионы записей ежедневно. 

 Попробуйте сделать так:  

  

На сервере организуйте хранение очереди (можно в Redis, но на крайний случай, можно и в mysql). Также потребуются функции уточнения статуса задания и скачивания результата.   

 (Если crm поддерживает такой формат работы, костылить нужно меньше)  

В противном случае, воркеру придется дожидаться результата запроса. (На shared хостинге, нагрузка превысит пределы)  

  

Скриптом принимайте запросы и помещайте в очередь.  

Добавьте задание в cron, чтобы из очереди извлекались данные и формировался запрос к удалённому api. Ответ можно поместить в хранилище, а потом забрать конечному пользователю.