Skip to content

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

Merged
bootjp merged 3 commits into
mainfrom
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 into
mainfrom
feat/sqs-htfifo-receipt-handle-v2

Conversation

@bootjp

@bootjpbootjp commented May 1, 2026

Copy link
Copy Markdown
Owner

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 = 0x01byte version = 0x02
[ 1..5 ](none)uint32 partition (BE) ← NEW
[ ?..? ]uint64 queue_gen (BE)uint64 queue_gen (BE)
[ ?..? ]16 bytes message_id16 bytes message_id
[ ?..? ]16 bytes receipt_token16 bytes receipt_token
Total41 bytes45 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
OwnerAuthor

@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

coderabbitaiBot commented May 1, 2026

Copy link
Copy Markdown

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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe 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✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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

claudeBot commented May 1, 2026

Copy link
Copy Markdown

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:

Fieldv1v2
b[0]version=0x01version=0x02
b[1:5]uint32 partition (4 B)
v1:b[1:9] / v2:b[5:13]uint64 queue_genuint64 queue_gen
v1:b[9:25] / v2:b[13:29]16 B message_id16 B message_id
v1:b[25:41] / v2:b[29:45]16 B receipt_token16 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=1sqsReceiptHandleV1IDOffset=sqsReceiptHandleV1GenOffset+8sqsReceiptHandleV1TokenOffset=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:

TestWhat it catches
V2_RoundTripencode→decode contract for v2, including partition=0 (the tricky ambiguous value)
V1_StillReportsV1regression: v1 encoder not accidentally upgraded
VersionDispatchdecode routes to correct layout based on version byte
RejectsLengthMismatch (5 cases)wrong-length blobs fail loudly
RejectsUnknownVersionunknown version bytes hit default branch
V2_RejectsBadInputsencoder validates token length and id format
VersionConstants_Distinctpins wire values and alias invariant
RejectsBase64Garbagebase64 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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

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 threadadapter/sqs_messages.go

@gemini-code-assistgemini-code-assistBot 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.

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.

}
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.

funcappendU32(dst []byte, vuint32) []byte {
varbuf [4]bytebinary.BigEndian.PutUint32(buf[:], v)
returnappend(dst, buf[:]...)
}
funcencodeReceiptHandleV2(partitionuint32, queueGenuint64, messageIDHexstring, 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
OwnerAuthor

@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

claudeBot commented May 1, 2026

Copy link
Copy Markdown

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

NitStatus
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 contractdecodedReceiptHandle 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 casesTestDecodeReceiptHandle_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:

ConstantValueCheck
V1VersionOffset0
V1GenOffset1✓ (1 byte for version)
V1IDOffset9✓ (1+8)
V1TokenOffset25✓ (1+8+16)
V1Size41✓ (1+8+16+16)
V2PartitionOffset1
V2GenOffset5✓ (1+4)
V2IDOffset13✓ (1+4+8)
V2TokenOffset29✓ (1+4+8+16)
V2Size45✓ (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
OwnerAuthor

@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

claudeBot commented May 1, 2026

Copy link
Copy Markdown

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:

TestWhat it catches
TestDecodeClientReceiptHandle_AcceptsV1Happy path: v1 handles flow through the wrapper unmodified
TestDecodeClientReceiptHandle_RejectsV2Gate test: v2 encoder still works (codec is dormant, not absent); low-level decoder still accepts v2; wrapper rejects v2 with "not yet enabled"
TestDecodeClientReceiptHandle_PassesThroughDecodeErrorsError 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

ItemStatus
Named per-field offset constantsV1GenOffset, 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 droppedsqsReceiptHandleVersion1/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 mainMay 2, 2026
9 checks passed
@bootjp
bootjp deleted the feat/sqs-htfifo-receipt-handle-v2 branch May 2, 2026 10:15
bootjp added a commit that referenced this pull request May 5, 2026
….D PR 8) (#739)
## Summary
Phase 3.D PR 8 — the doc-lifecycle bump for the split-queue-FIFO design.
Renames `2026_04_26_proposed_sqs_split_queue_fifo.md` →
`2026_04_26_partial_sqs_split_queue_fifo.md` because every milestone in
the rollout plan that produces shippable code has merged on `main` (#681
schema, #703 keyspace, #704 / #708 / #715 / #721 / #723 routing-layer
4-A through 4-B-3b, #724 / #731 / #732 / #734 data-plane fanout 5a
through 5b-3, #735 / #736 reaper 6a/6b, #737 / #738 metrics + Jepsen
7a/7b).
This is the milestone tracked as PR 8 in §11 of the design doc itself.
## Why "partial" rather than "implemented"
Per `docs/design/README.md`:
| Value | Meaning |
|---|---|
| `proposed` | No matching code on `main`, or the doc itself declares
itself a proposal. |
| `partial` | Some components exist but the full scope described in the
doc is not yet merged. |
| `implemented` | Concrete Go code exists on `main` that matches the
design's central subsystem. |
The central subsystem (the partitioned-FIFO data plane, reaper,
capability gate, metrics, and Jepsen workload) does exist on `main`,
which would suggest `implemented`. I chose `partial` because §10 (Open
Questions) and §12 (Alternatives Considered) explicitly note follow-on
work that extends the same surface — operator-configurable hash, online
resharding, cross-partition transactional admin. Each is out-of-scope
for this proposal but each would be an extension of the design.
`partial` leaves room for those without forcing them into a separate
proposal track. If you'd prefer `implemented`, I can amend the rename —
happy to take direction.
## Changes
- **`git mv`** preserves history via 97% similarity-based rename
detection — `git log --follow` continues to trace prior revisions.
- **Status header** updated from `Proposed` to `Partial` to mirror the
filename token.
- **§11 rollout table** annotated with shipped status anchored to merge
PR numbers, plus a "Status as of 2026-05-04" header paragraph explaining
the partial-vs-implemented call.
- **In-tree source-comment cross-references** updated from the
proposed-stage filename to the partial-stage filename:
- `main_sqs_leadership_refusal.go`
- `shard_config.go`
- `adapter/sqs_keys.go`
- `adapter/sqs_partitioning.go`
- `adapter/sqs_catalog.go`
These are doc-only comments; no behaviour change.
## Self-review (5 lenses, abbreviated)
1. **Data loss** — N/A; doc-only and comment-only changes.
2. **Concurrency** — N/A.
3. **Performance** — N/A.
4. **Data consistency** — N/A.
5. **Test coverage** — no tests added; doc-and-comment change only.
## Test plan
- [x] `go build ./...` clean.
- [x] `golangci-lint --config=.golangci.yaml run` clean (pre-commit hook
auto-ran).
- [x] No remaining references to the old filename anywhere in the tree
(Go / Clojure / Markdown).
## Refs
- `docs/design/2026_04_26_partial_sqs_split_queue_fifo.md` §11.
- `docs/design/README.md` "Filename convention" + "Moving or renaming".
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Updated design document references across multiple source files from
the proposed version to the partial version.
* Updated the design document status and rollout plan to reflect the
current shipped state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for freeto 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

@bootjp