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

apoc.index.IndexUpdateTransactionEventHandler Maven / Gradle / Ivy

There is a newer version: 4.4.0.34
Show newest version
package apoc.index;

import apoc.ApocConfiguration;
import apoc.Pools;
import apoc.util.Util;
import org.apache.commons.lang3.time.StopWatch;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.event.LabelEntry;
import org.neo4j.graphdb.event.PropertyEntry;
import org.neo4j.graphdb.event.TransactionData;
import org.neo4j.graphdb.event.TransactionEventHandler;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexManager;
import org.neo4j.helpers.collection.Iterables;
import org.neo4j.kernel.availability.AvailabilityGuard;
import org.neo4j.kernel.internal.GraphDatabaseAPI;
import org.neo4j.logging.Log;

import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static org.neo4j.helpers.collection.Iterables.stream;

/**
 * a transaction event handler that updates manual indexes based on configuration in graph properties
 * based on configuration the updates are process synchronously via {@link #beforeCommit(TransactionData)} or async via
 * {@link #afterCommit(TransactionData, Collection)}
 * @author Stefan Armbruster
 */
public class IndexUpdateTransactionEventHandler extends TransactionEventHandler.Adapter>> {

    private final GraphDatabaseService graphDatabaseService;
    private final boolean async;

    private final BlockingQueue> indexCommandQueue;
    private final boolean stopWatchEnabled;
    private final Log log;
    private Map>>> indexesByLabelAndProperty;
    private ScheduledFuture configUpdateFuture = null;

    // "magic" command for queue to perform a tx rollover
    private static boolean forceTxRolloverFlag = false;
    private static final Consumer FORCE_TX_ROLLOVER = aVoid -> forceTxRolloverFlag = true;

    public IndexUpdateTransactionEventHandler(GraphDatabaseAPI graphDatabaseService, Log log, boolean async, int queueCapacity, boolean stopWatchEnabled) {
        this.graphDatabaseService = graphDatabaseService;
        this.log = log;
        this.async = async;
        this.stopWatchEnabled = stopWatchEnabled;
        this.indexCommandQueue = async ? new LinkedBlockingQueue<>(queueCapacity) : null;
    }

    public BlockingQueue> getIndexCommandQueue() {
        return indexCommandQueue;
    }

    @FunctionalInterface
    interface IndexFunction {
        void apply (A a, B b, C c, D d, E e);
    }

    private  T logDuration(String message, Supplier supplier) {
        if (stopWatchEnabled) {
            StopWatch sw = new StopWatch();
            try {
                sw.start();
                return supplier.get();

            } finally {
                sw.stop();
                log.info(message + " took " + sw.getTime() + " millsi");
            }
        } else {
            return supplier.get();
        }
    }

    @Override
    public Collection> beforeCommit(TransactionData data) throws Exception {

        return logDuration("beforeCommit", () -> {
            getIndexesByLabelAndProperty();
            Collection> state = async ? new LinkedList<>() : null;

            iterateNodePropertyChange(stream(data.assignedNodeProperties()),false, (index, node, key, value, oldValue) -> indexUpdate(state, aVoid -> {
                if (oldValue != null) {
                    index.remove(node, key);
                    index.remove(node, FreeTextSearch.KEY);
                }
                index.add(node, key, value);
                index.add(node, FreeTextSearch.KEY, value);
            }));

            // filter out removedNodeProperties from node deletions
            iterateNodePropertyChange(stream(data.removedNodeProperties()).filter(nodePropertyEntry -> !data.isDeleted(nodePropertyEntry.entity())), true, (index, node, key, value, oldValue) -> indexUpdate(state, aVoid -> {
                index.remove(node, key);
                index.remove(node, FreeTextSearch.KEY);
            }));

            // performance tweak: converted created nodes to a set, so we can apply `contains` on it fast
            final Set createdNodes = Iterables.asSet(data.createdNodes());
            iterateLabelChanges(
                    stream(data.assignedLabels()).filter( labelEntry -> !createdNodes.contains( labelEntry.node() ) ),
                    (index, node, key, value, ignore) -> indexUpdate(state, aVoid -> {
                        index.add(node, key, value);
                        index.add(node, FreeTextSearch.KEY, value);
                    }));

            iterateLabelChanges(
                    stream(data.removedLabels()).filter( labelEntry -> !data.isDeleted(labelEntry.node()) ),
                    (index, node, key, value, ignore) -> indexUpdate(state, aVoid -> {
                        index.remove(node, key);
                        index.remove(node, FreeTextSearch.KEY);
                    }));

            iterateNodeDeletions(stream(data.removedLabels()).filter( labelEntry -> data.isDeleted(labelEntry.node())),
                    (nodeIndex, node, void1, void2, void3) -> indexUpdate(state, aVoid -> nodeIndex.remove(node)));
            return state;
        });
    }

    @Override
    public void afterCommit(TransactionData data, Collection> state) {
        logDuration("afterCommit", () -> {
            if (async) {
                for (Consumer consumer: state) {
                    try {
                        indexCommandQueue.put(consumer);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return null;
        });
    }

    private void iterateNodePropertyChange(Stream> stream, boolean propertyRemoved,
          IndexFunction, Node, String, Object, Object> function) {
        stream.forEach(nodePropertyEntry -> {
            final Node entity = nodePropertyEntry.entity();
            final String key = nodePropertyEntry.key();
            final Object value = propertyRemoved ? null : nodePropertyEntry.value();
            entity.getLabels().forEach(label -> {
                final String labelName = label.name();
                final Map>> propertyIndexMap = indexesByLabelAndProperty.get(labelName);
                if (propertyIndexMap!=null) {
                    final Collection> indices = propertyIndexMap.get(key);
                    if (indices!= null) {
                        for (Index index : indices) {
                            String indexKey = labelName + "." + key;
                            function.apply(index, entity, indexKey, value, nodePropertyEntry.previouslyCommitedValue());
                        }
                    }
                }
            });
        });
    }

    private void iterateLabelChanges(Stream stream, IndexFunction, Node, String, Object, Void> function) {
        stream.forEach(labelEntry -> {
            final String labelName = labelEntry.label().name();
            final Map>> propertyIndicesMap = indexesByLabelAndProperty.get(labelName);
            if (propertyIndicesMap != null) {
                final Node entity = labelEntry.node();
                for (String key : entity.getPropertyKeys()) {
                    Collection> indices = propertyIndicesMap.get(key);
                    if (indices != null) {
                        for (Index index : indices) {
                            Object value = entity.getProperty(key);
                            String indexKey = labelName + "." + key;
                            function.apply(index, entity, indexKey, value, null);
                        }
                    }
                }
            }
        });
    }

    private void iterateNodeDeletions(Stream stream, IndexFunction, Node, Void, Void, Void> function) {
        stream.forEach(labelEntry -> {
            final Map>> propertyIndicesMap = indexesByLabelAndProperty.get(labelEntry.label().name());
            if (propertyIndicesMap!=null) {
                for (Collection> indices: propertyIndicesMap.values()) {
                    for (Index index: indices) {
                        function.apply(index, labelEntry.node(), null, null, null);
                    }
                }
            }
        });
    }

    /**
     * in async mode add the index action to a collection for consumption in {@link #afterCommit(TransactionData, Collection)}, in sync mode, run it directly
     */
    private Void indexUpdate(Collection> state, Consumer indexAction) {
        if (async) {
            state.add(indexAction);
        } else {
            indexAction.accept(null);
        }
        return null;
    }

    public Map>>> getIndexesByLabelAndProperty() {
        if (indexesByLabelAndProperty == null ) {
            indexesByLabelAndProperty = initIndexConfiguration();
        }
        return indexesByLabelAndProperty;
    }

    public void resetConfiguration() {
        indexesByLabelAndProperty = null;
    }

    // might be run from a scheduler, so we need to make sure we have a transaction
    private synchronized Map>>> initIndexConfiguration() {
        Map>>> indexesByLabelAndProperty = new HashMap<>();
        try (Transaction tx = graphDatabaseService.beginTx() ) {

            final IndexManager indexManager = graphDatabaseService.index();
            for (String indexName : indexManager.nodeIndexNames()) {

                final Index index = indexManager.forNodes(indexName);
                Map indexConfig = indexManager.getConfiguration(index);

                if (Util.toBoolean(indexConfig.get("autoUpdate"))) {
                    String labels = indexConfig.getOrDefault("labels", "");
                    for (String label : labels.split(":")) {
                        Map>> propertyKeyToIndexMap = indexesByLabelAndProperty.computeIfAbsent(label, s -> new HashMap<>());
                        String[] keysForLabel = indexConfig.getOrDefault("keysForLabel:" + label, "").split(":");
                        for (String property : keysForLabel) {
                            propertyKeyToIndexMap.computeIfAbsent(property, s -> new ArrayList<>()).add(index);
                        }
                    }
                }
            }
            tx.success();
        }
        return indexesByLabelAndProperty;
    }

    public static class LifeCycle {
        private final GraphDatabaseAPI db;
        private final Log log;
        private IndexUpdateTransactionEventHandler indexUpdateTransactionEventHandler;

        public LifeCycle(GraphDatabaseAPI db, Log log) {
            this.db = db;
            this.log = log;
        }

        public void start() {
            boolean enabled = ApocConfiguration.isEnabled("autoIndex.enabled");
            if (enabled) {
                boolean async = ApocConfiguration.isEnabled("autoIndex.async");
                boolean stopWatchEnabled = ApocConfiguration.isEnabled("autoIndex.tx_handler_stopwatch");
                int queueCapacity = Integer.parseInt(ApocConfiguration.get("autoIndex.queue_capacity", "100000"));
                indexUpdateTransactionEventHandler = new IndexUpdateTransactionEventHandler(db, log, async, queueCapacity, stopWatchEnabled);
                if (async) {
                    startIndexTrackingThread(db, indexUpdateTransactionEventHandler.getIndexCommandQueue(),
                            Long.parseLong(ApocConfiguration.get("autoIndex.async_rollover_opscount", "50000")),
                            Long.parseLong(ApocConfiguration.get("autoIndex.async_rollover_millis", "5000")),
                            log
                    );
                }
                db.registerTransactionEventHandler(indexUpdateTransactionEventHandler);
                long indexConfigUpdateInternal = Util.toLong(ApocConfiguration.get("autoIndex.configUpdateInterval",10l));
                if (indexConfigUpdateInternal > 0) {
                    indexUpdateTransactionEventHandler.startPeriodicIndexConfigChangeUpdates(indexConfigUpdateInternal);
                }
            }
        }
        private void startIndexTrackingThread(GraphDatabaseAPI db, BlockingQueue> indexCommandQueue, long opsCountRollover, long millisRollover, Log log) {
            new Thread(() -> {
                Transaction tx = null;
                try {
                    final AvailabilityGuard availabilityGuard = db.getDependencyResolver().resolveDependency(AvailabilityGuard.class);
                    availabilityGuard.await(60_000);
                    tx = db.beginTx();
                    int opsCount = 0;
                    long lastCommit = System.currentTimeMillis();
                    while (true) {
                        Consumer indexCommand = indexCommandQueue.poll(millisRollover, TimeUnit.MILLISECONDS);

                        if (availabilityGuard.isShutdown()) {
                            log.debug("shutdown in progress. Aborting index tracking thread.");
                            break;
                        }

                        long now = System.currentTimeMillis();
                        if (
                                FORCE_TX_ROLLOVER.equals(indexCommand) ||
                                ((opsCount>0) && ((now - lastCommit > millisRollover) || (opsCount >= opsCountRollover)))

                                ) {
                            tx.success();
                            tx.close();
                            tx = db.beginTx();
                            log.info("background indexing thread doing tx rollover, opscount " + opsCount + ", millis since last rollover " + (now-lastCommit));
                            lastCommit = now;
                            opsCount = 0;
                        }
                        if (indexCommand == null) {
                            // in case we couldn't get anything from queue, we'll update lastcommit to prevent too early commits
                            if (opsCount == 0) {
                                lastCommit = now;
                            }
                        } else {
                            opsCount++;
                            indexCommand.accept(null);
                        }
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                    throw new RuntimeException(e);
                } finally {
                    if (tx!=null) {
                        tx.success();
                        try {
                            tx.close();
                            log.debug("final commit in background thread");
                        } catch (TransactionTerminatedException e) {
                            log.error(e.getMessage(), e);
                        }
                    }
                    log.info("stopping background thread for async index updates");
                }
            }).start();
            log.info("started background thread for async index updates");
        }

        public void stop() {
            if (indexUpdateTransactionEventHandler!=null) {
                db.unregisterTransactionEventHandler(indexUpdateTransactionEventHandler);
                indexUpdateTransactionEventHandler.stopPeriodicIndexConfigChangeUpdates();
            }
        }

        public void resetConfiguration() {
            if (indexUpdateTransactionEventHandler!=null) {
                indexUpdateTransactionEventHandler.resetConfiguration();
            }
        }

        public IndexUpdateTransactionEventHandler getIndexUpdateTransactionEventHandler() {
            return indexUpdateTransactionEventHandler;
        }

    }

    private void startPeriodicIndexConfigChangeUpdates(long indexConfigUpdateInternal) {
        configUpdateFuture = Pools.SCHEDULED.scheduleAtFixedRate(() ->
                indexesByLabelAndProperty = initIndexConfiguration(), indexConfigUpdateInternal, indexConfigUpdateInternal, TimeUnit.SECONDS);
    }

    private void stopPeriodicIndexConfigChangeUpdates() {
        if (configUpdateFuture!=null) {
            configUpdateFuture.cancel(false);
        }
    }

    /**
     * to be used from unit tests to ensure a tx rollover has happenend
     */
    public synchronized void forceTxRollover() {
        if (async) {
            try {
                forceTxRolloverFlag = false;
                indexCommandQueue.put(FORCE_TX_ROLLOVER);
                while (!forceTxRolloverFlag) {
                    Thread.sleep(5);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy