Мне бы очень хотелось подумать об этом подходе, который я реализую, чтобы обрабатывать очень длинные процессы в веб-приложении.
Эта проблема
У меня есть веб-приложение, все написанное на javascript, которое связывается с сервером через API. В этом приложении есть несколько «массовых действий», выполнение которых занимает много времени. Я хочу выполнить их безопасным способом, убедившись, что время ожидания сервера не истекло, и с обширной обратной связью с пользователем, чтобы он / она знали, что происходит.
Обычный подход
Как я вижу в своем исследовании, рекомендуемый метод выполнения — запуск фонового процесса на сервере и заставить его писать куда-нибудь, как он работает, чтобы вы могли отправлять запросы на проверку и давать обратную связь пользователю. Так как я использую php в бэк-энде, подход будет более или менее описанным здесь: http://humblecontributions.blogspot.com.br/2012/12/how-to-run-php-process-in-background.html
Добавление нескольких реквизитов
Поскольку я занимаюсь разработкой проекта с открытым исходным кодом (плагин WordPress), я хочу, чтобы он работал в различных ситуациях и средах. Я не хотел добавлять требования на стороне сервера, и, насколько мне известно, подход с использованием фоновых процессов может не работать в нескольких решениях общего хостинга.
Я хочу, чтобы он работал «из коробки» на (почти) любом сервере с типичной поддержкой WordPress, даже если в конечном итоге решение оказалось немного медленнее.
Мой подход
Идея состоит в том, чтобы разбить этот процесс таким образом, чтобы он постепенно выполнялся через множество небольших запросов.
Поэтому, когда браузер в первый раз отправляет запрос на запуск процесса, он запускает только небольшой шаг и возвращает полезную информацию, чтобы дать пользователю некоторую обратную связь. Затем браузер выполняет другой запрос и повторяет его, пока сервер не сообщит, что процесс завершен.
Чтобы сделать это, я бы сохранил этот объект в сеансе, поэтому первый запрос даст мне идентификатор, а следующие запросы отправят этот идентификатор на сервер, чтобы он манипулировал тем же объектом.
Вот концептуальный пример:
class LongProcess {
function __construct() {
$this->id = uniqid();
$_SESSION[$this->id] = $this;
$this->step = 1;
$this->total = 100;
}
function run() {
// do stuff based on the step you are in
$this->step = $this->step + 10;
if ($this->step >= $this->total)
return -1;
return $this->step;
}
}
function ajax_callback() {
session_start();
if (!isset($_POST['id']) || empty($_POST['id'])) {
$object = new LongProcess();
} else {
$object = $_SESSION[$_POST['id']];
}
$step = $object->run();
echo json_encode([
'id' => $object->id,
'step' => $return,
'total' => $object->total
]);
}
Благодаря этому мой клиент может рекурсивно отправлять запросы и обновлять отзывы пользователей по мере их получения.
function recursively_ajax(session_id)
{
$.ajax({
type:"POST",
async:false, // set async false to wait for previous response
url: "xxx-ajax.php",
dataType:"json",
data:{
action: 'bulk_edit',
id: session_id
},
success: function(data)
{
updateFeedback(data);
if(data.step != -1){
recursively_ajax(data.id);
} else {
updateFeedback('finish');
}
}
});
}
$('#button').click(function() {
recursively_ajax();
});
Конечно, это всего лишь подтверждение концепции, я даже не использую jQuery в реальном коде. Это просто чтобы выразить идею.
Обратите внимание, что этот объект, который хранится в сеансе, должен быть очень легким объектом. Любые обрабатываемые данные должны храниться в базе данных или файловой системе и ссылаться на них только в объекте, чтобы он знал, где искать вещи.
Типичным случаем будет обработка большого файла CSV. Файл будет храниться в файловой системе, а объект будет хранить указатель на последнюю обработанную строку, чтобы он знал, с чего начать в следующем запросе.
Объект также может возвращать более подробный журнал, описывающий все, что было сделано, и сообщающий об ошибках, чтобы пользователь имел полное представление о том, что было сделано.
Интерфейс, который я думаю, был бы отличным, это индикатор выполнения с кнопкой «посмотреть детали», которая открыла бы текстовое поле с этим подробным журналом.
Имеет ли это смысл?
Так что теперь я спрашиваю. Как это выглядит? Это жизнеспособный подход?
Есть ли лучший способ сделать это и убедиться, что он будет работать на очень ограниченных серверах?
Ваш подход имеет несколько недостатков:
Ваши тяжелые запросы могут блокировать другие запросы. Обычно у вас есть ограничение числа одновременных процессов PHP для обработки веб-запросов. Если ограничение равно 10, и все слоты заняты обработкой ваших сложных запросов, ваш веб-сайт не будет работать, пока некоторые из этих запросов не завершат освобождение слота для другого легкого запроса.
Вы (вероятно) не сможете оценить, сколько времени потребуется, чтобы закончить один шаг. В зависимости от нагрузки на сервер это может занять 5 или 50 секунд. И 50 секунд, вероятно, превысят лимит времени выполнения на большинстве общих хостингов.
Эта задача будет контролироваться клиентом — любое прерывание со стороны клиента (проблемы с сетью, закрытие вкладки браузера) будет прерывать задачу.
В зависимости от серверного сеанса использование сеанса для сохранения текущего состояния может привести к ошибкам состояния гонки — одновременный запрос от одного и того же клиента может перезаписать изменения в сеансе, выполненные фоновой задачей. По умолчанию PHP использует блокировку для сеанса, поэтому это не должно быть так, но если кто-то использует альтернативный бэкэнд для сеансов (DB, Redis) без блокировки, это приведет к серьезным и трудным для отладки ошибок.
Здесь есть очевидный компромисс. Для небольших веб-сайтов, где упрощение установки и настройки является приоритетом, ваш подход в порядке. В любом другом случае я бы использовал простую очередь на основе cron для выполнения задач в фоновом режиме и использовал AJAX-запрос только для получения текущего состояния задачи. До сих пор я не видел хостинга без поддержки cron, и добавление задачи в cron не должно быть таким сложным для конечного пользователя (с соответствующей документацией).
В обоих случаях я бы не использовал сессию как хранилище. Сохраните задачу и ее статус в базе данных и используйте некоторую систему блокировки, чтобы гарантировать, что только один процесс может изменять данные одной задачи. Это будет намного более надежным и гибким, чем использование сессии.
Спасибо за весь вклад. Я просто хочу документировать здесь некоторые очень хорошие ответы, которые я получил.
Некоторые плагины WordPress, называемые Woocommerce, содержат код из библиотеки «WP Background Processing», который больше не поддерживается, но реализует подход Cron с некоторыми важными улучшениями. Смотрите это сообщение в блоге:
https://deliciousbrains.com/background-processing-wordpress/
Фактическая библиотека живет здесь: https://github.com/A5hleyRich/wp-background-processing
Хотя это библиотека для WordPress, я думаю, что подход подходит для любой ситуации.
Для WordPress есть также библиотека под названием Action Scheduler, которая не только настраивает процессы в фоновом режиме, но и позволяет планировать их. Это стоит посмотреть: