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

com.indeed.proctor.common.TestRangeSelector Maven / Gradle / Ivy

The newest version!
package com.indeed.proctor.common;

import com.google.common.collect.Maps;
import com.indeed.proctor.common.model.Allocation;
import com.indeed.proctor.common.model.ConsumableTestDefinition;
import com.indeed.proctor.common.model.Range;
import com.indeed.proctor.common.model.TestBucket;
import com.indeed.proctor.common.model.TestDependency;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.el.ExpressionFactory;
import javax.el.FunctionMapper;
import javax.el.ValueExpression;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;

/**
 * This is perhaps not the greatest abstraction the world has seen; is meant to consolidate common
 * functionality needed for different types of choosers WITHOUT using inheritance
 *
 * @author ketan
 */
public class TestRangeSelector {
    private static final Logger LOGGER = LogManager.getLogger(TestRangeSelector.class);

    @Nonnull private final String testName;
    @Nonnull private final ConsumableTestDefinition testDefinition;
    @Nonnull private final String[] rules;
    @Nonnull private final TestBucket[][] rangeToBucket;
    private final RuleEvaluator ruleEvaluator;

    TestRangeSelector(
            @Nonnull final ExpressionFactory expressionFactory,
            @Nonnull final FunctionMapper functionMapper,
            final String testName,
            @Nonnull final ConsumableTestDefinition testDefinition) {
        this(
                new RuleEvaluator(expressionFactory, functionMapper, testDefinition.getConstants()),
                testName,
                testDefinition);
    }

    TestRangeSelector(
            @Nonnull final RuleEvaluator ruleEvaluator,
            @Nonnull final String testName,
            @Nonnull final ConsumableTestDefinition testDefinition) {
        this.ruleEvaluator = ruleEvaluator;

        this.testName = testName;
        this.testDefinition = testDefinition;

        final Map bucketValueToTest = Maps.newHashMap();
        for (final TestBucket testBucket : testDefinition.getBuckets()) {
            bucketValueToTest.put(testBucket.getValue(), testBucket);
        }

        final List allocations = testDefinition.getAllocations();
        this.rangeToBucket = new TestBucket[allocations.size()][];
        this.rules = new String[allocations.size()];
        for (int i = 0; i < allocations.size(); i++) {
            final Allocation allocation = allocations.get(i);
            rules[i] = allocation.getRule();
            final List ranges = allocation.getRanges();
            this.rangeToBucket[i] = new TestBucket[ranges.size()];
            for (int j = 0; j < ranges.size(); j++) {
                this.rangeToBucket[i][j] = bucketValueToTest.get(ranges.get(j).getBucketValue());
            }
        }
    }

    public TestBucket[] getBucketRange(final int index) {
        return rangeToBucket[index];
    }

    /** @deprecated Use findMatchingRuleWithValueExpr(Map, Map) instead, which is more efficient. */
    @Deprecated
    public int findMatchingRule(
            @Nonnull final Map values,
            @Nonnull final Map testGroups,
            @Nullable final String identifier) {
        return findMatchingRuleInternal(
                rule -> ruleEvaluator.evaluateBooleanRule(rule, values), testGroups, identifier);
    }

    public int findMatchingRuleWithValueExpr(
            @Nonnull final Map localContext,
            @Nonnull final Map testGroups,
            @Nullable final String identifier) {
        return findMatchingRuleInternal(
                rule -> ruleEvaluator.evaluateBooleanRuleWithValueExpr(rule, localContext),
                testGroups,
                identifier);
    }

    private int findMatchingRuleInternal(
            final Function evaluator,
            @Nonnull final Map testGroups,
            @Nullable final String identifier) {
        final TestDependency dependsOn = testDefinition.getDependsOn();
        if (dependsOn != null) {
            final TestBucket testBucket = testGroups.get(dependsOn.getTestName());
            if ((testBucket == null) || (testBucket.getValue() != dependsOn.getBucketValue())) {
                return -1;
            }
        }

        @Nullable final String rule = testDefinition.getRule();
        try {
            if (rule != null) {
                if (!evaluator.apply(rule)) {
                    return -1;
                }
            }

            return getMatchingAllocation(evaluator, identifier);
        } catch (final RuntimeException e) {
            LOGGER.error(
                    "Failed to evaluate test rule; ",
                    new InvalidRuleException(
                            e,
                            String.format(
                                    "Error evaluating rule '%s' for test '%s': '%s'. Failing evaluation and continuing.",
                                    rule, testName, e.getMessage())));
        }

        return -1;
    }

    protected int getMatchingAllocation(
            final Function evaluator, @Nullable final String identifier) {
        int i = 0;
        try {
            for (i = 0; i < rules.length; i++) {
                if (evaluator.apply(rules[i])) {
                    return i;
                }
            }
        } catch (final RuntimeException e) {
            LOGGER.error(
                    "Failed to evaluate test allocation rules; ",
                    new InvalidRuleException(
                            e,
                            String.format(
                                    "Error evaluating rule '%s' for test '%s': '%s'. Failing evaluation and continuing.",
                                    rules[i], testName, e.getMessage())));
        }
        return -1;
    }

    @Nonnull
    public String[] getRules() {
        return rules;
    }

    @Nonnull
    public ConsumableTestDefinition getTestDefinition() {
        return testDefinition;
    }

    /**
     * Do not evaluate the rule, do not use the pseudo-random allocation algorithm, do not collect
     * $200. This should ONLY be used by privileged code for debugging.
     *
     * @param value bucket number
     * @return a {@link TestBucket} with the specified value or null if none exists
     */
    @CheckForNull
    public TestBucket getTestBucket(final int value) {
        for (final TestBucket testBucket : testDefinition.getBuckets()) {
            if (testBucket.getValue() == value) {
                return testBucket;
            }
        }
        return null;
    }

    @Nonnull
    public String getTestName() {
        return testName;
    }

    /** appends testbuckets in a notation a bit similar to Json */
    protected void printTestBuckets(
            @Nonnull final PrintWriter writer, @Nonnull final Map parameters) {
        final NumberFormat fmt = NumberFormat.getPercentInstance(Locale.US);
        fmt.setMaximumFractionDigits(2);

        writer.printf("{ ");
        for (final Iterator> iterator = parameters.entrySet().iterator();
                iterator.hasNext(); ) {
            final Entry entry = iterator.next();
            writer.print(entry.getKey());
            writer.print("'");
            writer.print(entry.getValue());
            writer.print("'");
            if (iterator.hasNext()) {
                writer.print(", ");
            }
        }

        if (testDefinition.getRule() != null) {
            writer.printf(", rule='%s'", testDefinition.getRule());
        }
        final String separator;
        if (testDefinition.getAllocations().size() > 1) {
            separator = ",\n\t";
        } else {
            separator = ", ";
        }
        final List allocations = testDefinition.getAllocations();
        for (final Allocation allocation : allocations) {
            writer.print(separator);
            if (allocation.getRule() != null) {
                writer.printf("rule='%s', ", allocation.getRule());
            }
            final List ranges = allocation.getRanges();
            for (int j = 0; j < ranges.size(); j++) {
                // ignoring the trailing comma
                if (j > 0) {
                    writer.print(", ");
                }
                final Range range = ranges.get(j);
                writer.printf(
                        "%s%d='%s'",
                        testName, range.getBucketValue(), fmt.format(range.getLength()));
            }
        }
        writer.print(" },");
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy