All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.openhft.chronicle.queue.reader.ChronicleReader Maven / Gradle / Ivy

There is a newer version: 5.27ea0
Show newest version
/*
 * Copyright 2014 Higher Frequency Trading
 *
 *       https://chronicle.software
 *
 * 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 net.openhft.chronicle.queue.reader;

import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.queue.ChronicleQueue;
import net.openhft.chronicle.queue.ExcerptTailer;
import net.openhft.chronicle.queue.TailerDirection;
import net.openhft.chronicle.queue.impl.single.BinarySearch;
import net.openhft.chronicle.queue.impl.single.NotComparableException;
import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder;
import net.openhft.chronicle.queue.internal.reader.InternalDummyMethodReaderQueueEntryHandler;
import net.openhft.chronicle.queue.internal.reader.MessageCountingMessageConsumer;
import net.openhft.chronicle.queue.internal.reader.PatternFilterMessageConsumer;
import net.openhft.chronicle.queue.internal.reader.queueentryreaders.CustomPluginQueueEntryReader;
import net.openhft.chronicle.queue.internal.reader.queueentryreaders.MethodReaderQueueEntryReader;
import net.openhft.chronicle.queue.internal.reader.queueentryreaders.VanillaQueueEntryReader;
import net.openhft.chronicle.queue.reader.comparator.BinarySearchComparator;
import net.openhft.chronicle.queue.util.ToolsUtil;
import net.openhft.chronicle.threads.Pauser;
import net.openhft.chronicle.wire.*;
import org.jetbrains.annotations.NotNull;

import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import static net.openhft.chronicle.queue.TailerDirection.BACKWARD;
import static net.openhft.chronicle.queue.TailerDirection.FORWARD;
import static net.openhft.chronicle.queue.impl.StoreFileListener.NO_OP;

public class ChronicleReader implements Reader {
    private static final long UNSET_VALUE = Long.MIN_VALUE;

    private final List inclusionRegex = new ArrayList<>();
    private final List exclusionRegex = new ArrayList<>();
    private final Pauser pauser = Pauser.millis(1, 100);
    private Path basePath;
    private long startIndex = UNSET_VALUE;
    private boolean tailInputSource = false;
    private long maxHistoryRecords = UNSET_VALUE;
    private boolean readOnly = true;
    private ChronicleReaderPlugin customPlugin;
    private Consumer messageSink;
    private Function pollMethod = ExcerptTailer::readingDocument;
    private WireType wireType = WireType.TEXT;
    private Supplier entryHandlerFactory = () -> QueueEntryHandler.messageToText(wireType);
    private boolean displayIndex = true;
    private Class methodReaderInterface;
    private BinarySearchComparator binarySearch;
    private String arg;
    private boolean showMessageHistory;
    private volatile boolean running = true;
    private TailerDirection tailerDirection = TailerDirection.FORWARD;
    private long matchLimit = 0;
    private ContentBasedLimiter contentBasedLimiter;
    private String limiterArg;
    private String tailerId = null;

    static {
        ToolsUtil.warnIfResourceTracing();
    }

    private static boolean isSet(final long configValue) {
        return configValue != UNSET_VALUE;
    }

    public void execute() {
        configureContentBasedLimiter();
        validateArgs();
        long lastObservedTailIndex;
        long highestReachedIndex = 0L;
        boolean isFirstIteration = true;
        boolean retryLastOperation;
        boolean queueHasBeenModified;
        do {
            try (final ChronicleQueue queue = createQueue();
                 final ExcerptTailer tailer = queue.createTailer(tailerId);
                 final ExcerptTailer toEndTailer = queue.createTailer()) {
                MessageHistory.set(new VanillaMessageHistory());

                MessageCountingMessageConsumer messageConsumer = new MessageCountingMessageConsumer(matchLimit, createMessageConsumers());
                QueueEntryReader queueEntryReader = createQueueEntryReader(tailer, messageConsumer);

                do {
                    if (highestReachedIndex != 0L) {
                        tailer.moveToIndex(highestReachedIndex);
                    }
                    try {
                        moveToSpecifiedPosition(queue, tailer, isFirstIteration);
                        lastObservedTailIndex = tailer.index();
                        readWhileNotInterrupted(tailer, messageConsumer, queueEntryReader);
                    } finally {
                        highestReachedIndex = tailer.index();
                        isFirstIteration = false;
                    }
                    queueHasBeenModified = queueHasBeenModifiedSinceLastCheck(lastObservedTailIndex, toEndTailer);
                    retryLastOperation = false;
                    if (!running || messageConsumer.matchLimitReached())
                        return;
                } while (tailerDirection != BACKWARD && (tailInputSource || queueHasBeenModified));
            } catch (final RuntimeException e) {
                retryLastOperation = handleRuntimeException(e);
            } finally {
                MessageHistory.set(null);
            }
        } while (retryLastOperation);

    }

    /**
     * Ignore {@link DateTimeParseException} error - due to a race condition between the reader creating a Queue
     * (with default roll-cycle due to no files on disk) and the writer appending to the Queue with a non-default
     * roll-cycle.
     */
    private static boolean handleRuntimeException(RuntimeException e) {
        if (e.getCause() instanceof DateTimeParseException) {
            return true;
        } else {
            throw e;
        }
    }

    private void readWhileNotInterrupted(ExcerptTailer tailer, MessageCountingMessageConsumer messageConsumer, QueueEntryReader queueEntryReader) {
        while (!Thread.currentThread().isInterrupted()) {
            if (shouldHaltReadingDueToContentBasedLimit(tailer)) {
                running = false;
                break;
            }

            if (!queueEntryReader.read()) {
                if (tailInputSource) {
                    pauser.pause();
                }
                break;
            } else {
                if (messageConsumer.matchLimitReached()) {
                    break;
                }
            }
            pauser.reset();
        }
    }

    private void validateArgs() {
        if (tailerId != null && readOnly)
            throw new IllegalArgumentException("Named tailers only work on writable queues");
    }

    /**
     * Configure the content-based limiter if it was specified
     */
    private void configureContentBasedLimiter() {
        if (contentBasedLimiter != null) {
            contentBasedLimiter.configure(this);
        }
    }

    /**
     * Check if the content-based limit has been reached
     *
     * @param tailer The Tailer we're using to read the queue
     * @return true if we should halt reading, false otherwise
     */
    private boolean shouldHaltReadingDueToContentBasedLimit(ExcerptTailer tailer) {
        if (contentBasedLimiter == null) {
            return false;
        }
        long originalIndex = tailer.index();
        try (final DocumentContext documentContext = tailer.readingDocument()) {
            if (documentContext.isPresent()) {
                return contentBasedLimiter.shouldHaltReading(documentContext);
            }
            return false;
        } finally {
            tailer.moveToIndex(originalIndex);
        }
    }

    private QueueEntryReader createQueueEntryReader(ExcerptTailer tailer, MessageConsumer messageConsumer) {
        if (methodReaderInterface == null) {
            if (customPlugin == null) {
                return new VanillaQueueEntryReader(tailer, pollMethod, entryHandlerFactory.get(), messageConsumer);
            } else {
                return new CustomPluginQueueEntryReader(tailer, pollMethod, customPlugin, messageConsumer);
            }
        } else {
            return new MethodReaderQueueEntryReader(tailer, messageConsumer, wireType, methodReaderInterface, showMessageHistory);
        }
    }

    /**
     * Create the chain of message consumers according to config
     *
     * @return The head of the chain of message consumers
     */
    private MessageConsumer createMessageConsumers() {
        MessageConsumer tail = this::writeToSink;
        if (!exclusionRegex.isEmpty()) {
            tail = new PatternFilterMessageConsumer(exclusionRegex, false, tail);
        }
        if (!inclusionRegex.isEmpty()) {
            tail = new PatternFilterMessageConsumer(inclusionRegex, true, tail);
        }
        return tail;
    }

    private boolean writeToSink(long index, String text) {
        if (displayIndex)
            messageSink.accept("0x" + Long.toHexString(index) + ": ");
        messageSink.accept(text);
        return true;
    }

    public ChronicleReader withReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
        return this;
    }

    public ChronicleReader withMatchLimit(long matchLimit) {
        this.matchLimit = matchLimit;
        return this;
    }

    public ChronicleReader withMessageSink(final @NotNull Consumer messageSink) {
        this.messageSink = messageSink;
        return this;
    }

    public Consumer messageSink() {
        return messageSink;
    }

    public ChronicleReader withBasePath(final @NotNull Path path) {
        this.basePath = path;
        return this;
    }

    public ChronicleReader withInclusionRegex(final @NotNull String regex) {
        this.inclusionRegex.add(Pattern.compile(regex));
        return this;
    }

    public ChronicleReader withExclusionRegex(final @NotNull String regex) {
        this.exclusionRegex.add(Pattern.compile(regex));
        return this;
    }

    public ChronicleReader withCustomPlugin(final @NotNull ChronicleReaderPlugin customPlugin) {
        this.customPlugin = customPlugin;
        return this;
    }

    public ChronicleReader withStartIndex(final long index) {
        this.startIndex = index;
        return this;
    }

    public ChronicleReader tail() {
        this.tailInputSource = true;
        return this;
    }

    public ChronicleReader historyRecords(final long maxHistoryRecords) {
        this.maxHistoryRecords = maxHistoryRecords;
        return this;
    }

    public ChronicleReader asMethodReader(@NotNull String methodReaderInterface) {
        if (methodReaderInterface.isEmpty())
            entryHandlerFactory = () -> new InternalDummyMethodReaderQueueEntryHandler(wireType);
        else try {
            this.methodReaderInterface = Class.forName(methodReaderInterface);
        } catch (ClassNotFoundException e) {
            throw Jvm.rethrow(e);
        }
        return this;
    }

    @Override
    public ChronicleReader showMessageHistory(boolean showMessageHistory) {
        this.showMessageHistory = showMessageHistory;
        return this;
    }

    @Override
    public ChronicleReader withBinarySearch(@NotNull String binarySearchClass) {
        try {
            Class clazz = Class.forName(binarySearchClass);
            this.binarySearch = (BinarySearchComparator) clazz.getDeclaredConstructor().newInstance();
            // allow binary search to configure itself
            this.binarySearch.accept(this);
        } catch (Exception e) {
            throw Jvm.rethrow(e);
        }
        return this;
    }

    @Override
    public ChronicleReader withContentBasedLimiter(ContentBasedLimiter contentBasedLimiter) {
        this.contentBasedLimiter = contentBasedLimiter;
        return this;
    }

    @Override
    public ChronicleReader withArg(String arg) {
        this.arg = arg;
        return this;
    }

    @Override
    public ChronicleReader withLimiterArg(@NotNull String limiterArg) {
        this.limiterArg = limiterArg;
        return this;
    }

    public ChronicleReader withWireType(@NotNull WireType wireType) {
        this.wireType = wireType;
        return this;
    }

    public ChronicleReader inReverseOrder() {
        this.tailerDirection = TailerDirection.BACKWARD;
        return this;
    }

    public ChronicleReader suppressDisplayIndex() {
        this.displayIndex = false;
        return this;
    }

    @Override
    public String arg() {
        return arg;
    }

    @Override
    public String limiterArg() {
        return limiterArg;
    }

    @Override
    public Class methodReaderInterface() {
        return methodReaderInterface;
    }

    // visible for testing
    public ChronicleReader withDocumentPollMethod(final Function pollMethod) {
        this.pollMethod = pollMethod;
        return this;
    }

    public ChronicleReader withTailerId(String tailerId) {
        this.tailerId = tailerId;
        return this;
    }

    private boolean queueHasBeenModifiedSinceLastCheck(final long lastObservedTailIndex, ExcerptTailer tailer) {
        long currentTailIndex = indexOfEnd(tailer);
        return currentTailIndex > lastObservedTailIndex;
    }

    private void moveToSpecifiedPosition(final ChronicleQueue ic, final ExcerptTailer tailer, final boolean isFirstIteration) {
        if (isFirstIteration) {

            // Set the direction, if we're reading backwards, start at the end by default
            tailer.direction(tailerDirection);
            if (tailerDirection == BACKWARD) {
                tailer.toEnd();
            }

            if (isSet(startIndex)) {
                tryMoveToIndex(ic, tailer);
            } else if (binarySearch != null) {
                seekBinarySearch(tailer);
            }

            if (tailerDirection == FORWARD) {
                moveTailerToEnd(tailer);
            }
        }
    }

    private void moveTailerToEnd(ExcerptTailer tailer) {
        if (isSet(maxHistoryRecords)) {
            tailer.toEnd();
            moveToIndexNFromTheEnd(tailer, maxHistoryRecords);
        } else if (tailInputSource) {
            tailer.toEnd();
        }
    }

    private void tryMoveToIndex(ChronicleQueue ic, ExcerptTailer tailer) {
        if (startIndex < ic.firstIndex()) {
            throw new IllegalArgumentException(String.format("startIndex 0x%xd is less than first index 0x%xd",
                    startIndex, ic.firstIndex()));
        }

        if (tailerDirection == BACKWARD && startIndex > ic.lastIndex()) {
            throw new IllegalArgumentException(String.format("startIndex 0x%xd is greater than last index 0x%xd",
                    startIndex, ic.lastIndex()));
        }

        boolean firstTime = true;
        while (!tailer.moveToIndex(startIndex)) {
            if (firstTime) {
                messageSink.accept("Waiting for startIndex " + Long.toHexString(startIndex));
                firstTime = false;
            }
            Jvm.pause(100);
        }
    }

    private void seekBinarySearch(ExcerptTailer tailer) {
        TailerDirection originalDirection = tailer.direction();
        try {
            tailer.direction(FORWARD);
            final Wire key = binarySearch.wireKey();
            long rv = BinarySearch.search(tailer, key, binarySearch);
            if (rv == -1) {
                tailer.toStart();
            } else if (rv < 0) {
                scanToFirstEntryFollowingMatch(tailer, key, -rv);
            } else {
                scanToFirstMatchingEntry(tailer, key, rv);
            }
            tailer.direction(originalDirection);
        } catch (ParseException e) {
            throw Jvm.rethrow(e);
        }
    }

    /**
     * In the event the matched value is repeated, move to the first instance of it, taking into account traversal
     * direction
     *
     * @param tailer        The {@link net.openhft.chronicle.queue.ExcerptTailer} to move
     * @param key           The value we searched for
     * @param matchingIndex The index of a matching entry
     */
    private void scanToFirstMatchingEntry(ExcerptTailer tailer, Wire key, long matchingIndex) {
        long indexToMoveTo = matchingIndex;
        tailer.direction(tailerDirection == FORWARD ? BACKWARD : FORWARD);
        tailer.moveToIndex(indexToMoveTo);
        while (true) {
            try (DocumentContext dc = tailer.readingDocument()) {
                if (!dc.isPresent())
                    break;
                try {
                    if (binarySearch.compare(dc.wire(), key) == 0)
                        indexToMoveTo = dc.index();
                    else
                        break;
                } catch (NotComparableException e) {
                    // continue
                }
            }
        }
        tailer.moveToIndex(indexToMoveTo);
    }

    /**
     * In the event we couldn't find the specified value, move to the first entry that would
     * follow it, taking into account traversal direction
     *
     * @param tailer             The {@link net.openhft.chronicle.queue.ExcerptTailer} to move
     * @param key                The key we searched for
     * @param indexAdjacentMatch The index of an entry which would appear next to the match
     */
    private void scanToFirstEntryFollowingMatch(ExcerptTailer tailer, Wire key, long indexAdjacentMatch) {
        long indexToMoveTo = -1;
        tailer.direction(tailerDirection);
        tailer.moveToIndex(indexAdjacentMatch);
        while (true) {
            try (DocumentContext dc = tailer.readingDocument()) {
                if (!dc.isPresent())
                    break;
                try {
                    if ((tailer.direction() == TailerDirection.FORWARD && binarySearch.compare(dc.wire(), key) >= 0)
                            || (tailer.direction() == BACKWARD && binarySearch.compare(dc.wire(), key) <= 0)) {
                        indexToMoveTo = dc.index();
                        break;
                    }
                } catch (NotComparableException e) {
                    break;
                }
            }
        }
        if (indexToMoveTo >= 0) {
            tailer.moveToIndex(indexToMoveTo);
        }
    }

    private void moveToIndexNFromTheEnd(ExcerptTailer tailer, long numberOfEntriesFromEnd) {
        tailer.direction(TailerDirection.BACKWARD).toEnd();
        for (int i = 0; i < numberOfEntriesFromEnd - 1; i++) {
            try (final DocumentContext documentContext = tailer.readingDocument()) {
                if (!documentContext.isPresent()) {
                    break;
                }
            }
        }
        tailer.direction(FORWARD);
    }

    private long indexOfEnd(ExcerptTailer excerptTailer) {
        return excerptTailer.toEnd().index();
    }

    @NotNull
    private ChronicleQueue createQueue() {
        if (!Files.exists(basePath)) {
            throw new IllegalArgumentException(String.format("Path '%s' does not exist (absolute path '%s')", basePath, basePath.toAbsolutePath()));
        }
        return SingleChronicleQueueBuilder
                .binary(basePath.toFile())
                .readOnly(readOnly)
                .storeFileListener(NO_OP)
                .build();
    }

    public void stop() {
        running = false;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy