如何从 Web 委派长时间的后台任务,并在完成后恢复控制

How to delegate long background tasks from web, and recover control when done

我们有一个每月两次的ERP,所有最近两周的订单都必须开具账单。以便我们的客户 select 所有这些订单,按下 "generate bills" 按钮,然后完成一系列 顺序 ajax http 请求 ,每个发票,同时弹出消息通知他们该过程。

首先,如前所述,所有发票在数据库中依次生成,一旦完成此过程,就轮到生成PDF文件了。这也是通过顺序 ajax 请求进行的。

这很好,只要用户保持 window 不变。如果他们离开该页面或将其关闭,则整个过程(如果要生成许多发票可能需要几分钟)就会停止。

如果中途停止,可能会导致很多发票没有生成PDF文件。这很关键,因为当他们发送所有要打印的发票时,如果必须即时生成 PDF 内容并将其发送到打印机,则与从现有文件读取内容相比,此操作需要花费更多时间才能完成。

我可以更改流程,以便在生成一张发票后,下一步就是生成其文件,等等。但我想知道是否有某种方法可以通过 system()exec() 左右将进程发送到后台,并在流程完成后使用相同的 Web 应用程序,无论用户决定离开计费页面去执行其他任务。

大部分后台任务都是使用 Cron Jobs 完成的。 Cron 在后台执行,运行 无论代码是什么。这些可以在服务器端随时安排到 运行。在您的情况下,您可以使用以下表达式每月设置两次:

0 0 1,15 * *  ---Command here---

如果不知道安排 cron 作业,then这可能会有所帮助。

现在进入重点,完成工作后通知用户。您需要为每个 cron 将 start_timeend_timestatus 等信息存储在数据库 table 中。在 cron 开始时,cron 的状态保持为 0,在完成时应更改为 1。

可以随时使用数据库中的此信息通知用户。

我认为您应该利用后台任务运行器的优势。 当用户单击该按钮时,然后对后端系统执行一次 ajax 调用,告知将新任务(在您的情况下为多个任务)添加到您的任务队列中。

是的,您应该为 this.This 维护一个任务队列可以是一个数据库 table,它具有 task_type、task_data、task_status 等属性task_dependency.

由于您有多个任务,您可以将它们添加为 2 个主要任务

  • 创建所有发票
  • 生成 PDF 报告(添加以上任务 ID 作为此任务的依赖项)

应该有一个工作进程来查看您的任务队列并执行 them.This 进程将查找任务队列 table 固定时间间隔(每 1 分钟)然后如果有任务具有状态(0-待定),没有其他任务作为依赖项,尚未执行,然后它将执行 them.Task runner 将继续执行此操作,直到没有要执行的任务。

从前端你可以做一个Ajax long polling来检查天气你的pdf生成任务状态(1-completed)。如果是那么你可以通知用户。

为此你可以开发你的简单任务运行器(可能来自 Go,Nodejs) 或者你可以使用可用的任务运行器

我推荐使用一些队列服务。例如,为所有任务创建队列的 RabbitMQ。

您可以创建两个队列:

  1. 第一个用于在数据库中生成发票 --> 将项目添加到此队列 客户点击按钮 "Generate bills" 后。弹出消息 会立即通知用户账单数量和估计 所有任务发送到队列后的生成时间。你做 不必等到生成过程结束。

  2. 第二个用于生成 PDF 文件。在数据库中成功生成发票后,它会从第一个队列中接收一个项目。一位工人 (虽然是真正的过程)从这个队列中获取项目,生成 PDF,然后 如果创建了 PDF,则将项目标记为已完成。否则,工人标记 项目未完成并增加尝试计数器。最大后 达到尝试限制 worker 将项目标记为失败并将其从 第二队列.

结果,您可以看到现在生成了多少项目。记录不成功的生成并控制所有过程。

一个简单的例子:

发件人

创建队列并向其发送项目。在启动消费者之前启动发送方进程。

$params = array(
    'host' => 'localhost',
    'port' => 5672,
    'vhost' => '/',
    'login' => 'guest',
    'password' => 'guest'
);

$connection = new AMQPConnection($params);
$connection->connect();
$channel = new AMQPChannel($connection);

$exchange = new AMQPExchange($channel);
$exchange->setName('ex_hello');
$exchange->setType(AMQP_EX_TYPE_FANOUT);
$exchange->setFlags(AMQP_IFUNUSED | AMQP_AUTODELETE);
$exchange->declare();

$queue = new AMQPQueue($channel);
$queue->setName('invoice');
// ability to autodelete a queue after script is finished,
// AMQP_DURABLE says you cannot create two queues with same name
$queue->setFlags(AMQP_IFUNUSED | AMQP_AUTODELETE | AMQP_DURABLE); 
$queue->declare();
$queue->bind($exchange->getName(), '');

$result = $exchange->publish(json_encode("Invoice_ID"), '');

if ($result)
    echo 'sent'.PHP_EOL;
else
    echo 'error'.PHP_EOL;
# after sending an item close the connection
$connection->disconnect();

消费者

Worker 必须连接到 RabbitMQ、读取队列、创建作业并设置结果:

$params = array(
    'host' => 'localhost',
    'port' => 5672,
    'vhost' => '/',
    'login' => 'guest',
    'password' => 'guest'
);

$connection = new AMQPConnection();
$connection->connect();

$channel = new AMQPChannel($connection);

$exchange = new AMQPExchange($channel);
$exchange->setName('ex_hello');
$exchange->setType(AMQP_EX_TYPE_FANOUT);
$exchange->declare();

$queue = new AMQPQueue($channel);
$queue->setName('invoice');
// ability to autodelete a queue after script is finished,
// AMQP_DURABLE says you cannot create two queues with same name
$queue->setFlags(AMQP_IFUNUSED | AMQP_AUTODELETE | AMQP_DURABLE); 
$queue->declare();
$queue->bind($exchange->getName(), '');

while (true) {
    if ($envelope = $queue->get()) {
        $message = json_decode($envelope->getBody());
        echo "delivery tag: ".$envelope->getDeliveryTag().PHP_EOL;
        if (doWork($message)) {
            $queue->ack($envelope->getDeliveryTag());
        } else {
            // not successful result, we need to redo this job
            $queue->nack($envelope->getDelivaryTag(), AMQP_REQUEUE); 
        }
    }
}

$connection->disconnect();

此类任务不适用于网络uitable,因为它们会保留您的网络请求更长时间,如果您使用像 nodejs 这样的服务器,那么在单线程之后情况会变得非常糟糕型号。

无论如何,这是最简单的方法之一:

  1. 向服务器发送带有订单 ID 列表的 ajax 请求。服务器简单地将这些状态为 PENDING 的 orderids 插入到 dbtable "ORDERINVOICE" 中。服务器简单地响应 200 表示请求已接受

  2. 有一个查询 ORDERINVOICE 的后台作业 table 假设每 5 秒等待一次状态为 PENDING 的记录。此作业将生成发票并将状态标记为 INVOICED

  3. 还有另一个查询 ORDERINVOICE 的后台作业 table 假设每 5 秒等待一次状态为 INVOICED 的记录。此作业将生成 pdf 并将状态标记为完成


现在进入更新WEB的部分UI。

对于实时通知,您将被要求ui使用 Websockets,这将创建与您的服务器的持久连接,从而实现双向通信。

但是,如果您可以承受更新客户端进度的延迟,另一种方法可能是在 5/6 秒后通过网络 ui 通过 ajax 请求 return ORDERINVOICE table 的状态。喜欢 pending:10,进行中:20,完成:3 等等


扩展需求

上面的实现非常简单,使用中间件就可以完成w/o。但是,如果您计划扩展 long 运行 并希望避免对数据库进行不必要的查询,则您将不得不进行完全异步并进行一些繁重的维护。 (对于进行大量处理的系统来说,这应该是可取的方法)

