Skip to content

Commit

Permalink
Make random UUIDs reproducible in tests
Browse files Browse the repository at this point in the history
Today we use a random source of UUIDs for assigning allocation IDs,
cluster IDs, etc. Yet, the source of randomness for this is not
reproducible in tests. Since allocation IDs end up as keys in hash maps,
this means allocation decisions and not reproducible in tests and this
leads to non-reproducible test failures. This commit modifies the
behavior of random UUIDs so that they are reproducible under tests. The
behavior for production code is not changed, we still use a true source
of secure randomness but under tests we just use a reproducible source
of non-secure randomness.

It is important to note that there is a test,
UUIDTests#testThreadedRandomUUID that relies on the UUIDs being truly
random. Thus, we have to modify the setup for this test to use a true
source of randomness. Thus, this is one test that will never be
reproducible but it is intentionally so.

Relates #18808
  • Loading branch information
jasontedor authored Jun 10, 2016
1 parent 43e07c0 commit a25b8ee
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

package org.elasticsearch.common;


import java.io.IOException;
import java.util.Base64;
import java.util.Random;

Expand All @@ -32,7 +30,7 @@ class RandomBasedUUIDGenerator implements UUIDGenerator {
*/
@Override
public String getBase64UUID() {
return getBase64UUID(SecureRandomHolder.INSTANCE);
return getBase64UUID(Randomness.getSecure());
}

/**
Expand All @@ -49,12 +47,13 @@ public String getBase64UUID(Random random) {
* stamp (bits 4 through 7 of the time_hi_and_version field).*/
randomBytes[6] &= 0x0f; /* clear the 4 most significant bits for the version */
randomBytes[6] |= 0x40; /* set the version to 0100 / 0x40 */
/* Set the variant:

/* Set the variant:
* The high field of th clock sequence multiplexed with the variant.
* We set only the MSB of the variant*/
randomBytes[8] &= 0x3f; /* clear the 2 most significant bits */
randomBytes[8] |= 0x80; /* set the variant (MSB is set)*/
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}

}
39 changes: 36 additions & 3 deletions core/src/main/java/org/elasticsearch/common/Randomness.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import org.elasticsearch.common.settings.Settings;

