【问题标题】:How to delegate long background tasks from web, and recover control when done [closed]如何从网络委派长期后台任务,并在完成后恢复控制[关闭]
【发布时间】:2018-12-22 01:10:53
【问题描述】:

我们有一个每月两次的 ERP,必须对过去两周的所有订单进行结算。这样我们的客户选择所有这些订单,按下“生成账单”按钮,一系列顺序 ajax http 请求完成,每张发票一次,同时弹出消息通知他们过程。

首先,所有的发票都是按顺序在数据库中生成的,就像前面提到的那样,一旦这个过程完成,就轮到生成 PDF 文件了。这也是通过顺序 ajax 请求实现的。

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

如果过程中途停止,可能会导致许多发票没有生成 PDF 文件。这很关键,因为当他们发送所有要打印的发票时,如果必须动态生成 PDF 内容并将其发送到打印机,则此操作比从现有文件中读取内容要多得多。

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

【问题讨论】:

  • 生成 PDF 的队列(例如 beanstalkd)将是一个好的开始,并且可以在某个地方存储“用户是否收到有关生成的 PDF 的通知”
  • 有趣。我将对队列和beantalkd进行研究。第二部分,你说的是数据库吗?

标签: php ajax http pdf-generation background-process


【解决方案1】:

您可以使用此功能在后台进程中执行您的 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

在更改时,您可以从该日志文件/DB 中读取进度

当用户上传视频时,我已经为我的视频完成了此操作,我需要使用 FFMPEG 创建工具提示缩略图、海报等,等待时间太长,我只是在后台进程中运行我的脚本。

【讨论】:

    【解决方案2】:

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

    面临的挑战是您的用户无法控制 cron 作业何时运行,因此如果他们错过了安排作业的窗口,他们将无能为力。如果您在仍会生成 pdf 的应用程序中进行计算,那么它在 Web 服务器上也是资源密集型的

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

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

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

    创建一个 php 脚本,它将查询您的数据库以获取满足此条件的记录 - 用户已请求生成发票/pdf。

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

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

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

    Library to generate PDF using JS

    This线程我可以帮助您解决使用网络工作者时遇到的一些挑战

    使用 HTML5/Web Worker 将创建发票的负载转移到客户端/浏览器。用户选择订单,您的应用程序将订单的键值对 (uniqueId:state) 存储在浏览器本地存储中。

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

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

    这两个线程应该可以指导您。

    Chrome extension: accessing localStorage in content script

    Chrome extension that runs even if chrome closed

    【讨论】:

      【解决方案3】:

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

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

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

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

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


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

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

      但是,如果您可以承受延迟更新客户端的进度,另一种方法是在 5/6 秒后通过 ajax 请求从 web ui 轮询以返回 ORDERINVOICE 表的状态。 Like pending:10, In progress: 20, Done: 3 etc.


      扩展需求

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

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

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

      【讨论】:

      • 非常感谢!这就是我需要的。由于这不是一项密集的任务(每两周一次),我认为没有必要使用队列,也没有 websockets。我认为仅使用 ajax 轮询就可以解决问题。
      【解决方案4】:

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

      您可以创建两个队列:

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

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

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

      一个简单的例子:

      发件人

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

      $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();
      

      【讨论】:

      • 对代码进行一点解释和评论可以创造奇迹。只是说。
      【解决方案5】:

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

      是的,您应该为此维护一个任务队列。这可能是一个数据库表,具有诸如 task_type、task_data、task_status、task_dependency 等属性。

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

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

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

      您可以从前端执行 Ajax long polling 来检查您的 pdf 生成任务状态(1-已完成)。如果是,则可以通知用户。

      为此,您可以开发简单的任务运行程序(可能来自 Go、Nodejs) 否则你可以使用可用的任务运行器

      【讨论】:

      • 谢谢!听起来很麻烦,但现在这是最好的答案,因为它更好地解决了我的问题。
      【解决方案6】:

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

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

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

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

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

      【讨论】:

      • 我不确定 cron 作业在这里是否真的有帮助,因为这些作业是在固定时间安排的,而我所采取的行动会在用户需要时随时发生。
      • 但是如果想通过用户界面触发进程,则必须保持连接直到进程完成。否则进程在没有完成的情况下终止。
      猜你喜欢
      • 1970-01-01
      • 2010-12-24
      • 2016-06-02
      • 2016-01-31
      • 2011-12-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      相关资源
      最近更新 更多