|
66 | 66 | import org.springframework.kafka.event.ConsumerStartedEvent;
|
67 | 67 | import org.springframework.kafka.event.ConsumerStartingEvent;
|
68 | 68 | import org.springframework.kafka.event.ListenerContainerIdleEvent;
|
| 69 | +import org.springframework.kafka.listener.ContainerProperties.AckMode; |
69 | 70 | import org.springframework.kafka.listener.ContainerProperties.AssignmentCommitOption;
|
| 71 | +import org.springframework.kafka.support.Acknowledgment; |
70 | 72 | import org.springframework.kafka.test.utils.KafkaTestUtils;
|
71 | 73 | import org.springframework.kafka.transaction.KafkaAwareTransactionManager;
|
72 | 74 | import org.springframework.lang.Nullable;
|
@@ -904,7 +906,7 @@ void removeFromPartitionPauseRequestedWhenNotAssigned() throws InterruptedExcept
|
904 | 906 |
|
905 | 907 | @SuppressWarnings({ "unchecked", "rawtypes" })
|
906 | 908 | @Test
|
907 |
| - void pruneRevokedPartitionsFromRemainingRecordsWhenSeekAfterErrorFalseLagacyAssignor() throws InterruptedException { |
| 909 | + void pruneRevokedPartitionsFromRemainingRecordsWhenSeekAfterErrorFalseLegacyAssignor() throws InterruptedException { |
908 | 910 | TopicPartition tp0 = new TopicPartition("foo", 0);
|
909 | 911 | TopicPartition tp1 = new TopicPartition("foo", 1);
|
910 | 912 | TopicPartition tp2 = new TopicPartition("foo", 2);
|
@@ -1116,6 +1118,178 @@ public boolean handleOne(Exception thrownException, ConsumerRecord<?, ?> record,
|
1116 | 1118 | assertThat(recordsDelivered.get(2)).isEqualTo(record1);
|
1117 | 1119 | }
|
1118 | 1120 |
|
| 1121 | + @SuppressWarnings({ "unchecked", "rawtypes" }) |
| 1122 | + @Test |
| 1123 | + void pruneRevokedPartitionsFromPendingOutOfOrderCommitsLegacyAssignor() throws InterruptedException { |
| 1124 | + TopicPartition tp0 = new TopicPartition("foo", 0); |
| 1125 | + TopicPartition tp1 = new TopicPartition("foo", 1); |
| 1126 | + List<TopicPartition> allAssignments = Arrays.asList(tp0, tp1); |
| 1127 | + Map<TopicPartition, List<ConsumerRecord<String, String>>> allRecordMap = new HashMap<>(); |
| 1128 | + allRecordMap.put(tp0, |
| 1129 | + List.of(new ConsumerRecord("foo", 0, 0, null, "bar"), new ConsumerRecord("foo", 0, 1, null, "bar"))); |
| 1130 | + allRecordMap.put(tp1, |
| 1131 | + List.of(new ConsumerRecord("foo", 1, 0, null, "bar"), new ConsumerRecord("foo", 1, 1, null, "bar"))); |
| 1132 | + ConsumerRecords allRecords = new ConsumerRecords<>(allRecordMap); |
| 1133 | + List<TopicPartition> afterRevokeAssignments = Arrays.asList(tp1); |
| 1134 | + AtomicInteger pollPhase = new AtomicInteger(); |
| 1135 | + |
| 1136 | + Consumer consumer = mock(Consumer.class); |
| 1137 | + AtomicReference<ConsumerRebalanceListener> rebal = new AtomicReference<>(); |
| 1138 | + CountDownLatch subscribeLatch = new CountDownLatch(1); |
| 1139 | + willAnswer(invocation -> { |
| 1140 | + rebal.set(invocation.getArgument(1)); |
| 1141 | + subscribeLatch.countDown(); |
| 1142 | + return null; |
| 1143 | + }).given(consumer).subscribe(any(Collection.class), any()); |
| 1144 | + CountDownLatch pauseLatch = new CountDownLatch(1); |
| 1145 | + AtomicBoolean paused = new AtomicBoolean(); |
| 1146 | + willAnswer(inv -> { |
| 1147 | + paused.set(true); |
| 1148 | + pauseLatch.countDown(); |
| 1149 | + return null; |
| 1150 | + }).given(consumer).pause(any()); |
| 1151 | + ConsumerFactory cf = mock(ConsumerFactory.class); |
| 1152 | + given(cf.createConsumer(any(), any(), any(), any())).willReturn(consumer); |
| 1153 | + given(cf.getConfigurationProperties()) |
| 1154 | + .willReturn(Collections.singletonMap(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")); |
| 1155 | + ContainerProperties containerProperties = new ContainerProperties("foo"); |
| 1156 | + containerProperties.setGroupId("grp"); |
| 1157 | + containerProperties.setAckMode(AckMode.MANUAL); |
| 1158 | + containerProperties.setMessageListener(ackOffset1()); |
| 1159 | + containerProperties.setAsyncAcks(true); |
| 1160 | + ConcurrentMessageListenerContainer container = new ConcurrentMessageListenerContainer(cf, |
| 1161 | + containerProperties); |
| 1162 | + CountDownLatch pollLatch = new CountDownLatch(2); |
| 1163 | + CountDownLatch rebalLatch = new CountDownLatch(1); |
| 1164 | + CountDownLatch continueLatch = new CountDownLatch(1); |
| 1165 | + willAnswer(inv -> { |
| 1166 | + Thread.sleep(50); |
| 1167 | + pollLatch.countDown(); |
| 1168 | + switch (pollPhase.getAndIncrement()) { |
| 1169 | + case 0: |
| 1170 | + rebal.get().onPartitionsAssigned(allAssignments); |
| 1171 | + return allRecords; |
| 1172 | + case 1: |
| 1173 | + rebal.get().onPartitionsRevoked(allAssignments); |
| 1174 | + rebal.get().onPartitionsAssigned(afterRevokeAssignments); |
| 1175 | + rebalLatch.countDown(); |
| 1176 | + continueLatch.await(10, TimeUnit.SECONDS); |
| 1177 | + default: |
| 1178 | + return ConsumerRecords.empty(); |
| 1179 | + } |
| 1180 | + }).given(consumer).poll(any()); |
| 1181 | + container.start(); |
| 1182 | + assertThat(subscribeLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1183 | + KafkaMessageListenerContainer child = (KafkaMessageListenerContainer) KafkaTestUtils |
| 1184 | + .getPropertyValue(container, "containers", List.class).get(0); |
| 1185 | + assertThat(pollLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1186 | + assertThat(pauseLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1187 | + assertThat(rebalLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1188 | + Map offsets = KafkaTestUtils.getPropertyValue(child, "listenerConsumer.offsetsInThisBatch", Map.class); |
| 1189 | + assertThat(offsets).hasSize(0); |
| 1190 | + assertThat(KafkaTestUtils.getPropertyValue(child, "listenerConsumer.consumerPaused", Boolean.class)).isFalse(); |
| 1191 | + continueLatch.countDown(); |
| 1192 | + // no pause when re-assigned because all revoked |
| 1193 | + verify(consumer).pause(any()); |
| 1194 | + verify(consumer, never()).resume(any()); |
| 1195 | + container.stop(); |
| 1196 | + } |
| 1197 | + |
| 1198 | + @SuppressWarnings({ "unchecked", "rawtypes" }) |
| 1199 | + @Test |
| 1200 | + void pruneRevokedPartitionsFromPendingOutOfOrderCommitsCoopAssignor() throws InterruptedException { |
| 1201 | + TopicPartition tp0 = new TopicPartition("foo", 0); |
| 1202 | + TopicPartition tp1 = new TopicPartition("foo", 1); |
| 1203 | + List<TopicPartition> allAssignments = Arrays.asList(tp0, tp1); |
| 1204 | + Map<TopicPartition, List<ConsumerRecord<String, String>>> allRecordMap = new HashMap<>(); |
| 1205 | + allRecordMap.put(tp0, |
| 1206 | + List.of(new ConsumerRecord("foo", 0, 0, null, "bar"), new ConsumerRecord("foo", 0, 1, null, "bar"))); |
| 1207 | + allRecordMap.put(tp1, |
| 1208 | + List.of(new ConsumerRecord("foo", 1, 0, null, "bar"), new ConsumerRecord("foo", 1, 1, null, "bar"))); |
| 1209 | + ConsumerRecords allRecords = new ConsumerRecords<>(allRecordMap); |
| 1210 | + List<TopicPartition> afterRevokeAssignments = Arrays.asList(tp1); |
| 1211 | + AtomicInteger pollPhase = new AtomicInteger(); |
| 1212 | + |
| 1213 | + Consumer consumer = mock(Consumer.class); |
| 1214 | + AtomicReference<ConsumerRebalanceListener> rebal = new AtomicReference<>(); |
| 1215 | + CountDownLatch subscribeLatch = new CountDownLatch(1); |
| 1216 | + willAnswer(invocation -> { |
| 1217 | + rebal.set(invocation.getArgument(1)); |
| 1218 | + subscribeLatch.countDown(); |
| 1219 | + return null; |
| 1220 | + }).given(consumer).subscribe(any(Collection.class), any()); |
| 1221 | + CountDownLatch pauseLatch = new CountDownLatch(1); |
| 1222 | + AtomicBoolean paused = new AtomicBoolean(); |
| 1223 | + willAnswer(inv -> { |
| 1224 | + paused.set(true); |
| 1225 | + pauseLatch.countDown(); |
| 1226 | + return null; |
| 1227 | + }).given(consumer).pause(any()); |
| 1228 | + ConsumerFactory cf = mock(ConsumerFactory.class); |
| 1229 | + given(cf.createConsumer(any(), any(), any(), any())).willReturn(consumer); |
| 1230 | + given(cf.getConfigurationProperties()) |
| 1231 | + .willReturn(Collections.singletonMap(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")); |
| 1232 | + ContainerProperties containerProperties = new ContainerProperties("foo"); |
| 1233 | + containerProperties.setGroupId("grp"); |
| 1234 | + containerProperties.setAckMode(AckMode.MANUAL); |
| 1235 | + containerProperties.setMessageListener(ackOffset1()); |
| 1236 | + containerProperties.setAsyncAcks(true); |
| 1237 | + ConcurrentMessageListenerContainer container = new ConcurrentMessageListenerContainer(cf, |
| 1238 | + containerProperties); |
| 1239 | + CountDownLatch pollLatch = new CountDownLatch(2); |
| 1240 | + CountDownLatch rebalLatch = new CountDownLatch(1); |
| 1241 | + CountDownLatch continueLatch = new CountDownLatch(1); |
| 1242 | + willAnswer(inv -> { |
| 1243 | + Thread.sleep(50); |
| 1244 | + pollLatch.countDown(); |
| 1245 | + switch (pollPhase.getAndIncrement()) { |
| 1246 | + case 0: |
| 1247 | + rebal.get().onPartitionsAssigned(allAssignments); |
| 1248 | + return allRecords; |
| 1249 | + case 1: |
| 1250 | + rebal.get().onPartitionsRevoked(List.of(tp0)); |
| 1251 | + rebal.get().onPartitionsAssigned(List.of(new TopicPartition("foo", 2))); |
| 1252 | + rebalLatch.countDown(); |
| 1253 | + continueLatch.await(10, TimeUnit.SECONDS); |
| 1254 | + default: |
| 1255 | + return ConsumerRecords.empty(); |
| 1256 | + } |
| 1257 | + }).given(consumer).poll(any()); |
| 1258 | + container.start(); |
| 1259 | + assertThat(subscribeLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1260 | + KafkaMessageListenerContainer child = (KafkaMessageListenerContainer) KafkaTestUtils |
| 1261 | + .getPropertyValue(container, "containers", List.class).get(0); |
| 1262 | + assertThat(pollLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1263 | + assertThat(pauseLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1264 | + assertThat(rebalLatch.await(10, TimeUnit.SECONDS)).isTrue(); |
| 1265 | + Map offsets = KafkaTestUtils.getPropertyValue(child, "listenerConsumer.offsetsInThisBatch", Map.class); |
| 1266 | + assertThat(offsets).hasSize(1); |
| 1267 | + assertThat(offsets.get(tp1)).isNotNull(); |
| 1268 | + assertThat(KafkaTestUtils.getPropertyValue(child, "listenerConsumer.consumerPaused", Boolean.class)).isTrue(); |
| 1269 | + continueLatch.countDown(); |
| 1270 | + verify(consumer, times(2)).pause(any()); |
| 1271 | + verify(consumer, never()).resume(any()); |
| 1272 | + container.stop(); |
| 1273 | + } |
| 1274 | + |
| 1275 | + @SuppressWarnings("rawtypes") |
| 1276 | + private AcknowledgingMessageListener ackOffset1() { |
| 1277 | + return new AcknowledgingMessageListener() { |
| 1278 | + |
| 1279 | + @Override |
| 1280 | + public void onMessage(ConsumerRecord rec, @Nullable Acknowledgment ack) { |
| 1281 | + if (rec.offset() == 1) { |
| 1282 | + ack.acknowledge(); |
| 1283 | + } |
| 1284 | + } |
| 1285 | + |
| 1286 | + @Override |
| 1287 | + public void onMessage(Object data) { |
| 1288 | + } |
| 1289 | + |
| 1290 | + }; |
| 1291 | + } |
| 1292 | + |
1119 | 1293 | public static class TestMessageListener1 implements MessageListener<String, String>, ConsumerSeekAware {
|
1120 | 1294 |
|
1121 | 1295 | private static ThreadLocal<ConsumerSeekCallback> callbacks = new ThreadLocal<>();
|
|
0 commit comments