
com.indeed.proctor.common.Proctor Maven / Gradle / Ivy
package com.indeed.proctor.common;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Maps;
import com.indeed.util.varexport.VarExporter;
import com.indeed.proctor.common.model.Audit;
import com.indeed.proctor.common.model.ConsumableTestDefinition;
import com.indeed.proctor.common.model.TestBucket;
import com.indeed.proctor.common.model.TestMatrixArtifact;
import com.indeed.proctor.common.model.TestType;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
import javax.el.ExpressionFactory;
import javax.el.FunctionMapper;
/**
* The sole entry point for client applications determining the test buckets for a particular client. See {@link #determineTestGroups(Identifiers, java.util.Map, java.util.Map)}
* @author ketan
*/
public class Proctor {
public static final Proctor EMPTY = createEmptyProctor();
/**
* Factory method to do the setup and transformation of inputs
*/
@Nonnull
public static Proctor construct(@Nonnull final TestMatrixArtifact matrix, ProctorLoadResult loadResult, FunctionMapper functionMapper) {
final ExpressionFactory expressionFactory = RuleEvaluator.EXPRESSION_FACTORY;
final Map> testChoosers = Maps.newLinkedHashMap();
final Map versions = Maps.newLinkedHashMap();
for (final Entry entry : matrix.getTests().entrySet()) {
final String testName = entry.getKey();
final ConsumableTestDefinition testDefinition = entry.getValue();
final TestType testType = testDefinition.getTestType();
final TestChooser> testChooser;
if (TestType.RANDOM.equals(testType)) {
testChooser = new RandomTestChooser(expressionFactory, functionMapper, testName, testDefinition);
} else {
testChooser = new StandardTestChooser(expressionFactory, functionMapper, testName, testDefinition);
}
testChoosers.put(testName, testChooser);
versions.put(testName, testDefinition.getVersion());
}
return new Proctor(matrix, loadResult, testChoosers);
}
@Nonnull
private static Proctor createEmptyProctor() {
final Audit audit = new Audit();
audit.setUpdated(0);
audit.setUpdatedBy("nobody");
audit.setVersion(Audit.EMPTY_VERSION);
final TestMatrixArtifact testMatrix = new TestMatrixArtifact();
testMatrix.setAudit(audit);
final ProctorLoadResult loadResult = ProctorLoadResult.emptyResult();
final Map> choosers = Collections.emptyMap();
return new Proctor(testMatrix, loadResult, choosers);
}
static final long INT_RANGE = (long) Integer.MAX_VALUE - (long) Integer.MIN_VALUE;
private final TestMatrixArtifact matrix;
private final ProctorLoadResult loadResult;
@Nonnull
private final Map> testChoosers;
private final Map testDefinitions = Maps.newLinkedHashMap();
@VisibleForTesting
Proctor(
final TestMatrixArtifact matrix,
final ProctorLoadResult loadResult,
@Nonnull Map> testChoosers
) {
this.matrix = matrix;
this.loadResult = loadResult;
this.testChoosers = testChoosers;
for (final Entry> entry : testChoosers.entrySet()) {
this.testDefinitions.put(entry.getKey(), entry.getValue().getTestDefinition());
}
VarExporter.forNamespace(Proctor.class.getSimpleName()).includeInGlobal().export(this, "");
VarExporter.forNamespace(DetailedExport.class.getSimpleName()).export(new DetailedExport(), ""); // intentionally not in global
}
class DetailedExport {
/**
* TODO: export useful details about the parsed test matrix
*/
}
/**
* Determine which test buckets apply to a particular client.
* @param testType
* @param identifier a unique-ish {@link String} identifying the client. This should be consistent across requests from the same client.
* @param context a {@link Map} containing variables describing the context in which the request is executing. These will be supplied to any rules that execute to determine test eligibility.
* @return a {@link ProctorResult} containing the test buckets that apply to this client as well as the versions of the tests that were executed
* @deprecated use {@link Proctor#determineTestGroups(Identifiers, java.util.Map, java.util.Map)} instead
*/
@SuppressWarnings("UnusedDeclaration") // TODO Remove deprecated
@Nonnull
public ProctorResult determineTestGroups(final TestType testType, final String identifier, @Nonnull final Map context, @Nonnull final Map forceGroups) {
final Identifiers identifiers = new Identifiers(testType, identifier);
return determineTestGroups(identifiers, context, forceGroups);
}
/**
* Determine which test buckets apply to a particular client.
* @param identifiers a {@link Map} of unique-ish {@link String}s describing the request in the context of different {@link TestType}s.For example,
* {@link TestType#USER} has a CTK associated, {@link TestType#EMAIL} is an email address, {@link TestType#PAGE} might be a url-encoded String
* containing the normalized relevant page parameters
* @param inputContext a {@link Map} containing variables describing the context in which the request is executing. These will be supplied to any rules that
* execute to determine test eligibility.
* @param forceGroups a Map from a String test name to an Integer bucket value. For the specified test allocate the specified bucket (if valid) regardless
* of the standard logic
* @return a {@link ProctorResult} containing the test buckets that apply to this client as well as the versions of the tests that were executed
*/
@Nonnull
public ProctorResult determineTestGroups(@Nonnull final Identifiers identifiers, @Nonnull final Map inputContext, @Nonnull final Map forceGroups) {
return determineTestGroups(identifiers, inputContext, forceGroups, Collections.emptyList());
}
/**
* See determineTestGroups() above. Adds a test name filter for returning a subset of tests.
*
* This is useful for the Proctor REST API. It lacks a specification and needs a way to evaluate
* only the tests mentioned in the HTTP parameters by each particular query. Otherwise, there will be
* logged errors due to missing context variables.
*
* @param testNameFilter Only evaluates and returns the tests named in this collection. If empty, no filter is applied.
*/
@Nonnull
public ProctorResult determineTestGroups(@Nonnull final Identifiers identifiers,
@Nonnull final Map inputContext,
@Nonnull final Map forceGroups,
@Nonnull final Collection testNameFilter) {
final Map testGroups = Maps.newLinkedHashMap();
Map> filteredChoosers = testChoosers;
if (!testNameFilter.isEmpty()) {
filteredChoosers = Maps.filterKeys(filteredChoosers, Predicates.in(testNameFilter));
}
for (final Entry> entry : filteredChoosers.entrySet()) {
final String testName = entry.getKey();
final Integer forceGroupBucket = forceGroups.get(testName);
final TestChooser> testChooser = entry.getValue();
final String identifier;
if (testChooser instanceof StandardTestChooser) {
final TestType testType = testChooser.getTestDefinition().getTestType();
identifier = identifiers.getIdentifier(testType);
if (identifier == null) {
continue;
}
} else {
if (! identifiers.isRandomEnabled()) {
continue;
}
identifier = null;
}
if (forceGroupBucket != null) {
final TestBucket forcedTestBucket = testChooser.getTestBucket(forceGroupBucket);
if (forcedTestBucket != null) {
testGroups.put(testName, forcedTestBucket);
continue;
}
}
final TestBucket testBucket;
if (identifier == null) {
testBucket = ((RandomTestChooser) testChooser).choose(null, inputContext);
} else {
testBucket = ((StandardTestChooser) testChooser).choose(identifier, inputContext);
}
if (testBucket != null) {
testGroups.put(testName, testBucket);
testChooser.getTestDefinition();
}
}
// TODO Can we make getAudit nonnull?
final Audit audit = Preconditions.checkNotNull(matrix.getAudit(), "Missing audit");
return new ProctorResult(audit.getVersion(), testGroups, testDefinitions);
}
TestMatrixArtifact getArtifact() {
return matrix;
}
@SuppressWarnings("UnusedDeclaration") // TODO Needed?
public ConsumableTestDefinition getTestDefinition(final String name) {
return matrix.getTests().get(name);
}
public ProctorLoadResult getLoadResult() {
return loadResult;
}
public void appendAllTests(Writer sb) {
appendTests(sb, Predicates.>alwaysTrue());
}
@SuppressWarnings("UnusedDeclaration") // TODO needed?
public void appendTests(Writer sb, final TestType type) {
appendTests(sb, new Predicate>() {
@Override
public boolean apply(TestChooser> input) {
assert null != input;
return type == input.getTestDefinition().getTestType();
}
});
}
public void appendTestsNameFiltered(final Writer sb, final Collection testNameFilter)
{
final Function, String> getTestName = new Function, String>() {
@Override
public String apply(TestChooser> input) {
return input.getTestName();
}
};
appendTests(sb, Predicates.compose(Predicates.in(testNameFilter), getTestName));
}
public void appendTests(Writer sb, @Nonnull Predicate> shouldIncludeTest) {
final NumberFormat fmt = NumberFormat.getPercentInstance(Locale.US);
fmt.setMaximumFractionDigits(2);
final PrintWriter writer = new PrintWriter(sb);
for (final Entry> entry : testChoosers.entrySet()) {
final String testName = entry.getKey();
final TestChooser> chooser = entry.getValue();
if (shouldIncludeTest.apply(chooser)) {
writer.append(testName).append(" : ");
chooser.printTestBuckets(writer);
writer.println();
}
}
}
public void appendTestMatrix(final Writer writer) throws IOException {
ProctorUtils.serializeArtifact(writer, this.matrix);
}
public void appendTestMatrixFiltered(final Writer writer, final Collection testNameFilter) throws IOException {
// Create new matrix object copied from the old one,
// but keep only the tests with names in testNameFilter.
final TestMatrixArtifact filtered = new TestMatrixArtifact();
filtered.setAudit(this.matrix.getAudit());
filtered.setTests(Maps.filterKeys(this.matrix.getTests(), Predicates.in(testNameFilter)));
ProctorUtils.serializeArtifact(writer, filtered);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy