com.kolibrifx.plovercrest.server.internal.streams.TableStream Maven / Gradle / Ivy
Show all versions of plovercrest-server Show documentation
/*
* Copyright (c) 2010-2017, KolibriFX AS. Licensed under the Apache License, version 2.0.
*/
package com.kolibrifx.plovercrest.server.internal.streams;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import com.kolibrifx.common.Disposable;
import com.kolibrifx.plovercrest.client.TableMetadata;
import com.kolibrifx.plovercrest.client.TableSerializer;
import com.kolibrifx.plovercrest.server.PlovercrestEngine;
import com.kolibrifx.plovercrest.server.ReadObserver;
import com.kolibrifx.plovercrest.server.Table;
import com.kolibrifx.plovercrest.server.TableReader;
import com.kolibrifx.plovercrest.server.internal.EventType;
import com.kolibrifx.plovercrest.server.internal.TableEvent;
import com.kolibrifx.plovercrest.server.internal.TableListener;
import com.kolibrifx.plovercrest.server.streams.ReaderPosition;
import com.kolibrifx.plovercrest.server.streams.SeekableReader;
import com.kolibrifx.plovercrest.server.streams.SeekableStream;
import com.kolibrifx.plovercrest.server.streams.StreamDataTriggerer;
import com.kolibrifx.plovercrest.server.streams.StreamObserver;
import com.kolibrifx.plovercrest.server.streams.StreamWriter;
public class TableStream implements SeekableStream {
private final Table table;
private final TableSerializer serializer;
private final PlovercrestEngine engine;
private final TableStreamWriter writer;
/**
* Small wrapper for {@link StreamDataTriggerer} to make sure it is never triggered more than
* once.
*/
private static class DataTriggererWrapper {
private final StreamDataTriggerer triggerer;
private final AtomicBoolean done = new AtomicBoolean();
DataTriggererWrapper(final StreamDataTriggerer triggerer) {
this.triggerer = triggerer;
}
void wakeUpNow() {
if (done.compareAndSet(false, true)) {
triggerer.wakeUpNow();
}
}
}
private static class OneShotWakeUp implements Disposable {
private final Disposable disposable;
private volatile boolean closed = false;
OneShotWakeUp(final DataTriggererWrapper triggerer, final String tableName, final PlovercrestEngine engine) {
disposable = engine.addTableListener(tableName, new TableListener() {
@Override
public boolean shouldHandleTableEvent(final EventType type) {
switch (type) {
case ENTRY_WRITTEN:
case LAST_VALID_TIMESTAMP_UPDATED:
case TABLE_FROZEN:
return true;
default:
return false;
}
}
@Override
public void handleTableEvent(final TableEvent event) {
if (closed) {
return;
}
triggerer.wakeUpNow();
close();
}
});
}
@Override
public void close() {
closed = true;
if (disposable != null) {
disposable.close();
}
}
}
private class TableStreamReadObserver extends ReadObserver {
private boolean observedFrozen = false;
private final StreamObserver observer;
private final TableReader tableReader;
TableStreamReadObserver(final StreamObserver observer, final TableReader tableReader) {
this.observer = observer;
this.tableReader = tableReader;
}
void triggerEndEvents() {
observer.onObserveEnd(getLastValidTimestamp(), tableReader.getEntryIndex());
if (observedFrozen) {
observer.onObserveFrozen();
observedFrozen = false; // in case of unfreeze
}
}
@Override
public T observe(final long timestamp, final ByteBuffer bytes) {
final T element = serializer.unserialize(timestamp, bytes);
observer.onObserve(timestamp, tableReader.getEntryIndex(), element);
return element;
}
@Override
public void observeEnd(final long lastValidTimestamp) {
super.observeEnd(lastValidTimestamp);
// We cannot trigger onObserveEnd() here due to race conditions: it is done through triggerEndEvents() instead
}
@Override
public void observeFrozen() {
observedFrozen = true;
// observer.onObserveFrozen() is done through triggerEndEvents()
}
/**
* Has anything occured in the table that has not yet been observed? (new entries, last
* valid timestamp, frozen state)
*
* Note: here be race conditions; a true
return value can always be trusted,
* but a false
value can change at any time.
*/
boolean isObserverDataAvailable() {
if (table.getEntryCount() > tableReader.getEntryIndex()) {
return true;
}
if (table.getLastValidTimestamp() > getLastTimestamp()) {
return true;
}
if (table.getWriter().isFrozen() && !observedFrozen) {
return true;
}
return false;
}
}
public TableStream(final PlovercrestEngine engine, final Table table, final TableSerializer serializer) {
this.engine = engine;
this.table = table;
this.serializer = serializer;
this.writer = new TableStreamWriter(table.getWriter());
}
private SeekableReader createReaderImpl(final long timestampOrIndex, final boolean useIndex,
final StreamObserver observer) {
final TableReader tableReader = table.getReader();
final TableStreamReadObserver readObserver = new TableStreamReadObserver(observer, tableReader);
if (useIndex) {
tableReader.seekToEntryIndex(timestampOrIndex);
} else {
tableReader.seekTakePrevious(timestampOrIndex);
}
return new SeekableReader() {
private Disposable wakeUpListener;
@Override
public boolean poll() {
final T element = tableReader.read(readObserver);
if (element == null) {
readObserver.triggerEndEvents();
}
return element != null;
}
@Override
public void registerDataTriggerer(final StreamDataTriggerer triggerer) {
final DataTriggererWrapper triggererWrapper = new DataTriggererWrapper(triggerer);
if (readObserver.isObserverDataAvailable()) {
// more data is available now
triggererWrapper.wakeUpNow();
} else {
if (wakeUpListener != null) {
wakeUpListener.close();
}
wakeUpListener = new OneShotWakeUp(triggererWrapper, table.getName(), engine);
if (readObserver.isObserverDataAvailable()) {
// Corner case: new data became available while the listener was initialized.
// In this case the listener could have registered too late to get the write event,
// so we have to invoke the data triggerer now.
wakeUpListener.close();
wakeUpListener = null;
triggererWrapper.wakeUpNow();
}
}
}
@Override
public ReaderPosition getReaderPosition() {
return ReaderPosition.createIndexPosition(tableReader.getEntryIndex());
}
@Override
public long seekTakePrevious(final long timestamp) {
return tableReader.seekTakePrevious(timestamp);
}
};
}
@Override
public SeekableReader createReaderFromTimestamp(final long timestamp, final StreamObserver observer) {
return createReaderImpl(timestamp, false, observer);
}
@Override
public SeekableReader createReaderFromIndex(final long index, final StreamObserver observer) {
return createReaderImpl(index, true, observer);
}
@Override
public long getFirstTimestamp() {
return table.getFirstTimestamp();
}
@Override
public long getLastTimestamp() {
return table.getLastTimestamp();
}
@Override
public long getDataLengthInBytes() {
return table.getDataLength();
}
@Override
public long getEntryCount() {
return table.getEntryCount();
}
@Override
public TableMetadata getMetadata() {
return table.getInfo().getMetadata();
}
@Override
public long getLastValidTimestamp() {
return table.getLastValidTimestamp();
}
@Override
public boolean isFrozen() {
return table.getWriter().isFrozen();
}
@Override
public StreamWriter getWriter() {
return writer;
}
@Override
public boolean delete() {
return engine.delete(table.getName());
}
@Override
public boolean rename(final String newName) {
return engine.rename(table.getName(), newName) != null;
}
@Override
public String getName() {
return table.getName();
}
@Override
public Class getElementClass() {
return serializer.elementClass();
}
@Override
public long seekPreviousTimestamp(final long targetTimestamp) {
final TableReader reader = table.getReader();
return reader.seekTakePrevious(targetTimestamp);
}
}