Skip to content

Architecture ​

Hermes is a library inside your process. The queue is the only thing shared between the request and the worker.

emit() returns as soon as send() returns. handle() is the call that blocks. The handler never runs in the request that emitted the message.

What crosses the process boundary ​

Only the serialized message. MessageSerializer writes JSON with id, type, created, payload, execute_at, and retries. The handler class is not serialized. The worker must register handlers for the types it wants to run.

Priority is not in the JSON. Redis stores it by choosing which set key to SADD. After a delayed message is promoted, send() is called with the default priority, so the message lands on the default key. The same happens for a retry: Dispatcher::retryMessage() calls send() with one argument.

Who removes the message ​

The Redis drivers remove the message before the handler runs. From 6.0, RabbitMQ and SQS remove it only after the callback returns.

DriverRemoval
RedisSetDriver, PredisSetDriverSPOP on the set, then the callback
LazyRabbitMqDriverbasic_ack after the callback (6.0+)
AmazonSqsDriverunserialize, the callback, then deleteMessage (6.0+)

A handler that returns false cannot put that delivery back. A thrown exception can publish a new copy when retry is enabled. If the process dies after the pop and before handle() finishes, that delivery is gone.

On Redis and Predis this is at-most-once for a single delivery. On RabbitMQ and SQS (6.0+) it is at-least-once. Either way a process manager should restart a worker that exits.

Life of one message ​

Several workers ​

Three PHP workers connected to one Redis set

Start the same script more than once. Redis SPOP removes a member, so two workers do not both receive it. RabbitMQ and SQS do the equivalent for their own consumers. The workers do not coordinate with each other inside Hermes. See Production deployment.

What Hermes does not do ​

  • It does not restart the process. Shutdown only makes handle() return.
  • It does not sort Redis sets. SPOP is not FIFO.
  • It polls priority queues from the highest number down. 5.x used reverse registration order (issue #50).
  • It does not delay RabbitMQ or SQS messages. executeAt is stored in the JSON and ignored by those send() methods.
  • It does not ship a ZeroMQ driver. That driver left the package in 3.0.0 and lives in tomaj/hermes-zmq-driver.

FAQ ​

What crosses from the web process to the worker? ​

Only the serialized message JSON: id, type, created, payload, execute_at, and retries. Handlers, loggers, and connections are built separately in each process.

When is a message removed from the broker? ​

On Redis (SPOP), before the handler runs, so a crash during handle() loses that delivery. On RabbitMQ (basic_ack) and SQS (deleteMessage), from 6.0, only after the callback returns; a crash makes the broker redeliver it (#70, #69).

Is delivery at-least-once or at-most-once? ​

It depends on the driver. Redis and Predis are at-most-once: a killed process loses the delivery, and only a thrown exception can publish a new copy through Retry. RabbitMQ and SQS are at-least-once from 6.0: the broker redelivers a message that was not acknowledged, so handlers should be idempotent.

Do several workers coordinate? ​

No. Each worker pops from the broker, and the broker hands each message to one of them. Start more processes to scale. See Production deployment.

Does Hermes delay messages on RabbitMQ or SQS? ​

No. executeAt is stored in the JSON and ignored by those drivers, so the message is delivered at once. See Delayed execution.

Released under the MIT License.