Skip to content

feat(sqs): receipt-handle v2 codec for partitioned FIFO queues (Phase 3.D PR 5a)#724

Merged
bootjp merged 3 commits intomainfrom
feat/sqs-htfifo-receipt-handle-v2
May 2, 2026
Merged

feat(sqs): receipt-handle v2 codec for partitioned FIFO queues (Phase 3.D PR 5a)#724
bootjp merged 3 commits intomainfrom
feat/sqs-htfifo-receipt-handle-v2

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented May 1, 2026

Summary

Phase 3.D PR 5a — adds the v2 receipt-handle layout that PR 5b's send/receive partitioned fanout will consume. Pure scaffold: the v2 encoder is added but never called by any production path until PR 5b lifts the §11 PR 2 dormancy gate. Splitting the codec out as a small isolated PR keeps PR 5b's review focused on the gate-and-lift atomic sequence — codec correctness and gate-lift correctness are independent concerns.

Wire format

v1 (legacy, single partition) v2 (partitioned)
[ 0 ] byte version = 0x01 byte version = 0x02
[ 1..5 ] (none) uint32 partition (BE) ← NEW
[ ?..? ] uint64 queue_gen (BE) uint64 queue_gen (BE)
[ ?..? ] 16 bytes message_id 16 bytes message_id
[ ?..? ] 16 bytes receipt_token 16 bytes receipt_token
Total 41 bytes 45 bytes

decodedReceiptHandle gains Version (byte) and Partition (uint32) fields. decodeReceiptHandle dispatches on the version byte; v1 keeps Partition=0 by definition.

What does NOT change yet

  • encodeReceiptHandleV2 is dormant until PR 5b's SendMessage fanout dispatch wires it (gated by meta.PartitionCount > 1). Production traffic continues to use the v1 encoder verbatim — no behaviour change.
  • DeleteMessage / ChangeMessageVisibility still consume decodedReceiptHandle without inspecting Version or Partition. PR 5b adds the cross-version rejection contract (a v1 handle against a partitioned queue → ReceiptHandleIsInvalid, and vice-versa).

Test plan

  • TestEncodeReceiptHandleV2_RoundTrip — every (partition, queue_gen, message_id, token) tuple round-trips.
  • TestEncodeReceiptHandleV1_StillReportsV1 — regression: v1 encoder still produces v1-decodable handles with Partition=0.
  • TestDecodeReceiptHandle_VersionDispatch — v1 and v2 produce distinct on-wire bytes (different version + 4-byte size delta); decoder picks the right layout.
  • TestDecodeReceiptHandle_RejectsLengthMismatch — 5 cases (v1 byte / v2 length, v2 byte / v1 length, v1 truncated, v2 truncated, empty) all fail with the same opaque error.
  • TestDecodeReceiptHandle_RejectsUnknownVersion0x00, 0x03, 0x42, 0xFF all reject.
  • TestEncodeReceiptHandleV2_RejectsBadInputs — short/long token, non-hex/short/empty id all error.
  • TestReceiptHandleVersionConstants_Distinct — pins v1 != v2, v1 == 0x01, v2 == 0x02, legacy alias points at v1, on-wire size constants match.
  • TestDecodeReceiptHandle_RejectsBase64Garbage — base64 error surfaces correctly.
  • Existing TestSQSServer_ReceiptHandleCodecRoundTrip continues to pass (v1 path).
  • go test -race ./adapter/... pass.
  • golangci-lint ./adapter/... clean.

Self-review (per CLAUDE.md)

  1. Data loss — codec only; no FSM/Pebble path. No issue.
  2. Concurrency / distributed failures — no shared state; pure encode/decode. No issue.
  3. Performance — encode/decode are O(handle size) ≤ 45 bytes. Version-byte dispatch is one switch. No issue.
  4. Data consistency — v1 and v2 are length-distinct, version-byte-distinct, decoder rejects wrong-length blobs. A v1 client parsing a v2 handle (or vice versa) fails closed at the length check — pinned by the 5-case rejection test. No issue.
  5. Test coverage — 8 new tests covering the codec contract surface; existing v1 round-trip test continues to pass.

Summary by CodeRabbit

  • New Features

    • SQS message receipt handles now support versioned encoding format, enabling compatibility with partitioned FIFO queues while maintaining backward compatibility.
  • Tests

    • Added validation tests for receipt handle encoding and decoding wire-format contract across format versions.

… 3.D PR 5a)

Adds the v2 receipt-handle layout that PR 5b's send / receive
fanout will consume. Pure scaffold: the v2 encoder is added but
not yet called by any production path (PartitionCount > 1 is
still rejected by the §11 PR 2 dormancy gate, so SendMessage
never reaches a partitioned queue, so encodeReceiptHandleV2 stays
unreachable until PR 5b lifts the gate).

Splitting the codec out as a small isolated PR keeps PR 5b's
review focused on the gate-and-lift atomic sequence — codec
correctness and the gate-lift correctness are independent
concerns and a single mega-PR would conflate them.

Wire format

v1 (legacy, single partition):
  [ 0 ]    byte version = 0x01
  [ 1..9 ] uint64 queue_gen (BE)
  [ 9..25 ] 16 bytes message_id
  [ 25..41 ] 16 bytes receipt_token
  Total: 41 bytes

v2 (partitioned, PartitionCount > 1):
  [ 0 ]      byte version = 0x02
  [ 1..5 ]   uint32 partition (BE)            ← NEW
  [ 5..13 ]  uint64 queue_gen (BE)
  [ 13..29 ] 16 bytes message_id
  [ 29..45 ] 16 bytes receipt_token
  Total: 45 bytes

decodedReceiptHandle gains Version (byte) and Partition (uint32)
fields. decodeReceiptHandle dispatches by the version byte; v1
keeps Partition=0 by definition.

What does NOT change yet

- encodeReceiptHandleV2 is dormant until PR 5b's SendMessage
  partitioned-fanout dispatch wires it (gated by
  meta.PartitionCount > 1). Production traffic continues to use
  the v1 encoder verbatim — no behaviour change.
- DeleteMessage / ChangeMessageVisibility still consume
  decodedReceiptHandle without inspecting Version or Partition.
  PR 5b adds the cross-version rejection contract (a v1 handle
  against a partitioned queue → ReceiptHandleIsInvalid, and
  vice-versa).

Tests

- TestEncodeReceiptHandleV2_RoundTrip — every (partition,
  queue_gen, message_id, token) tuple round-trips with
  Version=v2 and the encoder's partition.
- TestEncodeReceiptHandleV1_StillReportsV1 — regression: v1
  encoder still produces v1-decodable handles with Partition=0.
- TestDecodeReceiptHandle_VersionDispatch — v1 and v2 produce
  distinct on-wire bytes (different version + 4-byte size delta);
  decoder picks the right layout for each.
- TestDecodeReceiptHandle_RejectsLengthMismatch — 5 cases (v1
  byte / v2 length, v2 byte / v1 length, v1 truncated, v2
  truncated, empty) all fail with the same opaque error.
- TestDecodeReceiptHandle_RejectsUnknownVersion — 0x00, 0x03,
  0x42, 0xFF all reject.
- TestEncodeReceiptHandleV2_RejectsBadInputs — short / long
  token, non-hex / short / empty id all surface as encoder errors.
- TestReceiptHandleVersionConstants_Distinct — pins the
  invariants v1 != v2, v1 == 0x01, v2 == 0x02, legacy alias
  points at v1, on-wire size constants match what encoders write.
- TestDecodeReceiptHandle_RejectsBase64Garbage — base64 errors
  surface at the right step.
- Existing TestSQSServer_ReceiptHandleCodecRoundTrip continues
  to pass unchanged (v1 path).

Self-review (per CLAUDE.md)

1. Data loss — codec only; no FSM/Pebble path. No issue.
2. Concurrency — no shared state; pure encode/decode functions.
   No issue.
3. Performance — encode/decode are O(handle size) ≤ 45 bytes.
   The version-byte dispatch is one switch. No issue.
4. Data consistency — v1 and v2 are length-distinct, version-byte-
   distinct, and decoder rejects wrong-length blobs. A v1 client
   parsing a v2 handle (or vice versa) fails closed at the
   length check — pinned by the 5-case rejection test. No issue.
5. Test coverage — 8 new tests across the codec contract surface;
   existing v1 round-trip test continues to pass.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 1, 2026

@claude review

Phase 3.D PR 5a — receipt-handle v2 codec scaffold. Pure-additive: v2 encoder is dormant until PR 5b lifts the dormancy gate. Splitting it out keeps PR 5b's review focused on the gate-and-lift atomic sequence. PR description has the full self-review and test plan.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Warning

Rate limit exceeded

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

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03cf8ae7-0e5b-4e69-bc51-c107325abc5a

📥 Commits

Reviewing files that changed from the base of the PR and between 0206a1c and c15d801.

📒 Files selected for processing (3)
  • adapter/sqs_messages.go
  • adapter/sqs_messages_batch.go
  • adapter/sqs_receipt_handle_v2_test.go
📝 Walkthrough

Walkthrough

SQS receipt handle encoding is extended from a single legacy format to a versioned scheme with v1 (single-partition) and v2 (partitioned FIFO) layouts. Decoding now inspects version bytes to dispatch to the appropriate decoder, with decoded handles carrying version and partition metadata.

Changes

Cohort / File(s) Summary
Receipt Handle Versioning
adapter/sqs_messages.go
Introduces dual-version receipt handle encoding: v1 retains original single-partition layout; v2 adds partition uint32 support. Decoder refactored to inspect version byte and dispatch to v1/v2-specific decoders. Error handling consolidated around "length or version mismatch" style messages.
Receipt Handle Tests
adapter/sqs_receipt_handle_v2_test.go
New comprehensive test suite validating v1 and v2 encode/decode round-trips, version dispatch correctness, partition extraction, error handling for truncated/invalid blobs, and rejection of unknown version bytes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Poem

🐰 Hop, hop! Receipt handles now bloom in two flavors,
v1 and v2 dance through version bytes with care,
Partitions nestle in the code, crisp and clear,
Decoders dispatch with swift logic, no despair,
Tests verify each format—a grand affair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing a v2 receipt-handle codec for partitioned FIFO queues, with explicit phase/PR reference for context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/sqs-htfifo-receipt-handle-v2

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
Review rate limit: 0/1 reviews remaining, refill in 46 minutes and 24 seconds.

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

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @bootjp's task in 3m 54s —— View job


Code Review — Phase 3.D PR 5a: v2 Receipt Handle Codec

  • Gather context and understand the request
  • Read adapter/sqs_messages.go changes
  • Read adapter/sqs_receipt_handle_v2_test.go
  • Analyze codec correctness, wire format, error handling
  • Check test coverage and edge cases
  • Post comprehensive review

Overall: The codec is correct, the tests are thorough, and the dormancy boundary is cleanly maintained. A few minor observations below — none are blocking.


Wire format correctness

Offsets in both encoders and decoders are verified against the size constants:

Field v1 v2
b[0] version=0x01 version=0x02
b[1:5] uint32 partition (4 B)
v1:b[1:9] / v2:b[5:13] uint64 queue_gen uint64 queue_gen
v1:b[9:25] / v2:b[13:29] 16 B message_id 16 B message_id
v1:b[25:41] / v2:b[29:45] 16 B receipt_token 16 B receipt_token

sqsReceiptHandleV1Size = 1+8+16+16 = 41
sqsReceiptHandleV2Size = 1+4+8+16+16 = 45
All slice indices in decodeReceiptHandleV1/decodeReceiptHandleV2 align correctly with these sizes.

The encodeReceiptHandleV2decodeReceiptHandle round-trip is correct. bytes.Clone on the receipt token slice is the right defensive move. The opaque error message for length/version mismatches is a sound security decision.


Observations

1. sqsReceiptHandleVersion alias has no production call site (nit)

adapter/sqs_messages.go:66

The comment says the alias preserves "existing call sites in this file" — but encodeReceiptHandle (the only call site that previously would have used it) was updated to use sqsReceiptHandleVersion1 explicitly in the same PR. The alias is only referenced by the test at sqs_receipt_handle_v2_test.go:232 to verify it equals v1.

The alias is harmless but the comment is slightly misleading. Either drop the alias (and the corresponding assertion in TestReceiptHandleVersionConstants_Distinct) or update the comment to say it's a test anchor, not a call-site guard. Fix this →

2. Magic offset numbers in the decoders (nit)

adapter/sqs_messages.go:353-356, 364-369

The offsets 9 (= 1+8) and 13 (= 1+4+8) are correct but are derived rather than named. If sqsMessageIDBytes ever changes, the hardcoded numbers would need a manual update. A minor self-documenting improvement would be named per-field offsets:

const (
    sqsReceiptHandleV1GenOffset   = 1
    sqsReceiptHandleV1IDOffset    = sqsReceiptHandleV1GenOffset + 8
    sqsReceiptHandleV1TokenOffset = sqsReceiptHandleV1IDOffset + sqsMessageIDBytes
    // ...analogous v2 offsets
)

Not required to merge, but makes layout changes self-consistent.

3. Open-ended receipt token slice (nit)

adapter/sqs_messages.go:355, 368

// Current:
ReceiptToken: bytes.Clone(b[9+sqsMessageIDBytes:]),
// More explicit:
ReceiptToken: bytes.Clone(b[9+sqsMessageIDBytes : 9+sqsMessageIDBytes+sqsReceiptTokenBytes]),

Functionally identical (the length check above guarantees no extra bytes), but an explicit upper bound makes the intent self-documenting.

4. decodedReceiptHandle docstring describes unenforced contract (minor)

adapter/sqs_messages.go:308-315

The docstring currently says callers "MUST inspect Version before consulting Partition" and that cross-version handles "are rejected with ReceiptHandleIsInvalid". That rejection contract doesn't exist yet — it's PR 5b's job. Since this is a pure scaffold PR the doc is technically forward-looking. Worth a note like // Enforcement of this contract is added by PR 5b. to avoid confusion during review of that PR.


Test coverage

All 8 new tests are well-targeted:

Test What it catches
V2_RoundTrip encode→decode contract for v2, including partition=0 (the tricky ambiguous value)
V1_StillReportsV1 regression: v1 encoder not accidentally upgraded
VersionDispatch decode routes to correct layout based on version byte
RejectsLengthMismatch (5 cases) wrong-length blobs fail loudly
RejectsUnknownVersion unknown version bytes hit default branch
V2_RejectsBadInputs encoder validates token length and id format
VersionConstants_Distinct pins wire values and alias invariant
RejectsBase64Garbage base64 error surfaces correctly

One minor gap: there's no case for a blob with version=v2 and len > sqsReceiptHandleV2Size (e.g., 46 bytes). The exact-length check in decodeReceiptHandleV2 would correctly reject it, but it's not explicitly exercised. Non-blocking.

The existing TestSQSServer_ReceiptHandleCodecRoundTrip doesn't assert on the new Version and Partition fields, but TestEncodeReceiptHandleV1_StillReportsV1 covers that regression gap. Good.


Self-review passes (CLAUDE.md §5 lenses)

  1. Data loss — codec only; no FSM/storage path. ✓
  2. Concurrency — pure encode/decode, no shared mutable state. ✓
  3. Performance — O(45 bytes), one switch dispatch, no allocation on the hot decode path beyond the returned struct. ✓
  4. Data consistency — v1/v2 are length-distinct and version-byte-distinct; decoder fails closed on any mismatch. The dormancy gate (v2 encoder never called) ensures no live traffic sees v2 handles until PR 5b lifts it. ✓
  5. Test coverage — comprehensive; items 1–4 above are all nits with no correctness impact. ✓

Ready to merge. The nits are worth a quick pass but none warrant holding the PR.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@adapter/sqs_messages.go`:
- Around line 324-343: The dispatcher currently accepts v2 handles which
prematurely exposes the new format; update decodeReceiptHandle so it only
dispatches to decodeReceiptHandleV1 for now and treats sqsReceiptHandleVersion2
(and any other non-v1 version) as a length/version mismatch error, keeping
decodeReceiptHandleV2 dormant; additionally add a regression/guard in the
DeleteMessage and ChangeMessageVisibility entry points to enforce that
downstream validation still only accepts v1-derived fields (specifically ensure
QueueGeneration and ReceiptToken checks reject any v2-encoded handles or handles
that would rely on v2-only fields/symbols) so the public API behavior remains
unchanged until PR 5b.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4d0102b-7d67-4030-ac58-09770f498293

📥 Commits

Reviewing files that changed from the base of the PR and between 9b1be20 and 0206a1c.

📒 Files selected for processing (2)
  • adapter/sqs_messages.go
  • adapter/sqs_receipt_handle_v2_test.go

Comment thread adapter/sqs_messages.go
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a versioned SQS receipt handle format to support partitioned FIFO queues. It adds a new v2 handle format incorporating a partition index, updates the decoding logic to handle both legacy and partitioned formats, and provides extensive unit tests. Feedback indicates that the appendU32 helper function used in the new encoding logic is missing, which will cause a compilation error.

Comment thread adapter/sqs_messages.go
}
buf := make([]byte, 0, sqsReceiptHandleV2Size)
buf = append(buf, sqsReceiptHandleVersion2)
buf = appendU32(buf, partition)
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.

high

The function appendU32 is called here but it is not defined in this file or any other provided file in the package. This will cause a compilation error. You should add a helper function similar to appendU64 for uint32 values. Ensure that such utility functions are centralized if they are intended for use across the repository to maintain consistency and avoid duplication.

func appendU32(dst []byte, v uint32) []byte {
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[:], v)
	return append(dst, buf[:]...)
}

func encodeReceiptHandleV2(partition uint32, queueGen uint64, messageIDHex string, receiptToken []byte) (string, error) {
References
  1. Centralize common logic into shared helper functions to avoid code duplication and improve maintainability.

Round 1 Claude review nits on PR #724:

1) Dropped sqsReceiptHandleVersion alias

  Round 1 added sqsReceiptHandleVersion as an alias of v1 "for
  existing call sites". In practice every call site now uses
  sqsReceiptHandleVersion1 explicitly, so the alias's only
  reference was a self-test. Dropped both the alias and the
  test assertion. The two version-byte constants
  (sqsReceiptHandleVersion1 / sqsReceiptHandleVersion2) are now
  the single source of truth.

2) Named per-field offset constants

  decodeReceiptHandleV1 / decodeReceiptHandleV2 used numeric
  offsets (1, 9, 13, 1+sqsMessageIDBytes, …) derived from layout
  arithmetic. Replaced with named constants:

    v1: V1VersionOffset, V1GenOffset, V1IDOffset,
        V1TokenOffset, V1Size
    v2: V2VersionOffset, V2PartitionOffset, V2GenOffset,
        V2IDOffset, V2TokenOffset, V2Size

  A future field-shape change touches the constants in one
  place instead of scattering numeric arithmetic through
  encoders / decoders.

3) Explicit upper bound on token slice

  bytes.Clone(b[start:]) → bytes.Clone(b[start:end]). The
  length check above guarantees the open-ended form would have
  read the right bytes, but the explicit upper bound is
  self-documenting and protects against a future refactor that
  forgets the length precondition.

4) Doc note on cross-version rejection

  decodedReceiptHandle's doc described the cross-version
  rejection contract ("a v1 handle against a partitioned queue
  → ReceiptHandleIsInvalid") that this scaffold PR does NOT
  enforce — that's PR 5b's gate-and-lift. Added an explicit
  note that enforcement is wired by PR 5b so future review of
  that PR doesn't go looking for the contract here.

5) New test case: v2 / v1 oversized

  TestDecodeReceiptHandle_RejectsLengthMismatch grew two cases
  (v2 + 1 byte, v1 + 1 byte) so the exact-length check on each
  decoder branch is independently exercised. The previous
  table covered "byte mismatch" and "truncated" but not "too
  long for the version".

Audit per the lessons-learned discipline

The semantic change here is the alias removal. grep -rn
sqsReceiptHandleVersion across the repo confirmed zero
remaining references after the test assertion was dropped.
The named-offset refactor preserves byte-for-byte the same
on-wire format; the constants ARE the layout.

go test -race ./adapter/... and golangci-lint clean.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 1, 2026

@claude review

Round 2 — addresses round 1's 4 nits + 1 test gap:

  1. Dropped sqsReceiptHandleVersion alias: only the self-test referenced it. Removed both. The two version-byte constants (sqsReceiptHandleVersion1 / sqsReceiptHandleVersion2) are now the single source of truth.
  2. Named per-field offset constants for v1 / v2: V1VersionOffset, V1GenOffset, V1IDOffset, V1TokenOffset, V1Size; same for v2 plus V2PartitionOffset. Decoders use these instead of magic numbers like 9 and 13.
  3. Explicit slice upper bound: bytes.Clone(b[start:]) → bytes.Clone(b[start:end]) for self-documenting intent.
  4. Doc note: decodedReceiptHandle doc explicitly states the cross-version rejection contract is wired by PR 5b, not this scaffold.
  5. New test cases: TestDecodeReceiptHandle_RejectsLengthMismatch grew two oversized-blob cases (v2+1 byte, v1+1 byte) — closes the gap noted in the round 1 review.

Audit per lessons-learned: grep -rn sqsReceiptHandleVersion across the repo confirmed zero remaining references after dropping the test assertion. Named-offset refactor preserves byte-for-byte the same on-wire format.

go test -race ./adapter/... and golangci-lint clean.

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @bootjp's task in 2m 45s —— View job


Code Review — Phase 3.D PR 5a: v2 Receipt Handle Codec (Round 2)

  • Gather context and understand the request
  • Read current state of adapter/sqs_messages.go
  • Read current state of adapter/sqs_receipt_handle_v2_test.go
  • Verify Round 1 nits were addressed (5 items)
  • Assess CodeRabbit's v2 dormancy concern on public API path
  • Assess Gemini's appendU32 compilation concern
  • Post comprehensive Round 2 review

Overall: All five Round 1 nits are cleanly addressed. The code is correct and the test coverage is solid. Two reviewer concerns from other tools are addressed below.


Round 1 nits — status

Nit Status
Dropped sqsReceiptHandleVersion alias ✓ Gone. sqsReceiptHandleVersion1 / sqsReceiptHandleVersion2 are now the sole sources of truth. The alias assertion is removed from the test.
Named per-field offset constants ✓ Full constant chains for v1 (V1VersionOffsetV1GenOffsetV1IDOffsetV1TokenOffsetV1Size) and v2 (same, plus V2PartitionOffset) at sqs_messages.go:238-250. Decoders use them exclusively.
Explicit slice upper bound ✓ All bytes.Clone calls use b[start:end], e.g. b[sqsReceiptHandleV1TokenOffset:sqsReceiptHandleV1Size].
Doc note on PR 5b contract decodedReceiptHandle comment at line 311 explicitly states: "Enforcement of the cross-version rejection contract is wired by Phase 3.D PR 5b together with the gate-and-lift."
Oversized blob test cases TestDecodeReceiptHandle_RejectsLengthMismatch grew from 5 to 7 cases with "v2 oversized" (V2Size+1) and "v1 oversized" (V1Size+1).

Gemini's concern: missing appendU32false positive

appendU32 is defined at adapter/sqs_keys.go:443-444 in the same adapter package. It's visible to sqs_messages.go without any import. The package compiles. No action needed.


CodeRabbit's concern: v2 dormancy on the public API path — noted, not blocking

CodeRabbit suggests that decodeReceiptHandle should reject v2 handles until PR 5b lifts the gate, so deleteMessage / changeMessageVisibility cannot accept a v2-versioned blob today.

The concern is real but the risk is negligible for this scaffold:

  1. encodeReceiptHandleV2 is never called in production. No v2 handle can originate from the system.
  2. If a crafted v2 handle arrives at deleteMessage, loadMessageForDelete checks meta.Generation == handle.QueueGeneration and then verifies the 16-byte CurrentReceiptToken (from crypto/rand) against handle.ReceiptToken. A crafted handle cannot forge this token. The path is either a no-op (token mismatch → 200) or ReceiptHandleIsInvalid (generation mismatch → 400).
  3. The Partition field is not used by any delete/change-visibility key construction (sqsMsgDataKey, sqsMsgVisKey — both in sqs_keys.go). A v2 handle is processed exactly like a v1 handle at those layers, with the Partition field silently ignored. This is correct: partitioned queues don't exist yet, so ignoring Partition has no behavioral consequence.

CodeRabbit's proposed guard (if handle.Version != sqsReceiptHandleVersion1 { reject } in parseQueueAndReceipt) would be a valid tightening, but it would also need to be reverted/extended in PR 5b. Since the decoder is correct and the 16-byte receipt token makes forgery infeasible, I'd classify this as a style preference — lean toward CodeRabbit's suggestion only if you want a belt-and-suspenders dormancy guarantee at the decode level, not just at the encode level.

If you do add the guard, the matching test case would be: TestDeleteMessage_RejectsV2HandleBeforePR5b — craft a valid-shaped v2 blob and assert the error code is ReceiptHandleIsInvalid.


Codec correctness re-verification

Offsets derived from the new named constants:

Constant Value Check
V1VersionOffset 0
V1GenOffset 1 ✓ (1 byte for version)
V1IDOffset 9 ✓ (1+8)
V1TokenOffset 25 ✓ (1+8+16)
V1Size 41 ✓ (1+8+16+16)
V2PartitionOffset 1
V2GenOffset 5 ✓ (1+4)
V2IDOffset 13 ✓ (1+4+8)
V2TokenOffset 29 ✓ (1+4+8+16)
V2Size 45 ✓ (1+4+8+16+16)

All slice indices in both decoders align with the constants. Wire format unchanged.


Self-review passes (CLAUDE.md §5 lenses)

  1. Data loss — codec only; no FSM/Pebble path. ✓
  2. Concurrency — pure encode/decode, no shared mutable state. ✓
  3. Performance — O(45 bytes), one switch dispatch. ✓
  4. Data consistency — v1/v2 length-distinct, version-byte-distinct, decoder fails closed. Dormancy maintained at encoder level. ✓
  5. Test coverage — 8 tests covering the contract surface, now closed on oversized blobs. ✓

Ready to merge.

CodeRabbit major on PR #724 round 2: the v2 codec is in the
binary AND reachable on the public API. A client can re-encode a
legitimately-issued v1 handle's (queue_gen, message_id,
receipt_token) under the v2 layout, and DeleteMessage /
ChangeMessageVisibility will accept it (downstream validation
only checks queue_gen + receipt_token). The behaviour is
technically correct (the v1 keyspace lookup still finds the
message) but it leaks the new wire format before PR 5b lands —
breaking the "no behavior change yet" guarantee.

Fix

decodeClientReceiptHandle wraps decodeReceiptHandle with the
dormancy gate: any v2 handle on the public API surfaces as
ReceiptHandleIsInvalid until PR 5b's queue-aware version replaces
the gate (v1 required on non-partitioned queues, v2 required on
partitioned ones).

Changed call sites

Three public-API decode points switch from decodeReceiptHandle
to decodeClientReceiptHandle:

  - parseQueueAndReceipt (sqs_messages.go) — DeleteMessage and
    ChangeMessageVisibility entry point.
  - DeleteMessageBatch entry decode (sqs_messages_batch.go).
  - ChangeMessageVisibilityBatch entry decode (sqs_messages_batch.go).

The low-level decodeReceiptHandle keeps working as a pure codec
so the existing v1 + v2 round-trip tests keep passing.

Tests

  - TestDecodeClientReceiptHandle_AcceptsV1: v1 handles flow
    through unchanged.
  - TestDecodeClientReceiptHandle_RejectsV2: a v2 handle on the
    public API surfaces as the dormancy-gate error. The
    low-level decoder still accepts it (the gate is at the API
    boundary, not in the codec).
  - TestDecodeClientReceiptHandle_PassesThroughDecodeErrors: a
    decode-step error (e.g. base64 garbage) is NOT masked by
    the dormancy-gate message — operators see the underlying
    error.

Audit per the lessons-learned discipline

The semantic change here is the rejection contract for v2
handles on the public API. grep -rn "decodeReceiptHandle"
across adapter/ showed 3 production call sites (parseQueueAndReceipt,
DeleteMessageBatch, ChangeMessageVisibilityBatch). All 3 updated.
The v2 codec round-trip tests intentionally keep using
decodeReceiptHandle directly so they exercise the codec, not
the public API.

Note on Gemini's "appendU32 is undefined" finding (round 2)

False positive. appendU32 is defined in adapter/sqs_keys.go:444
and has been since PR #703 merged. Gemini did not search across
files in the package; CI build is green and the round-1 v2
round-trip tests exercise the function. No code change needed.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 1, 2026

@claude review

Round 3 — addresses Round 2's two new findings:

CodeRabbit major (real, fixed): v2 codec was reachable on the public API. A client re-encoding a legitimately-issued v1 handle's (queue_gen, message_id, receipt_token) under the v2 layout would have been accepted by DeleteMessage / ChangeMessageVisibility (downstream validation only checks queue_gen + receipt_token), leaking the new wire format before PR 5b lands.

Added decodeClientReceiptHandle wrapper that rejects v2 handles at the public API boundary until PR 5b's queue-aware gate replaces it. Updated 3 call sites:

  • parseQueueAndReceipt (DeleteMessage / ChangeMessageVisibility entry)
  • DeleteMessageBatch entry decode
  • ChangeMessageVisibilityBatch entry decode

Low-level decodeReceiptHandle stays unchanged as a pure codec so the existing v1 + v2 round-trip tests keep exercising the codec. New 3 tests pin the wrapper contract: v1 accepted, v2 rejected with "not yet enabled", decode errors pass through unmodified.

Gemini high (false positive): "appendU32 is called but not defined" — appendU32 is defined in adapter/sqs_keys.go:444 since PR #703 merged. CI build is green; round-1 v2 round-trip tests already exercise the function. No code change.

Audit per lessons-learned: grep -rn "decodeReceiptHandle" confirmed exactly 3 production call sites for the public API — all 3 swapped to the wrapper. Tests intentionally still call decodeReceiptHandle directly to exercise the codec layer.

go test -race ./adapter/... and golangci-lint clean.

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @bootjp's task in 2m 2s —— View job


Code Review — Phase 3.D PR 5a: v2 Receipt Handle Codec (Round 3)

  • Gather context and understand the request
  • Read current state of adapter/sqs_messages.go
  • Read current state of adapter/sqs_messages_batch.go
  • Read current state of adapter/sqs_receipt_handle_v2_test.go
  • Verify decodeClientReceiptHandle wrapper and all call sites
  • Verify Gemini false positive (appendU32 in sqs_keys.go)
  • Post comprehensive Round 3 review

Overall: All Round 2 items remain in place. The CodeRabbit major is fully addressed with a clean wrapper + complete call-site sweep. Ready to merge.


CodeRabbit major — verified, fully addressed

decodeClientReceiptHandle wrapper (sqs_messages.go:399) correctly gates v2 at the public API boundary. The implementation is sound:

  • The internal decodeReceiptHandle remains an unrestricted pure codec (callable from tests and from decodeClientReceiptHandle itself).
  • The wrapper's error message ("not yet enabled on the public API") is never exposed to clients — all three call sites hardcode their own generic client-facing message:
    • parseQueueAndReceipt"receipt handle is not parseable"
    • deleteMessageBatch"receipt handle is not parseable"
    • applyChangeVisibilityBatchEntry"receipt handle is not parseable"
  • No production path bypasses the wrapper. grep -rn decodeReceiptHandle adapter/ --include="*.go" (excluding tests) confirms exactly 3 call sites — sqs_messages.go:1539, sqs_messages_batch.go:470, sqs_messages_batch.go:570 — and all three use decodeClientReceiptHandle. The raw decodeReceiptHandle is referenced only in its own definition, the wrapper, and tests.
  • encodeReceiptHandleV2 has zero production call sites (confirmed by grep). Both halves of the dormancy guarantee hold.

Gemini high — confirmed false positive

appendU32 is defined at adapter/sqs_keys.go:443-444 (same adapter package, in-tree since PR #703). The Round 1 v2 round-trip tests have been exercising it the entire time. No action needed.


New tests — status

Three new tests pin the wrapper contract precisely:

Test What it catches
TestDecodeClientReceiptHandle_AcceptsV1 Happy path: v1 handles flow through the wrapper unmodified
TestDecodeClientReceiptHandle_RejectsV2 Gate test: v2 encoder still works (codec is dormant, not absent); low-level decoder still accepts v2; wrapper rejects v2 with "not yet enabled"
TestDecodeClientReceiptHandle_PassesThroughDecodeErrors Error propagation: a malformed blob surfaces the underlying decode error, not the dormancy-gate message

The RejectsV2 test's two-step structure (codec-level accept then API-level reject) is particularly well-designed — it explicitly locks down the layering that makes the gate removable in PR 5b without touching the codec.


Round 2 nits — still in place

Item Status
Named per-field offset constants V1GenOffset, V2PartitionOffset, etc. throughout
Explicit slice upper bounds in bytes.Clone ✓ All b[start:end]
PR 5b doc note on decodedReceiptHandle ✓ Present
Oversized blob test cases ✓ 7 cases total (now includes "v2 oversized" and "v1 oversized")
sqsReceiptHandleVersion alias dropped sqsReceiptHandleVersion1/Version2 are sole sources of truth

Self-review passes (CLAUDE.md §5 lenses)

  1. Data loss — codec only; no FSM/storage path. ✓
  2. Concurrency — pure encode/decode, no shared mutable state. ✓
  3. Performance — O(45 bytes), one switch + one equality check for the wrapper. ✓
  4. Data consistency — encoder dormancy preserved (no production encodeReceiptHandleV2 call site); decoder dormancy enforced at wrapper level; wrapper error cannot leak to clients. ✓
  5. Test coverage — 11 tests (8 codec + 3 wrapper); oversized and error-propagation cases closed. ✓

Ready to merge.

@bootjp bootjp merged commit a564c60 into main May 2, 2026
9 checks passed
@bootjp bootjp deleted the feat/sqs-htfifo-receipt-handle-v2 branch May 2, 2026 10:15
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.

1 participant