@durable-streams/client-conformance-tests 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/typescript-adapter.cjs +75 -3
- package/dist/adapters/typescript-adapter.js +76 -4
- package/dist/{benchmark-runner-D-YSAvRy.js → benchmark-runner-CrE6JkbX.js} +86 -8
- package/dist/{benchmark-runner-BlKqhoXE.cjs → benchmark-runner-Db4he452.cjs} +87 -8
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +106 -6
- package/dist/index.d.ts +106 -6
- package/dist/index.js +1 -1
- package/dist/{protocol-3cf94Xyb.d.cts → protocol-D37G3c4e.d.cts} +80 -4
- package/dist/{protocol-DyEvTHPF.d.ts → protocol-Mcbiq3nQ.d.ts} +80 -4
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/package.json +3 -3
- package/src/adapters/typescript-adapter.ts +127 -6
- package/src/protocol.ts +85 -1
- package/src/runner.ts +178 -13
- package/src/test-cases.ts +110 -3
- package/test-cases/consumer/error-handling.yaml +42 -0
- package/test-cases/consumer/offset-handling.yaml +209 -0
- package/test-cases/producer/idempotent/autoclaim.yaml +214 -0
- package/test-cases/producer/idempotent/batching.yaml +98 -0
- package/test-cases/producer/idempotent/concurrent-requests.yaml +100 -0
- package/test-cases/producer/idempotent/epoch-management.yaml +333 -0
- package/test-cases/producer/idempotent/error-handling.yaml +194 -0
- package/test-cases/producer/idempotent/multi-producer.yaml +322 -0
- package/test-cases/producer/idempotent/sequence-validation.yaml +339 -0
- package/test-cases/producer/idempotent-json-batching.yaml +134 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
id: idempotent-multi-producer
|
|
2
|
+
name: Idempotent Producer - Multi-Producer Scenarios
|
|
3
|
+
description: |
|
|
4
|
+
Tests for multiple producers on the same stream and producer isolation.
|
|
5
|
+
Each (stream, producerId) has independent epoch/sequence state.
|
|
6
|
+
category: producer
|
|
7
|
+
tags:
|
|
8
|
+
- idempotent
|
|
9
|
+
- multi-producer
|
|
10
|
+
- isolation
|
|
11
|
+
|
|
12
|
+
tests:
|
|
13
|
+
- id: multiple-producers-independent
|
|
14
|
+
name: Multiple producers have independent state
|
|
15
|
+
description: Different producer IDs should have separate epoch/seq tracking
|
|
16
|
+
setup:
|
|
17
|
+
- action: create
|
|
18
|
+
as: streamPath
|
|
19
|
+
contentType: text/plain
|
|
20
|
+
operations:
|
|
21
|
+
# Producer A: seq=0
|
|
22
|
+
- action: server-append
|
|
23
|
+
path: ${streamPath}
|
|
24
|
+
data: "A0"
|
|
25
|
+
producerId: producer-A
|
|
26
|
+
producerEpoch: 0
|
|
27
|
+
producerSeq: 0
|
|
28
|
+
expect:
|
|
29
|
+
status: 200
|
|
30
|
+
# Producer B: seq=0 (independent)
|
|
31
|
+
- action: server-append
|
|
32
|
+
path: ${streamPath}
|
|
33
|
+
data: "B0"
|
|
34
|
+
producerId: producer-B
|
|
35
|
+
producerEpoch: 0
|
|
36
|
+
producerSeq: 0
|
|
37
|
+
expect:
|
|
38
|
+
status: 200
|
|
39
|
+
# Producer A: seq=1
|
|
40
|
+
- action: server-append
|
|
41
|
+
path: ${streamPath}
|
|
42
|
+
data: "A1"
|
|
43
|
+
producerId: producer-A
|
|
44
|
+
producerEpoch: 0
|
|
45
|
+
producerSeq: 1
|
|
46
|
+
expect:
|
|
47
|
+
status: 200
|
|
48
|
+
# Producer B: seq=1
|
|
49
|
+
- action: server-append
|
|
50
|
+
path: ${streamPath}
|
|
51
|
+
data: "B1"
|
|
52
|
+
producerId: producer-B
|
|
53
|
+
producerEpoch: 0
|
|
54
|
+
producerSeq: 1
|
|
55
|
+
expect:
|
|
56
|
+
status: 200
|
|
57
|
+
|
|
58
|
+
- id: producer-scope-per-stream
|
|
59
|
+
name: Same producer ID works independently on different streams
|
|
60
|
+
description: |
|
|
61
|
+
Producer state must be scoped per (stream, producerId), not globally
|
|
62
|
+
by producerId alone. A common implementation bug is to key producer
|
|
63
|
+
state only by producer ID, causing cross-stream interference.
|
|
64
|
+
setup:
|
|
65
|
+
- action: create
|
|
66
|
+
as: streamA
|
|
67
|
+
contentType: text/plain
|
|
68
|
+
- action: create
|
|
69
|
+
as: streamB
|
|
70
|
+
contentType: text/plain
|
|
71
|
+
operations:
|
|
72
|
+
# Same producer, epoch=0, seq=0 on stream A
|
|
73
|
+
- action: server-append
|
|
74
|
+
path: ${streamA}
|
|
75
|
+
data: "streamA-msg"
|
|
76
|
+
producerId: shared-producer
|
|
77
|
+
producerEpoch: 0
|
|
78
|
+
producerSeq: 0
|
|
79
|
+
expect:
|
|
80
|
+
status: 200
|
|
81
|
+
# Same producer, epoch=0, seq=0 on stream B - must be 200, not 204/403
|
|
82
|
+
- action: server-append
|
|
83
|
+
path: ${streamB}
|
|
84
|
+
data: "streamB-msg"
|
|
85
|
+
producerId: shared-producer
|
|
86
|
+
producerEpoch: 0
|
|
87
|
+
producerSeq: 0
|
|
88
|
+
expect:
|
|
89
|
+
status: 200
|
|
90
|
+
# Verify each stream has its own message
|
|
91
|
+
- action: read
|
|
92
|
+
path: ${streamA}
|
|
93
|
+
expect:
|
|
94
|
+
data: "streamA-msg"
|
|
95
|
+
upToDate: true
|
|
96
|
+
- action: read
|
|
97
|
+
path: ${streamB}
|
|
98
|
+
expect:
|
|
99
|
+
data: "streamB-msg"
|
|
100
|
+
upToDate: true
|
|
101
|
+
|
|
102
|
+
- id: producer-epoch-per-stream
|
|
103
|
+
name: Producer epochs are independent per stream
|
|
104
|
+
description: |
|
|
105
|
+
Epoch fencing on one stream must not affect the same producer on
|
|
106
|
+
another stream. Each (stream, producerId) has independent epoch state.
|
|
107
|
+
setup:
|
|
108
|
+
- action: create
|
|
109
|
+
as: streamA
|
|
110
|
+
contentType: text/plain
|
|
111
|
+
- action: create
|
|
112
|
+
as: streamB
|
|
113
|
+
contentType: text/plain
|
|
114
|
+
operations:
|
|
115
|
+
# Establish epoch=5 on stream A
|
|
116
|
+
- action: server-append
|
|
117
|
+
path: ${streamA}
|
|
118
|
+
data: "A-epoch5"
|
|
119
|
+
producerId: shared-producer
|
|
120
|
+
producerEpoch: 5
|
|
121
|
+
producerSeq: 0
|
|
122
|
+
expect:
|
|
123
|
+
status: 200
|
|
124
|
+
# epoch=0 on stream B should still work (different stream)
|
|
125
|
+
- action: server-append
|
|
126
|
+
path: ${streamB}
|
|
127
|
+
data: "B-epoch0"
|
|
128
|
+
producerId: shared-producer
|
|
129
|
+
producerEpoch: 0
|
|
130
|
+
producerSeq: 0
|
|
131
|
+
expect:
|
|
132
|
+
status: 200
|
|
133
|
+
# epoch=0 on stream A should be fenced
|
|
134
|
+
- action: server-append
|
|
135
|
+
path: ${streamA}
|
|
136
|
+
data: "A-epoch0-zombie"
|
|
137
|
+
producerId: shared-producer
|
|
138
|
+
producerEpoch: 0
|
|
139
|
+
producerSeq: 1
|
|
140
|
+
expect:
|
|
141
|
+
status: 403
|
|
142
|
+
producerEpoch: 5
|
|
143
|
+
|
|
144
|
+
- id: interleaved-producers-ordering
|
|
145
|
+
name: Interleaved producers maintain per-producer ordering
|
|
146
|
+
description: |
|
|
147
|
+
When multiple producers write interleaved, each producer's messages
|
|
148
|
+
should appear in their sequence order relative to each other.
|
|
149
|
+
setup:
|
|
150
|
+
- action: create
|
|
151
|
+
as: streamPath
|
|
152
|
+
contentType: text/plain
|
|
153
|
+
operations:
|
|
154
|
+
# Interleave writes from two producers
|
|
155
|
+
- action: server-append
|
|
156
|
+
path: ${streamPath}
|
|
157
|
+
data: "A0"
|
|
158
|
+
producerId: producer-A
|
|
159
|
+
producerEpoch: 0
|
|
160
|
+
producerSeq: 0
|
|
161
|
+
expect:
|
|
162
|
+
status: 200
|
|
163
|
+
- action: server-append
|
|
164
|
+
path: ${streamPath}
|
|
165
|
+
data: "B0"
|
|
166
|
+
producerId: producer-B
|
|
167
|
+
producerEpoch: 0
|
|
168
|
+
producerSeq: 0
|
|
169
|
+
expect:
|
|
170
|
+
status: 200
|
|
171
|
+
- action: server-append
|
|
172
|
+
path: ${streamPath}
|
|
173
|
+
data: "A1"
|
|
174
|
+
producerId: producer-A
|
|
175
|
+
producerEpoch: 0
|
|
176
|
+
producerSeq: 1
|
|
177
|
+
expect:
|
|
178
|
+
status: 200
|
|
179
|
+
- action: server-append
|
|
180
|
+
path: ${streamPath}
|
|
181
|
+
data: "B1"
|
|
182
|
+
producerId: producer-B
|
|
183
|
+
producerEpoch: 0
|
|
184
|
+
producerSeq: 1
|
|
185
|
+
expect:
|
|
186
|
+
status: 200
|
|
187
|
+
- action: server-append
|
|
188
|
+
path: ${streamPath}
|
|
189
|
+
data: "A2"
|
|
190
|
+
producerId: producer-A
|
|
191
|
+
producerEpoch: 0
|
|
192
|
+
producerSeq: 2
|
|
193
|
+
expect:
|
|
194
|
+
status: 200
|
|
195
|
+
# Read and verify A0 comes before A1 comes before A2
|
|
196
|
+
# and B0 comes before B1
|
|
197
|
+
- action: read
|
|
198
|
+
path: ${streamPath}
|
|
199
|
+
expect:
|
|
200
|
+
dataContainsAll:
|
|
201
|
+
- "A0"
|
|
202
|
+
- "A1"
|
|
203
|
+
- "A2"
|
|
204
|
+
- "B0"
|
|
205
|
+
- "B1"
|
|
206
|
+
upToDate: true
|
|
207
|
+
|
|
208
|
+
- id: delete-recreate-resets-producer-state
|
|
209
|
+
name: Deleting and recreating stream resets producer state
|
|
210
|
+
description: |
|
|
211
|
+
When a stream is deleted and recreated, producer state must be reset.
|
|
212
|
+
The new stream is a fresh log, so (epoch=0, seq=0) must be accepted
|
|
213
|
+
even if the same producer previously wrote to the old stream.
|
|
214
|
+
setup:
|
|
215
|
+
- action: create
|
|
216
|
+
as: streamPath
|
|
217
|
+
contentType: text/plain
|
|
218
|
+
operations:
|
|
219
|
+
# Write to original stream
|
|
220
|
+
- action: server-append
|
|
221
|
+
path: ${streamPath}
|
|
222
|
+
data: "original-msg"
|
|
223
|
+
producerId: test-producer
|
|
224
|
+
producerEpoch: 0
|
|
225
|
+
producerSeq: 0
|
|
226
|
+
expect:
|
|
227
|
+
status: 200
|
|
228
|
+
# Advance sequence
|
|
229
|
+
- action: server-append
|
|
230
|
+
path: ${streamPath}
|
|
231
|
+
data: "original-msg2"
|
|
232
|
+
producerId: test-producer
|
|
233
|
+
producerEpoch: 0
|
|
234
|
+
producerSeq: 1
|
|
235
|
+
expect:
|
|
236
|
+
status: 200
|
|
237
|
+
# Delete the stream
|
|
238
|
+
- action: delete
|
|
239
|
+
path: ${streamPath}
|
|
240
|
+
expect:
|
|
241
|
+
status: 200
|
|
242
|
+
# Recreate at same path
|
|
243
|
+
- action: create
|
|
244
|
+
path: ${streamPath}
|
|
245
|
+
contentType: text/plain
|
|
246
|
+
# Same producer, epoch=0, seq=0 must work (new stream)
|
|
247
|
+
- action: server-append
|
|
248
|
+
path: ${streamPath}
|
|
249
|
+
data: "recreated-msg"
|
|
250
|
+
producerId: test-producer
|
|
251
|
+
producerEpoch: 0
|
|
252
|
+
producerSeq: 0
|
|
253
|
+
expect:
|
|
254
|
+
status: 200
|
|
255
|
+
# Verify only the new message exists
|
|
256
|
+
- action: read
|
|
257
|
+
path: ${streamPath}
|
|
258
|
+
expect:
|
|
259
|
+
data: "recreated-msg"
|
|
260
|
+
upToDate: true
|
|
261
|
+
|
|
262
|
+
- id: producer-id-long
|
|
263
|
+
name: Long producer ID works
|
|
264
|
+
description: Very long producer IDs should be accepted
|
|
265
|
+
setup:
|
|
266
|
+
- action: create
|
|
267
|
+
as: streamPath
|
|
268
|
+
contentType: text/plain
|
|
269
|
+
operations:
|
|
270
|
+
- action: server-append
|
|
271
|
+
path: ${streamPath}
|
|
272
|
+
data: "long-id"
|
|
273
|
+
producerId: "very-long-producer-id-that-goes-on-for-quite-a-while-to-test-string-handling-in-the-server-implementation-and-make-sure-it-doesnt-truncate"
|
|
274
|
+
producerEpoch: 0
|
|
275
|
+
producerSeq: 0
|
|
276
|
+
expect:
|
|
277
|
+
status: 200
|
|
278
|
+
# Duplicate detection works with long ID
|
|
279
|
+
- action: server-append
|
|
280
|
+
path: ${streamPath}
|
|
281
|
+
data: "long-id"
|
|
282
|
+
producerId: "very-long-producer-id-that-goes-on-for-quite-a-while-to-test-string-handling-in-the-server-implementation-and-make-sure-it-doesnt-truncate"
|
|
283
|
+
producerEpoch: 0
|
|
284
|
+
producerSeq: 0
|
|
285
|
+
expect:
|
|
286
|
+
status: 204
|
|
287
|
+
|
|
288
|
+
- id: producer-id-with-special-chars
|
|
289
|
+
name: Producer ID with special characters works
|
|
290
|
+
description: Producer IDs can contain colons, slashes, and other special chars
|
|
291
|
+
setup:
|
|
292
|
+
- action: create
|
|
293
|
+
as: streamPath
|
|
294
|
+
contentType: text/plain
|
|
295
|
+
operations:
|
|
296
|
+
# Colon-separated ID (common pattern)
|
|
297
|
+
- action: server-append
|
|
298
|
+
path: ${streamPath}
|
|
299
|
+
data: "msg1"
|
|
300
|
+
producerId: "service:instance:123"
|
|
301
|
+
producerEpoch: 0
|
|
302
|
+
producerSeq: 0
|
|
303
|
+
expect:
|
|
304
|
+
status: 200
|
|
305
|
+
# Slash-separated ID (path-like)
|
|
306
|
+
- action: server-append
|
|
307
|
+
path: ${streamPath}
|
|
308
|
+
data: "msg2"
|
|
309
|
+
producerId: "region/zone/host"
|
|
310
|
+
producerEpoch: 0
|
|
311
|
+
producerSeq: 0
|
|
312
|
+
expect:
|
|
313
|
+
status: 200
|
|
314
|
+
# UUID format
|
|
315
|
+
- action: server-append
|
|
316
|
+
path: ${streamPath}
|
|
317
|
+
data: "msg3"
|
|
318
|
+
producerId: "550e8400-e29b-41d4-a716-446655440000"
|
|
319
|
+
producerEpoch: 0
|
|
320
|
+
producerSeq: 0
|
|
321
|
+
expect:
|
|
322
|
+
status: 200
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
id: idempotent-sequence-validation
|
|
2
|
+
name: Idempotent Producer - Sequence Validation
|
|
3
|
+
description: |
|
|
4
|
+
Tests for producer sequence number validation.
|
|
5
|
+
Sequences must start at 0 and increment monotonically within an epoch.
|
|
6
|
+
category: producer
|
|
7
|
+
tags:
|
|
8
|
+
- idempotent
|
|
9
|
+
- sequence
|
|
10
|
+
|
|
11
|
+
tests:
|
|
12
|
+
- id: first-append-accepted
|
|
13
|
+
name: First append with producer headers is accepted
|
|
14
|
+
description: Server should accept first append with epoch=0, seq=0
|
|
15
|
+
setup:
|
|
16
|
+
- action: create
|
|
17
|
+
as: streamPath
|
|
18
|
+
contentType: text/plain
|
|
19
|
+
operations:
|
|
20
|
+
- action: server-append
|
|
21
|
+
path: ${streamPath}
|
|
22
|
+
data: "hello"
|
|
23
|
+
producerId: test-producer
|
|
24
|
+
producerEpoch: 0
|
|
25
|
+
producerSeq: 0
|
|
26
|
+
expect:
|
|
27
|
+
status: 200
|
|
28
|
+
producerEpoch: 0
|
|
29
|
+
|
|
30
|
+
- id: sequential-sequences
|
|
31
|
+
name: Sequential producer sequences accepted
|
|
32
|
+
description: Server should accept monotonically increasing sequences
|
|
33
|
+
setup:
|
|
34
|
+
- action: create
|
|
35
|
+
as: streamPath
|
|
36
|
+
contentType: text/plain
|
|
37
|
+
operations:
|
|
38
|
+
- action: server-append
|
|
39
|
+
path: ${streamPath}
|
|
40
|
+
data: "msg0"
|
|
41
|
+
producerId: test-producer
|
|
42
|
+
producerEpoch: 0
|
|
43
|
+
producerSeq: 0
|
|
44
|
+
expect:
|
|
45
|
+
status: 200
|
|
46
|
+
- action: server-append
|
|
47
|
+
path: ${streamPath}
|
|
48
|
+
data: "msg1"
|
|
49
|
+
producerId: test-producer
|
|
50
|
+
producerEpoch: 0
|
|
51
|
+
producerSeq: 1
|
|
52
|
+
expect:
|
|
53
|
+
status: 200
|
|
54
|
+
- action: server-append
|
|
55
|
+
path: ${streamPath}
|
|
56
|
+
data: "msg2"
|
|
57
|
+
producerId: test-producer
|
|
58
|
+
producerEpoch: 0
|
|
59
|
+
producerSeq: 2
|
|
60
|
+
expect:
|
|
61
|
+
status: 200
|
|
62
|
+
|
|
63
|
+
- id: sequence-gap-rejected
|
|
64
|
+
name: Sequence gap returns 409
|
|
65
|
+
description: Skipping a sequence number should return 409 with expected/received seq
|
|
66
|
+
setup:
|
|
67
|
+
- action: create
|
|
68
|
+
as: streamPath
|
|
69
|
+
contentType: text/plain
|
|
70
|
+
operations:
|
|
71
|
+
- action: server-append
|
|
72
|
+
path: ${streamPath}
|
|
73
|
+
data: "msg0"
|
|
74
|
+
producerId: test-producer
|
|
75
|
+
producerEpoch: 0
|
|
76
|
+
producerSeq: 0
|
|
77
|
+
expect:
|
|
78
|
+
status: 200
|
|
79
|
+
# Skip seq=1, try seq=2
|
|
80
|
+
- action: server-append
|
|
81
|
+
path: ${streamPath}
|
|
82
|
+
data: "msg2"
|
|
83
|
+
producerId: test-producer
|
|
84
|
+
producerEpoch: 0
|
|
85
|
+
producerSeq: 2
|
|
86
|
+
expect:
|
|
87
|
+
status: 409
|
|
88
|
+
producerExpectedSeq: 1
|
|
89
|
+
producerReceivedSeq: 2
|
|
90
|
+
|
|
91
|
+
- id: new-producer-seq-not-zero
|
|
92
|
+
name: New producer starting with seq > 0 returns 409
|
|
93
|
+
description: A never-seen producer sending seq > 0 should get 409 (gap), not 400
|
|
94
|
+
setup:
|
|
95
|
+
- action: create
|
|
96
|
+
as: streamPath
|
|
97
|
+
contentType: text/plain
|
|
98
|
+
operations:
|
|
99
|
+
- action: server-append
|
|
100
|
+
path: ${streamPath}
|
|
101
|
+
data: "msg"
|
|
102
|
+
producerId: brand-new-producer
|
|
103
|
+
producerEpoch: 0
|
|
104
|
+
producerSeq: 5
|
|
105
|
+
expect:
|
|
106
|
+
status: 409
|
|
107
|
+
producerExpectedSeq: 0
|
|
108
|
+
producerReceivedSeq: 5
|
|
109
|
+
|
|
110
|
+
- id: duplicate-returns-204
|
|
111
|
+
name: Duplicate sequence returns 204 (idempotent success)
|
|
112
|
+
description: Retrying the same seq should return 204 without duplicating data
|
|
113
|
+
setup:
|
|
114
|
+
- action: create
|
|
115
|
+
as: streamPath
|
|
116
|
+
contentType: text/plain
|
|
117
|
+
operations:
|
|
118
|
+
- action: server-append
|
|
119
|
+
path: ${streamPath}
|
|
120
|
+
data: "hello"
|
|
121
|
+
producerId: test-producer
|
|
122
|
+
producerEpoch: 0
|
|
123
|
+
producerSeq: 0
|
|
124
|
+
expect:
|
|
125
|
+
status: 200
|
|
126
|
+
- action: server-append
|
|
127
|
+
path: ${streamPath}
|
|
128
|
+
data: "hello"
|
|
129
|
+
producerId: test-producer
|
|
130
|
+
producerEpoch: 0
|
|
131
|
+
producerSeq: 0
|
|
132
|
+
expect:
|
|
133
|
+
status: 204
|
|
134
|
+
duplicate: true
|
|
135
|
+
# Verify only one message in stream
|
|
136
|
+
- action: read
|
|
137
|
+
path: ${streamPath}
|
|
138
|
+
expect:
|
|
139
|
+
data: "hello"
|
|
140
|
+
upToDate: true
|
|
141
|
+
|
|
142
|
+
- id: duplicate-seq-zero-preserves-state
|
|
143
|
+
name: Duplicate of seq=0 does not corrupt state
|
|
144
|
+
description: Retrying seq=0 should not affect subsequent sequence tracking
|
|
145
|
+
setup:
|
|
146
|
+
- action: create
|
|
147
|
+
as: streamPath
|
|
148
|
+
contentType: text/plain
|
|
149
|
+
operations:
|
|
150
|
+
- action: server-append
|
|
151
|
+
path: ${streamPath}
|
|
152
|
+
data: "first"
|
|
153
|
+
producerId: test-producer
|
|
154
|
+
producerEpoch: 0
|
|
155
|
+
producerSeq: 0
|
|
156
|
+
expect:
|
|
157
|
+
status: 200
|
|
158
|
+
# Retry seq=0
|
|
159
|
+
- action: server-append
|
|
160
|
+
path: ${streamPath}
|
|
161
|
+
data: "first"
|
|
162
|
+
producerId: test-producer
|
|
163
|
+
producerEpoch: 0
|
|
164
|
+
producerSeq: 0
|
|
165
|
+
expect:
|
|
166
|
+
status: 204
|
|
167
|
+
# seq=1 should still work
|
|
168
|
+
- action: server-append
|
|
169
|
+
path: ${streamPath}
|
|
170
|
+
data: "second"
|
|
171
|
+
producerId: test-producer
|
|
172
|
+
producerEpoch: 0
|
|
173
|
+
producerSeq: 1
|
|
174
|
+
expect:
|
|
175
|
+
status: 200
|
|
176
|
+
|
|
177
|
+
- id: duplicate-ignores-different-data
|
|
178
|
+
name: Duplicate seq ignores payload differences
|
|
179
|
+
description: Same (producer, epoch, seq) returns 204 even with different data - dedup is by headers only
|
|
180
|
+
setup:
|
|
181
|
+
- action: create
|
|
182
|
+
as: streamPath
|
|
183
|
+
contentType: text/plain
|
|
184
|
+
operations:
|
|
185
|
+
- action: server-append
|
|
186
|
+
path: ${streamPath}
|
|
187
|
+
data: "original payload"
|
|
188
|
+
producerId: test-producer
|
|
189
|
+
producerEpoch: 0
|
|
190
|
+
producerSeq: 0
|
|
191
|
+
expect:
|
|
192
|
+
status: 200
|
|
193
|
+
# Retry with different data - should still be 204 (idempotent)
|
|
194
|
+
- action: server-append
|
|
195
|
+
path: ${streamPath}
|
|
196
|
+
data: "different payload"
|
|
197
|
+
producerId: test-producer
|
|
198
|
+
producerEpoch: 0
|
|
199
|
+
producerSeq: 0
|
|
200
|
+
expect:
|
|
201
|
+
status: 204
|
|
202
|
+
duplicate: true
|
|
203
|
+
# Verify only original data is in stream
|
|
204
|
+
- action: read
|
|
205
|
+
path: ${streamPath}
|
|
206
|
+
expect:
|
|
207
|
+
data: "original payload"
|
|
208
|
+
upToDate: true
|
|
209
|
+
|
|
210
|
+
- id: ordering-preserved-in-stream
|
|
211
|
+
name: Message ordering preserved in stream
|
|
212
|
+
description: Messages from idempotent producer maintain sequence order
|
|
213
|
+
setup:
|
|
214
|
+
- action: create
|
|
215
|
+
as: streamPath
|
|
216
|
+
contentType: application/json
|
|
217
|
+
operations:
|
|
218
|
+
- action: server-append
|
|
219
|
+
path: ${streamPath}
|
|
220
|
+
data: '{"seq": 0}'
|
|
221
|
+
producerId: order-test
|
|
222
|
+
producerEpoch: 0
|
|
223
|
+
producerSeq: 0
|
|
224
|
+
expect:
|
|
225
|
+
status: 200
|
|
226
|
+
- action: server-append
|
|
227
|
+
path: ${streamPath}
|
|
228
|
+
data: '{"seq": 1}'
|
|
229
|
+
producerId: order-test
|
|
230
|
+
producerEpoch: 0
|
|
231
|
+
producerSeq: 1
|
|
232
|
+
expect:
|
|
233
|
+
status: 200
|
|
234
|
+
- action: server-append
|
|
235
|
+
path: ${streamPath}
|
|
236
|
+
data: '{"seq": 2}'
|
|
237
|
+
producerId: order-test
|
|
238
|
+
producerEpoch: 0
|
|
239
|
+
producerSeq: 2
|
|
240
|
+
expect:
|
|
241
|
+
status: 200
|
|
242
|
+
- action: read
|
|
243
|
+
path: ${streamPath}
|
|
244
|
+
expect:
|
|
245
|
+
dataContainsAll:
|
|
246
|
+
- '"seq":0'
|
|
247
|
+
- '"seq":1'
|
|
248
|
+
- '"seq":2'
|
|
249
|
+
upToDate: true
|
|
250
|
+
|
|
251
|
+
- id: duplicate-old-sequence
|
|
252
|
+
name: Duplicate of old (non-tail) sequence returns 204
|
|
253
|
+
description: |
|
|
254
|
+
Retrying a sequence far back in history should still return 204.
|
|
255
|
+
This tests that the server tracks more than just the last sequence.
|
|
256
|
+
(Kafka distinguishes DuplicateSequenceException vs OutOfOrderSequenceException)
|
|
257
|
+
setup:
|
|
258
|
+
- action: create
|
|
259
|
+
as: streamPath
|
|
260
|
+
contentType: text/plain
|
|
261
|
+
operations:
|
|
262
|
+
# Send sequences 0-5
|
|
263
|
+
- action: server-append
|
|
264
|
+
path: ${streamPath}
|
|
265
|
+
data: "msg0"
|
|
266
|
+
producerId: test-producer
|
|
267
|
+
producerEpoch: 0
|
|
268
|
+
producerSeq: 0
|
|
269
|
+
expect:
|
|
270
|
+
status: 200
|
|
271
|
+
- action: server-append
|
|
272
|
+
path: ${streamPath}
|
|
273
|
+
data: "msg1"
|
|
274
|
+
producerId: test-producer
|
|
275
|
+
producerEpoch: 0
|
|
276
|
+
producerSeq: 1
|
|
277
|
+
expect:
|
|
278
|
+
status: 200
|
|
279
|
+
- action: server-append
|
|
280
|
+
path: ${streamPath}
|
|
281
|
+
data: "msg2"
|
|
282
|
+
producerId: test-producer
|
|
283
|
+
producerEpoch: 0
|
|
284
|
+
producerSeq: 2
|
|
285
|
+
expect:
|
|
286
|
+
status: 200
|
|
287
|
+
- action: server-append
|
|
288
|
+
path: ${streamPath}
|
|
289
|
+
data: "msg3"
|
|
290
|
+
producerId: test-producer
|
|
291
|
+
producerEpoch: 0
|
|
292
|
+
producerSeq: 3
|
|
293
|
+
expect:
|
|
294
|
+
status: 200
|
|
295
|
+
- action: server-append
|
|
296
|
+
path: ${streamPath}
|
|
297
|
+
data: "msg4"
|
|
298
|
+
producerId: test-producer
|
|
299
|
+
producerEpoch: 0
|
|
300
|
+
producerSeq: 4
|
|
301
|
+
expect:
|
|
302
|
+
status: 200
|
|
303
|
+
- action: server-append
|
|
304
|
+
path: ${streamPath}
|
|
305
|
+
data: "msg5"
|
|
306
|
+
producerId: test-producer
|
|
307
|
+
producerEpoch: 0
|
|
308
|
+
producerSeq: 5
|
|
309
|
+
expect:
|
|
310
|
+
status: 200
|
|
311
|
+
# Retry seq=1 (old, non-tail sequence)
|
|
312
|
+
- action: server-append
|
|
313
|
+
path: ${streamPath}
|
|
314
|
+
data: "msg1"
|
|
315
|
+
producerId: test-producer
|
|
316
|
+
producerEpoch: 0
|
|
317
|
+
producerSeq: 1
|
|
318
|
+
expect:
|
|
319
|
+
status: 204
|
|
320
|
+
duplicate: true
|
|
321
|
+
# Retry seq=0 (first sequence)
|
|
322
|
+
- action: server-append
|
|
323
|
+
path: ${streamPath}
|
|
324
|
+
data: "msg0"
|
|
325
|
+
producerId: test-producer
|
|
326
|
+
producerEpoch: 0
|
|
327
|
+
producerSeq: 0
|
|
328
|
+
expect:
|
|
329
|
+
status: 204
|
|
330
|
+
duplicate: true
|
|
331
|
+
# New sequence should still work
|
|
332
|
+
- action: server-append
|
|
333
|
+
path: ${streamPath}
|
|
334
|
+
data: "msg6"
|
|
335
|
+
producerId: test-producer
|
|
336
|
+
producerEpoch: 0
|
|
337
|
+
producerSeq: 6
|
|
338
|
+
expect:
|
|
339
|
+
status: 200
|