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

com.kolibrifx.plovercrest.server.internal.folds.FoldReader Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010-2017, KolibriFX AS. Licensed under the Apache License, version 2.0.
 */

package com.kolibrifx.plovercrest.server.internal.folds;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.tuple.ImmutablePair;
import com.kolibrifx.plovercrest.server.streams.PollableReader;
import com.kolibrifx.plovercrest.server.streams.ReaderPosition;
import com.kolibrifx.plovercrest.server.streams.Stream;
import com.kolibrifx.plovercrest.server.streams.StreamDataTriggerer;
import com.kolibrifx.plovercrest.server.streams.StreamObserver;
import com.kolibrifx.plovercrest.server.streams.folds.CombinatorStrategy;

/**
 * A generic {@link PollableReader} implementation for folds.
 *
 * @param 
 *            the output stream type
 * @param 
 *            the common input stream type (or {@link Object} if there is no common type)
 */
public class FoldReader implements PollableReader {
    private final List> peekableStreams = new ArrayList<>();
    private final CombinatorStrategy combinatorStrategy;
    private final FoldCallback foldCallback;
    private final StreamObserver outputObserver;
    private int outputIndex = 0;
    private long lastTimestamp;
    private int lastTimestampCount;
    private final Queue> outputQueue;

    public FoldReader(final long readerTimestamp,
                      final CombinatorStrategy combinatorStrategy,
                      final StreamObserver outputObserver,
                      final FoldReaderInputs inputSpec,
                      final FoldCallback foldCallback,
                      final Queue> outputQueue) {
        this.lastTimestamp = readerTimestamp;
        this.lastTimestampCount = 0;
        this.combinatorStrategy = combinatorStrategy;
        this.outputObserver = outputObserver;
        this.foldCallback = foldCallback;
        this.outputQueue = outputQueue;
        for (final ImmutablePair, Long> input : inputSpec.getInputs()) {
            peekableStreams.add(PeekableInputStreamUtils.create(input.left, input.right));
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean poll() {
        while (outputQueue.isEmpty()) {
            if (!combinatorStrategy.tryConsumeNext(peekableStreams, foldCallback)) {
                // Reaching the end can trigger output for some folds (due to take-previous behavior),
                // so do not assume that the output queue is empty here.
                break;
            }
        }
        if (outputQueue.isEmpty()) {
            outputObserver.onObserveEnd(lastTimestamp, outputIndex);
            return false;
        }
        assert !outputQueue.isEmpty();
        Timestamped latestOutput = outputQueue.poll();
        assert latestOutput != null;
        final long nextTimestamp = latestOutput.getTimestamp();
        if (nextTimestamp == lastTimestamp) {
            lastTimestampCount++;
        } else {
            lastTimestamp = nextTimestamp;
            lastTimestampCount = 1;
        }
        T value = latestOutput.getValue();
        // For binary data, peekable readers store byte arrays, but observers want ByteBuffer.
        // FIXME: this hack suggests that the generics setup for PeekableInputStream is flawed,
        // should find a better way to do this conversion.
        if (value instanceof byte[]) {
            value = (T) ByteBuffer.wrap((byte[]) value);
        }
        outputObserver.onObserve(lastTimestamp, outputIndex++, value);
        latestOutput = null;
        return true;
    }

    @Override
    public void registerDataTriggerer(final StreamDataTriggerer triggerer) {
        final StreamDataTriggerer childTriggerer = new StreamDataTriggerer() {
            AtomicBoolean done = new AtomicBoolean(false);

            @Override
            public void wakeUpNow() {
                if (!done.compareAndSet(false, true)) {
                    return;
                }
                triggerer.wakeUpNow();
            }

            @Override
            public void wakeUpAtTime(final long clockMillis) {
                // there's no easy "done" logic for wakeUpAtTime, so this could be triggered multiple times
                triggerer.wakeUpAtTime(clockMillis);
            }
        };
        int activeInputTriggerers = 0;
        for (final PeekableInputStream input : peekableStreams) {
            if (input.registerDataTriggererIfReachedEnd(childTriggerer)) {
                activeInputTriggerers++;
            }
        }
        if (activeInputTriggerers == 0) {
            // no inputs were at the end: this implies that data is available right now
            childTriggerer.wakeUpNow();
        }
    }

    @Override
    public ReaderPosition getReaderPosition() {
        return ReaderPosition.createTimestampPosition(lastTimestamp, lastTimestampCount);
    }
}