Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes a race issue with index population and concurrent updates
After an index have been fully populated it will now flip into an OnlineIndexProxy which have a forced idempotency mode instead of the normal mode. The next restart and clean start of such an online index will use the normal mode. More details as to why this is can be found in OnlineIndexProxy, copied into this commit message: There are two online "modes", you might say... - One is the pure starting of an already online index which was cleanly shut down and all that. This scenario is simple and doesn't need this idempotency mode. - The other is the creation or starting from an uncompleted population, where there will be a point in the future where this index will flip from a populating index proxy to an online index proxy. This is the problematic part. You see... we have been accidentally relying on the short-lived node entity locks for this to work. The scenario where they have saved indexes from getting duplicate nodes in them (one from populator and the other from a "normal" update is where a populator is nearing its completion and wants to flip. Another thread is in the middle of applying a transaction which in the end will feed an update to this index. Index updates are applied after store updates, so the populator may see the created node and add it, flips and then the updates comes in to the normal online index and gets added again. The read lock here will have the populator wait for the transaction to fully apply, e.g. also wait for the index update to reach the population job before adding that node and flipping (the update mechanism in a populator is idempotent). This strategy has changed slightly in 3.0 where transactions can be applied in whole batches and index updates for the whole batch will be applied in the end. This is for everything except the above scenario because the short-lived entity locks are per transaction, not per batch, and must be so to not interfere with transactions creating constraints inside this batch. We do need to apply index updates in batches because nowadays slave update pulling and application isn't special in any way, it's simply applying transactions in batches and this needs to be very fast to not have instances fall behind in a cluster. So the sum of this is that during the session (until the next restart of the db) an index gets created it will be in this forced idempotency mode where it applies additions idempotently, which may be slightly more costly, but shouldn't make that big of a difference hopefully.
- Loading branch information
Showing
13 changed files
with
241 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
community/neo4j/src/test/java/schema/IndexPopulationFlipRaceIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,158 @@ | |||
/* | |||
* Copyright (c) 2002-2016 "Neo Technology," | |||
* Network Engine for Objects in Lund AB [http://neotechnology.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 <http://www.gnu.org/licenses/>. | |||
*/ | |||
package schema; | |||
|
|||
import org.junit.Rule; | |||
import org.junit.Test; | |||
|
|||
import org.neo4j.graphdb.Label; | |||
import org.neo4j.graphdb.Node; | |||
import org.neo4j.graphdb.Transaction; | |||
import org.neo4j.helpers.collection.Pair; | |||
import org.neo4j.kernel.api.KernelAPI; | |||
import org.neo4j.kernel.api.KernelTransaction; | |||
import org.neo4j.kernel.api.Statement; | |||
import org.neo4j.kernel.api.index.IndexDescriptor; | |||
import org.neo4j.test.DatabaseRule; | |||
import org.neo4j.test.EmbeddedDatabaseRule; | |||
import org.neo4j.test.RandomRule; | |||
|
|||
import static org.junit.Assert.assertEquals; | |||
|
|||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
|
|||
import static org.neo4j.graphdb.Label.label; | |||
|
|||
public class IndexPopulationFlipRaceIT | |||
{ | |||
private static final int NODES_PER_INDEX = 10; | |||
|
|||
@Rule | |||
public final DatabaseRule db = new EmbeddedDatabaseRule(); | |||
@Rule | |||
public final RandomRule random = new RandomRule(); | |||
|
|||
@Test | |||
public void shouldAtomicallyFlipMultipleIndexes() throws Exception | |||
{ | |||
// A couple of times since this is probabilistic, but also because there seems to be a difference | |||
// in timings between the first time and all others... which is perhaps super obvious to some, but not to me. | |||
for ( int i = 0; i < 10; i++ ) | |||
{ | |||
// GIVEN | |||
createIndexesButDontWaitForThemToFullyPopulate( i ); | |||
|
|||
// WHEN | |||
Pair<long[],long[]> data = createDataThatGoesIntoToThoseIndexes( i ); | |||
|
|||
// THEN | |||
awaitIndexes(); | |||
verifyThatThereAreExactlyOneIndexEntryPerNodeInTheIndexes( i, data ); | |||
} | |||
} | |||
|
|||
private void awaitIndexes() | |||
{ | |||
try ( Transaction tx = db.beginTx() ) | |||
{ | |||
db.schema().awaitIndexesOnline( 10, SECONDS ); | |||
tx.success(); | |||
} | |||
} | |||
|
|||
private void createIndexesButDontWaitForThemToFullyPopulate( int i ) | |||
{ | |||
try ( Transaction tx = db.beginTx() ) | |||
{ | |||
db.schema().indexFor( labelA( i ) ).on( keyA( i ) ).create(); | |||
|
|||
if ( random.nextBoolean() ) | |||
{ | |||
db.schema().indexFor( labelB( i ) ).on( keyB( i ) ).create(); | |||
} | |||
else | |||
{ | |||
db.schema().constraintFor( labelB( i ) ).assertPropertyIsUnique( keyB( i ) ).create(); | |||
} | |||
tx.success(); | |||
} | |||
} | |||
|
|||
private String keyB( int i ) | |||
{ | |||
return "key_b" + i; | |||
} | |||
|
|||
private Label labelB( int i ) | |||
{ | |||
return label( "Label_b" + i ); | |||
} | |||
|
|||
private String keyA( int i ) | |||
{ | |||
return "key_a" + i; | |||
} | |||
|
|||
private Label labelA( int i ) | |||
{ | |||
return label( "Label_a" + i ); | |||
} | |||
|
|||
private Pair<long[],long[]> createDataThatGoesIntoToThoseIndexes( int i ) | |||
{ | |||
long[] dataA = new long[NODES_PER_INDEX]; | |||
long[] dataB = new long[NODES_PER_INDEX]; | |||
for ( int t = 0; t < NODES_PER_INDEX; t++ ) | |||
{ | |||
try ( Transaction tx = db.beginTx() ) | |||
{ | |||
Node nodeA = db.createNode( labelA( i ) ); | |||
nodeA.setProperty( keyA( i ), dataA[t] = nodeA.getId() ); | |||
Node nodeB = db.createNode( labelB( i ) ); | |||
nodeB.setProperty( keyB( i ), dataB[t] = nodeB.getId() ); | |||
tx.success(); | |||
} | |||
} | |||
return Pair.of( dataA, dataB ); | |||
} | |||
|
|||
private void verifyThatThereAreExactlyOneIndexEntryPerNodeInTheIndexes( int i, Pair<long[],long[]> data ) | |||
throws Exception | |||
{ | |||
try ( KernelTransaction tx = db.getDependencyResolver().resolveDependency( KernelAPI.class ).newTransaction(); | |||
Statement statement = tx.acquireStatement() ) | |||
{ | |||
int labelAId = statement.readOperations().labelGetForName( labelA( i ).name() ); | |||
int keyAId = statement.readOperations().propertyKeyGetForName( keyA( i ) ); | |||
int labelBId = statement.readOperations().labelGetForName( labelB( i ).name() ); | |||
int keyBId = statement.readOperations().propertyKeyGetForName( keyB( i ) ); | |||
|
|||
for ( int j = 0; j < NODES_PER_INDEX; j++ ) | |||
{ | |||
long nodeAId = data.first()[j]; | |||
assertEquals( 1, statement.readOperations().nodesCountIndexed( | |||
new IndexDescriptor( labelAId, keyAId ), nodeAId, nodeAId ) ); | |||
long nodeBId = data.other()[j]; | |||
assertEquals( 1, statement.readOperations().nodesCountIndexed( | |||
new IndexDescriptor( labelBId, keyBId ), nodeBId, nodeBId ) ); | |||
} | |||
} | |||
} | |||
} |