Skip to content

Commit ff5025a

Browse files
authored
KAFKA-19695: Fix bug in redundant offset calculation. (#20516)
* The `ShareCoordinatorShard` maintains the the record offset information for `SharePartitionKey`s in the `ShareCoordinatorOffsetsManager` class. * Replay of `ShareSnapshot`s in the shards are reflected in the offsets manager including records created due to delete state. * However, if the share partition delete is due to topic delete, no record will ever be written for the same `SharePartitionKey` post the delete tombstone (as topic id will not repeat). As a result the offset manager will always consider the deleted share partition's offset as the last redundant one. * The fix is to make the offset manager aware of the tombstone records and remove them from the redundant offset calculation. * Unit tests have been updated for the same. Reviewers: Andrew Schofield <[email protected]>, Apoorv Mittal <[email protected]>
1 parent 3512038 commit ff5025a

File tree

3 files changed

+90
-17
lines changed

3 files changed

+90
-17
lines changed

share-coordinator/src/main/java/org/apache/kafka/coordinator/share/ShareCoordinatorOffsetsManager.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,20 @@ public ShareCoordinatorOffsetsManager(SnapshotRegistry snapshotRegistry) {
6060
*
6161
* @param key - represents {@link SharePartitionKey} whose offset needs updating
6262
* @param offset - represents the latest partition offset for provided key
63+
* @param isDelete - true if the offset is for a tombstone record
6364
*/
64-
public void updateState(SharePartitionKey key, long offset) {
65+
public void updateState(SharePartitionKey key, long offset, boolean isDelete) {
6566
lastRedundantOffset.set(Math.min(lastRedundantOffset.get(), offset));
6667
offsets.put(key, offset);
6768

6869
Optional<Long> redundantOffset = findRedundantOffset();
6970
redundantOffset.ifPresent(lastRedundantOffset::set);
71+
72+
// If the share partition is deleted, we should not hold onto its offset in our calculations
73+
// as there is nothing beyond deletion which is going to update its state.
74+
if (isDelete) {
75+
offsets.remove(key);
76+
}
7077
}
7178

7279
private Optional<Long> findRedundantOffset() {

share-coordinator/src/main/java/org/apache/kafka/coordinator/share/ShareCoordinatorShard.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ private void handleShareSnapshot(ShareSnapshotKey key, ShareSnapshotValue value,
266266
}
267267
}
268268

269-
offsetsManager.updateState(mapKey, offset);
269+
offsetsManager.updateState(mapKey, offset, value == null);
270270
}
271271

272272
private void handleShareUpdate(ShareUpdateKey key, ShareUpdateValue value) {

share-coordinator/src/test/java/org/apache/kafka/coordinator/share/ShareCoordinatorOffsetsManagerTest.java

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.stream.Stream;
3333

3434
import static org.junit.jupiter.api.Assertions.assertEquals;
35+
import static org.junit.jupiter.api.Assertions.assertNull;
3536

3637
public class ShareCoordinatorOffsetsManagerTest {
3738

@@ -48,16 +49,19 @@ public void setUp() {
4849

4950
@Test
5051
public void testUpdateStateAddsToInternalState() {
51-
manager.updateState(KEY1, 0L);
52+
manager.updateState(KEY1, 0L, false);
5253
assertEquals(Optional.empty(), manager.lastRedundantOffset());
5354

54-
manager.updateState(KEY1, 10L);
55+
manager.updateState(KEY1, 10L, false);
5556
assertEquals(Optional.of(10L), manager.lastRedundantOffset()); // [0-9] offsets are redundant.
5657

57-
manager.updateState(KEY2, 15L);
58+
manager.updateState(KEY2, 15L, false);
5859
assertEquals(Optional.of(10L), manager.lastRedundantOffset()); // No update to last redundant after adding 15L so, still 10L.
5960

60-
assertEquals(10L, manager.curState().get(KEY1));
61+
manager.updateState(KEY1, 25L, true);
62+
assertEquals(Optional.of(15L), manager.lastRedundantOffset()); // KEY1 deleted, no longer part of calculation.
63+
64+
assertNull(manager.curState().get(KEY1));
6165
assertEquals(15L, manager.curState().get(KEY2));
6266
}
6367

@@ -66,15 +70,21 @@ static class TestTuple {
6670
final SharePartitionKey key;
6771
final long offset;
6872
final Optional<Long> expectedOffset;
73+
final boolean isDelete;
6974

70-
private TestTuple(SharePartitionKey key, long offset, Optional<Long> expectedOffset) {
75+
private TestTuple(SharePartitionKey key, long offset, Optional<Long> expectedOffset, boolean isDelete) {
7176
this.key = key;
7277
this.offset = offset;
7378
this.expectedOffset = expectedOffset;
79+
this.isDelete = isDelete;
7480
}
7581

7682
static TestTuple instance(SharePartitionKey key, long offset, Optional<Long> expectedOffset) {
77-
return new TestTuple(key, offset, expectedOffset);
83+
return new TestTuple(key, offset, expectedOffset, false);
84+
}
85+
86+
static TestTuple instance(SharePartitionKey key, long offset, Optional<Long> expectedOffset, boolean isDelete) {
87+
return new TestTuple(key, offset, expectedOffset, isDelete);
7888
}
7989
}
8090

@@ -96,27 +106,43 @@ static TestTuple instance(SharePartitionKey key, long offset, Optional<Long> exp
96106
static Stream<ShareOffsetTestHolder> generateNoRedundantStateCases() {
97107
return Stream.of(
98108
new ShareOffsetTestHolder(
99-
"no redundant state single key",
109+
"no redundant state single key.",
100110
List.of(
101111
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L))
102112
)
103113
),
104114

105115
new ShareOffsetTestHolder(
106-
"no redundant state multiple keys",
116+
"no redundant state single key with delete.",
117+
List.of(
118+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L), true)
119+
)
120+
),
121+
122+
new ShareOffsetTestHolder(
123+
"no redundant state multiple keys.",
107124
List.of(
108125
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L)),
109126
ShareOffsetTestHolder.TestTuple.instance(KEY4, 11L, Optional.of(10L)),
110127
ShareOffsetTestHolder.TestTuple.instance(KEY2, 13L, Optional.of(10L))
111128
)
129+
),
130+
131+
new ShareOffsetTestHolder(
132+
"no redundant state multiple keys with delete.",
133+
List.of(
134+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L), true),
135+
ShareOffsetTestHolder.TestTuple.instance(KEY4, 11L, Optional.of(11L), true),
136+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 13L, Optional.of(13L), true)
137+
)
112138
)
113139
);
114140
}
115141

116142
static Stream<ShareOffsetTestHolder> generateRedundantStateCases() {
117143
return Stream.of(
118144
new ShareOffsetTestHolder(
119-
"redundant state single key",
145+
"redundant state single key.",
120146
List.of(
121147
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L)),
122148
ShareOffsetTestHolder.TestTuple.instance(KEY1, 11L, Optional.of(11L)),
@@ -125,7 +151,7 @@ static Stream<ShareOffsetTestHolder> generateRedundantStateCases() {
125151
),
126152

127153
new ShareOffsetTestHolder(
128-
"redundant state multiple keys",
154+
"redundant state multiple keys.",
129155
// KEY1: 10 17
130156
// KEY2: 11 16
131157
// KEY3: 15
@@ -136,6 +162,20 @@ static Stream<ShareOffsetTestHolder> generateRedundantStateCases() {
136162
ShareOffsetTestHolder.TestTuple.instance(KEY2, 16L, Optional.of(10L)), // KEY2 11 redundant but should not be returned
137163
ShareOffsetTestHolder.TestTuple.instance(KEY1, 17L, Optional.of(15L))
138164
)
165+
),
166+
167+
new ShareOffsetTestHolder(
168+
"redundant state multiple keys with delete.",
169+
// KEY1: 10 17
170+
// KEY2: 11 16
171+
// KEY3: 15
172+
List.of(
173+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L)),
174+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 11L, Optional.of(10L)),
175+
ShareOffsetTestHolder.TestTuple.instance(KEY3, 15L, Optional.of(10L), true),
176+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 16L, Optional.of(10L)), // KEY2 11 redundant but should not be returned
177+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 17L, Optional.of(16L)) // Because we have removed KEY3 from calculation
178+
)
139179
)
140180
);
141181

@@ -144,7 +184,7 @@ static Stream<ShareOffsetTestHolder> generateRedundantStateCases() {
144184
static Stream<ShareOffsetTestHolder> generateComplexCases() {
145185
return Stream.of(
146186
new ShareOffsetTestHolder(
147-
"redundant state reverse key order",
187+
"redundant state reverse key order.",
148188
// Requests come in order KEY1, KEY2, KEY3, KEY3, KEY2, KEY1.
149189
List.of(
150190
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L)),
@@ -156,6 +196,18 @@ static Stream<ShareOffsetTestHolder> generateComplexCases() {
156196
)
157197
),
158198

199+
new ShareOffsetTestHolder(
200+
"redundant state reverse key order with delete.",
201+
List.of(
202+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L)),
203+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 11L, Optional.of(10L)),
204+
ShareOffsetTestHolder.TestTuple.instance(KEY3, 15L, Optional.of(10L)),
205+
ShareOffsetTestHolder.TestTuple.instance(KEY3, 18L, Optional.of(10L), true),
206+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 20L, Optional.of(10L), true),
207+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 25L, Optional.of(25L)) // Because KEY2 and KEY3 are gone.
208+
)
209+
),
210+
159211
new ShareOffsetTestHolder(
160212
"redundant state infrequently written partition.",
161213
List.of(
@@ -170,6 +222,20 @@ static Stream<ShareOffsetTestHolder> generateComplexCases() {
170222
ShareOffsetTestHolder.TestTuple.instance(KEY3, 28L, Optional.of(10L)),
171223
ShareOffsetTestHolder.TestTuple.instance(KEY1, 30L, Optional.of(27L))
172224
)
225+
),
226+
227+
new ShareOffsetTestHolder(
228+
"redundant state infrequently written partition with delete.",
229+
List.of(
230+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 10L, Optional.of(10L)),
231+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 11L, Optional.of(10L)),
232+
ShareOffsetTestHolder.TestTuple.instance(KEY3, 15L, Optional.of(10L)),
233+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 18L, Optional.of(10L)),
234+
ShareOffsetTestHolder.TestTuple.instance(KEY3, 20L, Optional.of(10L), true), //KEY3 no longer party to calculation
235+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 22L, Optional.of(10L)),
236+
ShareOffsetTestHolder.TestTuple.instance(KEY2, 27L, Optional.of(10L), true), //KEY2 no longer party to calculation
237+
ShareOffsetTestHolder.TestTuple.instance(KEY1, 30L, Optional.of(30L))
238+
)
173239
)
174240
);
175241
}
@@ -179,7 +245,7 @@ static Stream<ShareOffsetTestHolder> generateComplexCases() {
179245
public void testUpdateStateNoRedundantState(ShareOffsetTestHolder holder) {
180246
if (holder.shouldRun) {
181247
holder.tuples.forEach(tuple -> {
182-
manager.updateState(tuple.key, tuple.offset);
248+
manager.updateState(tuple.key, tuple.offset, tuple.isDelete);
183249
assertEquals(tuple.expectedOffset, manager.lastRedundantOffset(), holder.testName);
184250
});
185251
}
@@ -190,7 +256,7 @@ public void testUpdateStateNoRedundantState(ShareOffsetTestHolder holder) {
190256
public void testUpdateStateRedundantState(ShareOffsetTestHolder holder) {
191257
if (holder.shouldRun) {
192258
holder.tuples.forEach(tuple -> {
193-
manager.updateState(tuple.key, tuple.offset);
259+
manager.updateState(tuple.key, tuple.offset, tuple.isDelete);
194260
assertEquals(tuple.expectedOffset, manager.lastRedundantOffset(), holder.testName);
195261
});
196262
}
@@ -201,9 +267,9 @@ public void testUpdateStateRedundantState(ShareOffsetTestHolder holder) {
201267
public void testUpdateStateComplexCases(ShareOffsetTestHolder holder) {
202268
if (holder.shouldRun) {
203269
holder.tuples.forEach(tuple -> {
204-
manager.updateState(tuple.key, tuple.offset);
270+
manager.updateState(tuple.key, tuple.offset, tuple.isDelete);
205271
assertEquals(tuple.expectedOffset, manager.lastRedundantOffset(), holder.testName);
206272
});
207273
}
208274
}
209-
}
275+
}

0 commit comments

Comments
 (0)