Skip to content

[FEATURE] Add queue wrapper +semver: minor#545

Open
guibranco wants to merge 8 commits into
mainfrom
feature/add-queue-wrapper
Open

[FEATURE] Add queue wrapper +semver: minor#545
guibranco wants to merge 8 commits into
mainfrom
feature/add-queue-wrapper

Conversation

@guibranco

@guibranco guibranco commented Apr 14, 2026

Copy link
Copy Markdown
Owner

πŸ“‘ Description

Add queue wrapper

βœ… Checks

  • My pull request adheres to the code style of this project
  • My code requires changes to the documentation
  • I have updated the documentation as required
  • All the tests have passed

☒️ Does this introduce a breaking change?

  • Yes
  • No

Summary by Sourcery

Introduce an AMQP-based Queue abstraction with retry and dead-letter support, along with corresponding tests, documentation, and infrastructure updates.

New Features:

  • Add IQueue interface and Queue implementation wrapping AMQP for publishing and consuming messages with optional exponential-backoff retry and dead-letter queues.
  • Introduce a dedicated QueueException type to surface queue-related errors with operation context.

Build:

  • Add php-amqplib/php-amqplib as a runtime dependency in composer.json.
  • Extend docker-compose setup with a RabbitMQ service for local development and integration testing.

Documentation:

  • Add user guide documentation for the Queue component, including topology description, usage examples, and API reference.
  • Update changelog with a new v1.8 entry describing the Queue feature.

Tests:

  • Add unit tests for Queue URL parsing, retry counter handling, and interface compliance.
  • Add unit tests for QueueException behavior and metadata.
  • Add integration tests covering Queue publish/consume flows, retry routing, multi-server usage, and timeout/QoS options.

Summary by CodeRabbit

  • New Features

    • AMQP message queue support with automatic retries and exponential backoff
    • Multiple broker endpoints with failover and dead-letter routing
  • Documentation

    • Added comprehensive Queue user guide and added Queue entry to docs
    • New changelog entry for v1.8
  • Platform

    • Minimum PHP bumped to 8.4; RabbitMQ client dependency added
    • Local compose setup now includes a RabbitMQ service
  • Tests

    • Added unit and integration tests for queue behavior

@sourcery-ai

sourcery-ai Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a new AMQP Queue abstraction with retry and dead-letter support, wires in the php-amqplib dependency, documents the feature, and adds both unit and integration tests plus a RabbitMQ service for local testing.

Sequence diagram for Queue.publish with optional DLX topology

sequenceDiagram
    actor Producer
    participant Queue
    participant AMQPStreamConnection
    participant AMQPChannel
    participant RabbitMQ

    Producer->>Queue: publish(queueName, message, withDlx)
    Queue->>Queue: getPublishConnection()
    Queue->>Queue: getServers()
    Queue->>Queue: shuffle(servers)
    Queue->>AMQPStreamConnection: create_connection(shuffledServers, options)
    AMQPStreamConnection-->>Queue: connection
    Queue->>AMQPStreamConnection: channel()
    AMQPStreamConnection-->>Queue: AMQPChannel

    alt withDlx is true
        Queue->>AMQPChannel: declareQueueWithDLX(queueName)
        AMQPChannel->>RabbitMQ: queue_declare(main, retry, failed)
    else withDlx is false
        Queue->>AMQPChannel: declareQueueWithoutDLX(queueName)
        AMQPChannel->>RabbitMQ: queue_declare(main)
    end

    Queue->>AMQPMessage: __construct(message, properties)
    Queue->>AMQPChannel: basic_publish(msg, "", queueName)
    AMQPChannel->>RabbitMQ: publish message

    Queue->>AMQPChannel: close()
    Queue->>AMQPStreamConnection: close()
    Queue-->>Producer: return
Loading

Sequence diagram for Queue.consume with retry and DLX

