
io.fluxcapacitor.javaclient.tracking.client.CachingTrackingClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-client Show documentation
Show all versions of java-client Show documentation
Default Java client library for interfacing with Flux Capacitor.
/*
* Copyright (c) Flux Capacitor IP B.V. or its affiliates. All Rights Reserved.
*
* 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
* http://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 io.fluxcapacitor.javaclient.tracking.client;
import io.fluxcapacitor.common.Guarantee;
import io.fluxcapacitor.common.MessageType;
import io.fluxcapacitor.common.Registration;
import io.fluxcapacitor.common.api.SerializedMessage;
import io.fluxcapacitor.common.api.tracking.ClaimSegmentResult;
import io.fluxcapacitor.common.api.tracking.MessageBatch;
import io.fluxcapacitor.common.api.tracking.Position;
import io.fluxcapacitor.javaclient.FluxCapacitor;
import io.fluxcapacitor.javaclient.tracking.ConsumerConfiguration;
import io.fluxcapacitor.javaclient.tracking.IndexUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Predicate;
import static io.fluxcapacitor.common.ConsistentHashing.computeSegment;
import static java.time.Instant.now;
import static java.util.Optional.ofNullable;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
@RequiredArgsConstructor
@Slf4j
public class CachingTrackingClient implements TrackingClient {
@Getter
private final WebsocketTrackingClient delegate;
private final int maxCacheSize;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
private final AtomicBoolean started = new AtomicBoolean();
private volatile Registration registration;
private final ConcurrentSkipListMap cache = new ConcurrentSkipListMap<>();
private final Map waitingTrackers = new ConcurrentHashMap<>();
public CachingTrackingClient(WebsocketTrackingClient delegate) {
this(delegate, 1024);
}
@Override
public CompletableFuture read(String consumer, String trackerId, Long lastIndex,
ConsumerConfiguration config) {
if (started.compareAndSet(false, true)) {
ConsumerConfiguration cacheFillerConfig = ConsumerConfiguration.builder()
.ignoreSegment(true)
.clientControlledIndex(true)
.minIndex(IndexUtils.indexForCurrentTime())
.name(CachingTrackingClient.class.getSimpleName()).build();
registration = FluxCapacitor.getOptionally()
.map(fc -> DefaultTracker.start(this::cacheNewMessages, delegate.getMessageType(),
cacheFillerConfig, fc))
.orElseGet(() -> DefaultTracker.start(this::cacheNewMessages, cacheFillerConfig, delegate));
}
if (lastIndex != null && cache.containsKey(lastIndex)) {
Instant deadline = now().plus(config.getMaxWaitDuration());
return delegate.claimSegment(consumer, trackerId, lastIndex, config).thenCompose(r -> {
Long minIndex = r.getPosition().lowestIndexForSegment(r.getSegment()).orElse(null);
if (minIndex != null) {
CompletableFuture result = new CompletableFuture<>();
MessageBatch messageBatch = getMessageBatch(config, minIndex, r);
if (!messageBatch.isEmpty()) {
result.complete(messageBatch);
} else {
waitForMessages(consumer, trackerId,
ofNullable(messageBatch.getLastIndex()).orElse(minIndex),
config, r, deadline, result);
}
return result;
}
return delegate.read(consumer, trackerId, lastIndex, config);
});
}
return delegate.read(consumer, trackerId, lastIndex, config);
}
private void waitForMessages(String consumer, String trackerId, long minIndex,
ConsumerConfiguration config,
ClaimSegmentResult claimResult, Instant deadline,
CompletableFuture future) {
AtomicLong atomicIndex = new AtomicLong(minIndex);
long timeout = Duration.between(now(), deadline).toMillis();
if (timeout <= 0) {
future.complete(new MessageBatch(claimResult.getSegment(), List.of(), atomicIndex.get(), claimResult.getPosition()));
} else {
ScheduledFuture> timeoutSchedule = scheduler.schedule(() -> {
try {
if (future.complete(
new MessageBatch(claimResult.getSegment(), List.of(), atomicIndex.get(), claimResult.getPosition()))) {
waitingTrackers.remove(trackerId);
}
} finally {
if (atomicIndex.get() > minIndex) {
try {
storePosition(consumer, claimResult.getSegment(), atomicIndex.get()).get();
} catch (Exception e) {
log.error("Failed to update position of {}", consumer, e);
}
}
}
}, timeout, MILLISECONDS);
Runnable fetchTask = new Runnable() {
@Override
public void run() {
MessageBatch batch = getMessageBatch(config, atomicIndex.get(), claimResult);
if (!batch.isEmpty() && future.complete(batch) && waitingTrackers.remove(trackerId, this)) {
timeoutSchedule.cancel(false);
} else {
atomicIndex.updateAndGet(c -> Optional.ofNullable(batch.getLastIndex()).orElse(c));
}
}
};
waitingTrackers.put(trackerId, fetchTask);
}
}
protected MessageBatch getMessageBatch(ConsumerConfiguration config, long minIndex, ClaimSegmentResult claim) {
List unfiltered = cache.tailMap(minIndex, false).values().stream().limit(
config.getMaxFetchSize()).collect(toList());
Long lastIndex = unfiltered.isEmpty() ? null : unfiltered.get(unfiltered.size() - 1).getIndex();
return new MessageBatch(claim.getSegment(), filterMessages(
unfiltered, claim.getSegment(), claim.getPosition(), config), lastIndex, claim.getPosition());
}
protected List filterMessages(List messages, int[] segmentRange,
Position position, ConsumerConfiguration config) {
if (messages.isEmpty()) {
return messages;
}
Predicate predicate
= m -> (config.getTypeFilter() == null || m.getData().getType() == null
|| config.getTypeFilter().matches(m.getData().getType())) && position.isNewMessage(m);
if (!config.ignoreSegment()) {
predicate = predicate.and(m -> segmentRange[1] != 0 && m.getSegment() >= segmentRange[0]
&& m.getSegment() < segmentRange[1]);
}
return messages.stream().filter(predicate).collect(toList());
}
protected void cacheNewMessages(List messages) {
if (!messages.isEmpty()) {
Map messageMap = messages.stream().peek(m -> m.setSegment(
m.getSegment() == null ? computeSegment(m.getMessageId(), Position.MAX_SEGMENT) :
m.getSegment() % Position.MAX_SEGMENT))
.collect(toMap(SerializedMessage::getIndex, Function.identity()));
cache.putAll(messageMap);
waitingTrackers.values().forEach(Runnable::run);
removeOldMessages();
}
}
protected synchronized void removeOldMessages() {
int removeCount = cache.size() - maxCacheSize;
for (int i = 0; i < removeCount; i++) {
cache.pollFirstEntry();
}
}
@Override
public List readFromIndex(long minIndex, int maxSize) {
return delegate.readFromIndex(minIndex, maxSize);
}
@Override
public CompletableFuture storePosition(String consumer, int[] segment, long lastIndex, Guarantee guarantee) {
return delegate.storePosition(consumer, segment, lastIndex, guarantee);
}
@Override
public CompletableFuture resetPosition(String consumer, long lastIndex, Guarantee guarantee) {
return delegate.resetPosition(consumer, lastIndex, guarantee);
}
@Override
public Position getPosition(String consumer) {
return delegate.getPosition(consumer);
}
@Override
public CompletableFuture disconnectTracker(String consumer, String trackerId, boolean sendFinalEmptyBatch, Guarantee guarantee) {
return delegate.disconnectTracker(consumer, trackerId, sendFinalEmptyBatch, guarantee);
}
@Override
public MessageType getMessageType() {
return delegate.getMessageType();
}
@Override
public void close() {
ofNullable(registration).ifPresent(Registration::cancel);
scheduler.shutdown();
delegate.close();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy