如何在后台执行一个长任务?
How to execute a long task in background?
我有一个使用 Symfony 5 制作的应用程序,我有一个脚本可以将位于服务器上的视频上传到已登录的用户频道。
这里基本上是我的控制器的代码:
/**
* Upload a video to YouTube.
*
* @Route("/upload_youtube/{id}", name="api_admin_video_upload_youtube", methods={"POST"}, requirements={"id" = "\d+"})
*/
public function upload_youtube(int $id, Request $request, VideoRepository $repository, \Google_Client $googleClient): JsonResponse
{
$video = $repository->find($id);
if (!$video) {
return $this->json([], Response::HTTP_NOT_FOUND);
}
$data = json_decode(
$request->getContent(),
true
);
$googleClient->setRedirectUri($_SERVER['CLIENT_URL'] . '/admin/videos/youtube');
$googleClient->fetchAccessTokenWithAuthCode($data['code']);
$videoPath = $this->getParameter('videos_directory') . '/' . $video->getFilename();
$service = new \Google_Service_YouTube($googleClient);
$ytVideo = new \Google_Service_YouTube_Video();
$ytVideoSnippet = new \Google_Service_YouTube_VideoSnippet();
$ytVideoSnippet->setTitle($video->getTitle());
$ytVideo->setSnippet($ytVideoSnippet);
$ytVideoStatus = new \Google_Service_YouTube_VideoStatus();
$ytVideoStatus->setPrivacyStatus('private');
$ytVideo->setStatus($ytVideoStatus);
$chunkSizeBytes = 1 * 1024 * 1024;
$googleClient->setDefer(true);
$insertRequest = $service->videos->insert(
'snippet,status',
$ytVideo
);
$media = new \Google_Http_MediaFileUpload($googleClient, $insertRequest, 'video/*', null, true, $chunkSizeBytes);
$media->setFileSize(filesize($videoPath));
$uploadStatus = false;
$handle = fopen($videoPath, "rb");
while (!$uploadStatus && !feof($handle)) {
$chunk = fread($handle, $chunkSizeBytes);
$uploadStatus = $media->nextChunk($chunk);
}
fclose($handle);
}
这基本上可行,但问题是视频可能非常大(10G+),所以需要很长时间,基本上 Nginx 在结束之前终止并且 returns 一个“504 网关超时" 在上传完成之前。
无论如何,我不希望用户在上传页面时必须等待页面加载。
所以,我正在寻找一种方法,而不是立即 运行 该脚本,而是在某种后台线程中或以异步方式执行该脚本。
控制器 returns 向用户发送 200
,我可以告诉他正在上传,稍后再回来检查进度。
如何操作?
有很多方法可以实现这一点,但您基本上想要的是将动作触发器与其执行分离。
简单地:
- 从您的控制器中删除所有繁重的工作。您的控制器最多应该检查客户端提供的视频 ID 是否存在于您的
VideoRepository
.
存在吗?好,那么你需要把这个 "work order" 存储在某个地方。
针对此问题有很多解决方案,具体取决于您已经安装的内容、您觉得table使用哪种技术更舒服等等
为了简单起见,假设您有一个 PendingUploads
table,其中 videoId
、status
、createdAt
可能还有 userId
.所以你的控制器唯一会做的就是在这个 table 中创建一个新记录(也许检查作业不是 "queued" ,这种细节取决于你的实现)。
- 然后return
200
(或202
,即could be more appropriate in the circumstances)
然后您需要编写一个单独的过程。
很可能是您会定期执行的控制台命令(使用 cron
将是最简单的方法)
在每次执行时,该进程(将具有所有 Google_Client
逻辑,可能还有一个 PendingUploadsRepository
)将检查哪些作业正在等待上传,按顺序处理它们,然后设置 status
到您表示要完成的任何内容。例如,您可以将 status
设置为 0
(待处理)、1
(处理中)和 2
(已处理),并相应地设置每个步骤的状态脚本。
具体实现细节由您决定。这个问题太宽泛和自以为是了。选择你已经理解的东西,让你行动得更快。如果您将作业存储在 Rabbit、Redis、数据库或平面文件中,并不是特别重要。如果您的 "consumer" 以 cron
或 supervisor
开头,则两者都可以。
Symfony 有一个现成的组件,可以让您异步地解耦这种消息传递 (Symfony Messenger),它非常好。调查一下它是否适合您,但如果您不打算将它用于您的应用程序中的任何其他内容,我会在开始时保持简单。
我有一个使用 Symfony 5 制作的应用程序,我有一个脚本可以将位于服务器上的视频上传到已登录的用户频道。
这里基本上是我的控制器的代码:
/**
* Upload a video to YouTube.
*
* @Route("/upload_youtube/{id}", name="api_admin_video_upload_youtube", methods={"POST"}, requirements={"id" = "\d+"})
*/
public function upload_youtube(int $id, Request $request, VideoRepository $repository, \Google_Client $googleClient): JsonResponse
{
$video = $repository->find($id);
if (!$video) {
return $this->json([], Response::HTTP_NOT_FOUND);
}
$data = json_decode(
$request->getContent(),
true
);
$googleClient->setRedirectUri($_SERVER['CLIENT_URL'] . '/admin/videos/youtube');
$googleClient->fetchAccessTokenWithAuthCode($data['code']);
$videoPath = $this->getParameter('videos_directory') . '/' . $video->getFilename();
$service = new \Google_Service_YouTube($googleClient);
$ytVideo = new \Google_Service_YouTube_Video();
$ytVideoSnippet = new \Google_Service_YouTube_VideoSnippet();
$ytVideoSnippet->setTitle($video->getTitle());
$ytVideo->setSnippet($ytVideoSnippet);
$ytVideoStatus = new \Google_Service_YouTube_VideoStatus();
$ytVideoStatus->setPrivacyStatus('private');
$ytVideo->setStatus($ytVideoStatus);
$chunkSizeBytes = 1 * 1024 * 1024;
$googleClient->setDefer(true);
$insertRequest = $service->videos->insert(
'snippet,status',
$ytVideo
);
$media = new \Google_Http_MediaFileUpload($googleClient, $insertRequest, 'video/*', null, true, $chunkSizeBytes);
$media->setFileSize(filesize($videoPath));
$uploadStatus = false;
$handle = fopen($videoPath, "rb");
while (!$uploadStatus && !feof($handle)) {
$chunk = fread($handle, $chunkSizeBytes);
$uploadStatus = $media->nextChunk($chunk);
}
fclose($handle);
}
这基本上可行,但问题是视频可能非常大(10G+),所以需要很长时间,基本上 Nginx 在结束之前终止并且 returns 一个“504 网关超时" 在上传完成之前。
无论如何,我不希望用户在上传页面时必须等待页面加载。
所以,我正在寻找一种方法,而不是立即 运行 该脚本,而是在某种后台线程中或以异步方式执行该脚本。
控制器 returns 向用户发送 200
,我可以告诉他正在上传,稍后再回来检查进度。
如何操作?
有很多方法可以实现这一点,但您基本上想要的是将动作触发器与其执行分离。
简单地:
- 从您的控制器中删除所有繁重的工作。您的控制器最多应该检查客户端提供的视频 ID 是否存在于您的
VideoRepository
. 存在吗?好,那么你需要把这个 "work order" 存储在某个地方。
针对此问题有很多解决方案,具体取决于您已经安装的内容、您觉得table使用哪种技术更舒服等等
为了简单起见,假设您有一个
PendingUploads
table,其中videoId
、status
、createdAt
可能还有userId
.所以你的控制器唯一会做的就是在这个 table 中创建一个新记录(也许检查作业不是 "queued" ,这种细节取决于你的实现)。- 然后return
200
(或202
,即could be more appropriate in the circumstances)
然后您需要编写一个单独的过程。
很可能是您会定期执行的控制台命令(使用 cron
将是最简单的方法)
在每次执行时,该进程(将具有所有 Google_Client
逻辑,可能还有一个 PendingUploadsRepository
)将检查哪些作业正在等待上传,按顺序处理它们,然后设置 status
到您表示要完成的任何内容。例如,您可以将 status
设置为 0
(待处理)、1
(处理中)和 2
(已处理),并相应地设置每个步骤的状态脚本。
具体实现细节由您决定。这个问题太宽泛和自以为是了。选择你已经理解的东西,让你行动得更快。如果您将作业存储在 Rabbit、Redis、数据库或平面文件中,并不是特别重要。如果您的 "consumer" 以 cron
或 supervisor
开头,则两者都可以。
Symfony 有一个现成的组件,可以让您异步地解耦这种消息传递 (Symfony Messenger),它非常好。调查一下它是否适合您,但如果您不打算将它用于您的应用程序中的任何其他内容,我会在开始时保持简单。