sequenceDiagram
    actor Consumer
    participant Queue
    participant AMQPStreamConnection
    participant AMQPChannel
    participant RabbitMQ
    participant Callback

    Consumer->>Queue: consume(timeout, queueName, callback, withDlx, resetTimeoutOnReceive, qosCount)
    Queue->>Queue: getServers()

    loop for each server
        Queue->>AMQPStreamConnection: create_connection([server], options)
        alt connection fails
            AMQPStreamConnection-->>Queue: throws QueueException
            Queue->>Queue: try next server
        else connection ok
            AMQPStreamConnection-->>Queue: connection
            Queue->>AMQPStreamConnection: channel()
            AMQPStreamConnection-->>Queue: AMQPChannel

            alt withDlx is true
                Queue->>AMQPChannel: declareQueueWithDLX(queueName)
                AMQPChannel->>RabbitMQ: queue_declare(main, retry, failed)
            else withDlx is false
                Queue->>AMQPChannel: declareQueueWithoutDLX(queueName)
                AMQPChannel->>RabbitMQ: queue_declare(main)
            end

            Queue->>AMQPChannel: basic_qos(null, qosCount, null)
            Queue->>AMQPChannel: basic_consume(queueName, callbackWrapper)

            loop while is_consuming and before timeout
                AMQPChannel->>RabbitMQ: wait for message
                RabbitMQ-->>AMQPChannel: deliver AMQPMessage
                AMQPChannel-->>Queue: invoke callbackWrapper(msg)

                alt resetTimeoutOnReceive is true
                    Queue->>Queue: reset startTime
                end

                Queue->>Callback: callback(msg)
                alt callback returns false or throws
                    Queue->>Queue: handleRetry(channel, msg, queueName)
                    Queue->>AMQPMessage: ack()
                    Queue->>AMQPMessage: __construct(body, propertiesWithRetryCount)
                    Queue->>AMQPChannel: basic_publish(msg, "", retryOrFailedQueue)
                    AMQPChannel->>RabbitMQ: route to retry or failed queue
                else callback succeeds
                    Queue->>AMQPMessage: ack()
                end

                Queue->>Queue: check timeout
            end

            Queue->>AMQPChannel: close()
            Queue->>AMQPStreamConnection: close()
        end
    end

    Queue-->>Consumer: return
Loading

Class diagram for new AMQP Queue abstraction

classDiagram
    direction LR

    class IQueue {
        <<interface>>
        +publish(queueName, message, withDlx)
        +consume(timeout, queueName, callback, withDlx, resetTimeoutOnReceive, qosCount)
    }

    class Queue {
        -string[] connectionStrings
        -int[] retryDelaysMs
        -const DEFAULT_PORT
        -const DEFAULT_QOS_COUNT
        -const CONNECTION_TIMEOUT
        -const RETRY_COUNT_HEADER
        -const DEFAULT_RETRY_DELAYS_MS
        +__construct(connectionStrings, retryDelaysMs)
        -parseServer(connectionString)
        -getServers()
        -createConnection(servers)
        -getPublishConnection()
        -declareQueueWithoutDLX(channel, queueName)
        -declareQueueWithDLX(channel, queueName)
        -getRetryCount(msg)
        -handleRetry(channel, msg, queueName)
        +publish(queueName, message, withDlx)
        +consume(timeout, queueName, callback, withDlx, resetTimeoutOnReceive, qosCount)
    }

    class QueueException {
        -string operation
        +__construct(message, operation, code, previous)
        +getOperation()
    }

    class AMQPStreamConnection {
        +create_connection(servers, options)
        +channel()
        +close()
    }

    class AMQPChannel {
        +queue_declare(queueName, passive, durable, exclusive, autoDelete, nowait, arguments)
        +basic_publish(msg, exchange, routingKey)
        +basic_qos(prefetchSize, prefetchCount, global)
        +basic_consume(queue, consumerTag, noLocal, noAck, exclusive, nowait, callback)
        +is_consuming()
        +wait(callback, nonBlocking)
        +close()
    }

    class AMQPMessage {
        +string body
        +const DELIVERY_MODE_PERSISTENT
        +__construct(body, properties)
        +ack()
        +get(name)
    }

    class AMQPTable {
        +__construct(data)
        +getNativeData()
    }

    Queue ..|> IQueue
    QueueException --|> Exception

    Queue --> QueueException
    Queue --> AMQPStreamConnection
    Queue --> AMQPChannel
    Queue --> AMQPMessage
    Queue --> AMQPTable

    AMQPStreamConnection --> AMQPChannel
    AMQPChannel --> AMQPMessage
    AMQPMessage --> AMQPTable
Loading

File-Level Changes

Change Details Files
Add a Queue abstraction layer for AMQP with retry / DLX support and a corresponding exception type and interface.
  • Introduce IQueue interface defining publish and consume contracts with DLX and QoS parameters.
  • Implement Queue class that parses AMQP DSNs, manages single/multi-server connections, declares base/retry/failed queues, publishes messages, and consumes with exponential-backoff retry routing using a custom retry-count header.
  • Add QueueException class to wrap AMQP-related failures with an operation context (init, parse, connect, publish, consume).
src/Queue/IQueue.php
src/Queue/Queue.php
src/Queue/QueueException.php
Add tests covering Queue behavior (unit and integration) and the QueueException class.
  • Create integration tests that exercise end-to-end publishing/consuming, DLX topology, retry levels, failed-queue routing, multi-server behavior, connection failures, timeout resetting, and QoS prefetch handling against a real RabbitMQ instance.
  • Create unit tests for Queue covering constructor validation, invalid connection strings, interface implementation, parseServer and getRetryCount internals via reflection and stubs.
  • Add unit tests for QueueException to verify message, operation, code, chaining, defaults, and supported operation names.
tests/Integration/QueueTest.php
tests/Unit/Queue/QueueTest.php
tests/Unit/Queue/QueueExceptionTest.php
Document the new Queue feature and expose it in project docs and navigation.
  • Add a changelog entry for v1.8 describing the new Queue class and linking to issue [FEATURE] Implement Queue wrapperΒ #82.
  • Create a user-guide page explaining requirements, installation, topology, examples (basic, DLX with retries, multi-server, custom delays), and method signatures for Queue.
  • Register the new Queue documentation page in MkDocs navigation.
docs/changelog.md
docs/user-guide/queue.md
mkdocs.yml
Enable local RabbitMQ for development and add the AMQP library dependency.
  • Add a RabbitMQ service with management UI, environment defaults, healthcheck, and networking to docker-compose.yml for local/integration testing.
  • Declare php-amqplib/php-amqplib ^3.7 as a runtime dependency in composer.json.
docker-compose.yml
composer.json

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@guibranco guibranco enabled auto-merge (squash) April 14, 2026 17:53
@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@guibranco has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 21 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 20 minutes and 21 seconds.

βŒ› How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 88ad84db-8753-4491-b955-6ec482937263

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2fa9d11 and c8491f0.

πŸ“’ Files selected for processing (2)
  • .github/workflows/reusable-build.yml
  • .github/workflows/reusable-coverage.yml

Walkthrough

Adds a new AMQP Queue feature: interface, implementation, custom exception, docs, tests, composer dependency on php-amqplib, and a RabbitMQ service in docker-compose; enables DLX-based retry topology with exponential backoff and multi-server handling. (≀50 words)

Changes

Cohort / File(s) Summary
Dependency & Infrastructure
composer.json, docker-compose.yml
Adds runtime PHP >=8.4 and php-amqplib/php-amqplib:^3.7; adds rabbitmq:3-management service with ports 5672/15672, credentials, restart policy and healthcheck.
API & Implementation
src/Queue/IQueue.php, src/Queue/Queue.php, src/Queue/QueueException.php
Introduces IQueue interface and Queue class implementing publish/consume with multi-server failover, topology declaration (main, per-delay retry queues, -failed), TTL-based exponential-backoff retries tracked via x-pancake-retry-count; adds QueueException carrying operation context.
Documentation
docs/changelog.md, docs/user-guide/queue.md, mkdocs.yml
Adds v1.8 changelog entry and a user guide describing requirements, installation, DLX retry topology, API surface and examples; updates mkdocs navigation to include the Queue guide.
Tests
tests/Integration/QueueTest.php, tests/Unit/Queue/QueueExceptionTest.php, tests/Unit/Queue/QueueTest.php
Adds integration tests covering publish/consume flows, DLX retry routing, failed queue behavior, multi-server delivery, unreachable-server error; adds unit tests for QueueException and Queue constructor, parsing and internal retry-count logic.

Sequence Diagram(s)

sequenceDiagram
    participant Publisher as Publisher
    participant Queue as Queue Wrapper
    participant Broker as RabbitMQ
    participant Consumer as Consumer Callback

    Publisher->>Queue: publish(queueName, message, withDlx=true)
    Queue->>Broker: connect & declare topology (main, retry queues, failed)
    Queue->>Broker: publish persistent JSON (x-pancake-retry-count: 0)
    Queue->>Broker: close connection

    Consumer->>Queue: consume(timeout, queueName, callback, withDlx=true)
    Queue->>Broker: connect & declare topology
    Queue->>Broker: set QoS prefetch
    Broker-->>Queue: deliver message (headers + body)
    Queue->>Consumer: invoke callback(message)

    alt callback returns false or throws
        Consumer-->>Queue: false / Exception
        Queue->>Queue: handleRetry() read/increment x-pancake-retry-count
        Queue->>Broker: ack original message
        alt retry count < limit
            Queue->>Broker: publish to queue-retry-N (incremented header)
        else retries exhausted
            Queue->>Broker: publish to queue-failed
        end
    else callback succeeds
        Consumer-->>Queue: success
        Queue->>Broker: ack message
    end

    Queue->>Broker: close connection
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related Issues

Poem

🐰 A queue that hops with careful cheer,
Retries that pause and then reappear,
Headers count each hopeful try,
Failed ones rest, new routes nearby β€”
RabbitMQ hums, the messages clear.

πŸš₯ Pre-merge checks | βœ… 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title accurately describes the main change: adding a Queue wrapper (AMQP abstraction with retry and DLX support), which is the core feature across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/add-queue-wrapper

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gstraccini gstraccini Bot added the β˜‘οΈ auto-merge Automatic merging of pull requests (gstraccini-bot) label Apr 14, 2026
@github-actions github-actions Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Apr 14, 2026
This commit fixes the style issues introduced in 4078eac according to the output
from PHP CS Fixer.

Details: #545
@penify-dev

penify-dev Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

@deepsource-io

deepsource-io Bot commented Apr 14, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in f068525...c8491f0 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSourceΒ β†—

PR Report Card

Overall GradeΒ Β  SecurityΒ Β 

ReliabilityΒ Β 

ComplexityΒ Β 

HygieneΒ Β 

Code Review Summary

Analyzer Status Updated (UTC) Details
Code coverage Apr 14, 2026 10:16p.m. ReviewΒ β†—
SQL Apr 14, 2026 10:16p.m. ReviewΒ β†—
Secrets Apr 14, 2026 10:16p.m. ReviewΒ β†—
PHP Apr 14, 2026 10:16p.m. ReviewΒ β†—
Docker Apr 14, 2026 10:16p.m. ReviewΒ β†—

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 5 issues, and left some high level feedback:

  • In Queue::consume, the timeout is measured per-server rather than across the whole consume call, so when multiple servers are configured the overall runtime can be up to timeout * serverCount; consider enforcing a single global timeout if the intention is to cap total processing time.
  • Constructor and public methods on Queue/IQueue accept untyped parameters (e.g. $connectionStrings, $retryDelaysMs, $timeout), which can easily mask invalid usage; adding scalar/array type hints would make the API safer and align better with the documented types.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Queue::consume`, the timeout is measured per-server rather than across the whole consume call, so when multiple servers are configured the overall runtime can be up to `timeout * serverCount`; consider enforcing a single global timeout if the intention is to cap total processing time.
- Constructor and public methods on `Queue`/`IQueue` accept untyped parameters (e.g. `$connectionStrings`, `$retryDelaysMs`, `$timeout`), which can easily mask invalid usage; adding scalar/array type hints would make the API safer and align better with the documented types.

## Individual Comments

### Comment 1
<location path="src/Queue/Queue.php" line_range="297-306" />
<code_context>
+     *
+     * @throws QueueException On connection or publish failure.
+     */
+    public function publish($queueName, $message, $withDlx = true)
+    {
+        $connection = $this->getPublishConnection();
+        $channel = $connection->channel();
+
+        if ($withDlx) {
+            $this->declareQueueWithDLX($channel, $queueName);
+        } else {
+            $this->declareQueueWithoutDLX($channel, $queueName);
+        }
+
+        $msg = new AMQPMessage($message, [
+            'content_type'  => 'application/json',
+            'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
+        ]);
+
+        $channel->basic_publish($msg, '', $queueName);
+
+        $channel->close();
+        $connection->close();
+    }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Publish does not wrap channel-level exceptions into QueueException as documented.

Since the docblock promises `QueueException` on publish failure, any errors after establishing the connection (`channel()`, `queue_declare`, `basic_publish`, `close`) should also be wrapped. Please wrap the publish flow in a try/catch and rethrow as `QueueException` (e.g., with operation `'publish'`), mirroring `createConnection`, so callers reliably get the documented exception type.
</issue_to_address>

### Comment 2
<location path="src/Queue/Queue.php" line_range="350-300" />
<code_context>
+    ) {
+        $servers = $this->getServers();
+
+        foreach ($servers as $server) {
+            try {
+                $connection = $this->createConnection([$server]);
+            } catch (QueueException $e) {
+                continue; // Server unreachable – try the next one.
+            }
+
+            $channel = $connection->channel();
+
+            if ($withDlx) {
</code_context>
<issue_to_address>
**question (bug_risk):** Timeout semantics are per-server, not global, which may surprise callers.

The `consume()` docblock implies a single overall `$timeout`, but the timer is effectively reset for each server in the `foreach`. If you intend a global timeout across all servers, consider capturing a single `$globalStartTime` before the loop and computing remaining time per server. Otherwise, clarify in the docs (or enforce in code) that the timeout is per server to avoid much longer runtimes when multiple servers are configured.
</issue_to_address>

### Comment 3
<location path="src/Queue/Queue.php" line_range="395-398" />
<code_context>
+            $channel->basic_qos(null, $qosCount, null);
+            $channel->basic_consume($queueName, '', false, false, false, false, $fn);
+
+            while ($channel->is_consuming()) {
+                $channel->wait(null, true);
+
+                if ($startTime + $timeout < time()) {
+                    break;
+                }
</code_context>
<issue_to_address>
**issue (bug_risk):** Timeout is not enforced when no messages arrive because `wait()` is called without a timeout.

Because `wait(null, true)` can block indefinitely when there are no messages or heartbeats, the timeout condition may never be checked and this method may never return. To ensure `$timeout` is respected even when idle, pass a finite timeout to `wait()` (e.g. remaining time or a small fixed interval) and handle `AMQPTimeoutException` so the loop can exit cleanly.
</issue_to_address>

### Comment 4
<location path="src/Queue/Queue.php" line_range="259-268" />
<code_context>
+     * @param AMQPMessage $msg       The message that failed processing.
+     * @param string      $queueName Base queue name (used to derive retry/failed queue names).
+     */
+    private function handleRetry($channel, $msg, $queueName)
+    {
+        $retryCount = $this->getRetryCount($msg);
+        $maxRetries = count($this->retryDelaysMs);
+
+        $msg->ack();
+
+        $targetQueue = $retryCount < $maxRetries
+            ? $queueName . '-retry-' . ($retryCount + 1)
+            : $queueName . '-failed';
+
+        $newMsg = new AMQPMessage(
+            $msg->body,
+            [
+                'content_type'        => 'application/json',
+                'delivery_mode'       => AMQPMessage::DELIVERY_MODE_PERSISTENT,
+                'application_headers' => new AMQPTable([
+                    self::RETRY_COUNT_HEADER => $retryCount + 1,
+                ]),
+            ]
+        );
+
+        $channel->basic_publish($newMsg, '', $targetQueue);
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Acknowledging before publishing to the retry/failed queue can cause message loss on publish failures.

Because `ack()` happens before `basic_publish()`, any exception during `basic_publish()` (e.g. connection issues) will permanently drop the message. Consider publishing first and only acking on success, or catching failures from `basic_publish()` and requeueing or otherwise persisting the original message for later recovery, depending on your loss/duplication tolerance.

Suggested implementation:

```
     * Handles a failed message by routing it to the appropriate retry queue or
     * the failed queue when all retry levels are exhausted.
     *
     * The original message is acknowledged only after the new message is
     * successfully published, to avoid message loss on publish failures.
     *
     * @param AMQPChannel $channel   Active AMQP channel.
     * @param AMQPMessage $msg       The message that failed processing.
     * @param string      $queueName Base queue name (used to derive retry/failed queue names).
     */

```

```
    private function handleRetry($channel, $msg, $queueName)
    {
        $retryCount = $this->getRetryCount($msg);
        $maxRetries = count($this->retryDelaysMs);

        $targetQueue = $retryCount < $maxRetries
            ? $queueName . '-retry-' . ($retryCount + 1)
            : $queueName . '-failed';

        $newMsg = new AMQPMessage(
            $msg->body,
            [
                'content_type'        => 'application/json',
                'delivery_mode'       => AMQPMessage::DELIVERY_MODE_PERSISTENT,
                'application_headers' => new AMQPTable([
                    self::RETRY_COUNT_HEADER => $retryCount + 1,
                ]),
            ]
        );

        try {
            $channel->basic_publish($newMsg, '', $targetQueue);
            $msg->ack();
        } catch (\Throwable $e) {
            // Requeue the original message so it is not lost if publish fails.
            if (method_exists($msg, 'get')) {
                $deliveryTag = $msg->get('delivery_tag');
                $channel->basic_nack($deliveryTag, false, true);
            }

            throw $e;
        }
    }

```
</issue_to_address>

### Comment 5
<location path="docs/user-guide/queue.md" line_range="3-6" />
<code_context>
+# Queue
+
+## Table of content
+
+- [Queue](#queue)
+  - [Table of content](#table-of-content)
+  - [About](#about)
+  - [Requirements](#requirements)
</code_context>
<issue_to_address>
**suggestion (typo):** Use "Table of contents" instead of "Table of content" in the heading and link.

Also update the link text and its anchor to match the corrected heading.

```suggestion
## Table of contents

