Skip to content

feat(sqs): add dead-letter redrive ops for full 23/23 coverage - #94

Merged
skyoo2003 merged 6 commits into
mainfrom
feat/sqs-message-move-tasks
Jul 18, 2026
Merged

skyoo2003 merged 6 commits into
mainfrom
feat/sqs-message-move-tasks

Conversation

@skyoo2003

Copy link
Copy Markdown
Owner

Summary

Closes the last operation gap on SQS by implementing the four dead-letter redrive operations, bringing SQS to 23/23 operation coverage (roadmap Phase 1 — "deepen coverage on the 6 core services").

  • ListDeadLetterSourceQueues
  • StartMessageMoveTask
  • ListMessageMoveTasks
  • CancelMessageMoveTask

Details

  • Store (internal/services/sqs/store.go): adds a move-task registry + sourceQueuesForDLQ lookup, reusing the existing RedrivePolicy fields (MaxReceiveCount / DeadLetterTargetArn). Redrive runs synchronously in the in-memory store and drains one queue lock at a time to stay deadlock-free with the source→DLQ path in ReceiveMessage.
  • Provider (internal/services/sqs/provider.go): 4 JSON handlers + dispatch cases. Wire field casing matches the botocore SQS JSON model (queueUrls is lowercase for ListDeadLetterSourceQueues; the rest PascalCase).
  • Docs: corrects the stale docs/services/sqs.md (batch / DLQ / FIFO / message-attributes were wrongly listed as unsupported) and bumps the Tier-1 count in docs/services-matrix.md.

Deliberately out of scope

  • Query (form-encoded) path parity — the compat suite is JSON-only and no current client sends form-encoded SQS.
  • Real AddPermission / RemovePermission semantics — they remain 200-OK stubs.
  • Redrive is synchronous: no RUNNING status and MaxNumberOfMessagesPerSecond is not throttled (documented as a known limitation).

Testing

  • go test -race ./internal/services/sqs/... — pass (2 new store tests)
  • pytest -k sqs — 21 passed (2 new compat tests + existing suite, incl. the EventBridge→SQS dispatch test)

Implement the four remaining SQS operations, all in the dead-letter
redrive family:

- ListDeadLetterSourceQueues
- StartMessageMoveTask
- ListMessageMoveTasks
- CancelMessageMoveTask

The store gains a move-task registry and a source-queue lookup that
reuse the existing RedrivePolicy fields (MaxReceiveCount /
DeadLetterTargetArn). Redrive runs synchronously in the in-memory store
and drains one queue lock at a time to stay deadlock-free with the
source->DLQ path in ReceiveMessage.

Wire field casing matches the botocore SQS JSON model (queueUrls is
lowercase for ListDeadLetterSourceQueues; the rest PascalCase).

Adds store unit tests and boto3 compat tests, and corrects the stale
SQS docs (batch/DLQ/FIFO/message-attributes were wrongly listed as
unsupported).
@sourcery-ai

sourcery-ai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements full SQS dead-letter redrive support (ListDeadLetterSourceQueues, StartMessageMoveTask, ListMessageMoveTasks, CancelMessageMoveTask) across JSON and Query protocols, backed by an in-memory move-task registry and updated docs/tests for 23/23 operation coverage.

File-Level Changes

Change Details Files
Add provider wiring and request/response handling for the four SQS dead-letter redrive operations over both JSON and Query protocols.
  • Dispatch new operations in handleQueryRequest and handleJSONRequest to dedicated handlers
  • Define XML response structs for redrive operations and an error-mapping helper for StartMessageMoveTask
  • Implement JSON handlers that validate required parameters, call store methods, and shape responses to match botocore’s SQS JSON model
  • Implement Query (form-encoded) handlers that mirror JSON semantics and emit the appropriate XML responses
internal/services/sqs/provider.go
Introduce an in-memory move-task registry and synchronous dead-letter redrive semantics in the SQS store, including bounded task history and DLQ source discovery.
  • Extend QueueStore with move-task state (map, sequence counter) and error types for DLQ/redrive flows
  • Implement sourceQueuesForDLQ and ListDeadLetterSourceQueues using RedrivePolicy’s DeadLetterTargetArn
  • Implement StartMessageMoveTask to resolve DLQ/destination queues, synchronously move eligible messages, and record a COMPLETED task with ordering metadata
  • Add helpers to sort/prune move tasks per source and enforce a max history size
  • Implement ListMessageMoveTasks and CancelMessageMoveTask with AWS-like maxResults defaulting/clamping and non-cancellable completed tasks
internal/services/sqs/store.go
Add unit and compatibility tests to cover DLQ source listing, move-task lifecycle, multi-source routing, bounded registry behavior, and Query protocol redrive operations.
  • Add store tests for ListDeadLetterSourceQueues, basic move-task lifecycle, multi-source DLQ routing, and registry bounding/maxResults behavior
  • Add provider Query-protocol test that drives all four redrive operations via form-encoded requests and validates XML responses and error semantics
  • Extend Python compat tests to exercise DLQ wiring via RedrivePolicy, full message-move-task flows, error cases for missing/invalid params, and maxResults behavior
internal/services/sqs/store_test.go
internal/services/sqs/provider_test.go
tests/compatibility/test_sqs.py
Update SQS documentation to reflect full operation support, clarify DLQ redrive limitations, and bump Tier-1 service operation counts.
  • Refresh SQS supported API table to list all 23 operations, including batch, DLQ, FIFO, attributes, and redrive
  • Document synchronous redrive semantics, lack of permission enforcement, and other SQS limitations (queue-level attributes, retention, long polling)
  • Update services matrix to note SQS 23/23 op coverage and revised Tier-1 operation count
  • Add an unreleased changelog entry describing the new SQS dead-letter redrive operations
docs/services/sqs.md
docs/services-matrix.md
changes/unreleased/Added-20260718-081112.yaml

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Test code and test infrastructure services AWS service implementations labels Jul 17, 2026
@skyoo2003 skyoo2003 self-assigned this Jul 17, 2026

@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 2 issues, and left some high level feedback:

  • Consider pruning entries from moveTasks (e.g., on cancel or after listing) or adding an upper bound, since the map is currently append-only and can grow without limit over a long-running process.
  • In StartMessageMoveTask, implicit destination selection for multi-source DLQs always picks the first referencing queue; if that differs from AWS behavior, you might want to either document this more prominently or enforce an explicit DestinationArn when multiple sources exist.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider pruning entries from `moveTasks` (e.g., on cancel or after listing) or adding an upper bound, since the map is currently append-only and can grow without limit over a long-running process.
- In `StartMessageMoveTask`, implicit destination selection for multi-source DLQs always picks the first referencing queue; if that differs from AWS behavior, you might want to either document this more prominently or enforce an explicit `DestinationArn` when multiple sources exist.

## Individual Comments

### Comment 1
<location path="tests/compatibility/test_sqs.py" line_range="218" />
<code_context>
+    return src, dlq, dlq_arn
+
+
+def test_list_dead_letter_source_queues(sqs_client):
+    src, dlq, _ = _redrive_pair(sqs_client, "dlq-src", "dlq-target")
+    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=dlq)
</code_context>
<issue_to_address>
**suggestion (testing):** Add compat tests for queues with no sources and for NonExistentQueue behavior in list_dead_letter_source_queues

Currently only the "DLQ has at least one source" path is covered here; two key behaviors are still only tested at the store level:

1. For a queue that exists but is not configured as a DLQ (no RedrivePolicy references), `list_dead_letter_source_queues` should return an empty `queueUrls` list.
2. For a non-existent queue URL, `list_dead_letter_source_queues` should raise a `ClientError` with `error_code == "AWS.SimpleQueueService.NonExistentQueue"`.

Please add compat tests for these cases so the JSON handler behavior and error mapping are verified under these edge conditions.

Suggested implementation:

```python
def test_list_dead_letter_source_queues(sqs_client):
    src, dlq, _ = _redrive_pair(sqs_client, "dlq-src", "dlq-target")
    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=dlq)
    urls = resp["queueUrls"]
    assert any("dlq-src" in u for u in urls)


def test_list_dead_letter_source_queues_for_non_dlq_returns_empty_list(sqs_client):
    # Create a queue that is not configured as a DLQ (no RedrivePolicy references).
    queue_url = sqs_client.create_queue(QueueName="no-dlq")["QueueUrl"]

    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=queue_url)

    # For a queue that exists but is not configured as a DLQ, AWS returns an empty list.
    assert resp["queueUrls"] == []


def test_list_dead_letter_source_queues_nonexistent_queue_raises_client_error(sqs_client):
    # For a non-existent queue URL, AWS should raise a NonExistentQueue ClientError.
    non_existent_queue_url = (
        "https://sqs.us-east-1.amazonaws.com/123456789012/non-existent-queue"
    )

    with pytest.raises(ClientError) as exc:
        sqs_client.list_dead_letter_source_queues(QueueUrl=non_existent_queue_url)

    error = exc.value.response.get("Error", {})
    assert error.get("Code") == "AWS.SimpleQueueService.NonExistentQueue"


def test_message_move_task_flow(sqs_client):
    src, dlq, dlq_arn = _redrive_pair(sqs_client, "mmt-src", "mmt-dlq")

    sqs_client.send_message(QueueUrl=src, MessageBody="redrive-me")
    # Exceed maxReceiveCount so the message is moved to the DLQ.
    sqs_client.receive_message(QueueUrl=src, VisibilityTimeout=0)

```

1. Ensure `pytest` is imported at the top of `tests/compatibility/test_sqs.py`, e.g. `import pytest`, if it is not already present.
2. Ensure `ClientError` is imported, e.g. `from botocore.exceptions import ClientError`, if it is not already present.
3. If the test suite uses a specific region or account ID pattern for queue URLs, adjust `non_existent_queue_url` to match those conventions while still pointing to a non-existent queue.
</issue_to_address>

### Comment 2
<location path="tests/compatibility/test_sqs.py" line_range="225" />
<code_context>
+    assert any("dlq-src" in u for u in urls)
+
+
+def test_message_move_task_flow(sqs_client):
+    src, dlq, dlq_arn = _redrive_pair(sqs_client, "mmt-src", "mmt-dlq")
+
</code_context>
<issue_to_address>
**suggestion (testing):** Broaden message move task compat coverage: explicit destination, unsupported source, missing params, and unknown task handle

Current flow coverage only exercises basic redrive and cancel behavior; several important client-visible cases remain untested:

1. Add a test for `DestinationArn` routing (two source queues sharing a DLQ, then `start_message_move_task(SourceArn=dlq_arn, DestinationArn=src2_arn)` and assert the message lands on the second source).
2. Add a test for `SourceArn` pointing to a non-DLQ queue, asserting `AWS.SimpleQueueService.UnsupportedOperation` via `ClientError`.
3. Add tests that omit required parameters (`SourceArn` for start/list, `TaskHandle` for cancel) and assert `MissingParameter` errors.
4. Add a cancel test with an unknown `TaskHandle` and assert `ResourceNotFoundException`.

These will lock in routing and error-handling semantics for the new redrive APIs.

Suggested implementation:

```python
def test_list_dead_letter_source_queues(sqs_client):
    src, dlq, _ = _redrive_pair(sqs_client, "dlq-src", "dlq-target")
    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=dlq)
    urls = resp["queueUrls"]
    assert any("dlq-src" in u for u in urls)


def test_message_move_task_destination_arn_routing(sqs_client):
    # Two source queues share a DLQ. Start a move task from the DLQ to the second source
    # and assert the message lands on the second source.
    src1, dlq, dlq_arn = _redrive_pair(sqs_client, "mmt-src1", "mmt-dlq")

    # Create a second source queue wired to the same DLQ.
    src2 = sqs_client.create_queue(
        QueueName="mmt-src2",
        Attributes={
            "RedrivePolicy": (
                '{"deadLetterTargetArn":"%s","maxReceiveCount":"1"}' % dlq_arn
            )
        },
    )["QueueUrl"]

    src2_arn = sqs_client.get_queue_attributes(
        QueueUrl=src2, AttributeNames=["QueueArn"]
    )["Attributes"]["QueueArn"]

    # Seed the DLQ directly with a message.
    sqs_client.send_message(QueueUrl=dlq, MessageBody="dest-arn-routing")

    # Start message move task from DLQ to the second source.
    resp = sqs_client.start_message_move_task(
        SourceArn=dlq_arn,
        DestinationArn=src2_arn,
    )
    assert "TaskHandle" in resp

    # The message should be moved from DLQ to src2.
    msgs = sqs_client.receive_message(QueueUrl=src2, MaxNumberOfMessages=10)
    assert len(msgs.get("Messages", [])) == 1
    assert msgs["Messages"][0]["Body"] == "dest-arn-routing"

    # DLQ should now be empty.
    dlq_msgs = sqs_client.receive_message(QueueUrl=dlq, MaxNumberOfMessages=10)
    assert "Messages" not in dlq_msgs


def test_message_move_task_unsupported_source_arn(sqs_client):
    # SourceArn must be a DLQ; pointing to a non-DLQ queue should raise UnsupportedOperation.
    # Create a regular queue (no RedrivePolicy).
    src_url = sqs_client.create_queue(QueueName="mmt-unsupported-src")["QueueUrl"]
    src_arn = sqs_client.get_queue_attributes(
        QueueUrl=src_url, AttributeNames=["QueueArn"]
    )["Attributes"]["QueueArn"]

    with pytest.raises(ClientError) as excinfo:
        sqs_client.start_message_move_task(SourceArn=src_arn)

    err = excinfo.value.response["Error"]
    assert err["Code"] == "AWS.SimpleQueueService.UnsupportedOperation"


def test_start_message_move_task_missing_source_arn(sqs_client):
    # Missing required SourceArn should surface as MissingParameter.
    dlq_url = sqs_client.create_queue(QueueName="mmt-missing-srcarn-dlq")["QueueUrl"]
    dlq_arn = sqs_client.get_queue_attributes(
        QueueUrl=dlq_url, AttributeNames=["QueueArn"]
    )["Attributes"]["QueueArn"]

    with pytest.raises(ClientError) as excinfo:
        # Intentionally omit SourceArn.
        sqs_client.start_message_move_task(DestinationArn=dlq_arn)

    err = excinfo.value.response["Error"]
    assert err["Code"] == "MissingParameter"


def test_list_message_move_tasks_missing_source_arn(sqs_client):
    # SourceArn is required for list_message_move_tasks.
    with pytest.raises(ClientError) as excinfo:
        sqs_client.list_message_move_tasks()

    err = excinfo.value.response["Error"]
    assert err["Code"] == "MissingParameter"


def test_cancel_message_move_task_missing_task_handle(sqs_client):
    # TaskHandle is required for cancel_message_move_task.
    with pytest.raises(ClientError) as excinfo:
        sqs_client.cancel_message_move_task(TaskHandle="")

    err = excinfo.value.response["Error"]
    assert err["Code"] == "MissingParameter"


def test_cancel_message_move_task_unknown_task_handle(sqs_client):
    # Cancel with an unknown TaskHandle should raise ResourceNotFoundException.
    with pytest.raises(ClientError) as excinfo:
        sqs_client.cancel_message_move_task(TaskHandle="arn:aws:sqs:unknown-task-handle")

    err = excinfo.value.response["Error"]
    assert err["Code"] == "ResourceNotFoundException"


import pytest
from botocore.exceptions import ClientError

```

These tests assume:
1. `list_message_move_tasks`, `start_message_move_task`, and `cancel_message_move_task` are available on `sqs_client` and behave like the AWS SQS APIs, including error codes (`MissingParameter`, `AWS.SimpleQueueService.UnsupportedOperation`, `ResourceNotFoundException`).
2. The backend allows sending messages directly to the DLQ queue URL (standard SQS behavior).

If your existing test suite already defines helpers around message move tasks (e.g., polling for completion or seeding DLQs), you may want to:
- Reuse those helpers instead of the direct `send_message`/`receive_message` calls here.
- Adjust the exact assertion on missing-parameter behavior if your client raises a local parameter validation error before making the AWS call (e.g., `ParamValidationError` instead of `ClientError`).
</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 tests/compatibility/test_sqs.py
Comment thread tests/compatibility/test_sqs.py
…icit

Address code review on the redrive ops:

- StartMessageMoveTask now keeps only the latest task per source, so the
  moveTasks map can't grow without bound.
- When a DLQ has multiple source queues and no DestinationArn is given,
  redrive is rejected (ErrAmbiguousDest -> InvalidParameterValue) instead
  of silently routing to the first source; single-source still defaults.

Expand tests: store tests for the multi-source ambiguity, explicit-dest
routing, and the bounded registry; compat tests for empty/non-existent
list_dead_letter_source_queues, destination routing, ambiguous dest,
unsupported (non-DLQ) source, and cancel missing/unknown handle. Note
that omitting a required member (SourceArn) raises botocore's
ParamValidationError client-side, so that path is asserted as such.
@skyoo2003

Copy link
Copy Markdown
Owner Author

@sourcery-ai resolve

@skyoo2003

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@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 2 issues, and left some high level feedback:

  • The comment on sourceQueuesForDLQ says the caller must hold s.mu, but ListDeadLetterSourceQueues calls it under RLock; consider tightening the wording or consistently using the same lock mode to avoid confusion for future maintainers.
  • In StartMessageMoveTask, the error message for ErrAmbiguousDest (“when the source queue has multiple dead-letter source queues”) is a bit misleading given the logic is about multiple source queues for a DLQ; consider rephrasing to clearly describe the DLQ/multi‑source scenario.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The comment on `sourceQueuesForDLQ` says the caller must hold `s.mu`, but `ListDeadLetterSourceQueues` calls it under `RLock`; consider tightening the wording or consistently using the same lock mode to avoid confusion for future maintainers.
- In `StartMessageMoveTask`, the error message for `ErrAmbiguousDest` (“when the source queue has multiple dead-letter source queues”) is a bit misleading given the logic is about multiple source queues for a DLQ; consider rephrasing to clearly describe the DLQ/multi‑source scenario.

## Individual Comments

### Comment 1
<location path="tests/compatibility/test_sqs.py" line_range="266-268" />
<code_context>
+    )
+
+
+def test_message_move_task_flow(sqs_client):
+    src, dlq, dlq_arn = _redrive_pair(sqs_client, "mmt-src", "mmt-dlq")
+
+    sqs_client.send_message(QueueUrl=src, MessageBody="redrive-me")
+    # Exceed maxReceiveCount so the message is moved to the DLQ.
+    sqs_client.receive_message(QueueUrl=src, VisibilityTimeout=0)
+    sqs_client.receive_message(QueueUrl=src, VisibilityTimeout=0)
+
+    handle = sqs_client.start_message_move_task(SourceArn=dlq_arn)["TaskHandle"]
+    assert handle
+
+    tasks = sqs_client.list_message_move_tasks(SourceArn=dlq_arn)["Results"]
+    assert len(tasks) >= 1
+    assert tasks[0]["SourceArn"] == dlq_arn
+
+    cancel = sqs_client.cancel_message_move_task(TaskHandle=handle)
+    assert "ApproximateNumberOfMessagesMoved" in cancel
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add compat tests for list_message_move_tasks error handling and MaxResults semantics

Currently we only test the successful ListMessageMoveTasks flow. Please also cover:

- Calling list_message_move_tasks without SourceArn and asserting the "MissingParameter" error code.
- Creating more than 10 tasks for a SourceArn and verifying that Results length matches the MaxResults semantics (defaulting to 10 when MaxResults <= 0 and respecting the upper cap when provided).

These cases ensure the client-visible contract for error handling and pagination is exercised by the compat suite.

```suggestion
    cancel = sqs_client.cancel_message_move_task(TaskHandle=handle)
    assert "ApproximateNumberOfMessagesMoved" in cancel


def test_list_message_move_tasks_missing_source_arn(sqs_client):
    with pytest.raises(ClientError) as exc:
        sqs_client.list_message_move_tasks()
    assert exc.value.response["Error"]["Code"] == "MissingParameter"


def test_list_message_move_tasks_max_results_semantics(sqs_client):
    src, dlq, dlq_arn = _redrive_pair(sqs_client, "mmt-src-max", "mmt-dlq-max")

    # Create more than 10 tasks for the same SourceArn
    for i in range(15):
        sqs_client.send_message(QueueUrl=src, MessageBody=f"msg-{i}")
        # Exceed maxReceiveCount so the message is moved to the DLQ.
        sqs_client.receive_message(QueueUrl=src, VisibilityTimeout=0)
        sqs_client.receive_message(QueueUrl=src, VisibilityTimeout=0)
        sqs_client.start_message_move_task(SourceArn=dlq_arn)

    # Default MaxResults (and non-positive values) should return 10
    default_results = sqs_client.list_message_move_tasks(SourceArn=dlq_arn)["Results"]
    assert len(default_results) == 10

    zero_results = sqs_client.list_message_move_tasks(
        SourceArn=dlq_arn, MaxResults=0
    )["Results"]
    assert len(zero_results) == 10

    negative_results = sqs_client.list_message_move_tasks(
        SourceArn=dlq_arn, MaxResults=-5
    )["Results"]
    assert len(negative_results) == 10

    # Positive MaxResults should be honored but capped at the service upper bound (10)
    seven_results = sqs_client.list_message_move_tasks(
        SourceArn=dlq_arn, MaxResults=7
    )["Results"]
    assert len(seven_results) == 7

    capped_results = sqs_client.list_message_move_tasks(
        SourceArn=dlq_arn, MaxResults=25
    )["Results"]
    assert len(capped_results) == 10
```
</issue_to_address>

### Comment 2
<location path="tests/compatibility/test_sqs.py" line_range="234-238" />
<code_context>
+    assert any("dlq-src" in u for u in urls)
+
+
+def test_list_dead_letter_source_queues_no_sources(sqs_client):
+    # A queue that exists but is not used as a DLQ returns an empty list.
+    url = sqs_client.create_queue(QueueName="dlsq-no-sources")["QueueUrl"]
+    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=url)
+    assert resp["queueUrls"] == []
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a compat test for missing QueueUrl in ListDeadLetterSourceQueues

The JSON handler explicitly validates a missing `QueueUrl` and returns a `MissingParameter` error, but current compat tests only cover cases where `QueueUrl` is provided. Please add a test that calls `sqs_client.list_dead_letter_source_queues()` without `QueueUrl` and asserts the `MissingParameter` error code to cover this behavior and protect against regressions in parameter validation.

Suggested implementation:

```python
def test_list_dead_letter_source_queues(sqs_client):
    src, dlq, _ = _redrive_pair(sqs_client, "dlq-src", "dlq-target")
    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=dlq)
    urls = resp["queueUrls"]
    assert any("dlq-src" in u for u in urls)


def test_list_dead_letter_source_queues_no_sources(sqs_client):
    # A queue that exists but is not used as a DLQ returns an empty list.
    url = sqs_client.create_queue(QueueName="dlsq-no-sources")["QueueUrl"]
    resp = sqs_client.list_dead_letter_source_queues(QueueUrl=url)
    assert resp["queueUrls"] == []


def test_list_dead_letter_source_queues_missing_queue_url(sqs_client):
    # Missing required QueueUrl should raise a MissingParameter error
    with pytest.raises(ClientError) as exc:
        sqs_client.list_dead_letter_source_queues()
    err = exc.value.response["Error"]
    assert err["Code"] == "MissingParameter"


import pytest
from botocore.exceptions import ClientError, ParamValidationError

```

If the placement of `import pytest` and `ClientError`/`ParamValidationError` differs in your actual file (e.g., all imports are at the top), adjust the test insertion accordingly so that the new `test_list_dead_letter_source_queues_missing_queue_url` remains alongside the other ListDeadLetterSourceQueues compat tests and continues to use the existing imports.
</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 tests/compatibility/test_sqs.py Outdated
Comment thread tests/compatibility/test_sqs.py
… tests

- Retain the most recent 10 move-task records per source (was: one per
  source) so ListMessageMoveTasks pagination works while the registry
  stays bounded; clamp MaxResults to [1, 10] like the real API.
- Clarify the sourceQueuesForDLQ lock comment (caller holds s.mu read or
  write) and reword the ambiguous-destination error to describe the
  DLQ/multi-source scenario.
- Add compat tests: ListDeadLetterSourceQueues missing QueueUrl,
  ListMessageMoveTasks required SourceArn and MaxResults semantics
  (default/0/negative -> 10, in-range honored, over-cap clamped).

Required members (SourceArn/QueueUrl) are validated client-side by
botocore (ParamValidationError); the empty-string cases exercise the
server-side MissingParameter path.
@skyoo2003

Copy link
Copy Markdown
Owner Author

@sourcery-ai resolve

- Redrive only available DLQ messages; leave in-flight (received, undeleted)
  ones so a live consumer's receipt handle stays valid (no duplicate processing)
- ListMessageMoveTasks returns value copies taken under the lock, fixing a
  data race on moveTask.Status vs CancelMessageMoveTask
- Wire the four redrive ops into the Query (form) dispatcher with XML handlers,
  not just the JSON one — Query clients no longer get HTTP 400
- Order move tasks by a monotonic seq (not second-granular StartedUnix), so
  prune/list are deterministic and never drop the just-created task
- CancelMessageMoveTask rejects non-RUNNING tasks (ResourceNotFoundException);
  synchronous redrive is always COMPLETED, matching AWS
- ListMessageMoveTasks MaxResults defaults to 1 (AWS default), not the cap
- Emit StartedTimestamp in epoch milliseconds (AWS Long), not seconds
- Restore docs note that queue-level DelaySeconds/VisibilityTimeout are not enforced
- Hold s.mu only to resolve queue pointers; run drain/fill under per-queue locks
- Extract sortedTasksForSource to dedupe prune/list filter+sort
@skyoo2003

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@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 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/services/sqs/store_test.go" line_range="281-290" />
<code_context>
+func TestQueueStore_MessageMoveTaskRegistryBounded(t *testing.T) {
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen registry-bounding test to assert that the newest tasks are retained and older ones pruned

This currently only checks the cap and `ListMessageMoveTasks` max-results behavior, not *which* tasks are kept. Since ordering is `seq`-based, please also assert that early task handles are pruned and that the most recently started tasks are returned first. One way: capture the first few handles before the loop, then after the loop assert they’re absent from `store.moveTasks` and that `ListMessageMoveTasks` returns the last `maxTasksPerSource` handles in reverse insertion order to better guard future pruning/ordering changes.
</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 on lines +281 to +290
func TestQueueStore_MessageMoveTaskRegistryBounded(t *testing.T) {
store := NewQueueStore(0)
require.NoError(t, store.CreateQueue("bound-src", testAccount))
require.NoError(t, store.CreateQueue("bound-dlq", testAccount))

dlqArn := fmt.Sprintf("arn:aws:sqs:us-east-1:%s:bound-dlq", testAccount)
require.NoError(t, store.SetQueueAttributes("bound-src", testAccount, map[string]string{
"RedrivePolicy": `{"maxReceiveCount":1,"deadLetterTargetArn":"` + dlqArn + `"}`,
}))

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.

suggestion (testing): Strengthen registry-bounding test to assert that the newest tasks are retained and older ones pruned

This currently only checks the cap and ListMessageMoveTasks max-results behavior, not which tasks are kept. Since ordering is seq-based, please also assert that early task handles are pruned and that the most recently started tasks are returned first. One way: capture the first few handles before the loop, then after the loop assert they’re absent from store.moveTasks and that ListMessageMoveTasks returns the last maxTasksPerSource handles in reverse insertion order to better guard future pruning/ordering changes.

Strengthen TestQueueStore_MessageMoveTaskRegistryBounded to verify which
tasks survive pruning (oldest removed, newest maxTasksPerSource kept) and
that ListMessageMoveTasks returns them newest-first — guarding the
monotonic seq ordering when tasks share a StartedMillis.
@skyoo2003
skyoo2003 merged commit b16c632 into main Jul 18, 2026
9 checks passed
@skyoo2003
skyoo2003 deleted the feat/sqs-message-move-tasks branch July 18, 2026 01:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation services AWS service implementations tests Test code and test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant