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

io.deephaven.engine.table.impl.util.BaseArrayBackedInputTable Maven / Gradle / Ivy

There is a newer version: 0.37.1
Show newest version
/**
 * Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
 */
package io.deephaven.engine.table.impl.util;

import io.deephaven.base.verify.Assert;
import io.deephaven.engine.rowset.RowSetBuilderSequential;
import io.deephaven.engine.rowset.RowSetFactory;
import io.deephaven.engine.rowset.TrackingRowSet;
import io.deephaven.engine.table.ColumnDefinition;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.table.TableDefinition;
import io.deephaven.engine.table.WritableColumnSource;
import io.deephaven.engine.table.impl.sources.ArrayBackedColumnSource;
import io.deephaven.engine.util.input.InputTableStatusListener;
import io.deephaven.engine.util.input.InputTableUpdater;
import io.deephaven.engine.table.impl.UpdatableTable;
import io.deephaven.engine.table.ColumnSource;
import io.deephaven.util.annotations.TestUseOnly;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;

abstract class BaseArrayBackedInputTable extends UpdatableTable {

    /**
     * Queue of pending changes. Only synchronized access is permitted.
     */
    private final List pendingChanges = new ArrayList<>();
    /** The most recently enqueue change sequence. Only accessed under the monitor lock for {@code pendingChanges}. */
    private long enqueuedSequence = 0L;
    /**
     * The most recently processed change sequence. Only written under both the monitor lock for
     * {@code pendingChanges} and from an update thread. Only read under either the UPG's exclusive lock or the
     * monitor lock on {@code pendingChanges}.
     */
    private long processedSequence = 0L;

    private String description = getDefaultDescription();
    private Runnable onPendingChange = updateGraph::requestRefresh;

    long nextRow = 0;
    private long pendingProcessed = -1L;

    public BaseArrayBackedInputTable(TrackingRowSet rowSet, Map> nameToColumnSource,
            ProcessPendingUpdater processPendingUpdater) {
        super(rowSet, nameToColumnSource, processPendingUpdater);
        InputTableUpdater inputTableUpdater = makeUpdater();
        setAttribute(Table.INPUT_TABLE_ATTRIBUTE, inputTableUpdater);
        setRefreshing(true);
        processPendingUpdater.setThis(this);
    }

    public InputTableUpdater inputTable() {
        return (InputTableUpdater) getAttribute(Table.INPUT_TABLE_ATTRIBUTE);
    }

    public Table readOnlyCopy() {
        return copy(BaseArrayBackedInputTable::applicableForReadOnly);
    }

    private static boolean applicableForReadOnly(String attributeName) {
        return !Table.INPUT_TABLE_ATTRIBUTE.equals(attributeName);
    }

    protected static Map> makeColumnSourceMap(TableDefinition definition) {
        final Map> resultMap = new LinkedHashMap<>();
        for (final ColumnDefinition columnDefinition : definition.getColumns()) {
            resultMap.put(columnDefinition.getName(),
                    ArrayBackedColumnSource.getMemoryColumnSource(0, columnDefinition.getDataType()));
        }
        return resultMap;
    }

    static void processInitial(Table initialTable, BaseArrayBackedInputTable result) {
        final RowSetBuilderSequential builder = RowSetFactory.builderSequential();
        result.processPendingTable(initialTable, new RowSetChangeRecorder() {
            @Override
            public void addRowKey(long key) {
                builder.appendKey(key);
            }

            @Override
            public void removeRowKey(long key) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void modifyRowKey(long key) {
                throw new UnsupportedOperationException();
            }
        });
        result.getRowSet().writableCast().insert(builder.build());
        result.getRowSet().writableCast().initializePreviousValue();
        result.getUpdateGraph().addSource(result);
    }

    public BaseArrayBackedInputTable setDescription(String newDescription) {
        this.description = newDescription;
        return this;
    }

    /**
     * For unit test use only. Specify the function to invoke after enqueuing a pending change.
     *
     * @param onPendingChange The function to invoke after enqueuing a pending change, or null to restore the default
     *        behavior
     */
    @TestUseOnly
    void setOnPendingChange(final Runnable onPendingChange) {
        this.onPendingChange = onPendingChange == null
                ? updateGraph::requestRefresh
                : onPendingChange;
    }

    private void processPending(RowSetChangeRecorder rowSetChangeRecorder) {
        synchronized (pendingChanges) {
            for (PendingChange pendingChange : pendingChanges) {
                if (pendingChange.delete) {
                    processPendingDelete(pendingChange.table, rowSetChangeRecorder);
                } else {
                    processPendingTable(pendingChange.table, rowSetChangeRecorder);
                }
                pendingProcessed = pendingChange.sequence;
            }
            pendingChanges.clear();
        }
    }

    @Override
    public void run() {
        super.run();
        synchronized (pendingChanges) {
            if (pendingProcessed < 0) {
                return;
            }
            processedSequence = pendingProcessed;
            pendingProcessed = -1L;
            pendingChanges.notifyAll();
        }
    }

    protected abstract void processPendingTable(Table table, RowSetChangeRecorder rowSetChangeRecorder);

    protected abstract void processPendingDelete(Table table, RowSetChangeRecorder rowSetChangeRecorder);

    protected abstract String getDefaultDescription();

    protected abstract List getKeyNames();

    protected static class ProcessPendingUpdater implements Updater {
        private BaseArrayBackedInputTable baseArrayBackedInputTable;

        @Override
        public void accept(RowSetChangeRecorder rowSetChangeRecorder) {
            baseArrayBackedInputTable.processPending(rowSetChangeRecorder);
        }

        public void setThis(BaseArrayBackedInputTable keyedArrayBackedMutableTable) {
            this.baseArrayBackedInputTable = keyedArrayBackedMutableTable;
        }
    }

    private final class PendingChange {
        final boolean delete;
        @NotNull
        final Table table;
        final long sequence;
        String error;

        private PendingChange(@NotNull Table table, boolean delete) {
            Assert.holdsLock(pendingChanges, "pendingChanges");
            Assert.neqNull(table, "table");
            this.table = table;
            this.delete = delete;
            this.sequence = ++enqueuedSequence;
        }
    }

    ArrayBackedInputTableUpdater makeUpdater() {
        return new ArrayBackedInputTableUpdater();
    }

    protected class ArrayBackedInputTableUpdater implements InputTableUpdater {
        @Override
        public List getKeyNames() {
            return BaseArrayBackedInputTable.this.getKeyNames();
        }

        @Override
        public TableDefinition getTableDefinition() {
            return BaseArrayBackedInputTable.this.getDefinition();
        }

        @Override
        public void add(@NotNull final Table newData) throws IOException {
            checkBlockingEditSafety();
            PendingChange pendingChange = enqueueAddition(newData);
            blockingContinuation(pendingChange);
        }

        @Override
        public void addAsync(
                @NotNull final Table newData,
                @NotNull final InputTableStatusListener listener) {
            checkAsyncEditSafety(newData);
            final PendingChange pendingChange = enqueueAddition(newData);
            asynchronousContinuation(pendingChange, listener);
        }

        private PendingChange enqueueAddition(@NotNull final Table newData) {
            validateAddOrModify(newData);
            // we want to get a clean copy of the table; that can not change out from under us or result in long reads
            // during our UGP run
            final Table newDataSnapshot = snapshotData(newData);
            final PendingChange pendingChange;
            synchronized (pendingChanges) {
                pendingChange = new PendingChange(newDataSnapshot, false);
                pendingChanges.add(pendingChange);
            }
            onPendingChange.run();
            return pendingChange;
        }

        @Override
        public void delete(@NotNull final Table table) throws IOException {
            checkBlockingEditSafety();
            final PendingChange pendingChange = enqueueDeletion(table);
            blockingContinuation(pendingChange);
        }

        @Override
        public void deleteAsync(
                @NotNull final Table table,
                @NotNull final InputTableStatusListener listener) {
            checkAsyncEditSafety(table);
            final PendingChange pendingChange = enqueueDeletion(table);
            asynchronousContinuation(pendingChange, listener);
        }

        private PendingChange enqueueDeletion(@NotNull final Table table) {
            validateDelete(table);
            final Table oldDataSnapshot = snapshotData(table);
            final PendingChange pendingChange;
            synchronized (pendingChanges) {
                pendingChange = new PendingChange(oldDataSnapshot, true);
                pendingChanges.add(pendingChange);
            }
            onPendingChange.run();
            return pendingChange;
        }

        private Table snapshotData(@NotNull final Table data) {
            Table dataSnapshot;
            if (data.isRefreshing()) {
                dataSnapshot = data.snapshot();
            } else {
                dataSnapshot = data.select();
            }
            return dataSnapshot;
        }

        private void blockingContinuation(@NotNull final PendingChange pendingChange) throws IOException {
            waitForSequence(pendingChange.sequence);
            if (pendingChange.error != null) {
                throw new IOException(pendingChange.error);
            }
        }

        private void asynchronousContinuation(
                @NotNull final PendingChange pendingChange,
                @NotNull final InputTableStatusListener listener) {
            CompletableFuture.runAsync(() -> waitForSequence(pendingChange.sequence)).thenAccept((v) -> {
                if (pendingChange.error == null) {
                    listener.onSuccess();
                } else {
                    listener.onError(new IllegalArgumentException(pendingChange.error));
                }
            }).exceptionally(ex -> {
                listener.onError(ex);
                return null;
            });
        }

        private void checkBlockingEditSafety() {
            if (updateGraph.currentThreadProcessesUpdates()) {
                throw new UnsupportedOperationException("Attempted to make a blocking input table edit from a listener "
                        + "or notification. This is unsupported, because it will block the update graph from making "
                        + "progress.");
            }
        }

        private void checkAsyncEditSafety(@NotNull final Table changeData) {
            if (changeData.isRefreshing()
                    && updateGraph.currentThreadProcessesUpdates()
                    && !changeData.satisfied(updateGraph.clock().currentStep())) {
                throw new UnsupportedOperationException("Attempted to make an asynchronous input table edit from a "
                        + "listener or notification before the change data table is satisfied on the current cycle. "
                        + "This is unsupported, because it may block the update graph from making progress or produce "
                        + "inconsistent results.");
            }
        }

        void waitForSequence(long sequence) {
            if (updateGraph.exclusiveLock().isHeldByCurrentThread()) {
                // We're holding the lock. currentTable had better be refreshing. Wait on its UGP condition
                // in order to allow updates.
                while (processedSequence < sequence) {
                    try {
                        BaseArrayBackedInputTable.this.awaitUpdate();
                    } catch (InterruptedException ignored) {
                    }
                }
            } else {
                // we are not holding the lock, so should wait for the next run
                synchronized (pendingChanges) {
                    while (processedSequence < sequence) {
                        try {
                            pendingChanges.wait();
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }

        @NotNull
        private Map> buildSourcesMap(int capacity,
                List> columnDefinitions) {
            final Map> sources = new LinkedHashMap<>();
            for (final ColumnDefinition columnDefinition : columnDefinitions) {
                WritableColumnSource cs = ArrayBackedColumnSource.getMemoryColumnSource(
                        capacity, columnDefinition.getDataType());
                // noinspection unchecked
                final WritableColumnSource memoryColumnSource = (WritableColumnSource) cs;
                memoryColumnSource.ensureCapacity(capacity);
                sources.put(columnDefinition.getName(), memoryColumnSource);
            }
            return sources;
        }

    }
}