Seek Improvements

- add convenience methods to `AbstractConsumerSeekAware`

- compile tests with Java 11
This commit is contained in:
Gary Russell
2020-08-24 13:23:22 -04:00
committed by GitHub
parent 8ae9a8da1b
commit c8cf57c495
6 changed files with 239 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
dist: trusty
dist: bionic
language: java
jdk: oraclejdk8
jdk: openjdk11
install: true
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock

View File

@@ -133,6 +133,10 @@ subprojects { subproject ->
targetCompatibility = 1.8
}
compileTestJava {
sourceCompatibility = 11
}
compileTestKotlin {
kotlinOptions {
jvmTarget = '1.8'

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 the original author or authors.
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@ package org.springframework.kafka.listener;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -40,6 +42,8 @@ public abstract class AbstractConsumerSeekAware implements ConsumerSeekAware {
private final Map<TopicPartition, ConsumerSeekCallback> callbacks = new ConcurrentHashMap<>();
private final Map<ConsumerSeekCallback, List<TopicPartition>> callbacksToTopic = new ConcurrentHashMap<>();
@Override
public void registerSeekCallback(ConsumerSeekCallback callback) {
this.callbackForThread.set(callback);
@@ -49,13 +53,27 @@ public abstract class AbstractConsumerSeekAware implements ConsumerSeekAware {
public void onPartitionsAssigned(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
ConsumerSeekCallback threadCallback = this.callbackForThread.get();
if (threadCallback != null) {
assignments.keySet().forEach(tp -> this.callbacks.put(tp, threadCallback));
assignments.keySet().forEach(tp -> {
this.callbacks.put(tp, threadCallback);
this.callbacksToTopic.computeIfAbsent(threadCallback, key -> new LinkedList<>()).add(tp);
});
}
}
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
partitions.forEach(tp -> this.callbacks.remove(tp));
partitions.forEach(tp -> {
ConsumerSeekCallback removed = this.callbacks.remove(tp);
if (removed != null) {
List<TopicPartition> topics = this.callbacksToTopic.get(removed);
if (topics != null) {
topics.remove(tp);
if (topics.size() == 0) {
this.callbacksToTopic.remove(removed);
}
}
}
});
}
@Override
@@ -81,4 +99,38 @@ public abstract class AbstractConsumerSeekAware implements ConsumerSeekAware {
return Collections.unmodifiableMap(this.callbacks);
}
/**
* Return the currently registered callbacks and their associated {@link TopicPartition}(s).
* @return the map of callbacks and partitions.
* @since 2.6
*/
protected Map<ConsumerSeekCallback, List<TopicPartition>> getCallbacksAndTopics() {
return Collections.unmodifiableMap(this.callbacksToTopic);
}
/**
* Seek all assigned partitions to the beginning.
* @since 2.6
*/
public void seekToBeginning() {
getCallbacksAndTopics().forEach((cb, topics) -> cb.seekToBeginning(topics));
}
/**
* Seek all assigned partitions to the end.
* @since 2.6
*/
public void seekToEnd() {
getCallbacksAndTopics().forEach((cb, topics) -> cb.seekToEnd(topics));
}
/**
* Seek all assigned partitions to the offset represented by the timestamp.
* @param time the time to seek to.
* @since 2.6
*/
public void seekToTimestamp(long time) {
getCallbacksAndTopics().forEach((cb, topcis) -> cb.seekToTimestamp(topcis, time));
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.kafka.listener;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.kafka.common.TopicPartition;
import org.junit.jupiter.api.Test;
import org.springframework.kafka.listener.ConsumerSeekAware.ConsumerSeekCallback;
import org.springframework.kafka.test.utils.KafkaTestUtils;
/**
* @author Gary Russell
* @since 2.6
*
*/
public class ConsumerSeekAwareTests {
@SuppressWarnings("unchecked")
@Test
void beginningEndAndBulkSeekToTimestamp() throws Exception {
class CSA extends AbstractConsumerSeekAware {
}
AbstractConsumerSeekAware csa = new CSA();
var exec1 = Executors.newSingleThreadExecutor();
var exec2 = Executors.newSingleThreadExecutor();
var cb1 = mock(ConsumerSeekCallback.class);
var cb2 = mock(ConsumerSeekCallback.class);
var first = new AtomicBoolean(true);
var map1 = new LinkedHashMap<>(Map.of(new TopicPartition("foo", 0), 0L, new TopicPartition("foo", 1), 0L));
var map2 = new LinkedHashMap<>(Map.of(new TopicPartition("foo", 2), 0L, new TopicPartition("foo", 3), 0L));
var register = new Callable<Void>() {
@Override
public Void call() {
if (first.getAndSet(false)) {
csa.registerSeekCallback(cb1);
csa.onPartitionsAssigned(map1, null);
}
else {
csa.registerSeekCallback(cb2);
csa.onPartitionsAssigned(map2, null);
}
return null;
}
};
exec1.submit(register).get();
exec2.submit(register).get();
csa.seekToBeginning();
verify(cb1).seekToBeginning(new LinkedList<>(map1.keySet()));
verify(cb2).seekToBeginning(new LinkedList<>(map2.keySet()));
csa.seekToEnd();
verify(cb1).seekToEnd(new LinkedList<>(map1.keySet()));
verify(cb2).seekToEnd(new LinkedList<>(map2.keySet()));
csa.seekToTimestamp(42L);
verify(cb1).seekToTimestamp(new LinkedList<>(map1.keySet()), 42L);
verify(cb2).seekToTimestamp(new LinkedList<>(map2.keySet()), 42L);
var revoke1 = new Callable<Void>() {
@Override
public Void call() {
if (!first.getAndSet(true)) {
csa.onPartitionsRevoked(Collections.singletonList(map1.keySet().iterator().next()));
}
else {
csa.onPartitionsRevoked(Collections.singletonList(map2.keySet().iterator().next()));
}
return null;
}
};
exec1.submit(revoke1).get();
exec2.submit(revoke1).get();
map1.remove(map1.keySet().iterator().next());
map2.remove(map2.keySet().iterator().next());
csa.seekToTimestamp(43L);
verify(cb1).seekToTimestamp(new LinkedList<>(map1.keySet()), 43L);
verify(cb2).seekToTimestamp(new LinkedList<>(map2.keySet()), 43L);
var revoke2 = new Callable<Void>() {
@Override
public Void call() {
if (first.getAndSet(false)) {
csa.onPartitionsRevoked(Collections.singletonList(map1.keySet().iterator().next()));
}
else {
csa.onPartitionsRevoked(Collections.singletonList(map2.keySet().iterator().next()));
}
return null;
}
};
exec1.submit(revoke2).get();
exec2.submit(revoke2).get();
assertThat(KafkaTestUtils.getPropertyValue(csa, "callbacks", Map.class)).isEmpty();
assertThat(KafkaTestUtils.getPropertyValue(csa, "callbacksToTopic", Map.class)).isEmpty();
var checkTL = new Callable<Void>() {
@Override
public Void call() throws Exception {
csa.unregisterSeekCallback();
assertThat(KafkaTestUtils.getPropertyValue(csa, "callbackForThread", ThreadLocal.class).get()).isNull();
return null;
}
};
exec1.submit(checkTL).get();
exec2.submit(checkTL).get();
exec1.shutdown();
exec2.shutdown();
}
}

View File

@@ -2721,7 +2721,7 @@ NOTE: The `seekToBeginning` method that accepts a collection is useful, for exam
====
[source, java]
----
public class MyListener extends AbstractConsumerSeekAware {
public class MyListener implements ConsumerSeekAware {
...
@@ -2851,6 +2851,41 @@ public class SeekToLastOnIdleListener extends AbstractConsumerSeekAware {
----
====
Version 2.6 added convenience methods to the abstract class:
* `seekToBeginning()` - seeks all assigned partitions to the beginning
* `seekToEnd()` - seeks all assigned partitions to the end
* `seekToTimestamp(long time)` - seeks all assigned partitions to the offset represented by that timestamp.
Example:
====
[source, java]
----
public class MyListener extends AbstractConsumerSeekAware {
@KafkaListener(...)
void listn(...) {
...
}
}
public class SomeOtherBean {
MyListener listener;
...
void someMethod() {
this.listener.seekToTimestamp(System.currentTimeMillis - 60_000);
}
}
----
====
[[container-factory]]
==== Container factory

View File

@@ -26,3 +26,6 @@ When using manual partition assignment, you can now specify a wildcard for deter
In addition, if the listener implements `ConsumerSeekAware`, `onPartitionsAssigned()` is called after the manual assignment.
(Also added in version 2.5.5).
See <<manual-assignment>> for more information.
Convenience methods have been added to `AbstractConsumerSeekAware` to make seeking easier.
See <<seek>> for more information.