Náhľady obrázkov s RabbitMQ
Cieľ: po nahratí vytvoriť vo workeri náhľady v troch veľkostiach. Tento recept používa RabbitMQ cez LazyRabbitMqDriver, takže uvidíte, ako broker mení záruky.
Súbory
services:
rabbitmq:
image: rabbitmq:4-management
ports:
- "5672:5672" # AMQP
- "15672:15672" # management UI, guest / guest{
"require": {
"php": ">=8.4",
"ext-gd": "*",
"tomaj/hermes": "dev-master",
"php-amqplib/php-amqplib": "^3.7"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}<?php
declare(strict_types=1);
namespace App;
use PhpAmqpLib\Connection\AMQPLazyConnection;
use Tomaj\Hermes\Driver\LazyRabbitMqDriver;
final class Hermes
{
public static function driver(): LazyRabbitMqDriver
{
// Lazy: the TCP connection opens on the first send() or wait(), not here.
$connection = new AMQPLazyConnection(getenv('RABBITMQ_HOST') ?: '127.0.0.1', 5672, 'guest', 'guest');
// Queue "thumbnails", no extra AMQP message properties, sleep 1 second between empty polls.
return new LazyRabbitMqDriver($connection, 'thumbnails', [], 1);
}
}<?php
declare(strict_types=1);
use App\Hermes;
use Tomaj\Hermes\Emitter;
use Tomaj\Hermes\Message;
require __DIR__ . '/../vendor/autoload.php';
if (($_FILES['image']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
http_response_code(422);
echo "Upload a file as the image field.\n";
return;
}
$name = bin2hex(random_bytes(8)) . '.jpg';
$path = __DIR__ . '/../storage/originals/' . $name;
@mkdir(dirname($path), 0o775, true);
move_uploaded_file($_FILES['image']['tmp_name'], $path);
$emitter = new Emitter(Hermes::driver());
$emitter->emit(new Message('image.thumbnails', [
'path' => realpath($path),
'sizes' => [160, 640, 1280],
]));
http_response_code(202);
echo "Uploaded {$name}, thumbnails are on the way.\n";<?php
declare(strict_types=1);
namespace App;
use RuntimeException;
use Tomaj\Hermes\Handler\HandlerInterface;
use Tomaj\Hermes\MessageInterface;
final class ThumbnailHandler implements HandlerInterface
{
public function __construct(private string $directory) {}
#[\Override]
public function handle(MessageInterface $message): bool
{
['path' => $path, 'sizes' => $sizes] = $message->getPayload();
$source = @imagecreatefromstring((string) @file_get_contents($path));
if ($source === false) {
// A broken upload stays broken. Return false instead of throwing, so nothing is retried.
return false;
}
$base = pathinfo($path, PATHINFO_FILENAME);
foreach ($sizes as $width) {
$thumb = imagescale($source, (int) $width);
if ($thumb === false || !imagejpeg($thumb, "{$this->directory}/{$base}-{$width}.jpg", 82)) {
throw new RuntimeException("Cannot write the {$width}px thumbnail of {$path}");
}
}
return true;
}
}<?php
declare(strict_types=1);
use App\Hermes;
use App\ThumbnailHandler;
use Tomaj\Hermes\Dispatcher;
use Tomaj\Hermes\Shutdown\SharedFileShutdown;
require __DIR__ . '/../vendor/autoload.php';
$directory = __DIR__ . '/../storage/thumbnails';
@mkdir($directory, 0o775, true);
$driver = Hermes::driver();
// RabbitMQ has no Hermes shutdown helper of its own. A shared file works with any driver.
$driver->setShutdown(new SharedFileShutdown('/tmp/hermes-thumbnails-shutdown'));
$driver->setMaxProcessItems(500); // GD leaks a little with big images, start fresh now and then
$dispatcher = new Dispatcher($driver);
$dispatcher->registerHandler('image.thumbnails', new ThumbnailHandler($directory));
$dispatcher->handle();Spustenie
composer install
docker compose up -d
php bin/worker.php # terminal 1
php -S localhost:8003 -t public # terminal 2
curl -F image=@photo.jpg http://localhost:8003/
ls storage/thumbnailsObjavia sa tri súbory: …-160.jpg, …-640.jpg a …-1280.jpg. make e2e nahrá vygenerovaný JPEG a skontroluje tri veľkosti. Rozhranie RabbitMQ na http://localhost:15672 (guest / guest) ukazuje frontu thumbnails.
Ak chcete workera zastaviť po aktuálnej správe, touchnite zdieľaný súbor:
touch /tmp/hermes-thumbnails-shutdownÚskalia
- Vo verzii 5.x sa správy potvrdzujú pri prijatí. Driver v 5.x konzumuje s
no_ack = true. Ak worker zomrie počas zmenšovania, nahratý obrázok nedostane náhľady. Hermes 6.0 namiesto toho potvrdzuje po handleri, takže sa obrázok doručí znova (#70). - Bez oneskorenia, teda bez skutočného opakovania.
RetryTraitfunguje, ale kópia sa publikuje hneď a všetky pokusy prebehnú v priebehu sekúnd. Handler ho nepoužíva. Pokazený súbor vrátifalsea plný disk vyhodí výnimku. - Vo verzii 5.x fronta nie je durable. Reštart RabbitMQ zahodí frontu aj s jej správami. Hermes 6.0 ju deklaruje ako durable. Pozrite RabbitMQ.
- Bez priorít.
setupPriorityQueue()na tomto driveri vyhodíNotSupportedException. - GD potrebuje pamäť na celý obrázok. Fotka 6000 × 4000 potrebuje asi 100 MB.
memory_limitnastavte pre workera, nie pre webovú aplikáciu.
Celý zdrojový kód
Všetky súbory tohto receptu, s README.md, Makefile a kontrolou e2e.sh, sú v examples/thumbnails na GitHube. Naklonujte repozitár, spustite cd examples/thumbnails a make install up, potom make e2e.
Časté otázky
Čo sa stane, ak worker zomrie počas zmenšovania?
Vo verzii 5.x tento obrázok nedostane náhľady, pretože RabbitMQ doručenie uzavrel pri prijatí (no_ack). Majte príkaz, ktorý znova vloží obrázky bez náhľadov. Vo verzii 6.0 ho RabbitMQ doručí znova.
Prečo tu nie je opakovanie?
RabbitMQ ignoruje executeAt, takže opakovanie by prebehlo hneď. Pokazený obrázok namiesto toho vráti false.
Zachovajú sa správy po reštarte RabbitMQ?
Vo verzii 5.x nie, tá deklaruje non-durable frontu. Od 6.0 je fronta durable a správy sú perzistentné. Pozrite RabbitMQ.
Ako zastaviť workera?
Touchnite /tmp/hermes-thumbnails-shutdown. SharedFileShutdown funguje s akýmkoľvek driverom.
Koľko pamäte GD potrebuje?
Približne šírka × výška × 4 bajty na dekódovaný obrázok, teda zhruba 100 MB pre fotku 6000 × 4000.