Skip to content

Broadcastable trait pins broadcast jobs to the event-sourcing queue, making publish retries unconfigurable #1

Description

@EgorGruzdev

Summary

TTBooking\TicketAllocator\Concerns\Broadcastable declares:

public string $queue = 'event-sourcing';

Illuminate\Broadcasting\BroadcastManager::queue() reads that property when picking the queue for the BroadcastEvent job, and it does so before any framework-level routing (Laravel 13.21.1):

$queue = match (true) {
    method_exists($event, 'broadcastQueue') => $event->broadcastQueue(),
    isset($event->broadcastQueue)          => $event->broadcastQueue,
    isset($event->queue)                   => $event->queue,   // ← the trait wins here
    default                                => null,
};

if (is_null($queue)) {                                          // ← never reached for package events
    $queue = $this->getAttributeValue($event, QueueAttribute::class, 'queue')
        ?? $this->resolveQueueFromQueueRoute($event)
        ?? null;
}

So an application cannot move package broadcasts to a dedicated queue: both Queue::route(ShouldBroadcast::class, ...) (Laravel 11+) and #[Queue] are short-circuited by the property. The only workarounds are patching the vendor file or overriding every event class.

Why it matters

Publishing to a websocket server is a network call that fails transiently, and its retry policy needs to differ from the projector queue's:

  • the projector queue is usually one worker (balance: simple, processes: 1) with a small tries and no backoff — retries fire back to back within milliseconds;
  • a failed publish needs a spaced-out ladder, otherwise every retry hits the same network blip and the event is lost for good.

Numbers from a 5-day window on our install (self-hosted Reverb, transient TLS timeouts on the route between the app host and the websocket host), failed BroadcastEvent jobs by queue:

queue failed jobs
event-sourcing (package events) 266
broadcasts (app events, retried with backoff) 31

The 266 are TicketMetricsAdjusted (144), TicketClosed (41), TicketCreated (36), TicketBound (28), Operator* (16) — every one a UI update that never reached the operators' screens. The application-level fix (dedicated queue + backoff ladder) had been deployed for two weeks and, unnoticed, applied to none of the package events.

There is a second effect: publishes and projector jobs share one single-process queue, so a stalled publish (up to connect_timeout) blocks the projector jobs queued behind it, and server-side state lags during an episode as well.

Proposal

Add a broadcastQueue() method to the trait, backwards compatible by default:

public function broadcastQueue(): ?string
{
    return config('ticket-allocator.broadcast_queue', $this->queue);
}

with 'broadcast_queue' => env('TICKET_ALLOCATOR_BROADCAST_QUEUE') in the package config — unset means today's behaviour. Returning null is useful in itself: BroadcastManager then falls through to #[Queue] / Queue::route() / the default queue, letting the app apply its own global broadcast policy.

A method rather than a public $broadcastQueue property on purpose — broadcastWith() in the same trait builds the payload by reflecting over public properties, so a new public property is the riskier option.

Happy to send a PR if this shape looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions