Как сделать, чтобы асинхронные операции async выполнялись параллельно
На сервере цикл осуществляет по базе определенную работу. В основном файле есть 2 кнопки. На одной начало цикла, на второй приостановка. Первая подает команду - цикл начинает осуществляться. К кнопкам на js через fetch привязаны обработчики, осуществляющие дальнейшее выполнение команд. В цикле предусмотрена проверка на изменение флага в отдельном файле продолжать-остановить. Вторая команда подает сигнал на отдельный файл - изменить файл где флаг. По идее изменение флага должно останавливать выполнение php скрипта. Но на практике fetch от кнопки остановки получает ответ лишь после завершения всего цикла в скрипте php. Получается, что 2 асинхронные операции работают последовательно. Перечитал множество постов, но решения не нашел Немного кода:
<script>
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const statusDiv = document.getElementById('status');
let isSending = false;
async function start_work() {
isSending = true;
startButton.disabled = true;
stopButton.disabled = false;
statusDiv.textContent = 'Работаем...';
try {
// Используем fetch для запроса к серверному скрипту
const response = await fetch('start_work.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({'isDoing': true})
});
const result = await response.json();
isDoing=result.message;
statusDiv.textContent = result.message;
console.log(result.message);
} catch (error) {
console.error('Ошибка при запуске:', error);
statusDiv.textContent = 'Ошибка при запуске: ' + error.message;
} finally {
// После завершения (успешного или нет) кнопка "запуск" становится активна, а "прервать" — нет
isDoing= false;
startButton.disabled = false;
stopButton.disabled = true;
}
}
async function stop_work() {
statusDiv.textContent = 'Прерывание...';
try {
const response = await fetch('stop_work.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ start_work: false })
});
const result = await response.json();
statusDiv.textContent = result.message;
console.log(result.message);
} catch (error) {
console.error('Ошибка при остановке:', error);
statusDiv.textContent = 'Ошибка при остановке: ' + error.message;
} finally {
isSending = false;
startButton.disabled = false;
stopButton.disabled = true;
}
}
startButton.addEventListener('click', start_work);
stopButton.addEventListener('click', stop_work);
</script>
php: stop_work.php
session_start(); // Запускаем сессию для отслеживания статуса
$data = json_decode(file_get_contents('php://input'), true);
// Устанавливаем флаг остановки в файле
file_put_contents('doing_status.txt', 'stopped');
$_SESSION['doing_status']=$data['isDoing']; // для подстраховки
$isDoing =false;
start_work.php
<?php
require_once('../../../private/initialize.php');
$data = json_decode(file_get_contents('php://input'), true);
session_start(); // Запускаем сессию для отслеживания статуса
use App\Mailsender;
use App\User;
use App\Subsciber;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
$user=User::find_by_id($session->user_id);
$errors=[];
$emailMessage=Mailsender::find_by_id(1);
$mail = new PHPMailer(true);
$isWorking = $data['isWorking'] ?? false;
$_SESSION['doing_status'] = $isWorking;
file_put_contents('doing_status.txt', 'running');
$recipients = Subsciber::find_all();
$mail_settings=mail_settings($user->email)['mail_settings_prod'];
$mail->isSMTP(); //Send using SMTP
$mail->Host = $mail_settings['host']; //Set the SMTP server to send through
$mail->SMTPAuth = $mail_settings['auth']; //Enable SMTP authentication
$mail->Username = $mail_settings['username']; //SMTP username
$mail->Password = $mail_settings['password']; //SMTP password
$mail->SMTPSecure = $mail_settings['secure']; //Enable implicit SSL encryption
$mail->Port = $mail_settings['port']; //Enable port
$mail->CharSet = $mail_settings['charset']; //Enable port
$mail->setFrom($mail_settings['from_email'], $mail_settings['from_name']);
$mail->isHTML($mail_settings['is_html']); //Set email format to HTML
$mail->Subject = $emailMessage->subject;
$mail->Body .= "<p>{$emailMessage->body}</p>";
$mail->Body .= "<p>С уважением,</p>";
$mail->Body .= "<p>{$user->full_name()}</p>";
$mail->Body .= "<p>{$user->position}</p>";
$emailMessage->body=$mail->Body;
$mail->Body .= "<p></p>";
$mail->AltBody = 'Test';
$html_content = file_get_contents('../../letter_templates/cerberus-fluid.php');
if ($html_content !== false) {
// Содержимое файла успешно прочитано и сохранено в $html_content
$html_content = str_replace('{{subject}}', $emailMessage->subject, $html_content);
// $html_content = str_replace('{{dates}}', $emailMessage->body, $html_content);
$mail->body .= "<h1>{$emailMessage->subject}</h1>{$html_content}";
} else {
// Обработка ошибки, если файл не удалось прочитать
$errors[] = "Ошибка: Не удалось прочитать файл.\n";
}
$emailMessage->Body=$mail->Body;//$mail->Body;
echo json_encode(['message' => 'Рассылка запущена','isWorking'=>$isWorking]);
foreach ($recipients as $recipient) {
$mail->addAddress($recipient->email);
if (file_get_contents('mailing_status.txt') !== 'running') {
echo json_encode(['message' => 'Рассылка остановлена','isWorking'=>false]);
break; // Прерываем цикл, если отправка остановлена
}
try {
$result=$mail->send();
// ====================================================================
if ($result) {
$args=[];
$result = $emailMessage->save();
if($result === true) {
$new_id = $emailMessage->id;
} else {
// show errors
$session->message(show_errors($errors));
redirect_to(url_for('/staff/mailcampaigns/index.php'));
}
}
}
catch (Exception $e) {
$session->message(show_errors($errors), "Mailer Error: " . $mail->ErrorInfo);
echo json_encode(['success' => false, 'message' => "Ошибка рассылки: {$mail->ErrorInfo}"]);
redirect_to(url_for('/staff/subscribers/show.php?id=' . $recipient->id));
}
$mail->clearAddresses();
sleep(1);
}
$_SESSION['doing_status'] = false; // Сбрасываем флаг после завершения цикла
echo json_encode(['message' => 'Рассылка завершена','isWorking'=>$_SESSION['doing_status']]);
?>
Ответы (1 шт):
Задача может быть решена несколькими способами. При желании можно решения даже комбинировать.
(Примеры на работоспособность не проверяю, просто излагаю идеи. Пожалуйста, тестируйте сами)
1. Прерывание fetch с помощью AbortController
start_work() запускает длительный fetch, получающий результат start_work.php.
stop_work() подаёт сигнал в выполняющийся fetch для прерывания. И я бы не делал функции асинхронными с использованием await. Возвращать промисы - полезная и гибкая возможность.
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const statusDiv = document.getElementById('status');
let isSending = false;
let controller;
startButton.addEventListener('click', start_work);
stopButton.addEventListener('click', stop_work);
function start_work() {
isSending = true;
startButton.disabled = true;
stopButton.disabled = false;
statusDiv.textContent = 'Работаем...';
controller = new AbortController();
return fetch('start_work.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({'isDoing': true}),
signal: controller.signal
})
.then(response => response.json())
.then(result => {
console.log(result.message);
isDoing=result.message;
statusDiv.textContent = result.message;
return result;
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Запрос отменен');
} else {
console.error('Ошибка при запуске:', error);
statusDiv.textContent = 'Ошибка при запуске: ' + error.message;
}
})
.finally(() => {
// После завершения (успешного или нет) кнопка "запуск" становится активна, а "прервать" — нет
isDoing= false;
startButton.disabled = false;
stopButton.disabled = true;
});
}
function stop_work() {
statusDiv.textContent = 'Прерывание...';
if (controller) {
controller.abort();
}
}
Плюсы - задача решается полностью на фронте.
Минусы - php-скрипт непредсказуемо прервётся и для надёжности в нем придётся делать хэндлер завершения через register_shutdown_function().
2. Решение на стороне start_work.php с использованием ignore_user_abort
Скрипт на php может продолжить работу, отдав ответ во фронт. Файлы использовать не обязательно, ибо открытие/закрытие сессии по факту делает то же самое, но гибче, так как может быть настроено на работу с БД/памятью и т.д. Так что будем периодически смотреть статус и ждать, пока другой скрипт не укажет там, что пора остановиться.
start_work.php
<?
ignore_user_abort(1);
set_time_limit(600); //Всё-таки ограничим выполнение каким-то разумным временем
ob_start();
session_start();
$_SESSION['status']='working';
session_write_close();
//Вывод JSON фронту
$size = ob_get_length();
header("Connection: close",1);
header("Content-Length: $size");
ob_end_flush();
flush();
if(function_exists('fastcgi_finish_request')) fastcgi_finish_request();
//Длительный процесс уже без связи с фронтом с периодической проверкой сессии
do{
session_start();
//Проверяем свой статус
$working= $_SESSION['status']=='working';
//Кладём в сессию что-нибудь полезное
$_SESSION['progress']=[];
session_write_close();
//Полезная долгая работа
}while($working)
session_start();
$_SESSION['status']='finished';
session_write_close();
Остановку осуществляем отдельным скриптом изменяя сессию.
stop_work.php
<?
session_start();
$_SESSION['status']='stop';
//о результате сообщаем фронту, например
echo json_encode(['ststus'=>'finished']);
session_write_close();
Проверяем статус отдельным скриптом
status_work.php
<?
session_start();
//Что-нибудь отвечаем фронту
echo json_encode($_SESSION['progress']);
session_write_close();
Плюсы - управление осуществляется на стороне сервера, фронт работает с быстро завершающимися fetch, не требующими ожидания.
Минусы - если фронт "забудет" о бэке, сервер будет заниматься никому не нужной работой.
3. Полноценный процесс, оторванный от веб-сервера.
Приводить код не буду, но суть состоит в том, чтобы написать отдельную программу, выполняющую работу в фоне и способную слушать внешние сигналы для старта, останова и отдавания статуса. Это могут быть linux-сокеты, файлы, БД, TCP/UDP. Это может быть и не php вовсе.
Задача же php-скриптов будет передавать команды в этот процесс и опрашивать статус, отдавая это всё фронту.