- [Queue](#queue)
  - [Table of contents](#table-of-contents)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click πŸ‘ or πŸ‘Ž on each comment and I'll use the feedback to improve your reviews.

Comment thread src/Queue/Queue.php
Comment thread src/Queue/Queue.php
Comment thread src/Queue/Queue.php
Comment thread src/Queue/Queue.php
Comment thread docs/user-guide/queue.md Outdated
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docker-compose.yml`:
- Around line 49-51: The docker-compose RabbitMQ environment uses
RABBITMQ_DEFAULT_USER and RABBITMQ_DEFAULT_PASS which differ from the
integration test expectations (RABBITMQ_USER and RABBITMQ_PASS); update the
environment keys in docker-compose.yml to match the test variables (use
RABBITMQ_USER and RABBITMQ_PASS) or set both pairs so tests and runtime read the
same creds, ensuring you change the entries that reference
RABBITMQ_DEFAULT_USER/RABBITMQ_DEFAULT_PASS to the matching
RABBITMQ_USER/RABBITMQ_PASS names (or add duplicates) so authentication remains
consistent with the tests.

In `@src/Queue/Queue.php`:
- Around line 352-357: The consume() loop over $servers swallows all
QueueException errors from createConnection() and returns normally when every
server fails, making an all-servers-down situation look like β€œno messages”;
update the logic in consume() (the foreach that calls
createConnection([$server])) to detect when no connection could be established
and throw a QueueException instead of silently continuingβ€”collect or track
failures while iterating, and after the loop throw a new QueueException (or
rethrow an aggregated/last exception) indicating connection/consume failure;
apply the same change to the other identical block around the createConnection
calls at the 405-407 area so callers receive an error per the IQueue contract.
- Around line 352-357: In consume() the overall timeout is broken because
$startTime is reset inside the foreach ($servers as $server) loop and wait() is
used in non-blocking mode without catching
AMQPTimeoutException/AMQPRuntimeException; fix by moving the $startTime
initialization outside the servers loop (so the total elapsed time across
servers is measured), compute remaining timeout per-server before calling
$channel->wait(null, true), and wrap the $channel->wait(null, true) call in a
try/catch that handles AMQPTimeoutException and AMQPRuntimeException (and any
socket-related exceptions) to break/continue gracefully and respect the overall
timeout; ensure createConnection([$server]) behavior remains unchanged and that
consume() returns or stops when the overall timeout is exceeded.
- Around line 381-390: The try/catch currently always calls
handleRetry($channel, $msg, $queueName) which republishes to -retry and -failed
queues even when DLX is disabled, causing silent drops; update the failure path
in the block around the callback invocation (the code that calls handleRetry and
$msg->ack()) to check the DLX flag (e.g. $this->withDlx or the local $withDlx)
and only call handleRetry when DLX is enabled; when DLX is disabled, instead
nack/reject the message using the channel/message API to either requeue
(basic_nack/basic_reject with requeue=true) or reject without publishing to
non-existent queues (choose requeue behavior consistent with your retry policy),
and ensure the same change is applied in both the result===false branch and the
catch (\Throwable $e) branch so failed messages are not published to undeclared
-retry/-failed queues.
- Around line 91-107: The vhost handling must decode percent-encodings and
normalize double-slash cases: after parse_url($connectionString) compute the raw
path (e.g. $rawPath = $url['path'] ?? '') then derive $vhostCandidate =
rawurldecode(ltrim($rawPath, '/')); if $vhostCandidate === '' set 'vhost' => '/'
else set 'vhost' => $vhostCandidate; keep the rest of the returned array (host,
port using self::DEFAULT_PORT, user, password) unchanged so amqp://host//,
amqp://host/, amqp://host and amqp://host/%2F all resolve correctly.
- Around line 266-283: Wrap the ack+publish in an AMQP transaction so both
succeed or both fail: call $channel->tx_select() before performing $msg->ack(),
then publish the new AMQPMessage (constructed with self::RETRY_COUNT_HEADER,
$msg->body, delivery options) to $targetQueue via $channel->basic_publish(), and
finally call $channel->tx_commit(); on any exception, call
$channel->tx_rollback() and rethrow or handle the error (e.g., do not ack or
requeue) so you avoid losing the message when $channel->basic_publish() fails.

In `@tests/Integration/QueueTest.php`:
- Around line 257-263: The test
testPublishThrowsQueueExceptionWhenAllServersDown uses a loopback IP (127.0.0.2)
that can be reachable and makes the failure-path flaky; update the Queue
instantiation in that test to use a guaranteed-invalid endpoint (e.g., an
.invalid hostname) instead of 'amqp://guest:guest@127.0.0.2:5672/' so the call
to Queue->publish('irrelevant', '{}', false) reliably fails and triggers the
expected QueueException.
- Around line 139-147: The tests are redeclaring retry queues without DLX which
causes RabbitMQ PRECONDITION_FAILED; update the consume calls in tests (the
Queue::consume(...) invocation that currently passes false) to pass true for the
last parameter when the queue name matches the retry pattern (e.g., contains
'-retry-') or otherwise make the $withDlx argument conditional so retry queues
are declared with declareQueueWithDLX() instead of declareQueueWithoutDLX();
reference Queue::consume, declareQueueWithDLX and declareQueueWithoutDLX to
locate and change the call sites (also apply the same fix for the other
occurrence around lines 213-221).
- Around line 38-45: The DSN composition uses the raw $vhost which causes a
double-slash and mis-parsing for the default '/' vhost; encode the vhost before
injecting it into self::$dsn (e.g. use rawurlencode on the value returned by
getenv('RABBITMQ_VHOST') or on $vhost) and then build self::$dsn with the
encoded vhost so parse_url() and Queue::... logic receive a correctly encoded
path.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ad22aeb-ef63-466b-86a6-9ea651b2a26f

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between f068525 and 42b3794.

πŸ“’ Files selected for processing (11)
  • composer.json
  • docker-compose.yml
  • docs/changelog.md
  • docs/user-guide/queue.md
  • mkdocs.yml
  • src/Queue/IQueue.php
  • src/Queue/Queue.php
  • src/Queue/QueueException.php
  • tests/Integration/QueueTest.php
  • tests/Unit/Queue/QueueExceptionTest.php
  • tests/Unit/Queue/QueueTest.php

Comment thread docker-compose.yml
Comment thread src/Queue/Queue.php
Comment thread src/Queue/Queue.php
Comment thread src/Queue/Queue.php
Comment thread src/Queue/Queue.php
Comment thread tests/Integration/QueueTest.php
Comment thread tests/Integration/QueueTest.php
Comment thread tests/Integration/QueueTest.php
@guibranco

Copy link
Copy Markdown
Owner Author

@gstraccini composer update lock

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Updating composer.lock via composer update --no-interaction! πŸ”’

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Workflow update-composer-lock.yml failed: ❌

{"message":"Not Found","documentation_url":"https://docs.github.com/rest/actions/workflows#create-a-workflow-dispatch-event","status":"404"}

@guibranco

Copy link
Copy Markdown
Owner Author

@gstraccini composer update lock

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Updating composer.lock via composer update --no-interaction! πŸ”’

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Workflow update-composer-lock.yml failed: ❌

{"message":"Unexpected inputs provided: [\"checkRunId\"]","documentation_url":"https://docs.github.com/rest/actions/workflows#create-a-workflow-dispatch-event","status":"422"}

@guibranco

Copy link
Copy Markdown
Owner Author

@gstraccini composer update lock

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Updating composer.lock via composer update --no-interaction! πŸ”’

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

βœ… Update composer lock result:

Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires phpunit/phpunit 13.1.1 -> satisfiable by phpunit/phpunit[13.1.1].
    - phpunit/phpunit 13.1.1 requires php >=8.4.1 -> your php version (8.2.30) does not satisfy that requirement.

::error ::Your requirements could not be resolved to an installable set of packages.%0A%0A  Problem 1%0A    - Root composer.json requires phpunit/phpunit 13.1.1 -> satisfiable by phpunit/phpunit[13.1.1].%0A    - phpunit/phpunit 13.1.1 requires php >=8.4.1 -> your php version (8.2.30) does not satisfy that requirement.%0A

@guibranco

Copy link
Copy Markdown
Owner Author

@gstraccini composer update lock

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Updating composer.lock via composer update --no-interaction! πŸ”’

@gstraccini

gstraccini Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

βœ… Update composer lock result:

Loading composer repositories with package information
Updating dependencies
Lock file operations: 4 installs, 2 updates, 0 removals
  - Locking paragonie/constant_time_encoding (v3.1.3)
  - Locking paragonie/random_compat (v9.99.100)
  - Locking php-amqplib/php-amqplib (v3.7.4)
  - Locking phpseclib/phpseclib (3.0.51)
  - Upgrading phpunit/php-code-coverage (14.1.0 => 14.1.1)
  - Upgrading sebastian/comparator (8.1.1 => 8.1.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 30 installs, 0 updates, 0 removals
  - Downloading paragonie/random_compat (v9.99.100)
  - Downloading paragonie/constant_time_encoding (v3.1.3)
  - Downloading phpseclib/phpseclib (3.0.51)
  - Downloading php-amqplib/php-amqplib (v3.7.4)
  - Downloading staabm/side-effects-detector (1.0.5)
  - Downloading sebastian/version (7.0.0)
  - Downloading sebastian/type (7.0.0)
  - Downloading sebastian/recursion-context (8.0.0)
  - Downloading sebastian/object-reflector (6.0.0)
  - Downloading sebastian/object-enumerator (8.0.0)
  - Downloading sebastian/global-state (9.0.0)
  - Downloading sebastian/git-state (1.0.0)
  - Downloading sebastian/exporter (8.0.1)
  - Downloading sebastian/environment (9.2.0)
  - Downloading sebastian/diff (8.1.0)
  - Downloading sebastian/comparator (8.1.2)
  - Downloading sebastian/cli-parser (5.0.0)
  - Downloading phpunit/php-timer (9.0.0)
  - Downloading phpunit/php-text-template (6.0.0)
  - Downloading phpunit/php-invoker (7.0.0)
  - Downloading phpunit/php-file-iterator (7.0.0)
  - Downloading theseer/tokenizer (2.0.1)
  - Downloading nikic/php-parser (v5.7.0)
  - Downloading sebastian/lines-of-code (5.0.0)
  - Downloading sebastian/complexity (6.0.0)
  - Downloading phpunit/php-code-coverage (14.1.1)
  - Downloading phar-io/version (3.2.1)
  - Downloading phar-io/manifest (2.0.4)
  - Downloading myclabs/deep-copy (1.13.4)
  - Downloading phpunit/phpunit (13.1.1)
  - Installing paragonie/random_compat (v9.99.100): Extracting archive
  - Installing paragonie/constant_time_encoding (v3.1.3): Extracting archive
  - Installing phpseclib/phpseclib (3.0.51): Extracting archive
  - Installing php-amqplib/php-amqplib (v3.7.4): Extracting archive
  - Installing staabm/side-effects-detector (1.0.5): Extracting archive
  - Installing sebastian/version (7.0.0): Extracting archive
  - Installing sebastian/type (7.0.0): Extracting archive
  - Installing sebastian/recursion-context (8.0.0): Extracting archive
  - Installing sebastian/object-reflector (6.0.0): Extracting archive
  - Installing sebastian/object-enumerator (8.0.0): Extracting archive
  - Installing sebastian/global-state (9.0.0): Extracting archive
  - Installing sebastian/git-state (1.0.0): Extracting archive
  - Installing sebastian/exporter (8.0.1): Extracting archive
  - Installing sebastian/environment (9.2.0): Extracting archive
  - Installing sebastian/diff (8.1.0): Extracting archive
  - Installing sebastian/comparator (8.1.2): Extracting archive
  - Installing sebastian/cli-parser (5.0.0): Extracting archive
  - Installing phpunit/php-timer (9.0.0): Extracting archive
  - Installing phpunit/php-text-template (6.0.0): Extracting archive
  - Installing phpunit/php-invoker (7.0.0): Extracting archive
  - Installing phpunit/php-file-iterator (7.0.0): Extracting archive
  - Installing theseer/tokenizer (2.0.1): Extracting archive
  - Installing nikic/php-parser (v5.7.0): Extracting archive
  - Installing sebastian/lines-of-code (5.0.0): Extracting archive
  - Installing sebastian/complexity (6.0.0): Extracting archive
  - Installing phpunit/php-code-coverage (14.1.1): Extracting archive
  - Installing phar-io/version (3.2.1): Extracting archive
  - Installing phar-io/manifest (2.0.4): Extracting archive
  - Installing myclabs/deep-copy (1.13.4): Extracting archive
  - Installing phpunit/phpunit (13.1.1): Extracting archive
3 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
25 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

@socket-security

socket-security Bot commented Apr 14, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcomposer/​php-amqplib/​php-amqplib@​3.7.4.010010090100100

View full report

@github-actions

Copy link
Copy Markdown
Contributor

Infisical secrets check: βœ… No secrets leaked!

πŸ’» Scan logs
2026-04-14T22:15:49Z INF scanning for exposed secrets...
10:15PM INF 445 commits scanned.
2026-04-14T22:15:50Z INF scan completed in 549ms
2026-04-14T22:15:50Z INF no leaks found

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

β˜‘οΈ auto-merge Automatic merging of pull requests (gstraccini-bot) size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant