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

lui*_*yen 5 php ajax pdf-generation http background-process

我们有一个ERP,每月两次,过去两周的所有订单都需要收费.为了让我们的客户选择所有这些订单,按下"生成账单"按钮,每个发票一次完成一系列连续的ajax http请求,同时弹出消息通知他们该过程.

首先,所有的发票都是在DB中按顺序生成的,正如前面提到的那样,一旦完成这个过程,那么轮到生成PDF文件了.这也是顺序ajax请求.

这是好的,只要用户保持窗口不变.它们离开该页面或关闭它,整个过程,如果有许多发票要生成,可能需要几分钟才能停止.

如果在中间停止进程,则可能会生成许多没有生成PDF文件的发票.这一点至关重要,因为当他们发送所有要打印的发票时,如果必须动态生成PDF内容并将其发送到打印机,则此操作需要做更多工作,而不是从现有文件中读取内容.

我可以更改流程,以便在生成一张发票后,下一步操作是生成其文件,依此类推.但我想知道是否有某种方法可以通过system(),exec()等方式将进程发送到后台,并在进程完成后在同一个Web应用程序中收到通知,无论用户是否决定离开结算页面做其他任务.

Mik*_*gin 5

我建议使用一些队列服务.例如,RabbitMQ用于为所有任务创建队列.

您可以创建两个队列:

  1. 第一个用于在DB中生成发票 - >在客户端单击"生成账单"按钮后将项添加到此队列.在所有任务将被发送到队列之后,弹出消息将立即通知用户关于账单数量和估计的生成时间.您不必等到生成过程结束.

  2. 第二个用于生成PDF文件.在DB中成功生成发票后,它会从第一个队列中收到一个项目.工作人员(而真实流程)从此队列中获取项目,生成PDF,并在创建PDF时将项目标记为已完成.否则,工作人员将项目标记为未完成并增加尝试的计数器.达到最大尝试次数限制后,工作人员将该项目标记为失败,并将其从第二个队列中删除.

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

一个简单的例子:

SENDER

创建一个队列并向其发送一个项目.启动使用者之前启动发件人进程

$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();
Run Code Online (Sandbox Code Playgroud)

消费者

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();
Run Code Online (Sandbox Code Playgroud)

  • 对代码进行一些解释和评论会产生奇迹.只是说. (2认同)

hel*_*ava 5

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

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

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

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

  3. 还有另一个后台作业查询 ORDERINVOICE 表,可以说每 5 秒等待状态为 INVOICED 的记录。此作业将生成 pdf 并将状态标记为 DONE


现在来到更新WEB UI的部分。

对于实时通知,您将需要使用 Websockets,它将创建到您的服务器的持久连接,从而实现双向通信。

但是,如果您可以承受更新客户端进度的延迟,另一种方法可能是在 5/6 秒后通过 ajax 请求从 web ui 轮询以返回 ORDERINVOICE 表的状态。像待定:10,进行中:20,完成:3 等。


扩展需求

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

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

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