Skip to content

Support iterators in MessageDispatcher::dispatch to reduce memory footprint #235

@lgrossi

Description

@lgrossi

Hey Frank, hope you're doing well!

I ran into something interesting while working with MessageDispatcher. I was dispatching a big batch of messages when I noticed a memory spike. After digging in, I realized it’s because dispatch(Message ...$messages) forces everything into memory, even if you're using a generator.

Would it make sense to tweak it to also accept an iterable ?
That way, it could process messages in chunks instead of loading everything upfront.

Current Issue

  • Message ...$messages always expands all values into an array before calling the method.
  • This means generators are fully materialized, which can cause unnecessary high memory usage for large batches.

Proposed Solution

Keep the existing variadic behavior but also support iterables efficiently:

interface MessageDispatcher
{
    /**
     * @param Message|iterable<Message> $messages
     */
    public function dispatch(Message|iterable $messages, Message ...$variadic);
}

✅ This allows:

  • Comma-separated messagesdispatch($msg1, $msg2, $msg3)
  • Efficient batch dispatching using generatorsdispatch($generator)

Example: Batched Dispatching Implementation

final class DispatchMessagesInChunks implements MessageDispatcher
{
    public function __construct(
        private readonly MessageDispatcher $dispatcher,
        private readonly int $batchSize,
    ) {}

    /**
     * @param Message|iterable<Message> $messages
     */
    public function dispatch(Message|iterable $messages, Message ...$variadic): void
    {
        $messages = is_iterable($messages) ? $messages : [$messages];

        // The actual chunk implementation is out of scope
        foreach (chunk($messages, $this->batchSize) as $chunk) {
            $this->dispatcher->dispatch(...$chunk);
        }

        if ($variadic !== []) {
            $this->dispatcher->dispatch([], ...$variadic);
        }
    }
}

Benchmark

I ran a quick test comparing the variadic approach vs generator-based dispatching:

class BenchMarkDispatcher implements MessageDispatcher
{
    /**
     * @param Message|iterable<Message> $messages
     */
    public function dispatch(Message|iterable $messages, Message ...$variadic): void
    {
        $count = 0;
        $messages = is_iterable($messages) ? $messages : [$messages];

        foreach ([...$messages, ...$variadic] as $ignored) {
            $count++;
        }

        echo "Messages dispatched: $count\n";
    }
}

$generator = $messages = (function (int $size) {
    for ($i = 0; $i < $size; $i++) {
        yield new Message("Message #$i");
    }
});

$dispatcher = new DispatchMessagesInChunks(new BenchMarkDispatcher(), 1_000);

// $dispatcher->dispatch(...$generator(1_000_000));
// $dispatcher->dispatch($generator(1_000_000));

echo "Peak memory usage: " . memory_get_peak_usage() . " bytes\n";

Variadic Version (Forces Everything into Memory)

...
Batch finished, memory usage: 154648280 bytes
Messages dispatched: 1000000
Peak memory usage: 162721552 bytes

Generator Version (Streaming Efficiently)

...
Batch finished, memory usage: 556936 bytes
Messages dispatched: 1000000
Peak memory usage: 693984 bytes

Why This Matters?

I get that dispatching millions of messages might not be a typical use case, but having a built-in way to handle big batches efficiently means we don’t need special wrappers around the core classes. This keeps everything within the existing framework while avoiding unnecessary memory overhead.

Would love to hear your thoughts, let me know what you think! 🚀

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions