io.fluxcapacitor.javaclient.tracking.client.DefaultTracker Maven / Gradle / Ivy
Show all versions of java-client Show documentation
/*
* Copyright (c) 2016-2017 Flux Capacitor.
*
* 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.Registration;
import io.fluxcapacitor.common.api.SerializedMessage;
import io.fluxcapacitor.common.api.tracking.MessageBatch;
import io.fluxcapacitor.javaclient.tracking.Tracker;
import io.fluxcapacitor.javaclient.tracking.TrackingConfiguration;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static io.fluxcapacitor.common.TimingUtils.retryOnFailure;
import static io.fluxcapacitor.javaclient.tracking.BatchInterceptor.join;
import static java.lang.Thread.currentThread;
/**
* A tracker keeps reading messages until it is stopped (generally only when the application is shut down).
*
* A tracker is always running in a single thread. To balance the processing load over multiple threads create multiple
* trackers with the same name but different tracker id.
*
* Trackers with different names will receive the same messages. Trackers with the same name will not. (Flux Capacitor
* will load balance between trackers with the same name).
*
* Tracking stops if the provided message consumer throws an exception while handling messages (i.e. the tracker will
* need to be manually restarted in that case). However, if the tracker encounters an exception while fetching messages
* it will retry fetching indefinitely until this succeeds.
*
* Trackers can choose a desired maximum batch size for consuming. By default this batch size will be the same as the
* batch size the tracker uses to fetch messages from Flux Capacitor. Each time the consumer has finished consuming a
* batch the tracker will update its position with Flux Capacitor.
*
* Trackers can be configured to use batch interceptors. A batch interceptor manages the invocation of the message
* consumer. It is therefore typically used to manage a database transaction around the invocation of the consumer. Note
* that if the interceptor gives rise to an exception the tracker will be stopped.
*/
@Slf4j
public class DefaultTracker implements Runnable, Registration {
private final String name;
private final String trackerId;
private final TrackingConfiguration configuration;
private final Consumer processor;
private final Consumer> consumer;
private final TrackingClient trackingClient;
private final AtomicBoolean running = new AtomicBoolean();
private final AtomicReference thread = new AtomicReference<>();
private volatile boolean processing;
public DefaultTracker(String name, String trackerId, TrackingConfiguration configuration,
Consumer> consumer, TrackingClient trackingClient) {
this.name = name;
this.trackerId = trackerId;
this.configuration = configuration;
this.processor =
join(configuration.getBatchInterceptors()).intercept(this::processAll, new Tracker(name, trackerId));
this.consumer = consumer;
this.trackingClient = trackingClient;
}
@Override
public void run() {
if (running.compareAndSet(false, true)) {
thread.set(currentThread());
MessageBatch batch = fetch(null);
Long lastKnownIndex = batch.getLastIndex();
while (running.get()) {
processor.accept(batch);
batch = fetch(lastKnownIndex);
if (batch.getLastIndex() != null) {
lastKnownIndex = batch.getLastIndex();
}
}
}
}
@Override
public void cancel() {
if (running.compareAndSet(true, false)) {
//wait for processing to complete
if (processing) {
while (processing) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
currentThread().interrupt();
return;
}
}
} else {
//interrupt message fetching
try {
thread.get().interrupt();
} catch (Exception e) {
log.warn("Not allowed to cancel tracker {}", name, e);
} finally {
thread.set(null);
}
}
}
}
protected MessageBatch fetch(Long lastIndex) {
return retryOnFailure(() -> trackingClient.readAndWait(name, trackerId, lastIndex, configuration),
configuration.getRetryDelay(), e -> running.get());
}
protected void processAll(MessageBatch messageBatch) {
try {
processing = true;
List messages = messageBatch.getMessages();
if (messages.isEmpty() || !running.get()) {
return;
}
if (messages.size() > configuration.getMaxConsumerBatchSize()) {
for (int i = 0; i < messages.size(); i += configuration.getMaxConsumerBatchSize()) {
List batch =
messages.subList(i, Math.min(i + configuration.getMaxConsumerBatchSize(), messages.size()));
processPart(batch, messageBatch.getSegment());
}
} else {
processPart(messages, messageBatch.getSegment());
}
} finally {
processing = false;
}
}
protected void processPart(List batch, int[] segment) {
try {
consumer.accept(batch);
} catch (Exception e) {
log.error("Consumer {} failed to handle batch of {} messages and did not handle exception. "
+ "Tracker will be stopped.", name, batch.size(), e);
cancel();
throw e;
}
retryOnFailure(() -> updatePosition(segment, batch.get(batch.size() - 1).getIndex()),
configuration.getRetryDelay(), e -> running.get());
}
@SneakyThrows
private void updatePosition(int[] segment, Long lastIndex) {
trackingClient.storePosition(name, segment, lastIndex).await();
}
}