tomaj/hermes

简单的 PHP 背景处理库

4.2.0 2024-04-29 12:22 UTC

README

后台任务处理 PHP 库

Scrutinizer Code Quality Latest Stable Version Phpstan

什么是 Hermes?

如果您需要在您的 web 应用程序外部处理一些任务,您可以使用 Hermes。Hermes 为从 HTTP 线程向离线处理作业发送消息提供消息代理。推荐用于发送电子邮件、调用其他 API 或其他耗时操作。

Hermes 的另一个目标是可变,可以使用各种消息代理,如 Redis、rabbit、数据库,并能够轻松为其他消息解决方案创建新驱动程序。还可以简单地创建工作者以在指定事件上执行任务。

安装

此库需要 PHP 7.2 或更高版本。

推荐的安装方法是使用 Composer

$ composer require tomaj/hermes

该库符合 PSR-1PSR-2PSR-3PSR-4

可选依赖

Hermes 能够使用与 psr/log 接口兼容的记录器记录活动。有关更多信息,请参阅 psr/log

该库无需记录器即可运行,但维护者建议安装 monolog 以进行记录。

支持驱动程序

目前 Hermes 库附带 3 个驱动程序和一个独立包中的一个驱动程序

注意:您必须安装所有第三方库以初始化到这些驱动程序的连接。例如,您必须将 nrk/predis 添加到您的 composer.json 并创建到您的 Redis 实例的连接。

概念 - Hermes 如何工作?

Hermes 作为事件从您的 PHP 请求在 web 服务器上到 CLI 上特定处理器的发射器和调度器工作。基本上像这样

--> HTTP request to /file.php -> emit(Message) -> Hermes Emitter
                                                             \  
                                                 Queue (Redis, rabbit etc.)
                                                             /
--> running PHP CLI file waiting for new Message-s from Queue
        when received a new message it calls registered handler to process it.

您必须在您的应用程序中实现以下四个步骤

  1. 选择您想使用的驱动程序并将其注册到调度器和发射器
  2. 在您需要后台处理某些内容时发出事件
  3. 编写一个处理程序类以处理第 2 步中的消息
  4. 创建一个将在您的服务器上“永远运行”的 PHP 文件并在此处运行调度器

如何使用

此简单示例演示了使用 Redis 驱动程序,并展示了如何在后台发送电子邮件的示例。

发出事件

发射消息(在应用程序的任何位置,简单快捷)。

use Redis;
use Tomaj\Hermes\Message;
use Tomaj\Hermes\Emitter;
use Tomaj\Hermes\Driver\RedisSetDriver;

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$driver = new RedisSetDriver($redis);
$emitter = new Emitter($driver);

$message = new Message('send-email', [
	'to' => 'test@test.com',
	'subject' => 'Testing hermes email',
	'message' => 'Hello from hermes!'
]);

$emitter->emit($message);

处理事件

为了处理事件,我们需要创建一些将在CLI中运行的PHP文件。我们可以实现一个简单的实现并注册这个简单的处理器。

# file handler.php
use Redis;
use Tomaj\Hermes\Driver\RedisSetDriver;
use Tomaj\Hermes\Dispatcher;
use Tomaj\Hermes\Handler\HandlerInterface;

class SendEmailHandler implements HandlerInterface
{
    // here you will receive message that was emitted from web application
    public function handle(MessageInterface $message)
    {
    	$payload = $message->getPayload();
    	mail($payload['to'], $payload['subject'], $payload['message']);
    	return true;
    }
}


// create dispatcher like in the first snippet
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$driver = new RedisSetDriver($redis);
$dispatcher = new Dispatcher($driver);

// register handler for event
$dispatcher->registerHandler('send-email', new SendEmailHandler());

// at this point this script will wait for new message
$dispatcher->handle();

要在服务器上运行 handler.php,您可以使用像 upstartsupervisordmonitgod 或任何其他替代工具。

日志记录

Hermes可以使用任何psr/log 日志记录器。您可以为分发器或发射器设置日志记录器,查看发送到分发器或发射器的消息类型以及处理器处理消息的时间。如果您将 Psr\Log\LoggerAwareTrait(或实现 Psr\Log\LoggerAwareInterface)特质添加到处理器中,您也可以在处理器中使用日志记录器(分发器和发射器会自动注入)。

使用 monolog 的基本示例

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// create a log channel
$log = new Logger('hermes');
$log->pushHandler(new StreamHandler('hermes.log'));

// $driver = ....

$dispatcher = new Dispatcher($driver, $log);

如果想在处理器中记录一些信息

use Redis;
use Tomaj\Hermes\Driver\RedisSetDriver;
use Tomaj\Hermes\Dispatcher;
use Tomaj\Hermes\Handler\HandlerInterface;
use Psr\Log\LoggerAwareTrait;

class SendEmailHandlerWithLogger implements HandlerInterface
{
    // enable logger
    use LoggerAwareTrait;

    public function handle(MessageInterface $message)
    {
        $payload = $message->getPayload();

        // log info message
    	$this->logger->info("Trying to send email to {$payload['to']}");

    	mail($payload['to'], $payload['subject'], $payload['message']);
    	return true;
    }
}

重试

如果您需要重试,当它们因某些原因失败时,可以在处理器中添加 RetryTrait。如果您想,您可以覆盖此特质中的 maxRetry() 方法来指定Hermes将尝试运行handle()多少次。注意:如果您想使用重试,必须使用支持延迟执行的驱动程序($executeAt 消息参数)

declare(strict_types=1);

namespace Tomaj\Hermes\Handler;

use Tomaj\Hermes\MessageInterface;

class EchoHandler implements HandlerInterface
{
    use RetryTrait;

    public function handle(MessageInterface $message): bool
    {
        throw new \Exception('this will always fail');
    }
    
    // optional - default is 25
    public function maxRetry(): int
    {
        return 10;
    }
}

优先级

您可以使用不同的优先级声明多个队列,并确保高优先级队列中的消息首先被处理。

使用Redis驱动的示例

use Tomaj\Hermes\Driver\RedisSetDriver;
use Tomaj\Hermes\Emitter;
use Tomaj\Hermes\Message;
use Tomaj\Hermes\Dispatcher;

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$driver = new RedisSetDriver($redis);
$driver->setupPriorityQueue('hermes_low', Dispatcher::DEFAULT_PRIORITY - 10);
$driver->setupPriorityQueue('hermes_high', Dispatcher::DEFAULT_PRIORITY + 10);

$emitter = new Emitter($driver);
$emitter->emit(new Message('type1', ['a' => 'b'], Dispatcher::DEFAULT_PRIORITY - 10));
$emitter->emit(new Message('type1', ['c' => 'd'], Dispatcher::DEFAULT_PRIORITY + 10));

一些细节

  • 您可以使用来自 Dispatcher 类的优先级常量,但您也可以使用任何数字
  • 高优先级队列的消息将被首先处理
  • Dispatcher::handle() 方法中,您可以提供一个队列名称数组,创建一个只处理一个或多个选定队列的工作进程

优雅关闭

Hermes工作进程可以优雅地停止。

如果初始化 Dispatcher 时提供了 Tomaj\Hermes\Shutdoown\ShutdownInteface 的实现,Hermes将在处理每个消息后检查 ShutdwnInterface::shouldShutdown()。如果它返回 true,Hermes将关闭(请注意,记录了通知)。

警告:此库不提供重启功能,应由您使用的进程控制器处理以保持Hermes运行(例如,launchd、daemontools、supervisord等)。

目前实现了两种方法。

共享文件关闭

通过触摸预定义文件来启动关闭。

$shutdownFile = '/tmp/hermes_shutdown';
$shutdown = Tomaj\Hermes\Shutdown\SharedFileShutdown($shutdownFile);

// $log = ...
// $driver = ....
$dispatcher = new Dispatcher($driver, $log, $shutdown);

// ...

// shutdown can be triggered be calling `ShutdownInterface::shutdown()`
$shutdown->shutdown();

Redis关闭

通过将时间戳存储到Redis的预定义关闭键来启动关闭。

$redisClient = new Predis\Client();
$redisShutdownKey = 'hermes_shutdown'; // can be omitted; default value is `hermes_shutdown`
$shutdown = Tomaj\Hermes\Shutdown\RedisShutdown($redisClient, $redisShutdownKey);

// $log = ...
// $driver = ....
$dispatcher = new Dispatcher($driver, $log, $shutdown);

// ...

// shutdown can be triggered be calling `ShutdownInteface::shutdown()`
$shutdown->shutdown();

扩展Hermes

如果您需要处理许多消息,您可以非常快速地扩展您的Hermes工作进程。您只需运行多个处理器的实例 - 将注册处理器到分发器并运行 $dispatcher->handle() 的CLI文件。您也可以将您的源代码放置在多台机器上,并将其扩展到您想要的节点数。但这将有助于您拥有支持这两件事的驱动程序

  1. 驱动程序需要能够在网络上工作
  2. 一条消息必须只能被一个工作进程接收

如果您确保这一点,Hermes将正常工作。Rabbit驱动程序或Redis驱动程序可以处理这些问题,并且这些产品也是为处理大量负载而设计的。

扩展Hermes

Hermes是用相互依赖的接口编写的独立类。您可以轻松更改类的实现。例如,您可以创建一个新的驱动程序,使用另一个日志记录器。或者,如果您真的想,您还可以创建自己的消息格式,该格式将通过您的自定义序列化器序列化并发送到驱动程序。

如何编写您的驱动程序

每个驱动程序都必须实现具有2个方法(sendwait)的Tomaj\Hermes\Driver\DriverInterface。一个简单的驱动程序将使用Gearman作为驱动程序。

namespace My\Custom\Driver;

use Tomaj\Hermes\Driver\DriverInterface;
use Tomaj\Hermes\Message;
use Closure;

class GearmanDriver implements DriverInterface
{
	private $client;

	private $worker;

	private $channel;

	private $serializer;

	public function __construct(GearmanClient $client, GearmanWorker $worker, $channel = 'hermes')
	{
		$this->client = $client;
		$this->worker = $worker;
		$this->channel = $channel;
		$this->serializer = $serialier;
	}

	public function send(Message $message)
	{
		$this->client->do($this->channel, $this->serializer->serialize($message));
	}

	public function wait(Closure $callback)
	{
		$worker->addFunction($this->channel, function ($gearmanMessage) use ($callback) {
			$message = $this->serializer->unserialize($gearmanMessage);
			$callback($message);
		});
		while ($this->worker->work());
	}
}

如何编写自己的序列化器

如果你想在驱动程序中使用自己的序列化器,你必须创建一个新的类来实现Tomaj\Hermes\MessageSerializer,并且需要一个支持它的驱动程序。你可以在你的驱动程序中添加Tomaj\Hermes\Driver\SerializerAwareTrait特质,这将向你的驱动程序添加setSerializer方法。

简单的序列化器,它将使用库jms/serializer

namespace My\Custom\Serializer;

use Tomaj\Hermes\SerializerInterface;
use Tomaj\Hermes\MessageInterface;

class JmsSerializer implements SerializerInterface
{
	public function serialize(MessageInterface $message)
	{
		$serializer = JMS\Serializer\SerializerBuilder::create()->build();
		return $serializer->serialize($message, 'json');
	}

	public function unserialize($string)
	{
		$serializer = JMS\Serializer\SerializerBuilder::create()->build();
		return $serializer->deserialize($message, 'json');
	}
}

计划执行

从版本2.0开始,你可以将一个未来的时间戳作为第四个参数添加到消息中。这个消息将在该时间之后被处理。目前这个功能在RedisSetDriver和PredisSetDriver中得到支持。

升级

从v3到v4

  • 将“重启”重命名为“关闭”
    • 命名变更以反映Hermes的功能。它可以优雅地停止自己的进程,但Hermes的重启(重新启动)必须由外部进程/库来处理。因此这被称为关闭而不是重启。
    • 将RestartInterface重命名为ShutdownInterface
    • 以及所有实现都更改了命名空间名称和类名称

变更日志

请参阅CHANGELOG以获取有关最近更改的更多信息。

测试

$ composer test

贡献

请参阅CONTRIBUTINGCONDUCT以获取详细信息。

安全

如果你发现任何与安全相关的问题,请通过电子邮件tomasmajer@gmail.com联系,而不是使用问题跟踪器。

许可协议

MIT许可(MIT)。有关更多信息,请参阅许可文件