使用 Kafka/RabbitMQ 等排队解决方案的完整异步方式

  1. 上面的步骤1仍然保持不变。(提供持久存储)
  2. 创建一个生产者,它只读取 PENDING 记录并将订单推送到 INVOICING QUEUE
  3. 根据规模,您可以将 n 个消费者添加到此 INVOCING QUEUE 中,并行执行您的开票工作,完成后更新状态并将记录推送到另一个 PDFQUEUE。
  4. 再次加快和扩展流程,您将让消费者收听此 PDFQUEUE 并进行 pdf 生成工作。完成后,他们将更新状态并将消息推送到 NOTIFYQUEUE。
  5. websocket 服务器将成为我们 NOTIFYQUEUE 的消费者,它会简单地更新 web 浏览器的完成状态。您需要为此传递一个唯一的 user/visitor id。检查 https://socket.io/ 网络套接字。

您可以使用此功能在后台进程中执行 PHP 脚本:

    function Execute($CMD)
    {
        $OS = strtoupper(PHP_OS_FAMILY);

        if ($OS == 'WINDOWS') {
            return pclose(popen("start /B {$CMD}", "r"));
        } else {
            return shell_exec("{$CMD} > /dev/null 2>/dev/null &");
        }
    }

您的命令开始后,开始记录有关它的一些信息(好像已经完成 | 正在处理)

有一些记录信息的方法:

  • 在单个文件中记录信息
  • 使用内存数据库 (Redis)
  • 或MySQL

在更改过程中,您可以从该日志中读取进度 file/DB

I have done this for my videos when the user uploads a video I need to create tooltip thumbnails, poster, etc using FFMPEG, It takes too much time to wait and I just run my scripts at the background process.

选项 1:使用计划的 cron 作业排队

这样做的挑战是您的用户无法控制 cron 作业何时 运行 因此,如果他们错过了计划作业的 window,他们可以没做什么。如果您在仍然会生成 pdfs

的应用程序中进行计算,那么它在 Web 服务器上也是资源密集型的

一个可能的解决方案是通过电子邮件、应用程序通知 and/or 短信发送提醒,当您 window 到 运行 运行 cron 作业临近。在 Web 和数据库服务器之间分配负载时找到平衡点。

在您的数据库中添加一个带有标志的字段,例如 requiresProcessing,其值显示当前状态,例如什么都不做、启动处理、处理、完成、未完成等,指示需要对订单执行的操作。

一旦您的用户select 收到他们想要生成发票的订单,更改标志以启动处理。在界面上通知您的用户作业已排队等待处理,可能会告诉他们完成作业所需的时间。

创建一个 php 脚本,该脚本将在您的数据库中查询满足此条件的记录 - 用户已请求 invoice/pdf 生成。

在您的服务器上设置一个 cron 作业,当您的服务器不是很忙时,该作业将 运行。此 cron 作业将 运行 上面的 PHP 脚本。它将检索需要发票的记录(工作订单),对将出现在发票上的字段进行计算,然后创建 PDF。在创建和完成每张发票时,更改标志。如果发生某些事情并且未创建发票,请存储该状态。

在您的 Web 应用程序 UI 上,您可以使用显示进度的 AJAX 请求获得某种形式的通知(状态更新),假设您的用户在 cron 作业之前发出请求已启动,因此可以看到更新。

选项 2:客户端使用 HTML5 网络工作者和本地存储

Library to generate PDF using JS

我的线程可以帮助您解决使用 Web Worker 时面临的一些挑战

使用 HTML5/Web 名工人将创建发票的工作转移给 client/browser 名员工。用户 select 下订单,您的应用程序将订单的键值对 (uniqueId:state) 存储在浏览器本地存储中。

有一个生成发票的网络工作者。从本地存储中删除已成功完成的订单的 uniqueId。本地存储是持久的,所以即使他们关闭 window 数据仍然存在。一旦他们重新打开 window,就会有一项检查本地存储并继续生成发票的服务。

选项 2:扩展 作为选项 2,但开发一个浏览器扩展,即使用户已退出您的应用程序或浏览器也可以执行这些任务。

这两个话题应该可以指导您。

Chrome extension: accessing localStorage in content script