Skip to content

Commit

Permalink
HSEARCH-3725 Add a reproducer for Gson bug 764
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere authored and fax4ever committed Oct 3, 2019
1 parent 3b91ab8 commit d324194
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 11 deletions.
@@ -0,0 +1,108 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.integrationtest.backend.elasticsearch.gson;

import org.hibernate.search.engine.backend.document.IndexFieldReference;
import org.hibernate.search.engine.backend.document.IndexObjectFieldReference;
import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement;
import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaObjectField;
import org.hibernate.search.integrationtest.backend.tck.testsupport.util.rule.SearchSetupHelper;
import org.hibernate.search.util.impl.test.annotation.TestForIssue;

import org.junit.Rule;
import org.junit.Test;

/**
* Reproducer for HSEARCH-3725,
* which is caused by a bug in Gson: https://github.com/google/gson/issues/764
* <p>
* The bug happens very rarely, so we run the test multiple times,
* but there's no guarantee we will actually reproduce it.
* On my machine (8 CPU cores), I've seen the test run hundreds of times in a row
* without the bug occurring even once.
* But sometimes, if you're lucky, it will occur (I've seen it).
* <p>
* For the bug to occur, we need to:
* <ul>
* <li>Initialize multiple indexes in parallel</li>
* <li>Serialize a mapping, or at least convert it to JsonElements, as part of index initialization</li>
* </ul>
* If we reach some state where two threads are initializing the same Gson TypeAdapter simultaneously,
* there is a small chance FutureTypeAdapter#write(JsonWriter, Object)
* will see a null adapter and throw an exception.
*/
@TestForIssue(jiraKey = "HSEARCH-3725")
public class ElasticsearchGsonConcurrencyIT {

/*
* Please keep these constants reasonably low so that routine builds don't take forever:
* test duration will be proportional to REPRODUCER_ATTEMPTS*INDEX_COUNT_PER_ATTEMPT.
* However, you will need to raise them in your local copy of the code
* to have a high chance of reproducing the bug.
*/
private static final int REPRODUCER_ATTEMPTS = 20;
// This must be at least 2, but you don't need more than the number of CPU cores.
private static final int INDEX_COUNT_PER_ATTEMPT = 4;

private static final String INDEX_NAME_PREFIX = "IndexName_";

@Rule
public SearchSetupHelper setupHelper = new SearchSetupHelper();

@Test
public void repeatedlyStartMultipleIndexesSerializingWithGsonInParallel() {
for ( int i = 0; i < REPRODUCER_ATTEMPTS; i++ ) {
startMultipleIndexesSerializingWithGsonInParallel();
setupHelper.cleanUp();
}
}

private void startMultipleIndexesSerializingWithGsonInParallel() {
SearchSetupHelper.SetupContext setupCtx = setupHelper.start();

for ( int i = 0; i < INDEX_COUNT_PER_ATTEMPT; i++ ) {
setupCtx = setupCtx.withIndex(
INDEX_NAME_PREFIX + i,
ctx -> new IndexMapping( ctx.getSchemaElement() )
);
}

setupCtx.setup();
}

private static class IndexMapping {
final ObjectMapping child;

IndexMapping(IndexSchemaElement root) {
// Two levels of nested properties are necessary to reproduce the Gson bug causing HSEARCH-3725
child = new ObjectMapping( root, "child", 3 );
}
}

private static class ObjectMapping {
final IndexObjectFieldReference self;
final IndexFieldReference<String> text;
final ObjectMapping child;

ObjectMapping(IndexSchemaElement parent, String name, int depth) {
IndexSchemaObjectField objectField = parent.objectField( name );
self = objectField.toReference();
text = objectField.field(
"text",
f -> f.asString()
)
.toReference();
if ( depth > 1 ) {
child = new ObjectMapping( objectField, name, depth - 1 );
}
else {
child = null;
}
}
}

}
Expand Up @@ -86,23 +86,28 @@ private Statement statement(final Statement base, final Description description)
Statement wrapped = new Statement() {
@Override
public void evaluate() throws Throwable {
try ( Closer<RuntimeException> closer = new Closer<>() ) {
try {
base.evaluate();
}
finally {
closer.pushAll(
SearchIntegrationPartialBuildState::closeOnFailure, integrationPartialBuildStates );
integrationPartialBuildStates.clear();
closer.pushAll( SearchIntegration::close, integrations );
integrations.clear();
}
try {
base.evaluate();
}
finally {
cleanUp();
}
}
};
return delegateRule.apply( wrapped, description );
}

public void cleanUp() {
try ( Closer<RuntimeException> closer = new Closer<>() ) {
closer.pushAll(
SearchIntegrationPartialBuildState::closeOnFailure, integrationPartialBuildStates
);
integrationPartialBuildStates.clear();
closer.pushAll( SearchIntegration::close, integrations );
integrations.clear();
}
}

public class SetupContext {

private final ConfigurationPropertyChecker unusedPropertyChecker;
Expand Down

0 comments on commit d324194

Please sign in to comment.