laravel-kafka
repository·master·Indexed 20 days ago
https://github.com/mateusjunges/laravel-kafkaA developer-friendly package for integrating Apache Kafka into Laravel applications. It provides a clean syntax for producing and consuming messages, featuring support for manual and asynchronous commits, custom committers, custom loggers, middlewares for message filtering, and graceful shutdown handling via pcntl.
What's inside laravel-kafka
- Laravel Kafka is a package designed to provide a clean and intuitive syntax for producing and consuming Kafka messages within Laravel applications. It aims to solve the issues of poor developer experience and difficult testing processes found in other Kafka integrations for Laravel.
Implement a custom committer
masterBy default, the library uses committers provided by
DefaultCommitterFactory. To implement custom logic for how messages are committed (e.g., only committing on success or handling DLQ logic), you must create a class that implements theCommitterinterface.The
Committerinterface provides hooks for both automatic and manual commit operations, ensuring that whether the consumer is performing automatic background commits or your code is calling manual commit methods, the logic remains consistent.use Junges\Kafka\Contracts\Committer; use RdKafka\Message; class MyCommitter implements Committer { public function commitMessage(Message $message, bool $success) : void { // Logic for automatic commits } public function commitDlq(Message $message) : void { // Logic for dead letter queue commits } public function commit(mixed $messageOrOffsets = null): void { // Logic for manual synchronous commits } public function commitAsync(mixed $messageOrOffsets = null): void { // Logic for manual asynchronous commits } }Important considerations for partition assignment
masterWhen working with partition discovery and assignment, keep the following in mind:
- Timing: Assignments occur during consumer group rebalancing (when consumers join or leave).
- Consumer Groups: Using
assignPartitions()(manual assignment) overrides the automatic behavior of consumer groups. - Rebalancing: Callbacks in
withPartitionAssignmentCallback()andassignPartitionsWithOffsets()are triggered every time a rebalance occurs. - Performance: Assignment callbacks should be fast; they block the rebalancing process.
- Error Handling: Exceptions inside callbacks can disrupt the rebalancing process; ensure they are handled gracefully.
Add context metadata to Dead Letter Queue messages
masterTo enrich DLQ messages with custom metadata (like IDs or correlation keys), throw an exception that implements the
Junges\Kafka\Contracts\ContextAwareinterface.When such an exception is thrown, the consumer merges the following into the message headers:
- Original message headers.
- Throwable headers (
kafka_throwable_message,kafka_throwable_code,kafka_throwable_class_name). - The array returned by the exception's
getContext()method.
Note: Header values must be strings. Arrays, objects, numbers, or empty string keys are ignored.
use Junges\Kafka\Contracts\ContextAware; use RuntimeException; use Throwable; class OrderProcessingException extends RuntimeException implements ContextAware { public function __construct( private array $context, string $message = 'Order processing failed', int $code = 0, ?Throwable $previous = null, ) { parent::__construct($message, $code, $previous); } public function getContext(): array { return $this->context; } } // Usage in a handler $consumer = \Junges\Kafka\Facades\Kafka::consumer() ->subscribe('orders') ->withDlq() ->withHandler(function($message) { $payload = $message->getBody(); throw new OrderProcessingException([ 'x-order-id' => (string)($payload['order_id'] ?? 'unknown'), 'x-user-id' => (string)($payload['user_id'] ?? 'unknown'), ]); }) ->build(); $consumer->consume();Configure Commit Modes: Auto vs Manual
masterYou can control when message offsets are committed to Kafka using two modes:
Auto Commit (Default)
Messages are automatically committed after the handler successfully processes them. This is the simplest mode.
Manual Commit
Provides full control over when offsets are committed. This is recommended for guaranteed processing or complex error handling. When using manual commit, the handler receives the
$consumerinstance, which provides the following methods:commit(): Commit current assignment offsets (synchronous).commit($message): Commit a specific message offset (synchronous).commitAsync(): Commit current assignment offsets (asynchronous).commitAsync($message): Commit a specific message offset (asynchronous).
// Manual Commit Example $consumer = \Junges\Kafka\Facades\Kafka::consumer() ->withManualCommit() ->withHandler(function($message, $consumer) { try { processMessage($message); $consumer->commit($message); // Synchronous commit } catch (Exception $e) { // Handle error } }) ->build(); $consumer->consume();Upgrade to v2.8 from v2.x
masterThe breaking change in v2.8 involves the
Junges\Kafka\Contracts\Handlercontract. Thehandlemethod (or__invokein handler classes) now requires a second parameter of typeJunges\Kafka\Contracts\MessageConsumer.If you use a closure as a handler, no changes are required as the closure signature already supports the second parameter.
class MyHandler implements Handler { - public function __invoke(ConsumerMessage $message): void { + public function __invoke(ConsumerMessage $message, MessageConsumer $consumer): void { // Process message here... } }Upgrade to v2.10 from v2.9
masterUpgrading to v2.10 contains no breaking changes. Key improvements include:
- ContextAware exceptions: Exceptions implementing
Junges\Kafka\Contracts\ContextAwarewill have their context forwarded as headers when messages are sent to the Dead Letter Queue (DLQ). - Async producer flush callback: You can now use
withFlushCallback()on the producer builder to receive a notification when async messages are flushed. - Public Interfaces:
@internalannotations have been removed from public interfaces and traits, making them safe for use in your own code.
- ContextAware exceptions: Exceptions implementing
Create a Kafka consumer using the Kafka facade
masterTo read messages from a Kafka topic, you must instantiate a consumer object using the
Kafka::consumer()method. This method returns an instance ofJunges\Kafka\Consumers\ConsumerBuilder, which allows you to configure the consumer's behavior before starting it.use Junges\Kafka\Facades\Kafka; $consumer = Kafka::consumer();Report bugs or ask questions via GitHub Issues
masterIf you encounter bugs, have general questions, or wish to suggest improvements forlaravel-kafka, please create an issue on the official GitHub repository. This is the preferred method for non-security related concerns.Use Queueable handlers to process Kafka messages in Laravel queues
masterYou can offload Kafka message processing to the Laravel queue system by implementing the
Illuminate\\Contracts\\Queue\\ShouldQueueinterface in your handler class. When a message is received by the Kafka consumer, a new job is dispatched to your Laravel queue for each message.Important Limitation: Because queued handlers run asynchronously via Laravel's queue workers, they do not have access to a
MessageConsumerinstance. You cannot perform actions that require a live connection to the Kafka consumer within the__invokemethod of a queued handler.use Illuminate\Contracts\Queue\ShouldQueue; use Junges\Kafka\Contracts\Handler as HandlerContract; use Junges\Kafka\Contracts\KafkaConsumerMessage; class Handler implements HandlerContract, ShouldQueue { public function __invoke(KafkaConsumerMessage $message): void { // Handle the consumed message. } }Create a Kafka Consumer class
masterKafka consumers in this package are implemented as standard Laravel Command classes. You define the Kafka topic, brokers, and the message handling logic within the
handle()method of your command.To handle messages, use
->withHandler()which provides aConsumerMessageinstance and aMessageConsumerinstance. If you need manual control over message commits, use->withManualCommit()and call$consumer->commit($message)inside the handler.<?php declare(strict_types=1); namespace App\Console\Commands\Consumers; use Illuminate\Console\Command; use Junges\Kafka\Facades\Kafka; use Junges\Kafka\Contracts\MessageConsumer; use Junges\Kafka\Contracts\ConsumerMessage; class MyTopicConsumer extends Command { protected $signature = "consume:my-topic"; protected $description = "Consume Kafka messages from 'my-topic'." public function handle() { $consumer = Kafka::consumer(['my-topic']) ->withBrokers('localhost:8092') ->withAutoCommit() ->withHandler(function(ConsumerMessage $message, MessageConsumer $consumer) { // Handle your message here // For manual commit control, use ->withManualCommit() and call $consumer->commit($message) }) ->build(); $consumer->consume(); } }Upgrade to v2.x from v1.13.x: Set `onStopConsuming` during build
masterIn v2.x,
onStopConsumingcallbacks must be defined during the consumer building process, rather than after thebuild()method has been called.$consumer = Kafka::consumer(['topic']) ->withConsumerGroupId('group') ->withHandler(new Handler) + ->onStopConsuming(static function () { + // Do something when the consumer stop consuming messages + }) ->build() - ->onStopConsuming(static function () { - // Do something when the consumer stop consuming messages - })