import java.lang.reflect.Method;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.List;
import java.util.Random;
Expand All @@ -44,6 +47,7 @@
* DiscoveryService#NODE_ID_SEED_SETTING)).
*/
public final class Randomness {

private static final Method currentMethod;
private static final Method getRandomMethod;

Expand Down Expand Up @@ -72,7 +76,7 @@ private Randomness() {}
* @param setting the setting to access the seed
* @return a reproducible source of randomness
*/
public static Random get(Settings settings, Setting<Long> setting) {
public static Random get(final Settings settings, final Setting<Long> setting) {
if (setting.exists(settings)) {
return new Random(setting.get(settings));
} else {
Expand All @@ -98,7 +102,7 @@ public static Random get(Settings settings, Setting<Long> setting) {
public static Random get() {
if (currentMethod != null && getRandomMethod != null) {
try {
Object randomizedContext = currentMethod.invoke(null);
final Object randomizedContext = currentMethod.invoke(null);
return (Random) getRandomMethod.invoke(randomizedContext);
} catch (ReflectiveOperationException e) {
// unexpected, bail
Expand All @@ -109,13 +113,42 @@ public static Random get() {
}
}

/**
* Provides a source of randomness that is reproducible when
* running under the Elasticsearch test suite, and otherwise
* produces a non-reproducible source of secure randomness.
* Reproducible sources of randomness are created when the system
* property "tests.seed" is set and the security policy allows
* reading this system property. Otherwise, non-reproducible
* sources of secure randomness are created.
*
* @return a source of randomness
* @throws IllegalStateException if running tests but was not able
* to acquire an instance of Random from
* RandomizedContext or tests are
* running but tests.seed is not set
*/
public static Random getSecure() {
if (currentMethod != null && getRandomMethod != null) {
return get();
} else {
return getSecureRandomWithoutSeed();
}
}

@SuppressForbidden(reason = "ThreadLocalRandom is okay when not running tests")
private static Random getWithoutSeed() {
assert currentMethod == null && getRandomMethod == null : "running under tests but tried to create non-reproducible random";
return ThreadLocalRandom.current();
}

public static void shuffle(List<?> list) {
private static SecureRandom getSecureRandomWithoutSeed() {
assert currentMethod == null && getRandomMethod == null : "running under tests but tried to create non-reproducible random";
return SecureRandomHolder.INSTANCE;
}

public static void shuffle(final List<?> list) {
Collections.shuffle(list, get());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.index.Index;
import org.elasticsearch.test.ESTestCase;
import org.junit.BeforeClass;

import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -54,11 +55,17 @@ public class ClusterChangedEventTests extends ESTestCase {

private static final ClusterName TEST_CLUSTER_NAME = new ClusterName("test");
private static final String NODE_ID_PREFIX = "node_";
private static final String INITIAL_CLUSTER_ID = UUIDs.randomBase64UUID();
// the initial indices which every cluster state test starts out with
private static final List<Index> initialIndices = Arrays.asList(new Index("idx1", UUIDs.randomBase64UUID()),
new Index("idx2", UUIDs.randomBase64UUID()),
new Index("idx3", UUIDs.randomBase64UUID()));
private static String INITIAL_CLUSTER_ID;
private static List<Index> initialIndices;

@BeforeClass
public static void beforeClass() {
INITIAL_CLUSTER_ID = UUIDs.randomBase64UUID();
// the initial indices which every cluster state test starts out with
initialIndices = Arrays.asList(new Index("idx1", UUIDs.randomBase64UUID()),
new Index("idx2", UUIDs.randomBase64UUID()),
new Index("idx3", UUIDs.randomBase64UUID()));
}

/**
* Test basic properties of the ClusterChangedEvent class:
Expand Down
17 changes: 15 additions & 2 deletions core/src/test/java/org/elasticsearch/common/UUIDTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import org.elasticsearch.test.ESTestCase;

import java.security.SecureRandom;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

public class UUIDTests extends ESTestCase {
Expand All @@ -41,7 +43,18 @@ public void testThreadedTimeUUID() {
}

public void testThreadedRandomUUID() {
testUUIDThreaded(randomUUIDGen);
// we can not use a reproducible source of randomness for this
// test, the test explicitly relies on each thread having a
// unique source of randomness; thus, we fake what production
// code does when using a RandomBasedUUIDGenerator
testUUIDThreaded(new RandomBasedUUIDGenerator() {
private final SecureRandom sr = SecureRandomHolder.INSTANCE;

@Override
public String getBase64UUID() {
return getBase64UUID(sr);
}
});
}

Set<String> verifyUUIDSet(int count, UUIDGenerator uuidSource) {
Expand Down Expand Up @@ -98,6 +111,6 @@ public void testUUIDThreaded(UUIDGenerator uuidSource) {
for (UUIDGenRunner runner : runners) {
globalSet.addAll(runner.uuidSet);
}
assertEquals(count*uuids, globalSet.size());
assertEquals(count * uuids, globalSet.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,30 @@
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
import org.junit.BeforeClass;

import static org.hamcrest.Matchers.containsString;

/**
* Tests that indexing from an index back into itself fails the request.
*/
public class ReindexSameIndexTests extends ESTestCase {
private static final ClusterState STATE = ClusterState.builder(new ClusterName("test")).metaData(MetaData.builder()
.put(index("target", "target_alias", "target_multi"), true)
.put(index("target2", "target_multi"), true)
.put(index("foo"), true)
.put(index("bar"), true)
.put(index("baz"), true)
.put(index("source", "source_multi"), true)
.put(index("source2", "source_multi"), true)).build();

private static ClusterState STATE;
private static final IndexNameExpressionResolver INDEX_NAME_EXPRESSION_RESOLVER = new IndexNameExpressionResolver(Settings.EMPTY);
private static final AutoCreateIndex AUTO_CREATE_INDEX = new AutoCreateIndex(Settings.EMPTY, INDEX_NAME_EXPRESSION_RESOLVER);
private static AutoCreateIndex AUTO_CREATE_INDEX = new AutoCreateIndex(Settings.EMPTY, INDEX_NAME_EXPRESSION_RESOLVER);

@BeforeClass
public static void beforeClass() {
STATE = ClusterState.builder(new ClusterName("test")).metaData(MetaData.builder()
.put(index("target", "target_alias", "target_multi"), true)
.put(index("target2", "target_multi"), true)
.put(index("foo"), true)
.put(index("bar"), true)
.put(index("baz"), true)
.put(index("source", "source_multi"), true)
.put(index("source2", "source_multi"), true)).build();
}

public void testObviousCases() throws Exception {
fails("target", "target");
Expand Down

0 comments on commit a25b8ee

Please sign in to comment.