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

org.neo4j.kernel.impl.index.labelscan.NativeLabelScanWriter Maven / Gradle / Ivy

/*
 * Copyright (c) 2018-2020 "Graph Foundation,"
 * Graph Foundation, Inc. [https://graphfoundation.org]
 *
 * This file is part of ONgDB.
 *
 * ONgDB is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
/*
 * Copyright (c) 2002-2020 "Neo4j,"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.kernel.impl.index.labelscan;

import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;

import org.neo4j.index.internal.gbptree.GBPTree;
import org.neo4j.index.internal.gbptree.ValueMerger;
import org.neo4j.index.internal.gbptree.Writer;
import org.neo4j.kernel.api.labelscan.LabelScanWriter;
import org.neo4j.kernel.api.labelscan.NodeLabelUpdate;
import org.neo4j.storageengine.api.schema.LabelScanReader;

import static java.lang.Long.min;
import static java.lang.Math.toIntExact;
import static org.neo4j.kernel.impl.index.labelscan.LabelScanValue.RANGE_SIZE;

/**
 * {@link LabelScanWriter} for {@link NativeLabelScanStore}, or rather an {@link Writer} for its
 * internal {@link GBPTree}.
 * 

* {@link #write(NodeLabelUpdate) updates} are queued up to a maximum batch size and, for performance, * applied in sorted order (by label and node id) when reaches batch size or on {@link #close()}. *

* Updates aren't visible to {@link LabelScanReader readers} immediately, rather when queue happens to be applied. *

* Incoming {@link NodeLabelUpdate updates} are actually modified from representing physical before/after * state to represent logical to-add/to-remove state. These changes are done directly inside the provided * {@link NodeLabelUpdate#getLabelsAfter()} and {@link NodeLabelUpdate#getLabelsBefore()} arrays, * relying on the fact that those arrays are returned in its essential form, instead of copies. * This conversion is done like so mostly to reduce garbage. * * @see PhysicalToLogicalLabelChanges */ class NativeLabelScanWriter implements LabelScanWriter { /** * {@link Comparator} for sorting the node id ranges, used in batches to apply updates in sorted order. */ private static final Comparator UPDATE_SORTER = Comparator.comparingLong( NodeLabelUpdate::getNodeId ); /** * {@link ValueMerger} used for adding label->node mappings, see {@link LabelScanValue#add(LabelScanValue)}. */ private final ValueMerger addMerger; /** * {@link ValueMerger} used for removing label->node mappings, see {@link LabelScanValue#remove(LabelScanValue)}. */ private final ValueMerger removeMerger; private final WriteMonitor monitor; /** * {@link Writer} acquired when acquiring this {@link NativeLabelScanWriter}, * acquired from {@link GBPTree#writer()}. */ private Writer writer; /** * Instance of {@link LabelScanKey} acting as place to read keys into and also to set for each applied update. */ private final LabelScanKey key = new LabelScanKey(); /** * Instance of {@link LabelScanValue} acting as place to read values into and also to update * for each applied update. */ private final LabelScanValue value = new LabelScanValue(); /** * Batch currently building up as {@link #write(NodeLabelUpdate) updates} come in. Cursor for where * to place new updates is {@link #pendingUpdatesCursor}. Length of this queue is decided in constructor * and defines the maximum batch size. */ private final NodeLabelUpdate[] pendingUpdates; /** * Cursor into {@link #pendingUpdates}, where to place new {@link #write(NodeLabelUpdate) updates}. * When full the batch is applied and this cursor reset to {@code 0}. */ private int pendingUpdatesCursor; /** * There are two levels of batching, one for {@link NodeLabelUpdate updates} and one when applying. * This variable helps keeping track of the second level where updates to the actual {@link GBPTree} * are batched per node id range, i.e. to add several labelId->nodeId mappings falling into the same * range, all of those updates are made into one {@link LabelScanValue} and then issues as one update * to the tree. There are additions and removals, this variable keeps track of which. */ private boolean addition; /** * When applying {@link NodeLabelUpdate updates} (when batch full or in {@link #close()}), updates are * applied labelId by labelId. All updates are scanned through multiple times, with one label in mind at a time. * For each round the current round tries to figure out which is the closest higher labelId to apply * in the next round. This variable keeps track of that next labelId. */ private long lowestLabelId; interface WriteMonitor { default void range( long range, int labelId ) { } default void prepareAdd( long txId, int offset ) { } default void prepareRemove( long txId, int offset ) { } default void mergeAdd( LabelScanValue existingValue, LabelScanValue newValue ) { } default void mergeRemove( LabelScanValue existingValue, LabelScanValue newValue ) { } default void flushPendingUpdates() { } default void writeSessionEnded() { } default void force() { } default void close() { } } static WriteMonitor EMPTY = new WriteMonitor() { }; NativeLabelScanWriter( int batchSize, WriteMonitor monitor ) { this.pendingUpdates = new NodeLabelUpdate[batchSize]; this.addMerger = ( existingKey, newKey, existingValue, newValue ) -> { monitor.mergeAdd( existingValue, newValue ); return existingValue.add( newValue ); }; this.removeMerger = ( existingKey, newKey, existingValue, newValue ) -> { monitor.mergeRemove( existingValue, newValue ); return existingValue.remove( newValue ); }; this.monitor = monitor; } NativeLabelScanWriter initialize( Writer writer ) { this.writer = writer; this.pendingUpdatesCursor = 0; this.addition = false; this.lowestLabelId = Long.MAX_VALUE; return this; } /** * Queues a {@link NodeLabelUpdate} to this writer for applying when batch gets full, * or when {@link #close() closing}. */ @Override public void write( NodeLabelUpdate update ) throws IOException { if ( pendingUpdatesCursor == pendingUpdates.length ) { flushPendingChanges(); } pendingUpdates[pendingUpdatesCursor++] = update; PhysicalToLogicalLabelChanges.convertToAdditionsAndRemovals( update ); checkNextLabelId( update.getLabelsBefore() ); checkNextLabelId( update.getLabelsAfter() ); } private void checkNextLabelId( long[] labels ) { if ( labels.length > 0 && labels[0] != -1 ) { lowestLabelId = min( lowestLabelId, labels[0] ); } } private void flushPendingChanges() throws IOException { Arrays.sort( pendingUpdates, 0, pendingUpdatesCursor, UPDATE_SORTER ); monitor.flushPendingUpdates(); long currentLabelId = lowestLabelId; value.clear(); key.clear(); while ( currentLabelId != Long.MAX_VALUE ) { long nextLabelId = Long.MAX_VALUE; for ( int i = 0; i < pendingUpdatesCursor; i++ ) { NodeLabelUpdate update = pendingUpdates[i]; long nodeId = update.getNodeId(); nextLabelId = extractChange( update.getLabelsAfter(), currentLabelId, nodeId, nextLabelId, true, update.getTxId() ); nextLabelId = extractChange( update.getLabelsBefore(), currentLabelId, nodeId, nextLabelId, false, update.getTxId() ); } currentLabelId = nextLabelId; } flushPendingRange(); pendingUpdatesCursor = 0; } private long extractChange( long[] labels, long currentLabelId, long nodeId, long nextLabelId, boolean addition, long txId ) throws IOException { long foundNextLabelId = nextLabelId; for ( int li = 0; li < labels.length; li++ ) { long labelId = labels[li]; if ( labelId == -1 ) { break; } // Have this check here so that we can pick up the next labelId in our change set if ( labelId == currentLabelId ) { change( currentLabelId, nodeId, addition, txId ); // We can do a little shorter check for next labelId here straight away, // we just check the next if it's less than what we currently think is next labelId // and then break right after if ( li + 1 < labels.length && labels[li + 1] != -1 ) { long nextLabelCandidate = labels[li + 1]; if ( nextLabelCandidate < currentLabelId ) { throw new IllegalArgumentException( "The node label update contained unsorted label ids " + Arrays.toString( labels ) ); } if ( nextLabelCandidate > currentLabelId ) { foundNextLabelId = min( foundNextLabelId, nextLabelCandidate ); } } break; } else if ( labelId > currentLabelId ) { foundNextLabelId = min( foundNextLabelId, labelId ); } } return foundNextLabelId; } private void change( long currentLabelId, long nodeId, boolean add, long txId ) throws IOException { int labelId = toIntExact( currentLabelId ); long idRange = rangeOf( nodeId ); if ( labelId != key.labelId || idRange != key.idRange || addition != add ) { flushPendingRange(); // Set key to current and reset value key.labelId = labelId; key.idRange = idRange; addition = add; monitor.range( idRange, labelId ); } int offset = toIntExact( nodeId % RANGE_SIZE ); value.set( offset ); if ( addition ) { monitor.prepareAdd( txId, offset ); } else { monitor.prepareRemove( txId, offset ); } } private void flushPendingRange() throws IOException { if ( value.bits != 0 ) { // There are changes in the current range, flush them writer.merge( key, value, addition ? addMerger : removeMerger ); // TODO: after a remove we could check if the tree value is empty and if so remove it from the index // hmm, or perhaps that could be a feature of ValueAmender? value.clear(); } } private static long rangeOf( long nodeId ) { return nodeId / RANGE_SIZE; } /** * Applies {@link #write(NodeLabelUpdate) queued updates} which has not not yet been applied. * After this call no more {@link #write(NodeLabelUpdate)} can be applied. */ @Override public void close() throws IOException { try { flushPendingChanges(); monitor.writeSessionEnded(); } finally { writer.close(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy