Skip to content

Commit afd2b1c

Browse files
authored
feat(sqs): Jepsen HT-FIFO workload (Phase 3.D PR 7b) (#738)
## Summary Phase 3.D PR 7b — Jepsen HT-FIFO workload that stresses partitioned-FIFO queues against the three contracts AWS HT-FIFO is supposed to honour even under partition and node-loss faults: **within-group ordering**, **no message loss**, **no duplicates**. Pattern follows [aphyr's Jepsen RabbitMQ analysis](https://aphyr.com/posts/315-jepsen-rabbitmq): track every `:send` and `:recv` in the operation history, then a custom checker verifies the contracts against the recorded events at the end of the run. ## What's in this PR - **`jepsen/project.clj`** — Adds `com.cognitect.aws/sqs` at the same version as the existing dynamodb dep, so the SDK wire protocol (auth, retry classification, error parsing) is exercised end-to-end against elastickv rather than a hand-rolled HTTP layer. - **`jepsen/src/elastickv/db.clj`** — Extends `start-node!` to accept `:sqs-port` (port spec like `:dynamo-port`) and `:sqs-region`. Both are optional, so existing dynamodb / s3 / redis test specs are byte-identical at the args level when `sqs-port` is absent. - **`jepsen/src/elastickv/jepsen_test.clj`** — Registers `elastickv-sqs-htfifo-test` alongside the other workloads. - **`jepsen/src/elastickv/sqs_htfifo_workload.clj`** (new, ~430 lines) — The workload. Uses cognitect/aws-api SQS, creates an HT-FIFO queue with `PartitionCount=4` + `ContentBasedDeduplication`, runs sends and receives across N `MessageGroupId` values, and the custom `ht-fifo-checker` validates the three contracts. - **`jepsen/test/elastickv/sqs_htfifo_workload_test.clj`** (new) — Pure-function tests for the checker plus integration smoke tests for the test-spec builder. 11 tests / 27 assertions. ## Checker contracts For each `MessageGroupId` independently: 1. **Within-group ordering** — the sequence of received `seq` values, sorted by global completion time across all consumers, is monotonically non-decreasing. 2. **No loss** — every `(group, seq)` successfully `:sent` eventually appears in the `:recv` history. Sends with `:info` status are treated as possibly-committed and not counted as lost. 3. **No duplicates** — every `(group, seq)` appears at most once in the `:recv` history. `ContentBasedDeduplication` on the queue + a unique `(group, seq)` body is what enforces this server-side; a duplicate here is a real bug (e.g. a deletion that did not commit). ## Open-endpoint mode The elastickv server starts without `--sqsCredentialsFile`, so the SQS adapter accepts any signed request (mirroring how the S3 adapter is wired in jepsen today). The SDK client signs with dummy credentials, so the SigV4 path still exercises end-to-end at the protocol level. ## Self-review (5 lenses) 1. **Data loss** — N/A; this is a test-only PR. The workload's whole purpose is to *detect* data loss in the system under test. 2. **Concurrency** — The shared per-group `seq-counter` is an `atom` updated via `swap!` (CAS-based), so concurrent sends from different worker threads always assign distinct seqs. The checker is pure; no shared mutable state. 3. **Performance** — Test-only code, runs at low rate (5 ops/sec/worker). Not on any hot path. 4. **Data consistency** — The checker compares committed sends against the receive history globally, so all the consistency assertions are at end-of-run with a complete picture. Sends with `:info` (uncertain commit) are correctly excluded from the loss set, matching Jepsen's standard approach. 5. **Test coverage** — 11 unit tests for the checker pin the contract surface (clean / loss / info-not-loss / duplicates / within-group ordering / cross-group interleaving / failed-send-not-counted / empty-receive). Integration smoke tests pin the test-spec builder. The workload itself is exercised end-to-end on a real cluster via `lein run -m elastickv.sqs-htfifo-workload`. ## Test plan - [x] `lein test elastickv.sqs-htfifo-workload-test` — 11 tests / 27 assertions pass - [x] `lein test` for non-redis suite (dynamodb / dynamodb-types / s3 / cli / sqs-htfifo) — 21 tests / 41 assertions pass - [ ] End-to-end live cluster run — operator-driven (out of scope for the merge gate; relies on a 3-node cluster setup) The `elastickv.redis-workload` namespace fails to load due to the empty `redis/src/` tree, which is pre-existing on main and unrelated to this PR. ## Out of scope (next milestones) - Wiring the workload into `scripts/run-jepsen-local.sh` — the existing script is dynamodb-only; an sqs counterpart lands as a follow-up. - Multi-shard cluster topology that lands distinct partitions on distinct Raft groups. This PR's `PartitionCount=4` routes to the default group on a single-shard cluster — partitioning logic (different keys per partition, ordering preserved within group) is fully exercised, but the cross-shard scaling story is gated on separate work. - Design-doc lifecycle rename (`*_proposed_*.md` → `*_partial_*.md`) — that is §11 PR 8 in the design doc and is tracked separately. ## Refs - `docs/design/2026_04_26_proposed_sqs_split_queue_fifo.md` §11 PR 7. - Closes the testing half of §11 PR 7. PR 7a (metrics) shipped at #737. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added AWS SQS integration with HT‑FIFO support, SQS port/region configuration, and runtime options to exercise FIFO dedupe/order semantics * **Tests** * Added comprehensive unit and workload tests validating ordering, no‑loss, no‑duplicates, and option handling * **Chores** * CI updated to run the SQS HT‑FIFO workload as part of Jepsen test runs <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 675adc3 + de3eb7c commit afd2b1c

7 files changed

Lines changed: 823 additions & 3 deletions

File tree

.github/workflows/jepsen-test.yml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
RAFT_REDIS_MAP="127.0.0.1:50051=127.0.0.1:63791,127.0.0.1:50052=127.0.0.1:63792,127.0.0.1:50053=127.0.0.1:63793"
5050
RAFT_S3_MAP="127.0.0.1:50051=127.0.0.1:63901,127.0.0.1:50052=127.0.0.1:63902,127.0.0.1:50053=127.0.0.1:63903"
5151
RAFT_DYNAMO_MAP="127.0.0.1:50051=127.0.0.1:63801,127.0.0.1:50052=127.0.0.1:63802,127.0.0.1:50053=127.0.0.1:63803"
52+
RAFT_SQS_MAP="127.0.0.1:50051=127.0.0.1:63501,127.0.0.1:50052=127.0.0.1:63502,127.0.0.1:50053=127.0.0.1:63503"
5253
5354
: > /tmp/elastickv-demo.pid
5455
for node in 1 2 3; do
@@ -57,6 +58,7 @@ jobs:
5758
--redisAddress "127.0.0.1:6379${node}" \
5859
--dynamoAddress "127.0.0.1:6380${node}" \
5960
--s3Address "127.0.0.1:6390${node}" \
61+
--sqsAddress "127.0.0.1:6350${node}" \
6062
--metricsAddress "" \
6163
--pprofAddress "" \
6264
--raftId "n${node}" \
@@ -65,15 +67,17 @@ jobs:
6567
--raftRedisMap "$RAFT_REDIS_MAP" \
6668
--raftS3Map "$RAFT_S3_MAP" \
6769
--raftDynamoMap "$RAFT_DYNAMO_MAP" \
70+
--raftSqsMap "$RAFT_SQS_MAP" \
6871
> "/tmp/elastickv-demo-n${node}.log" 2>&1 &
6972
echo $! >> /tmp/elastickv-demo.pid
7073
done
7174
72-
echo "Waiting for redis (63791-63793), dynamo (63801-63803), and s3 (63901-63903) listeners..."
75+
echo "Waiting for redis (63791-63793), dynamo (63801-63803), s3 (63901-63903), and sqs (63501-63503) listeners..."
7376
for i in {1..90}; do
7477
if nc -z 127.0.0.1 63791 && nc -z 127.0.0.1 63792 && nc -z 127.0.0.1 63793 \
7578
&& nc -z 127.0.0.1 63801 && nc -z 127.0.0.1 63802 && nc -z 127.0.0.1 63803 \
76-
&& nc -z 127.0.0.1 63901 && nc -z 127.0.0.1 63902 && nc -z 127.0.0.1 63903; then
79+
&& nc -z 127.0.0.1 63901 && nc -z 127.0.0.1 63902 && nc -z 127.0.0.1 63903 \
80+
&& nc -z 127.0.0.1 63501 && nc -z 127.0.0.1 63502 && nc -z 127.0.0.1 63503; then
7781
echo "Cluster is up"
7882
exit 0
7983
fi
@@ -142,6 +146,26 @@ jobs:
142146
timeout-minutes: 3
143147
run: |
144148
timeout 120 ~/lein run -m elastickv.s3-workload --local --time-limit 5 --rate 10 --concurrency 10 --s3-ports 63901,63902,63903 --host 127.0.0.1
149+
- name: Run SQS HT-FIFO Jepsen workload against elastickv
150+
working-directory: jepsen
151+
# The HT-FIFO workload runs sends and receives across a 4-partition
152+
# FIFO queue with content-based deduplication. The custom checker
153+
# validates within-group ordering, no loss, and no duplicates.
154+
# See jepsen/src/elastickv/sqs_htfifo_workload.clj.
155+
#
156+
# --drain-time 15: in --local mode the nemesis is a no-op, so no
157+
# message can become invisible due to partition/kill — the 40s
158+
# default drain (which protects against fault-induced
159+
# visibility-timeout races) is overkill here. 15s leaves ample
160+
# headroom under the 120s shell timeout against JVM startup and
161+
# the 5s main phase.
162+
timeout-minutes: 3
163+
run: |
164+
timeout 120 ~/lein run -m elastickv.sqs-htfifo-workload --local \
165+
--time-limit 5 --rate 5 --concurrency 5 \
166+
--partition-count 4 --group-count 6 \
167+
--drain-time 15 \
168+
--sqs-ports 63501,63502,63503 --host 127.0.0.1
145169
- name: Stop demo cluster
146170
if: always()
147171
run: |

.github/workflows/jepsen.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ jobs:
8484
--faults ${{ github.event.inputs['faults'] || github.event.inputs.faults }} \
8585
--concurrency 10"
8686
87+
- name: Run SQS HT-FIFO Jepsen workload
88+
working-directory: jepsen
89+
env:
90+
HOME: ${{ github.workspace }}/jepsen/tmp-home
91+
LEIN_HOME: ${{ github.workspace }}/jepsen/.lein
92+
LEIN_JVM_OPTS: -Duser.home=${{ github.workspace }}/jepsen/tmp-home
93+
run: |
94+
vagrant ssh ctrl -c "cd ~/elastickv/jepsen && \
95+
lein run -m elastickv.sqs-htfifo-workload \
96+
--nodes n1,n2,n3,n4,n5 \
97+
--time-limit ${{ github.event.inputs['time-limit'] || github.event.inputs.time-limit }} \
98+
--rate ${{ github.event.inputs['rate'] || github.event.inputs.rate }} \
99+
--faults ${{ github.event.inputs['faults'] || github.event.inputs.faults }} \
100+
--partition-count 4 --group-count 8 \
101+
--concurrency 8"
102+
87103
- name: Collect Jepsen artifacts
88104
if: always()
89105
working-directory: jepsen

jepsen/project.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
[com.cognitect.aws/api "0.8.692"]
1414
[com.cognitect.aws/endpoints "1.1.12.626"]
1515
[com.cognitect.aws/dynamodb "847.2.1365.0"]
16+
[com.cognitect.aws/sqs "847.2.1365.0"]
1617
[org.slf4j/slf4j-nop "2.0.9"]]
1718
:main elastickv.jepsen-test)

jepsen/src/elastickv/db.clj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
(clojure.string/join ","))))
9898

9999
(defn- start-node!
100-
[test node {:keys [bootstrap-node grpc-port redis-port dynamo-port s3-port data-dir raft-groups shard-ranges raft-engine]}]
100+
[test node {:keys [bootstrap-node grpc-port redis-port dynamo-port s3-port sqs-port sqs-region data-dir raft-groups shard-ranges raft-engine]}]
101101
(when (and (seq raft-groups)
102102
(> (count raft-groups) 1)
103103
(nil? shard-ranges))
@@ -110,6 +110,8 @@
110110
(node-addr node (port-for dynamo-port node)))
111111
s3 (when s3-port
112112
(node-addr node (port-for s3-port node)))
113+
sqs (when sqs-port
114+
(node-addr node (port-for sqs-port node)))
113115
raft-redis-map (build-raft-redis-map (:nodes test) grpc-port redis-port raft-groups)
114116
bootstrap? (= node bootstrap-node)
115117
args (cond-> [server-bin
@@ -121,6 +123,8 @@
121123
"--raftRedisMap" raft-redis-map]
122124
dynamo (conj "--dynamoAddress" dynamo)
123125
s3 (conj "--s3Address" s3)
126+
sqs (conj "--sqsAddress" sqs)
127+
(and sqs sqs-region) (conj "--sqsRegion" sqs-region)
124128
(seq raft-groups) (conj "--raftGroups" (build-raft-groups-arg node raft-groups))
125129
(seq shard-ranges) (conj "--shardRanges" shard-ranges)
126130
bootstrap? (conj "--raftBootstrap"))]

jepsen/src/elastickv/jepsen_test.clj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[elastickv.dynamodb-workload :as dynamodb-workload]
55
[elastickv.dynamodb-types-workload :as dynamodb-types-workload]
66
[elastickv.s3-workload :as s3-workload]
7+
[elastickv.sqs-htfifo-workload :as sqs-htfifo-workload]
78
[jepsen.cli :as cli]))
89

910
(defn elastickv-test []
@@ -19,6 +20,14 @@
1920
(defn elastickv-s3-test []
2021
(s3-workload/elastickv-s3-test {}))
2122

23+
(defn elastickv-sqs-htfifo-test
24+
"HT-FIFO Jepsen test (PR 7b). Run via the workload's own -main:
25+
`lein run -m elastickv.sqs-htfifo-workload [opts]`. Same pattern
26+
as elastickv-dynamodb-test / elastickv-s3-test — each workload
27+
exposes its own -main so this -main only dispatches Redis."
28+
([] (elastickv-sqs-htfifo-test {}))
29+
([opts] (sqs-htfifo-workload/elastickv-sqs-htfifo-test opts)))
30+
2231
(defn -main
2332
[& args]
2433
(cli/run! (cli/single-test-cmd {:test-fn elastickv-test}) args))

0 commit comments

Comments
 (0)