Commit e105815
committed
feat(sqs): per-queue throttling (Phase 3.C)
Implements the design from
docs/design/2026_04_26_proposed_sqs_per_queue_throttling.md.
Schema (sqs_catalog.go):
- sqsQueueMeta gains an optional Throttle *sqsQueueThrottle field
with six float64 sub-fields (Send/Recv/Default x Capacity/RefillPerSecond).
- Six new ThrottleSendCapacity / ThrottleSendRefillPerSecond /
ThrottleRecvCapacity / ThrottleRecvRefillPerSecond /
ThrottleDefaultCapacity / ThrottleDefaultRefillPerSecond attributes
go through the standard sqsAttributeAppliers dispatch.
- validateThrottleConfig enforces the cross-field rules from the
design: each (capacity, refill) pair must be both-zero or
both-positive; capacity must be >= refill so the bucket can burst;
Send/Recv capacities must be >= 10 (the SendMessageBatch /
DeleteMessageBatch max charge) so a small capacity does not make
every full batch permanently unserviceable; per-field hard ceiling
of 100k. Validator runs once at the end of applyAttributes so a
multi-field update sees the post-apply state as a whole.
- queueMetaToAttributes surfaces the configured Throttle* fields so
GetQueueAttributes("All") round-trips. Extracted into
addThrottleAttributes to stay under the cyclop ceiling.
Bucket store (sqs_throttle.go, new):
- bucketStore: sync.Map[bucketKey]*tokenBucket; per-bucket sync.Mutex
on the hot path so cross-queue traffic never serialises on a
process-wide lock. Per Gemini medium on PR #664 the single-mutex
alternative was rejected. Lazy idle-evict sweep removes inactive
buckets after 1h to bound memory.
- charge(): lock-free Load, LoadOrStore on miss (race-tolerant -- both
racers compute identical capacity/refill from the same meta), then
per-bucket lock for refill + take + release. Refill is elapsed *
refillRate, capped at capacity. On reject, computes Retry-After
from the actual refillRate and the requestedCount per the §3.4
formula (numerator is requested count, not 1, so a batch verb does
not get told to retry in 1s when it really needs 10s).
- resolveActionConfig() handles the Default* fall-through: when only
DefaultCapacity is set, Send and Recv requests share the same
bucketKey{action:"*"} so Default behaves as one shared cap rather
than three independent quotas.
- invalidateQueue() drops every bucket belonging to a queue. Called
after the Raft commit on SetQueueAttributes / DeleteQueue so new
limits take effect on the very next request, not after the 1h
idle-evict sweep. Without this step the §3.1 cache-invalidation
contract fails and operators see stale enforcement.
Charging (sqs_messages.go, sqs_messages_batch.go):
- All seven message-plane handlers wired:
SendMessage / SendMessageBatch (Send bucket; batch charges by entry
count), ReceiveMessage / DeleteMessage / DeleteMessageBatch /
ChangeMessageVisibility / ChangeMessageVisibilityBatch (Receive
bucket).
- Throttle check sits OUTSIDE the OCC retry loop per §4.2 -- a
rejected request never reaches the coordinator, so the existing
sendMessageWithRetry et al. cannot busy-loop on a permanent
rate-limit failure.
- sendMessage extracted into prepareSendMessage + validateSend +
body to stay under the cyclop ceiling once the throttle branch
was added.
Error envelope (sqs.go):
- New sqsErrThrottling code + writeSQSThrottlingError helper that
writes 400 + AWS-shaped JSON body + x-amzn-ErrorType + Retry-After
header. The envelope is the same shape AWS uses; SDKs that key off
x-amzn-ErrorType handle it without changes.
Tests:
- adapter/sqs_throttle_test.go: 18 unit tests covering bucket math
(fresh capacity, refill elapsed, refill cap, batch reject preserves
partial credit, Retry-After uses requested count, sub-1-RPS floor,
per-action / per-queue / Default-fallthrough isolation,
invalidateQueue, concurrent -race, default-off short-circuit) and
the validator (nil/empty canonicalisation, both-zero-or-both-positive,
capacity >= 10 batch floor, Default* exempt, capacity >= refill,
parseThrottleFloat range checks, computeRetryAfter floor).
- adapter/sqs_throttle_integration_test.go: 8 end-to-end tests
covering the §6 testing strategy (default-off allows unbounded,
send/recv reject after capacity with correct envelope + Retry-After,
batch charges by entry count, SetQueueAttributes invalidation,
DeleteQueue + CreateQueue lifecycle invalidation, GetQueueAttributes
round-trip, validator rejects below batch min). Run with -race
clean.
Out of scope for this PR (deferred to a follow-up):
- Prometheus counter (sqs_throttled_requests_total) and gauge
(sqs_throttle_tokens_remaining) per the design's §4.1 monitoring/
registry.go entry. The wiring needs a new prometheus collector and
hooking through the Registry seam; punted to keep this PR focused
on the data-plane behaviour. The bucket store's chargeOutcome
already exposes tokensAfter so the gauge wiring is one site.
- §6.5 cross-protocol (Query) parity test: the Query protocol layer
lives on PR #662 and the throttle envelope already has the same
shape on both protocols (writeSQSError handles both), so the test
is a follow-up after #662 lands.
- §6.6 failover behaviour test: the §3.1 "fresh bucket on failover"
contract is implementation-correct (the bucket map is per-process,
no Raft state) but a 3-node failover test needs the cluster
scaffolding and is best added with the Jepsen workload.1 parent d246cc3 commit e105815
7 files changed
Lines changed: 1390 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
55 | 56 | | |
56 | 57 | | |
57 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
58 | 64 | | |
59 | 65 | | |
60 | 66 | | |
| |||
76 | 82 | | |
77 | 83 | | |
78 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
79 | 90 | | |
80 | 91 | | |
81 | 92 | | |
| |||
98 | 109 | | |
99 | 110 | | |
100 | 111 | | |
| 112 | + | |
101 | 113 | | |
102 | 114 | | |
103 | 115 | | |
| |||
267 | 279 | | |
268 | 280 | | |
269 | 281 | | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
92 | 93 | | |
93 | 94 | | |
94 | 95 | | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
95 | 132 | | |
96 | 133 | | |
97 | 134 | | |
| |||
384 | 421 | | |
385 | 422 | | |
386 | 423 | | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
387 | 437 | | |
388 | 438 | | |
389 | 439 | | |
| |||
419 | 469 | | |
420 | 470 | | |
421 | 471 | | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
422 | 622 | | |
423 | 623 | | |
424 | 624 | | |
| |||
623 | 823 | | |
624 | 824 | | |
625 | 825 | | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
626 | 834 | | |
627 | 835 | | |
628 | 836 | | |
| |||
954 | 1162 | | |
955 | 1163 | | |
956 | 1164 | | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
957 | 1171 | | |
958 | 1172 | | |
959 | 1173 | | |
| |||
989 | 1203 | | |
990 | 1204 | | |
991 | 1205 | | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
992 | 1215 | | |
993 | 1216 | | |
994 | 1217 | | |
| |||
0 commit comments