Skip to content

Fix delayed retry push middleware pipeline#274

Closed
samdark wants to merge 3 commits intorm-push-middlewaresfrom
fix-delay-middleware-pipeline
Closed

Fix delayed retry push middleware pipeline#274
samdark wants to merge 3 commits intorm-push-middlewaresfrom
fix-delay-middleware-pipeline

Conversation

@samdark
Copy link
Copy Markdown
Member

@samdark samdark commented Apr 30, 2026

Summary

  • Keep delayed retries inside the real push middleware pipeline.
  • Expose runtime push middleware overrides on QueueInterface.
  • Preserve debug collection when QueueDecorator returns queue instances with overridden middleware stacks.
  • Remove NoopMessageHandlerPush, which made delay middleware run around a no-op instead of the actual push.
  • Add a regression test using a real Queue and a delay-aware adapter.

Why

DelayMiddlewareInterface extends MiddlewarePushInterface, so a delay implementation may reasonably apply behavior around the downstream handler rather than only transforming the message.

Old behavior was effectively:

set delay = 60
push message to adapter
clear delay

The current branch changed that to:

set delay = 60
noop handler returns message, no adapter push happens
clear delay

push message to adapter, but delay is no longer active

This PR changes ExponentialDelayMiddleware to use:

$queue
    ->withMiddlewaresAdded($delayMiddleware->withDelay($delay))
    ->push($envelope);

That makes the delay middleware wrap the real final push handler again:

set delay = 60
push message to adapter
clear delay

The regression test uses a real Queue and an AdapterInterface implementation that records the active delay value at the exact moment push() is called. The old no-op-handler approach records null; the fixed pipeline records the configured delay.

Validation

  • git diff --check
  • php -l src/QueueInterface.php
  • php -l src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php
  • php -l src/Debug/QueueDecorator.php
  • php -l stubs/StubQueue.php
  • php -l tests/App/DummyQueue.php
  • php -l tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php
  • ./vendor/bin/phpunit tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php tests/Integration/MiddlewareTest.php
  • ./vendor/bin/phpunit

Copilot AI review requested due to automatic review settings April 30, 2026 17:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes delayed retry behavior by ensuring delay middlewares run inside the real push middleware pipeline (wrapping the actual adapter push), and exposes runtime push middleware overrides on QueueInterface to support that reliably.

Changes:

  • Add withMiddlewares() / withMiddlewaresAdded() to QueueInterface and implement them across queue implementations/decorators.
  • Update ExponentialDelayMiddleware to retry via a queue instance with the delay middleware added to the push pipeline (removing the prior no-op handler approach).
  • Add a regression test that verifies the delay is active at the exact moment the adapter’s push() is invoked; update affected mocks accordingly.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/QueueInterface.php Exposes runtime push middleware override APIs used by retry/delay handling.
src/Middleware/FailureHandling/Implementation/ExponentialDelayMiddleware.php Uses withMiddlewaresAdded(...)->push(...) so delay middleware wraps the actual push.
src/Debug/QueueDecorator.php Preserves debug collection when returning queue instances with overridden middleware stacks.
src/Middleware/Push/NoopMessageHandlerPush.php Removed now-unnecessary no-op push handler.
stubs/StubQueue.php Updated to satisfy new QueueInterface methods.
tests/App/DummyQueue.php Updated to satisfy new QueueInterface methods.
tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php Adds regression coverage with a delay-aware adapter + updates mocks.
tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php Updates queue mock to support new middleware override call chain.
tests/Integration/MiddlewareTest.php Updates queue mock to support new middleware override call chain.
Comments suppressed due to low confidence (1)

tests/Unit/Middleware/FailureHandling/Implementation/ExponentialDelayMiddlewareTest.php:280

  • These helper classes are declared in the main test namespace/file. To reduce the chance of class-name collisions across the test suite and to make intent clearer, consider moving them into a dedicated Support namespace/file (as done in other middleware tests) or using anonymous classes scoped to the test method.
final class AdapterContextDelayMiddleware implements DelayMiddlewareInterface
{
    private float $delay = 0.0;

    public function __construct(
        private readonly DelayAwareAdapter $adapter,
    ) {}

    public function withDelay(float $seconds): self
    {
        $new = clone $this;
        $new->delay = $seconds;

        return $new;
    }

    public function processPush(MessageInterface $message, MessageHandlerPushInterface $handler): MessageInterface
    {
        $this->adapter->activeDelay = $this->delay;

        try {
            return $handler->handlePush($message);
        } finally {
            $this->adapter->activeDelay = null;
        }
    }
}

final class DelayAwareAdapter implements AdapterInterface
{
    /**
     * @var list<float|null>
     */
    public array $delaysDuringPush = [];

    public ?float $activeDelay = null;

    public function runExisting(callable $handlerCallback): void {}

    public function status(string|int $id): MessageStatus
    {
        return MessageStatus::DONE;
    }

    public function push(MessageInterface $message): MessageInterface
    {
        $this->delaysDuringPush[] = $this->activeDelay;

        return $message;
    }

    public function subscribe(callable $handlerCallback): void {}
}

final class ThrowingFailureHandler implements MessageFailureHandlerInterface
{
    public function handleFailure(FailureHandlingRequest $request): FailureHandlingRequest
    {
        throw $request->getException();
    }
}


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/QueueInterface.php
Comment on lines 20 to +22

public function withMiddlewares(MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions): self;

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withMiddlewares() / withMiddlewaresAdded() are new public API methods but are missing docblocks, unlike the rest of QueueInterface. Please document what they do, what argument formats are supported, and (importantly) the middleware execution order when adding/replacing definitions so callers know where the added middleware sits in the pipeline.

Suggested change
public function withMiddlewares(MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions): self;
/**
* Returns a queue instance with push middleware definitions replaced by the specified ones.
*
* Supported middleware definition formats are:
* - a {@see MiddlewarePushInterface} instance;
* - a callable middleware definition;
* - a string service ID or class name;
* - an array definition understood by the implementation/container.
*
* The resulting middleware pipeline uses the provided definitions in the same order they are passed
* to this method. Replacing definitions discards any previously configured middleware.
*
* @param MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions Middleware definitions
* to use for push handling.
*
* @return self A queue instance configured with exactly the specified push middleware definitions.
*/
public function withMiddlewares(MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions): self;
/**
* Returns a queue instance with additional push middleware definitions appended to the existing ones.
*
* Supported middleware definition formats are:
* - a {@see MiddlewarePushInterface} instance;
* - a callable middleware definition;
* - a string service ID or class name;
* - an array definition understood by the implementation/container.
*
* The resulting middleware pipeline preserves the existing middleware order and then appends the provided
* definitions in the same order they are passed to this method. In other words, previously configured
* middleware executes before the middleware added here.
*
* @param MiddlewarePushInterface|callable|array|string ...$middlewareDefinitions Middleware definitions
* to append to the push handling pipeline.
*
* @return self A queue instance configured with the existing push middleware followed by the specified
* additional middleware definitions.
*/

Copilot uses AI. Check for mistakes.
@samdark samdark closed this Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants