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

io.deephaven.engine.table.impl.remote.ConstructSnapshot 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.remote;

import io.deephaven.base.formatters.FormatBitSet;
import io.deephaven.base.log.LogOutput;
import io.deephaven.base.log.LogOutputAppendable;
import io.deephaven.base.verify.Assert;
import io.deephaven.configuration.Configuration;
import io.deephaven.datastructures.util.CollectionUtil;
import io.deephaven.engine.context.ExecutionContext;
import io.deephaven.engine.exceptions.SnapshotUnsuccessfulException;
import io.deephaven.engine.updategraph.*;
import io.deephaven.engine.rowset.*;
import io.deephaven.engine.table.SharedContext;
import io.deephaven.engine.table.impl.sources.ReinterpretUtils;
import io.deephaven.engine.table.impl.util.*;
import io.deephaven.engine.updategraph.NotificationQueue.Dependency;
import io.deephaven.engine.updategraph.impl.PeriodicUpdateGraph;
import io.deephaven.io.log.LogEntry;
import io.deephaven.engine.table.ColumnDefinition;
import io.deephaven.engine.exceptions.CancellationException;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.table.TableDefinition;
import io.deephaven.util.datastructures.LongSizedDataStructure;
import io.deephaven.engine.liveness.LivenessManager;
import io.deephaven.engine.liveness.LivenessScope;
import io.deephaven.engine.liveness.LivenessScopeStack;
import io.deephaven.engine.table.impl.BaseTable;
import io.deephaven.engine.table.impl.NotificationStepSource;
import io.deephaven.engine.table.ColumnSource;
import io.deephaven.chunk.*;
import io.deephaven.util.SafeCloseable;
import io.deephaven.UncheckedDeephavenException;
import io.deephaven.internal.log.LoggerFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Array;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Stream;

import io.deephaven.chunk.attributes.Values;

/**
 * A Set of static utilities for computing values from a table while avoiding the use of an update graph lock. This
 * class supports snapshots in both position space and key space.
 */
public class ConstructSnapshot {

    public static class NoSnapshotAllowedException extends UnsupportedOperationException {
        public NoSnapshotAllowedException() {
            super();
        }

        public NoSnapshotAllowedException(String reason) {
            super(reason);
        }
    }

    private static final io.deephaven.io.logger.Logger log = LoggerFactory.getLogger(ConstructSnapshot.class);

    /**
     * The maximum number of allowed attempts to construct a snapshot concurrently with {@link PeriodicUpdateGraph} run
     * processing. After this many attempts, we fall back and wait until we can block refreshes.
     */
    private static final int MAX_CONCURRENT_ATTEMPTS =
            Configuration.getInstance().getIntegerWithDefault("ConstructSnapshot.maxConcurrentAttempts", 2);

    /**
     * The maximum duration of an attempt to construct a snapshot concurrently with {@link PeriodicUpdateGraph} run
     * processing. If an unsuccessful attempt takes longer than this timeout, we will fall back and wait until we can
     * block refreshes.
     */
    private static final int MAX_CONCURRENT_ATTEMPT_DURATION_MILLIS = Configuration.getInstance()
            .getIntegerWithDefault("ConstructSnapshot.maxConcurrentAttemptDurationMillis", 5000);

    // TODO (deephaven-core#188): use ChunkPoolConstants.LARGEST_POOL_CHUNK_CAPACITY when JS API allows multiple batches
    // default enables more than 100MB of 8-byte values in a single record batch
    public static final int SNAPSHOT_CHUNK_SIZE = Configuration.getInstance()
            .getIntegerWithDefault("ConstructSnapshot.snapshotChunkSize", 1 << 24);


    public interface State {

        /**
         * Test that determines whether the currently-active concurrent snapshot attempt has become inconsistent. Always
         * returns {@code false} if there is no snapshot attempt active, or if there is a locked attempt active
         * (necessarily at lower depth than the lowest concurrent attempt).
         *
         * @return Whether the clock or sources have changed in such a way as to make the currently-active concurrent
         *         snapshot attempt inconsistent
         */
        boolean concurrentAttemptInconsistent();

        /**
         * Check that fails if the currently-active concurrent snapshot attempt has become inconsistent. source. This is
         * a no-op if there is no snapshot attempt active, or if there is a locked attempt active (necessarily at lower
         * depth than the lowest concurrent attempt).
         *
         * @throws SnapshotInconsistentException If the currently-active concurrent snapshot attempt has become
         *         inconsistent
         */
        void failIfConcurrentAttemptInconsistent();

        /**
         * Wait for a dependency to become satisfied on the current cycle if we're trying to use current values for the
         * currently-active concurrent snapshot attempt. This is a no-op if there is no snapshot attempt active, or if
         * there is a locked attempt active (necessarily at lower depth than the lowest concurrent attempt).
         *
         * @param dependency The dependency, which may be null in order to avoid redundant checks in calling code
         * @throws SnapshotInconsistentException If we cannot wait for this dependency on the current step because the
         *         step changed
         */
        void maybeWaitForSatisfaction(@Nullable NotificationQueue.Dependency dependency);

        /**
         * Return the currently-active concurrent snapshot attempt's "before" clock value, or zero if there is no
         * concurrent attempt active.
         *
         * @return The concurrent snapshot attempt's "before" clock value, or zero
         */
        long getConcurrentAttemptClockValue();

        /**
         * Append clock info that pertains to the concurrent attempt state to {@code logOutput}.
         *
         * @param logOutput The {@link LogOutput}
         * @return {@code logOutput}
         */
        LogOutput appendConcurrentAttemptClockInfo(@NotNull LogOutput logOutput);
    }

    /**
     * Holder for thread-local state.
     */
    private static class StateImpl implements State {

        /**
         * ThreadLocal to hold an instance per thread.
         */
        private static final ThreadLocal threadState = ThreadLocal.withInitial(StateImpl::new);

        /**
         * Get this thread's State instance.
         *
         * @return This thread's State instance
         */
        private static StateImpl get() {
            return threadState.get();
        }

        /**
         * Holder for per-snapshot attempt parameters.
         */
        private static class ConcurrentAttemptParameters {

            /**
             * The {@link SnapshotControl} controlling this snapshot attempt.
             */
            private final SnapshotControl control;

            /**
             * The clock value before this snapshot was attempted.
             */
            private final long beforeClockValue;

            /**
             * Whether this snapshot attempt is using previous values.
             */
            private final boolean usingPreviousValues;

            private ConcurrentAttemptParameters(
                    @NotNull final SnapshotControl control,
                    final long beforeClockValue,
                    final boolean usingPreviousValues) {
                this.control = control;
                this.beforeClockValue = beforeClockValue;
                this.usingPreviousValues = usingPreviousValues;
            }
        }

        /**
         * The {@link UpdateGraph} we're interacting with in the current attempt (concurrent or locked). {@code null} if
         * there are no snapshots in progress on this thread.
         */
        private UpdateGraph updateGraph;

        /**
         * {@link ConcurrentAttemptParameters Per-snapshot attempt parameters} for the lowest-depth concurrent snapshot
         * on this thread. {@code null} if there are no concurrent snapshots in progress at any depth on this thread.
         */
        private ConcurrentAttemptParameters activeConcurrentAttempt;

        /**
         * The depth of nested concurrent snapshots. Used to avoid releasing the update graph lock if it's acquired by a
         * nested snapshot. Zero if there are no concurrent snapshots in progress on this thread.
         */
        private int concurrentSnapshotDepth;

        /**
         * The depth of nested locked snapshots. Used to treat nested locked snapshots as non-concurrent for purposes of
         * consistency checks and to avoid releasing the update graph lock if it's acquired by a nested snapshot. Zero
         * if there are no concurrent snapshots in progress on this thread.
         */
        private int lockedSnapshotDepth;

        /**
         * Whether this thread currently has a permit on the shared update graph lock.
         */
        private boolean acquiredLock;

        /**
         * The last {@link LogicalClock} value that this state has observed.
         */
        private long lastObservedClockValue;

        /**
         * Called before starting a concurrent snapshot in order to increase depth and record per-snapshot attempt
         * parameters.
         *
         * @param control The {@link SnapshotControl control} to record
         * @param beforeClockValue The "before" clock value to record
         * @param usingPreviousValues Whether this attempt will use previous values
         * @return An opaque object describing the enclosing attempt, to be supplied to
         *         {@link #endConcurrentSnapshot(Object)}
         */
        private Object startConcurrentSnapshot(
                @NotNull final SnapshotControl control,
                final long beforeClockValue,
                final boolean usingPreviousValues) {
            checkAndRecordUpdateGraph(control.getUpdateGraph());
            Assert.assertion(!locked(updateGraph) && !acquiredLock, "!locked() && !acquiredLock");
            final Object enclosingAttemptState = activeConcurrentAttempt;
            activeConcurrentAttempt = new ConcurrentAttemptParameters(control, beforeClockValue, usingPreviousValues);
            ++concurrentSnapshotDepth;
            lastObservedClockValue = beforeClockValue;
            return enclosingAttemptState;
        }

        /**
         * Called after finishing a concurrent snapshot attempt in order to record the decrease in depth and restore the
         * enclosing attempt's parameters.
         *
         * @param enclosingAttemptParameters The opaque state object returned from
         *        {@link #startConcurrentSnapshot(SnapshotControl, long, boolean)}
         */
        private void endConcurrentSnapshot(final Object enclosingAttemptParameters) {
            --concurrentSnapshotDepth;
            this.activeConcurrentAttempt = (ConcurrentAttemptParameters) enclosingAttemptParameters;
            maybeClearUpdateGraph();
        }

        /**
         * Called before starting a locked snapshot in order to increase depth and acquire the update graph lock if
         * needed.
         *
         * @param control The {@link SnapshotControl control} to record
         */
        private void startLockedSnapshot(@NotNull final SnapshotControl control) {
            checkAndRecordUpdateGraph(control.getUpdateGraph());
            ++lockedSnapshotDepth;
            maybeAcquireLock();
        }

        /**
         * Called after finishing a concurrent snapshot in order to decrease depth and release the update graph lock if
         * needed.
         */
        private void endLockedSnapshot() {
            --lockedSnapshotDepth;
            maybeReleaseLock();
            maybeClearUpdateGraph();
        }

        private void checkAndRecordUpdateGraph(@NotNull final UpdateGraph updateGraph) {
            if (this.updateGraph == null) {
                this.updateGraph = updateGraph;
                return;
            }
            if (this.updateGraph == updateGraph) {
                return;
            }
            throw new UnsupportedOperationException(String.format(
                    "Cannot nest snapshots that use different update graphs: currently using %s, attempting to use %s",
                    this.updateGraph, updateGraph));
        }

        private void maybeClearUpdateGraph() {
            if (concurrentSnapshotDepth == 0 && lockedSnapshotDepth == 0) {
                this.updateGraph = null;
            }
        }

        /**
         * Is the current lowest-depth snapshot attempt concurrent?
         *
         * @return Whether there is a concurrent snapshot attempt active at the lowest depth
         */
        private boolean concurrentAttemptActive() {
            return concurrentSnapshotDepth > 0 && lockedSnapshotDepth == 0;
        }

        @Override
        public boolean concurrentAttemptInconsistent() {
            if (!concurrentAttemptActive()) {
                return false;
            }
            if (!clockConsistent(
                    activeConcurrentAttempt.beforeClockValue,
                    lastObservedClockValue = updateGraph.clock().currentValue(),
                    activeConcurrentAttempt.usingPreviousValues)) {
                return true;
            }
            return !activeConcurrentAttempt.control.snapshotConsistent(
                    lastObservedClockValue,
                    activeConcurrentAttempt.usingPreviousValues);
        }

        @Override
        public void failIfConcurrentAttemptInconsistent() {
            if (concurrentAttemptInconsistent()) {
                throw new SnapshotInconsistentException();
            }
        }

        @Override
        public void maybeWaitForSatisfaction(@Nullable final NotificationQueue.Dependency dependency) {
            if (!concurrentAttemptActive()
                    || dependency == null
                    || activeConcurrentAttempt.usingPreviousValues
                    || LogicalClock.getState(activeConcurrentAttempt.beforeClockValue) == LogicalClock.State.Idle) {
                // No cycle or dependency to wait for.
                return;
            }
            final long beforeStep = LogicalClock.getStep(activeConcurrentAttempt.beforeClockValue);
            // Wait for satisfaction if necessary
            if (dependency.satisfied(beforeStep)
                    || WaitNotification.waitForSatisfaction(beforeStep, dependency)) {
                return;
            }
            lastObservedClockValue = updateGraph.clock().currentValue();
            // Blow up if we've detected a step change
            if (LogicalClock.getStep(lastObservedClockValue) != beforeStep) {
                throw new SnapshotInconsistentException();
            }
        }

        @Override
        public long getConcurrentAttemptClockValue() {
            return concurrentAttemptActive() ? activeConcurrentAttempt.beforeClockValue : 0;
        }

        @Override
        public LogOutput appendConcurrentAttemptClockInfo(@NotNull final LogOutput logOutput) {
            logOutput.append("concurrent snapshot state: ");
            if (concurrentAttemptActive()) {
                logOutput.append("active, beforeClockValue=").append(activeConcurrentAttempt.beforeClockValue)
                        .append(", usingPreviousValues=").append(activeConcurrentAttempt.usingPreviousValues);
            } else {
                logOutput.append("inactive");
            }
            return logOutput.append(", lastObservedClockValue=").append(lastObservedClockValue);
        }

        /**
         * Check whether this thread currently holds a lock on {@code updateGraph}.
         *
         * @param updateGraph The {@link UpdateGraph}
         * @return Whether this thread currently holds a lock on {@code updateGraph}.
         */
        private static boolean locked(@NotNull final UpdateGraph updateGraph) {
            return updateGraph.sharedLock().isHeldByCurrentThread()
                    || updateGraph.exclusiveLock().isHeldByCurrentThread();
        }

        /**
         * Acquire a shared update graph lock if necessary.
         */
        private void maybeAcquireLock() {
            if (updateGraph.currentThreadProcessesUpdates() || locked(updateGraph)) {
                return;
            }
            updateGraph.sharedLock().lock();
            acquiredLock = true;
        }

        /**
         * Release a shared update graph lock if necessary.
         */
        private void maybeReleaseLock() {
            if (acquiredLock && concurrentSnapshotDepth == 0 && lockedSnapshotDepth == 0) {
                updateGraph.sharedLock().unlock();
                acquiredLock = false;
            }
        }
    }

    /**
     * Exception thrown for "fail-fast" purposes when it's been detected that a snapshot will fail.
     */
    public static class SnapshotInconsistentException extends UncheckedDeephavenException {
    }

    /**
     * Test whether the logical clock has remained sufficiently consistent to allow a snapshot function to succeed. Note
     * that this is a necessary but not sufficient test in most cases; this test is invoked (in short-circuit fashion)
     * before invoking @link SnapshotControl#snapshotConsistent(long, boolean)} (long, boolean)}.
     *
     * @param beforeClockValue The clock value from before the snapshot was attempted
     * @param afterClockValue The clock value from after the snapshot was attempted
     * @param usedPrev Whether the snapshot used previous values
     * @return Whether the snapshot succeeded (based on clock changes)
     */
    private static boolean clockConsistent(
            final long beforeClockValue,
            final long afterClockValue,
            final boolean usedPrev) {
        final boolean stepSame = LogicalClock.getStep(beforeClockValue) == LogicalClock.getStep(afterClockValue);
        final boolean stateSame = LogicalClock.getState(beforeClockValue) == LogicalClock.getState(afterClockValue);
        return stepSame && (stateSame || !usedPrev);
    }

    /**
     * Get the currently-active snapshot state.
     *
     * @return the currently-active snapshot state
     */
    public static State state() {
        return StateImpl.get();
    }

    /**
     * Test that determines whether the currently-active concurrent snapshot attempt has become inconsistent. Always
     * returns {@code false} if there is no snapshot attempt active, or if there is a locked attempt active (necessarily
     * at lower depth than the lowest concurrent attempt).
     *
     * 

* Equivalent to {@code state().concurrentAttemptInconsistent()}. * * @return Whether the clock or sources have changed in such a way as to make the currently-active concurrent * snapshot attempt inconsistent * @see State#concurrentAttemptInconsistent() */ public static boolean concurrentAttemptInconsistent() { return state().concurrentAttemptInconsistent(); } /** * Check that fails if the currently-active concurrent snapshot attempt has become inconsistent. source. This is a * no-op if there is no snapshot attempt active, or if there is a locked attempt active (necessarily at lower depth * than the lowest concurrent attempt). * *

* Equivalent to {@code state().failIfConcurrentAttemptInconsistent()}. * * @throws SnapshotInconsistentException If the currently-active concurrent snapshot attempt has become inconsistent * @see State#failIfConcurrentAttemptInconsistent() */ public static void failIfConcurrentAttemptInconsistent() { state().failIfConcurrentAttemptInconsistent(); } /** * Wait for a dependency to become satisfied on the current cycle if we're trying to use current values for the * currently-active concurrent snapshot attempt. This is a no-op if there is no snapshot attempt active, or if there * is a locked attempt active (necessarily at lower depth than the lowest concurrent attempt). * *

* Equivalent to {@code state().maybeWaitForSatisfaction(dependency)}. * * @param dependency The dependency, which may be null in order to avoid redundant checks in calling code * @throws SnapshotInconsistentException If we cannot wait for this dependency on the current step because the step * changed * @see State#maybeWaitForSatisfaction(Dependency) */ public static void maybeWaitForSatisfaction(@Nullable final NotificationQueue.Dependency dependency) { state().maybeWaitForSatisfaction(dependency); } /** * Return the currently-active concurrent snapshot attempt's "before" clock value, or zero if there is no concurrent * attempt active. * *

* Equivalent to {@code state().getConcurrentAttemptClockValue()}. * * @return The concurrent snapshot attempt's "before" clock value, or zero * @see State#getConcurrentAttemptClockValue() */ public static long getConcurrentAttemptClockValue() { return state().getConcurrentAttemptClockValue(); } /** * Append clock info that pertains to the concurrent attempt state to {@code logOutput}. * *

* Equivalent to {@code state().appendConcurrentAttemptClockInfo(logOutput)}. * * @param logOutput The {@link LogOutput} * @return {@code logOutput} * @see State#appendConcurrentAttemptClockInfo(LogOutput) */ @SuppressWarnings("UnusedReturnValue") public static LogOutput appendConcurrentAttemptClockInfo(@NotNull final LogOutput logOutput) { return state().appendConcurrentAttemptClockInfo(logOutput); } /** * Create a {@link InitialSnapshot snapshot} of the entire table specified. Note that this method is * notification-oblivious, i.e. it makes no attempt to ensure that notifications are not missed. * * @param logIdentityObject An object used to prepend to log rows. * @param table the table to snapshot. * @return a snapshot of the entire base table. */ public static InitialSnapshot constructInitialSnapshot( @NotNull final Object logIdentityObject, @NotNull final BaseTable table) { return constructInitialSnapshot(logIdentityObject, table, null, null); } /** * Create a {@link InitialSnapshot snapshot} of the specified table using a set of requested columns and keys. Note * that this method uses a RowSet that is in key space, and that it is notification-oblivious, i.e. it makes no * attempt to ensure that notifications are not missed. * * @param logIdentityObject An object used to prepend to log rows. * @param table the table to snapshot. * @param columnsToSerialize A {@link BitSet} of columns to include, null for all * @param keysToSnapshot An RowSet of keys within the table to include, null for all * @return a snapshot of the entire base table. */ public static InitialSnapshot constructInitialSnapshot( @NotNull final Object logIdentityObject, @NotNull final BaseTable table, @Nullable final BitSet columnsToSerialize, @Nullable final RowSet keysToSnapshot) { return constructInitialSnapshot(logIdentityObject, table, columnsToSerialize, keysToSnapshot, makeSnapshotControl(false, table.isRefreshing(), table)); } static InitialSnapshot constructInitialSnapshot( @NotNull final Object logIdentityObject, @NotNull final BaseTable table, @Nullable final BitSet columnsToSerialize, @Nullable final RowSet keysToSnapshot, @NotNull final SnapshotControl control) { final UpdateGraph updateGraph = table.getUpdateGraph(); try (final SafeCloseable ignored = ExecutionContext.getContext().withUpdateGraph(updateGraph).open()) { final InitialSnapshot snapshot = new InitialSnapshot(); final SnapshotFunction doSnapshot = (usePrev, beforeClockValue) -> serializeAllTable( usePrev, snapshot, table, logIdentityObject, columnsToSerialize, keysToSnapshot); snapshot.step = callDataSnapshotFunction(System.identityHashCode(logIdentityObject), control, doSnapshot); return snapshot; } } /** * Create a {@link InitialSnapshot snapshot} of the specified table using a set of requested columns and positions. * Note that this method uses a RowSet that is in position space, and that it is notification-oblivious, i.e. it * makes no attempt to ensure that notifications are not missed. * * @param logIdentityObject An object used to prepend to log rows. * @param table the table to snapshot. * @param columnsToSerialize A {@link BitSet} of columns to include, null for all * @param positionsToSnapshot An RowSet of positions within the table to include, null for all * @return a snapshot of the entire base table. */ public static InitialSnapshot constructInitialSnapshotInPositionSpace( @NotNull final Object logIdentityObject, @NotNull final BaseTable table, @Nullable final BitSet columnsToSerialize, @Nullable final RowSet positionsToSnapshot) { try (final SafeCloseable ignored = ExecutionContext.getContext().withUpdateGraph( table.getUpdateGraph()).open()) { return constructInitialSnapshotInPositionSpace(logIdentityObject, table, columnsToSerialize, positionsToSnapshot, makeSnapshotControl(false, table.isRefreshing(), table)); } } static InitialSnapshot constructInitialSnapshotInPositionSpace( @NotNull final Object logIdentityObject, @NotNull final BaseTable table, @Nullable final BitSet columnsToSerialize, @Nullable final RowSet positionsToSnapshot, @NotNull final SnapshotControl control) { final InitialSnapshot snapshot = new InitialSnapshot(); final SnapshotFunction doSnapshot = (usePrev, beforeClockValue) -> { final RowSet keysToSnapshot; if (positionsToSnapshot == null) { keysToSnapshot = null; } else { keysToSnapshot = (usePrev ? table.getRowSet().prev() : table.getRowSet()) .subSetForPositions(positionsToSnapshot); } return serializeAllTable(usePrev, snapshot, table, logIdentityObject, columnsToSerialize, keysToSnapshot); }; try (final SafeCloseable ignored = ExecutionContext.getContext().withUpdateGraph( table.getUpdateGraph()).open()) { snapshot.step = callDataSnapshotFunction(System.identityHashCode(logIdentityObject), control, doSnapshot); } return snapshot; } /** * Create a {@link BarrageMessage snapshot} of the specified table including all columns and rows. Note that this * method is notification-oblivious, i.e. it makes no attempt to ensure that notifications are not missed. * * @param logIdentityObject An object used to prepend to log rows. * @param table the table to snapshot. * @return a snapshot of the entire base table. */ public static BarrageMessage constructBackplaneSnapshot( @NotNull final Object logIdentityObject, @NotNull final BaseTable table) { return constructBackplaneSnapshotInPositionSpace(logIdentityObject, table, null, null, null); } /** * Create a {@link BarrageMessage snapshot} of the specified table using a set of requested columns and positions. * Note that this method uses a RowSet that is in position space, and that it is notification-oblivious, i.e. it * makes no attempt to ensure that notifications are not missed. * * @param logIdentityObject An object used to prepend to log rows. * @param table the table to snapshot. * @param columnsToSerialize A {@link BitSet} of columns to include, null for all * @param positionsToSnapshot An RowSet of positions within the table to include, null for all * @return a snapshot of the entire base table. */ public static BarrageMessage constructBackplaneSnapshotInPositionSpace( @NotNull final Object logIdentityObject, @NotNull final BaseTable table, @Nullable final BitSet columnsToSerialize, @Nullable final RowSequence positionsToSnapshot, @Nullable final RowSequence reversePositionsToSnapshot) { return constructBackplaneSnapshotInPositionSpace(logIdentityObject, table, columnsToSerialize, positionsToSnapshot, reversePositionsToSnapshot, makeSnapshotControl(false, table.isRefreshing(), table)); } /** * Create a {@link BarrageMessage snapshot} of the specified table using a set of requested columns and positions. * Note that this method uses a RowSet that is in position space. * * @param logIdentityObject An object used to prepend to log rows. * @param table the table to snapshot. * @param columnsToSerialize A {@link BitSet} of columns to include, null for all * @param positionsToSnapshot A RowSequence of positions within the table to include, null for all * @param reversePositionsToSnapshot A RowSequence of reverse positions within the table to include, null for all * @param control A {@link SnapshotControl} to define the parameters and consistency for this snapshot * @return a snapshot of the entire base table. */ public static BarrageMessage constructBackplaneSnapshotInPositionSpace( @NotNull final Object logIdentityObject, @NotNull final BaseTable table, @Nullable final BitSet columnsToSerialize, @Nullable final RowSequence positionsToSnapshot, @Nullable final RowSequence reversePositionsToSnapshot, @NotNull final SnapshotControl control) { final UpdateGraph updateGraph = table.getUpdateGraph(); try (final SafeCloseable ignored1 = ExecutionContext.getContext().withUpdateGraph(updateGraph).open()) { final BarrageMessage snapshot = new BarrageMessage(); snapshot.isSnapshot = true; snapshot.shifted = RowSetShiftData.EMPTY; final SnapshotFunction doSnapshot = (usePrev, beforeClockValue) -> { final RowSet keysToSnapshot; if (positionsToSnapshot == null && reversePositionsToSnapshot == null) { keysToSnapshot = null; } else { final RowSet rowSetToUse = usePrev ? table.getRowSet().prev() : table.getRowSet(); final WritableRowSet forwardKeys = positionsToSnapshot == null ? null : rowSetToUse.subSetForPositions(positionsToSnapshot); final RowSet reverseKeys = reversePositionsToSnapshot == null ? null : rowSetToUse.subSetForReversePositions(reversePositionsToSnapshot); if (forwardKeys != null) { if (reverseKeys != null) { forwardKeys.insert(reverseKeys); reverseKeys.close(); } keysToSnapshot = forwardKeys; } else { keysToSnapshot = reverseKeys; } } try (final RowSet ignored = keysToSnapshot) { return serializeAllTable(usePrev, snapshot, table, logIdentityObject, columnsToSerialize, keysToSnapshot); } }; snapshot.step = callDataSnapshotFunction(System.identityHashCode(logIdentityObject), control, doSnapshot); snapshot.firstSeq = snapshot.lastSeq = snapshot.step; return snapshot; } } /** * Constructs {@link InitialSnapshot}s for the entirety of the tables. Note that this method is * notification-oblivious, i.e. it makes no attempt to ensure that notifications are not missed. * * @param logIdentityObject identifier prefixing the log message * @param tables tables to snapshot * @return list of the resulting {@link InitialSnapshot}s */ public static List constructInitialSnapshots( @NotNull final Object logIdentityObject, @NotNull final BaseTable... tables) { if (tables.length == 0) { return Collections.emptyList(); } final UpdateGraph updateGraph = NotificationQueue.Dependency.getUpdateGraph(null, tables); try (final SafeCloseable ignored = ExecutionContext.getContext().withUpdateGraph(updateGraph).open()) { final List snapshots = new ArrayList<>(); final SnapshotControl snapshotControl = tables.length == 1 ? makeSnapshotControl(false, tables[0].isRefreshing(), tables[0]) : makeSnapshotControl(false, Arrays.stream(tables).anyMatch(BaseTable::isRefreshing), tables); final SnapshotFunction doSnapshot = (usePrev, beforeClockValue) -> serializeAllTables(usePrev, snapshots, tables, logIdentityObject); callDataSnapshotFunction(System.identityHashCode(logIdentityObject), snapshotControl, doSnapshot); return snapshots; } } @FunctionalInterface public interface SnapshotFunction { /** * A function that would like to take a snapshot of data guarded by a retry loop with data consistency tests. * * @param usePrev Whether data from the previous cycle should be used (otherwise use this cycle) * @param beforeClockValue The clock value that we captured before the function began; the function can use this * value to bail out early if it notices something has gone wrong; {@value LogicalClock#NULL_CLOCK_VALUE} * for static snapshots * @return true if the function was successful, false if it should be retried */ boolean call(boolean usePrev, long beforeClockValue); } /** * Interface for {@link #usePreviousValues(long)}. */ @FunctionalInterface public interface UsePreviousValues { /** *

* Determine if previous values should be used in table data access for the given {@link LogicalClock clock} * value. *

* Expected to never request previous values during the idle phase of a cycle. *

* Must never request previous values for a source that has already been updated on the current cycle, unless it * can be proven that that source was not instantiated on the current cycle. *

* Must be safe to call more than once, exactly once per snapshot attempt. * * @param beforeClockValue The current clock value before the snapshot function will be invoked * @return A {@link Boolean} with the following meaning: *

    *
  • {@code true} if previous values should be used
  • *
  • {@code false} if they should not
  • *
  • {@code null} if a clock discrepancy was detected and we must retry with a new * {@code beforeClockValue}
  • *
*/ Boolean usePreviousValues(long beforeClockValue); } /** * Interface for {@link #snapshotConsistent(long, boolean)}. */ @FunctionalInterface public interface SnapshotConsistent { /** *

* Determine (from within a snapshot function) if the snapshot appears to still be consistent. *

* This should be no more restrictive than the associated {@link SnapshotCompletedConsistently}. *

* Can assume as a precondition that the clock step has not been observed to change since the last time the * associated {@link UsePreviousValues#usePreviousValues(long)} was invoked, and that the clock state has not * been observed to change if previous values were used. See {@link #clockConsistent(long, long, boolean)}. * * @param currentClockValue The current clock value * @param usingPreviousValues Whether the snapshot function is using previous values * @return True if we can no longer expect that the snapshot function's result will be consistent */ boolean snapshotConsistent(long currentClockValue, boolean usingPreviousValues); } /** * Interface for {@link #snapshotCompletedConsistently(long, boolean)}. */ @FunctionalInterface public interface SnapshotCompletedConsistently { /** *

* Determine if a snapshot was consistent according to the clock cycle. Intended to be paired with a * {@link UsePreviousValues} function. *

* Can assume as a precondition that the clock step has not been observed to change since the last time the * associated {@link UsePreviousValues#usePreviousValues(long)} was invoked, and that the clock state has not * been observed to change if previous values were used. See {@link #clockConsistent(long, long, boolean)}. *

* Will be called at most once per snapshot attempt. Will be called for all possibly-successful snapshot * attempts. May be called after unsuccessful concurrent snapshot attempts. * * @param afterClockValue The current clock value after the snapshot function was invoked * @param usedPreviousValues If previous values were used * @return Whether the snapshot is provably consistent * @throws RuntimeException If the snapshot was consistent but the snapshot function failed to satisfy this * function's expectations; this will be treated as a failure of the snapshot function */ boolean snapshotCompletedConsistently(long afterClockValue, boolean usedPreviousValues); } /** * Interface used to control snapshot behavior, including previous value usage and consistency testing. */ public interface SnapshotControl extends UsePreviousValues, SnapshotConsistent, SnapshotCompletedConsistently { @Override default boolean snapshotCompletedConsistently(final long afterClockValue, final boolean usedPreviousValues) { return snapshotConsistent(afterClockValue, usedPreviousValues); } /** * @return The {@link UpdateGraph} that applies for this snapshot; {@code null} for snapshots of static data, * which can skip all consistency-related considerations */ UpdateGraph getUpdateGraph(); } /** * An implementation of {@link SnapshotControl} for sources that cannot update. */ public static final class StaticSnapshotControl implements SnapshotControl { public static final SnapshotControl INSTANCE = new StaticSnapshotControl(); private StaticSnapshotControl() {} @Override public Boolean usePreviousValues(long beforeClockValue) { // noinspection AutoBoxing return false; } @Override public boolean snapshotConsistent(long currentClockValue, boolean usingPreviousValues) { return true; } @Override public UpdateGraph getUpdateGraph() { return null; } } /** * Make a {@link SnapshotControl} from individual function objects. * * @param updateGraph The {@link UpdateGraph} for the snapshot * @param usePreviousValues The {@link UsePreviousValues} to use * @param snapshotConsistent The {@link SnapshotConsistent} to use * @param snapshotCompletedConsistently The {@link SnapshotCompletedConsistently} to use, or null to use {@code * snapshotConsistent} */ public static SnapshotControl makeSnapshotControl( @NotNull final UpdateGraph updateGraph, @NotNull final UsePreviousValues usePreviousValues, @NotNull final SnapshotConsistent snapshotConsistent, @Nullable final SnapshotCompletedConsistently snapshotCompletedConsistently) { return snapshotCompletedConsistently == null ? new SnapshotControlAdapter( updateGraph, usePreviousValues, snapshotConsistent, snapshotConsistent::snapshotConsistent) : new SnapshotControlAdapter( updateGraph, usePreviousValues, snapshotConsistent, snapshotCompletedConsistently); } /** * Adapter to combine the individual component functions of {@link SnapshotControl} into a valid snapshot control. */ private static class SnapshotControlAdapter implements SnapshotControl { private final UpdateGraph updateGraph; private final UsePreviousValues usePreviousValues; private final SnapshotConsistent snapshotConsistent; private final SnapshotCompletedConsistently snapshotCompletedConsistently; /** * Make a SnapshotControlAdapter. * * @param updateGraph The {@link UpdateGraph} to use * @param usePreviousValues The {@link UsePreviousValues} to use * @param snapshotConsistent The {@link SnapshotConsistent} to use * @param snapshotCompletedConsistently The {@link SnapshotCompletedConsistently} to use */ private SnapshotControlAdapter( @NotNull final UpdateGraph updateGraph, @NotNull final UsePreviousValues usePreviousValues, @NotNull final SnapshotConsistent snapshotConsistent, @NotNull final SnapshotCompletedConsistently snapshotCompletedConsistently) { this.updateGraph = updateGraph; this.usePreviousValues = usePreviousValues; this.snapshotConsistent = snapshotConsistent; this.snapshotCompletedConsistently = snapshotCompletedConsistently; } @Override public Boolean usePreviousValues(final long beforeClockValue) { return usePreviousValues.usePreviousValues(beforeClockValue); } @Override public boolean snapshotConsistent(final long currentClockValue, final boolean usingPreviousValues) { return snapshotConsistent.snapshotConsistent(currentClockValue, usingPreviousValues); } @Override public boolean snapshotCompletedConsistently(final long afterClockValue, final boolean usedPreviousValues) { return snapshotCompletedConsistently.snapshotCompletedConsistently(afterClockValue, usedPreviousValues); } @Override public UpdateGraph getUpdateGraph() { return updateGraph; } } /** * Make a default {@link SnapshotControl} for a single source. * * @param notificationAware Whether the result should be concerned with not missing notifications * @param refreshing Whether the data source (usually a {@link Table} table) is refreshing (vs static) * @param source The source * @return An appropriate {@link SnapshotControl} */ public static SnapshotControl makeSnapshotControl( final boolean notificationAware, final boolean refreshing, @NotNull final NotificationStepSource source) { return refreshing ? notificationAware ? new NotificationAwareSingleSourceSnapshotControl(source) : new NotificationObliviousSingleSourceSnapshotControl(source) : StaticSnapshotControl.INSTANCE; } /** * Make a default {@link SnapshotControl} for one or more sources. * * @param notificationAware Whether the result should be concerned with not missing notifications * @param refreshing Whether any of the data sources (usually {@link Table tables}) are refreshing (vs static) * @param sources The sources * @return An appropriate {@link SnapshotControl} */ public static SnapshotControl makeSnapshotControl( final boolean notificationAware, final boolean refreshing, @NotNull final NotificationStepSource... sources) { if (sources.length == 1) { return makeSnapshotControl(notificationAware, refreshing, sources[0]); } return refreshing ? notificationAware ? new NotificationAwareMultipleSourceSnapshotControl(sources) : new NotificationObliviousMultipleSourceSnapshotControl(sources) : StaticSnapshotControl.INSTANCE; } /** * Base class for SnapshotControl implementations driven by a single data source. */ private static abstract class SingleSourceSnapshotControl implements ConstructSnapshot.SnapshotControl { protected final NotificationStepSource source; public SingleSourceSnapshotControl(@NotNull final NotificationStepSource source) { this.source = source; } @Override public Boolean usePreviousValues(final long beforeClockValue) { final long beforeStep = LogicalClock.getStep(beforeClockValue); final LogicalClock.State beforeState = LogicalClock.getState(beforeClockValue); try { // noinspection AutoBoxing return beforeState == LogicalClock.State.Updating && source.getLastNotificationStep() != beforeStep && !source.satisfied(beforeStep); } catch (ClockInconsistencyException e) { return null; } } @Override public UpdateGraph getUpdateGraph() { return source.getUpdateGraph(); } } /** * A SnapshotControl implementation driven by a single data source for use cases when the snapshot function must not * miss a notification. For use when instantiating concurrent consumers of all updates from a source. */ private static class NotificationAwareSingleSourceSnapshotControl extends SingleSourceSnapshotControl { private NotificationAwareSingleSourceSnapshotControl(@NotNull final NotificationStepSource source) { super(source); } @Override public boolean snapshotConsistent(final long currentClockValue, final boolean usingPreviousValues) { if (!usingPreviousValues) { // Cycle was Idle or we had already ticked, and so we're using current values on the current cycle: // Success return true; } // If we didn't miss an update then we're succeeding using previous values, else we've failed return source.getLastNotificationStep() != LogicalClock.getStep(currentClockValue); } } /** * A SnapshotControl implementation driven by a single data source for use cases when the snapshot function doesn't * care if it misses a notification. For use by consistent consumers of consistent current state. */ private static class NotificationObliviousSingleSourceSnapshotControl extends SingleSourceSnapshotControl { private NotificationObliviousSingleSourceSnapshotControl(@NotNull final NotificationStepSource source) { super(source); } @Override public boolean snapshotConsistent(final long currentClockValue, final boolean usingPreviousValues) { return true; } } /** * Base class for SnapshotControl implementations driven by multiple data sources. */ private static abstract class MultipleSourceSnapshotControl implements ConstructSnapshot.SnapshotControl { protected final NotificationStepSource[] sources; private final UpdateGraph updateGraph; private MultipleSourceSnapshotControl(@NotNull final NotificationStepSource... sources) { this.sources = sources; // Note that we can avoid copying a suffix of the sources array by just effectively passing source[0] twice. updateGraph = sources[0].getUpdateGraph(sources); } @SuppressWarnings("AutoBoxing") @Override public Boolean usePreviousValues(final long beforeClockValue) { if (LogicalClock.getState(beforeClockValue) != LogicalClock.State.Updating) { return false; } final long beforeStep = LogicalClock.getStep(beforeClockValue); final NotificationStepSource[] notYetSatisfied; try { notYetSatisfied = Stream.of(sources) .filter((final NotificationStepSource source) -> source.getLastNotificationStep() != beforeStep && !source.satisfied(beforeStep)) .toArray(NotificationStepSource[]::new); } catch (ClockInconsistencyException e) { return null; } if (notYetSatisfied.length == sources.length) { return true; } if (notYetSatisfied.length > 0 && !WaitNotification.waitForSatisfaction(beforeStep, notYetSatisfied)) { if (updateGraph.clock().currentStep() != beforeStep) { // If we missed a step change, we've already failed, request a do-over. return null; } } return false; } @Override public UpdateGraph getUpdateGraph() { return updateGraph; } } /** * A SnapshotControl implementation driven by multiple data sources for use cases when the snapshot function must * not miss a notification. Waits for all sources to be notified on this cycle if any has been notified on this * cycle. For use when instantiating concurrent consumers of all updates from a set of sources. */ private static class NotificationAwareMultipleSourceSnapshotControl extends MultipleSourceSnapshotControl { private NotificationAwareMultipleSourceSnapshotControl(@NotNull final NotificationStepSource... sources) { super(sources); } @Override public boolean snapshotConsistent(final long currentClockValue, final boolean usingPreviousValues) { if (!usingPreviousValues) { // Cycle was Idle or we had already ticked, and so we're using current values on the current cycle: // Success return true; } // If we didn't miss an update then we're succeeding using previous values, else we've failed final long currentStep = LogicalClock.getStep(currentClockValue); return Stream.of(sources) .allMatch((final NotificationStepSource source) -> source.getLastNotificationStep() != currentStep); } } /** * A SnapshotControl implementation driven by multiple data sources for use cases when the snapshot function must * not miss a notification. Waits for all sources to be notified on this cycle if any has been notified on this * cycle. For use by consistent consumers of consistent current state. */ private static class NotificationObliviousMultipleSourceSnapshotControl extends MultipleSourceSnapshotControl { private NotificationObliviousMultipleSourceSnapshotControl(@NotNull final NotificationStepSource... sources) { super(sources); } @Override public boolean snapshotConsistent(final long currentClockValue, final boolean usingPreviousValues) { return true; } } private static long callDataSnapshotFunction( final int logInt, @NotNull final SnapshotControl control, @NotNull final SnapshotFunction function) { return callDataSnapshotFunction(logOutput -> logOutput.append(logInt), control, function); } /** * Invokes the snapshot function in a loop until it succeeds with provably consistent results, or until * {@code MAX_CONCURRENT_ATTEMPTS} or {@code MAX_CONCURRENT_ATTEMPT_DURATION_MILLIS} are exceeded. Falls back to * acquiring a shared update graph lock for a final attempt. * * @param logPrefix A prefix for our log messages * @param control A {@link SnapshotControl} to define the parameters and consistency for this snapshot * @param function The function to execute * @return The logical clock step that applied to this snapshot */ public static long callDataSnapshotFunction( @NotNull final String logPrefix, @NotNull final SnapshotControl control, @NotNull final SnapshotFunction function) { return callDataSnapshotFunction((final LogOutput logOutput) -> logOutput.append(logPrefix), control, function); } /** * Invokes the snapshot function in a loop until it succeeds with provably consistent results, or until * {@code MAX_CONCURRENT_ATTEMPTS} or {@code MAX_CONCURRENT_ATTEMPT_DURATION_MILLIS} are exceeded. Falls back to * acquiring a shared update graph lock for a final attempt. *

* The supplied {@link SnapshotControl}'s {@link SnapshotControl#usePreviousValues usePreviousValues} will be * invoked at the start of any snapshot attempt, and its {@link SnapshotControl#snapshotCompletedConsistently * snapshotCompletedConsistently} will be invoked at the end of any snapshot attempt that is not provably * inconsistent. *

* If the supplied {@link SnapshotControl} provides a null {@link SnapshotControl#getUpdateGraph UpdateGraph}, then * this method will perform a static snapshot without locks or retrying. In this case, the {@link SnapshotControl}'s * {@link SnapshotControl#usePreviousValues usePreviousValues} must return {@code false}, * {@link SnapshotControl#snapshotCompletedConsistently snapshotCompletedConsistently} must return {@code true}, and * the {@link LogicalClock#NULL_CLOCK_VALUE NULL_CLOCK_VALUE} will be supplied to {@code usePreviousValues} and * {@code snapshotCompletedConsistently}. * * @param logPrefix A prefix for our log messages * @param control A {@link SnapshotControl} to define the parameters and consistency for this snapshot * @param function The function to execute * @return The logical clock step that applied to this snapshot; {@value LogicalClock#NULL_CLOCK_VALUE} for static * snapshots */ public static long callDataSnapshotFunction( @NotNull final LogOutputAppendable logPrefix, @NotNull final SnapshotControl control, @NotNull final SnapshotFunction function) { final long overallStart = System.currentTimeMillis(); final StateImpl state = StateImpl.get(); final UpdateGraph updateGraph = control.getUpdateGraph(); if (updateGraph == null) { // This is a snapshot of static data. Just call the function with no frippery. final boolean controlUsePrev = control.usePreviousValues(LogicalClock.NULL_CLOCK_VALUE); if (controlUsePrev) { throw new SnapshotUnsuccessfulException("Static snapshot requested previous values"); } final boolean functionSuccessful = function.call(false, LogicalClock.NULL_CLOCK_VALUE); if (!functionSuccessful) { throw new SnapshotUnsuccessfulException("Static snapshot failed to execute snapshot function"); } if (log.isDebugEnabled()) { final long duration = System.currentTimeMillis() - overallStart; log.debug().append(logPrefix) .append(" Static snapshot function elapsed time ").append(duration).append(" ms").endl(); } // notify control of successful snapshot final boolean controlSuccessful = control.snapshotCompletedConsistently(LogicalClock.NULL_CLOCK_VALUE, false); if (!controlSuccessful) { throw new SnapshotUnsuccessfulException("Static snapshot function succeeded but control failed"); } return LogicalClock.NULL_CLOCK_VALUE; } final boolean onUpdateThread = updateGraph.currentThreadProcessesUpdates(); final boolean alreadyLocked = StateImpl.locked(updateGraph); boolean snapshotSuccessful = false; boolean functionSuccessful = false; Exception caughtException = null; long step = 0; int delay = 10; int numConcurrentAttempts = 0; final LivenessManager initialLivenessManager = LivenessScopeStack.peek(); while (numConcurrentAttempts < MAX_CONCURRENT_ATTEMPTS && !alreadyLocked && !onUpdateThread) { ++numConcurrentAttempts; final long beforeClockValue = updateGraph.clock().currentValue(); final long attemptStart = System.currentTimeMillis(); final Boolean previousValuesRequested = control.usePreviousValues(beforeClockValue); if (previousValuesRequested == null) { // usePreviousValues detected a step change; we should try again continue; } // noinspection AutoUnboxing final boolean usePrev = previousValuesRequested; if (LogicalClock.getState(beforeClockValue) == LogicalClock.State.Idle && usePrev) { // noinspection ThrowableNotThrown Assert.statementNeverExecuted("Previous values requested while not updating: " + beforeClockValue); } final long attemptDurationMillis; final LivenessScope snapshotLivenessScope = new LivenessScope(); try (final SafeCloseable ignored = LivenessScopeStack.open(snapshotLivenessScope, true)) { final Object startObject = state.startConcurrentSnapshot(control, beforeClockValue, usePrev); try { functionSuccessful = function.call(usePrev, beforeClockValue); } catch (NoSnapshotAllowedException ex) { // Breaking here will force an update graph lock acquire. // TODO: Optimization. If this exception is only used for cases when we can't use previous values, // then we could simply wait for the source to become satisfied on this cycle, rather than // waiting for the update graph lock. Likely requires work for all code that uses this pattern. if (log.isDebugEnabled()) { log.debug().append(logPrefix).append(" Disallowed concurrent snapshot function took ") .append(System.currentTimeMillis() - attemptStart).append("ms") .append(", beforeClockValue=").append(beforeClockValue) .append(", afterClockValue=").append(updateGraph.clock().currentValue()) .append(", usePrev=").append(usePrev) .endl(); } break; } catch (Exception e) { functionSuccessful = false; caughtException = e; } finally { state.endConcurrentSnapshot(startObject); } final long afterClockValue = updateGraph.clock().currentValue(); try { snapshotSuccessful = clockConsistent(beforeClockValue, afterClockValue, usePrev) && control.snapshotCompletedConsistently(afterClockValue, usePrev); } catch (Exception e) { if (functionSuccessful) { // Treat this exception as a snapshot function failure despite consistent snapshot functionSuccessful = false; caughtException = e; snapshotSuccessful = true; } else if (log.isDebugEnabled()) { log.debug().append(logPrefix) .append(" Suppressed exception from snapshot success function: ").append(e).endl(); } } attemptDurationMillis = System.currentTimeMillis() - attemptStart; if (log.isDebugEnabled()) { log.debug().append(logPrefix).append(" Concurrent snapshot function took ") .append(attemptDurationMillis).append("ms") .append(", snapshotSuccessful=").append(snapshotSuccessful) .append(", functionSuccessful=").append(functionSuccessful) .append(", beforeClockValue=").append(beforeClockValue) .append(", afterClockValue=").append(afterClockValue) .append(", usePrev=").append(usePrev) .endl(); } if (snapshotSuccessful) { if (functionSuccessful) { step = LogicalClock.getStep(beforeClockValue) - (usePrev ? 1 : 0); snapshotLivenessScope.transferTo(initialLivenessManager); } break; } } if (attemptDurationMillis > MAX_CONCURRENT_ATTEMPT_DURATION_MILLIS) { if (log.isDebugEnabled()) { log.debug().append(logPrefix).append(" Failed concurrent execution exceeded maximum duration (") .append(attemptDurationMillis).append(" ms > ") .append(MAX_CONCURRENT_ATTEMPT_DURATION_MILLIS).append(" ms)").endl(); } break; } else { try { Thread.sleep(delay); delay *= 2; } catch (InterruptedException interruptIsCancel) { throw new CancellationException("Interrupt detected", interruptIsCancel); } } } if (snapshotSuccessful) { state.maybeReleaseLock(); if (!functionSuccessful) { final String message = "Failed to execute function concurrently despite consistent state"; if (caughtException != null) { throw new SnapshotUnsuccessfulException(message, caughtException); } else { throw new SnapshotUnsuccessfulException(message); } } } else { if (log.isDebugEnabled()) { if (numConcurrentAttempts == 0) { log.debug().append(logPrefix).append(" Already held lock, proceeding to locked snapshot").endl(); } else { log.debug().append(logPrefix) .append(" Failed to obtain clean execution without blocking run processing").endl(); } } state.startLockedSnapshot(control); try { final long beforeClockValue = updateGraph.clock().currentValue(); final Boolean previousValuesRequested = control.usePreviousValues(beforeClockValue); if (!Boolean.FALSE.equals(previousValuesRequested)) { Assert.statementNeverExecuted(String.format( "Previous values requested or inconsistent %s: beforeClockValue=%d, previousValuesRequested=%s", onUpdateThread ? "from update-processing thread" : "while locked", beforeClockValue, previousValuesRequested)); } final long attemptStart = System.currentTimeMillis(); functionSuccessful = function.call(false, beforeClockValue); Assert.assertion(functionSuccessful, "functionSuccessful"); final long afterClockValue = updateGraph.clock().currentValue(); Assert.eq(beforeClockValue, "beforeClockValue", afterClockValue, "afterClockValue"); final boolean consistent = control.snapshotCompletedConsistently(afterClockValue, false); if (!consistent) { Assert.statementNeverExecuted(String.format( "Consistent execution not achieved %s", onUpdateThread ? "from update-processing thread" : "while locked")); } if (log.isDebugEnabled()) { log.debug().append(logPrefix).append(" Non-concurrent Snapshot Function took ") .append(System.currentTimeMillis() - attemptStart).append("ms").endl(); } step = LogicalClock.getStep(afterClockValue); } finally { state.endLockedSnapshot(); } } if (log.isDebugEnabled()) { final long duration = System.currentTimeMillis() - overallStart; log.debug().append(logPrefix).append(" Total snapshot function elapsed time ") .append(duration).append(" ms").append(", step=").append(step).endl(); } return step; } /** *

* Populate an {@link InitialSnapshot} with the specified keys and columns to snapshot. *

* Note that care must be taken while using this method to ensure the underlying table is locked or does not change, * otherwise the resulting snapshot may be inconsistent. In general users should instead use * {@link #constructInitialSnapshot} for simple use cases or {@link #callDataSnapshotFunction} for more advanced * uses. * * @param usePrev Whether to use previous values * @param snapshot The snapshot to populate * @param logIdentityObject An object for use with log() messages * @param columnsToSerialize A {@link BitSet} of columns to include, null for all * @param keysToSnapshot A RowSet of keys within the table to include, null for all * * @return Whether the snapshot succeeded */ private static boolean serializeAllTable( final boolean usePrev, @NotNull final InitialSnapshot snapshot, @NotNull final BaseTable table, @NotNull final Object logIdentityObject, @Nullable final BitSet columnsToSerialize, @Nullable final RowSet keysToSnapshot) { snapshot.rowSet = (usePrev ? table.getRowSet().prev() : table.getRowSet()).copy(); if (keysToSnapshot != null) { snapshot.rowsIncluded = snapshot.rowSet.intersect(keysToSnapshot); } else { snapshot.rowsIncluded = snapshot.rowSet; } LongSizedDataStructure.intSize("construct snapshot", snapshot.rowsIncluded.size()); final String[] columnSources = table.getDefinition().getColumnNamesArray(); snapshot.dataColumns = new Object[columnSources.length]; try (final SharedContext sharedContext = (columnSources.length > 1) ? SharedContext.makeSharedContext() : null) { for (int ii = 0; ii < columnSources.length; ii++) { if (columnsToSerialize != null && !columnsToSerialize.get(ii)) { continue; } if (concurrentAttemptInconsistent()) { if (log.isDebugEnabled()) { final LogEntry logEntry = log.debug().append(System.identityHashCode(logIdentityObject)) .append(" Bad snapshot before column ").append(ii); appendConcurrentAttemptClockInfo(logEntry); logEntry.endl(); } return false; } final ColumnSource columnSource = table.getColumnSource(columnSources[ii]); snapshot.dataColumns[ii] = getSnapshotData(columnSource, sharedContext, snapshot.rowsIncluded, usePrev); } } if (log.isDebugEnabled()) { log.debug().append(System.identityHashCode(logIdentityObject)) .append(": Snapshot candidate step=") .append((usePrev ? -1 : 0) + LogicalClock.getStep(getConcurrentAttemptClockValue())) .append(", rows=").append(snapshot.rowsIncluded).append("/").append(keysToSnapshot) .append(", cols=").append(FormatBitSet.arrayToLog(snapshot.dataColumns)).append("/") .append((columnsToSerialize != null) ? FormatBitSet.formatBitSet(columnsToSerialize) : FormatBitSet.arrayToLog(snapshot.dataColumns)) .append(", usePrev=").append(usePrev).endl(); } return true; } /** *

* Populate a BarrageMessage with the specified positions to snapshot and columns. *

* Note that care must be taken while using this method to ensure the underlying table is locked or does not change, * otherwise the resulting snapshot may be inconsistent. In general users should instead use * {@link #constructBackplaneSnapshot} for simple use cases or {@link #callDataSnapshotFunction} for more advanced * uses. * * @param usePrev Use previous values? * @param snapshot The snapshot to populate * @param logIdentityObject an object for use with log() messages * @param columnsToSerialize A {@link BitSet} of columns to include, null for all * @param keysToSnapshot A RowSet of keys within the table to include, null for all * @return true if the snapshot was computed with an unchanged clock, false otherwise. */ private static boolean serializeAllTable( final boolean usePrev, @NotNull final BarrageMessage snapshot, @NotNull final BaseTable table, @NotNull final Object logIdentityObject, @Nullable final BitSet columnsToSerialize, @Nullable final RowSet keysToSnapshot) { snapshot.rowsAdded = (usePrev ? table.getRowSet().prev() : table.getRowSet()).copy(); snapshot.rowsRemoved = RowSetFactory.empty(); snapshot.addColumnData = new BarrageMessage.AddColumnData[table.getColumnSources().size()]; // TODO (core#412): when sending app metadata; this can be reduced to a zero-len array snapshot.modColumnData = new BarrageMessage.ModColumnData[table.getColumnSources().size()]; if (keysToSnapshot != null) { snapshot.rowsIncluded = snapshot.rowsAdded.intersect(keysToSnapshot); } else { snapshot.rowsIncluded = snapshot.rowsAdded.copy(); } final String[] columnSources = table.getDefinition().getColumnNamesArray(); try (final SharedContext sharedContext = (columnSources.length > 1) ? SharedContext.makeSharedContext() : null) { for (int ii = 0; ii < columnSources.length; ++ii) { if (concurrentAttemptInconsistent()) { if (log.isDebugEnabled()) { final LogEntry logEntry = log.debug().append(System.identityHashCode(logIdentityObject)) .append(" Bad snapshot before column ").append(ii); appendConcurrentAttemptClockInfo(logEntry); logEntry.endl(); } return false; } final ColumnSource columnSource = table.getColumnSource(columnSources[ii]); final BarrageMessage.AddColumnData acd = new BarrageMessage.AddColumnData(); snapshot.addColumnData[ii] = acd; final boolean columnIsEmpty = columnsToSerialize != null && !columnsToSerialize.get(ii); final RowSet rows = columnIsEmpty ? RowSetFactory.empty() : snapshot.rowsIncluded; // Note: cannot use shared context across several calls of differing lengths and no sharing necessary // when empty final ColumnSource sourceToUse = ReinterpretUtils.maybeConvertToPrimitive(columnSource); acd.data = getSnapshotDataAsChunkList(sourceToUse, columnIsEmpty ? null : sharedContext, rows, usePrev); acd.type = columnSource.getType(); acd.componentType = columnSource.getComponentType(); acd.chunkType = sourceToUse.getChunkType(); final BarrageMessage.ModColumnData mcd = new BarrageMessage.ModColumnData(); snapshot.modColumnData[ii] = mcd; mcd.rowsModified = RowSetFactory.empty(); mcd.data = getSnapshotDataAsChunkList(sourceToUse, null, RowSetFactory.empty(), usePrev); mcd.type = acd.type; mcd.componentType = acd.componentType; mcd.chunkType = sourceToUse.getChunkType(); } } if (log.isDebugEnabled()) { final LogEntry logEntry = log.debug().append(System.identityHashCode(logIdentityObject)) .append(": Snapshot candidate step=") .append((usePrev ? -1 : 0) + LogicalClock.getStep(getConcurrentAttemptClockValue())) .append(", rows=").append(snapshot.rowsIncluded).append("/").append(keysToSnapshot) .append(", cols="); if (columnsToSerialize == null) { logEntry.append("ALL"); } else { logEntry.append(FormatBitSet.formatBitSet(columnsToSerialize)); } logEntry.append(", usePrev=").append(usePrev).endl(); } return true; } private static boolean serializeAllTables( final boolean usePrev, @NotNull final List snapshots, @NotNull final BaseTable[] tables, @NotNull final Object logIdentityObject) { snapshots.clear(); for (final BaseTable table : tables) { final InitialSnapshot snapshot = new InitialSnapshot(); snapshots.add(snapshot); if (!serializeAllTable(usePrev, snapshot, table, logIdentityObject, null, null)) { snapshots.clear(); return false; } } return true; } private static Object getSnapshotData( @NotNull final ColumnSource columnSource, @Nullable final SharedContext sharedContext, @NotNull final RowSet rowSet, final boolean usePrev) { final ColumnSource sourceToUse = ReinterpretUtils.maybeConvertToPrimitive(columnSource); final Class type = sourceToUse.getType(); final int size = rowSet.intSize(); try (final ColumnSource.FillContext context = sourceToUse.makeFillContext(size, sharedContext)) { final ChunkType chunkType = sourceToUse.getChunkType(); final Object resultArray = chunkType.makeArray(size); final WritableChunk result = chunkType.writableChunkWrap(resultArray, 0, size); if (usePrev) { sourceToUse.fillPrevChunk(context, result, rowSet); } else { sourceToUse.fillChunk(context, result, rowSet); } if (chunkType == ChunkType.Object) { // noinspection unchecked final T[] values = (T[]) Array.newInstance(type, size); for (int ii = 0; ii < values.length; ++ii) { // noinspection unchecked values[ii] = (T) result.asObjectChunk().get(ii); } return values; } return resultArray; } } private static ArrayList> getSnapshotDataAsChunkList( @NotNull final ColumnSource columnSource, @Nullable final SharedContext sharedContext, @NotNull final RowSet rowSet, final boolean usePrev) { long offset = 0; final long size = rowSet.size(); final ArrayList> result = new ArrayList<>(); if (size == 0) { return result; } final int maxChunkSize = (int) Math.min(size, SNAPSHOT_CHUNK_SIZE); try (final ColumnSource.FillContext context = columnSource.makeFillContext(maxChunkSize, sharedContext); final RowSequence.Iterator it = rowSet.getRowSequenceIterator()) { int chunkSize = maxChunkSize; while (it.hasMore()) { final RowSequence reducedRowSet = it.getNextRowSequenceWithLength(chunkSize); final ChunkType chunkType = columnSource.getChunkType(); // create a new chunk WritableChunk currentChunk = chunkType.makeWritableChunk(chunkSize); if (usePrev) { columnSource.fillPrevChunk(context, currentChunk, reducedRowSet); } else { columnSource.fillChunk(context, currentChunk, reducedRowSet); } // add the chunk to the current list result.add(currentChunk); // increment the offset for the next chunk (using the actual values written) offset += currentChunk.size(); // recompute the size of the next chunk if (size - offset > maxChunkSize) { chunkSize = maxChunkSize; } else { chunkSize = (int) (size - offset); } if (sharedContext != null) { // a shared context is good for only one chunk of rows sharedContext.reset(); } } } return result; } /** * Estimate the size of a complete table snapshot in bytes. * * @param table the table to estimate * @return the estimated snapshot size in bytes. */ public static long estimateSnapshotSize(@NotNull final Table table) { final BitSet columns = new BitSet(table.numColumns()); columns.set(0, table.numColumns()); return estimateSnapshotSize(table.getDefinition(), columns, table.size()); } /** * Make a rough guess at the size of a snapshot, using the column types and common column names. The use case is * when a user requests something from the GUI; we'd like to know if it is ridiculous before actually doing it. * * @param tableDefinition the table definition * @param columns a bitset indicating which columns are included * @param rowCount how many rows of this data we'll be snapshotting * @return the estimated size of the snapshot */ public static long estimateSnapshotSize( @NotNull final TableDefinition tableDefinition, @NotNull final BitSet columns, final long rowCount) { long sizePerRow = 0; long totalSize = 0; final int numColumns = tableDefinition.numColumns(); final List> columnDefinitions = tableDefinition.getColumns(); for (int ii = 0; ii < numColumns; ++ii) { if (!columns.get(ii)) { continue; } totalSize += 44; // for an array final ColumnDefinition definition = columnDefinitions.get(ii); if (definition.getDataType() == byte.class || definition.getDataType() == char.class || definition.getDataType() == Boolean.class) { sizePerRow += 1; } else if (definition.getDataType() == short.class) { sizePerRow += 2; } else if (definition.getDataType() == int.class || definition.getDataType() == float.class) { sizePerRow += 4; } else if (definition.getDataType() == long.class || definition.getDataType() == double.class || definition.getDataType() == Instant.class || definition.getDataType() == ZonedDateTime.class) { sizePerRow += 8; } else { switch (definition.getName()) { case "Date": sizePerRow += 5; totalSize += 10; break; case "USym": sizePerRow += 5; totalSize += Math.min(rowCount, 10000) * 10; break; case "Sym": sizePerRow += 5; totalSize += Math.min(rowCount, 1000000) * 30; break; case "Parity": sizePerRow += 5; totalSize += 30; break; case "SecurityType": sizePerRow += 5; totalSize += 100; break; case "Exchange": sizePerRow += 5; totalSize += 130; break; default: sizePerRow += (42 + 8); // how big a dummy object was on a test + a pointer } } } return totalSize + (sizePerRow * rowCount); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy