com.indeed.proctor.common.TestChooser Maven / Gradle / Ivy
The newest version!
package com.indeed.proctor.common;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.indeed.proctor.common.model.Allocation;
import com.indeed.proctor.common.model.ConsumableTestDefinition;
import com.indeed.proctor.common.model.Payload;
import com.indeed.proctor.common.model.Range;
import com.indeed.proctor.common.model.TestBucket;
import com.indeed.proctor.common.model.TestType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.el.ValueExpression;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
interface TestChooser {
void printTestBuckets(@Nonnull final PrintWriter writer);
@Nullable
TestBucket getTestBucket(final int value);
@Nonnull
String[] getRules();
@Nonnull
ConsumableTestDefinition getTestDefinition();
@Nonnull
String getTestName();
/**
* Do not directly call this outside this interface. We should call {@link #choose(Object, Map,
* Map, ForceGroupsOptions)}, instead.
*/
@Nonnull
TestChooser.Result chooseInternal(
@Nullable IdentifierType identifier,
@Nonnull final Map localContext,
@Nonnull Map testGroups);
@Nonnull
TestChooser.Result choose(
@Nullable final String identifier,
@Nonnull final Map localContext,
@Nonnull final Map testGroups,
@Nonnull final ForceGroupsOptions forceGroupsOptions,
@Nonnull final Set testTypesWithInvalidIdentifier,
final boolean isRandomEnabled);
@Nonnull
default TestChooser.Result choose(
@Nullable final IdentifierType identifier,
@Nonnull final Map localContext,
@Nonnull final Map testGroups,
@Nonnull final ForceGroupsOptions forceGroupsOptions) {
final String testName = getTestName();
final Optional forceGroupBucket =
forceGroupsOptions.getForcedBucketValue(testName);
if (forceGroupBucket.isPresent()) {
final TestBucket forcedTestBucket = getTestBucket(forceGroupBucket.get());
if (forcedTestBucket != null) {
final Optional forcePayloadValues =
forceGroupsOptions.getForcedPayloadValue(testName);
final Payload currentPayload = forcedTestBucket.getPayload();
if (currentPayload != null
&& currentPayload != Payload.EMPTY_PAYLOAD
&& forcePayloadValues.isPresent()) {
final TestBucket forcedTestBucketWithForcedPayload =
TestBucket.builder()
.from(forcedTestBucket)
.payload(
validateForcePayload(
currentPayload, forcePayloadValues.get()))
.build();
return new Result(forcedTestBucketWithForcedPayload, null);
}
// use a forced bucket, skip choosing an allocation
return new Result(forcedTestBucket, null);
}
}
if (forceGroupsOptions.getDefaultMode().equals(ForceGroupsDefaultMode.FALLBACK)) {
// skip choosing a test bucket and an allocation
return Result.EMPTY;
}
final TestChooser.Result result = chooseInternal(identifier, localContext, testGroups);
if (forceGroupsOptions.getDefaultMode().equals(ForceGroupsDefaultMode.MIN_LIVE)) {
// replace the bucket with the minimum active bucket in the resolved allocation.
return Optional.ofNullable(result.getAllocation())
.map(Allocation::getRanges)
.map(Collection::stream)
.orElse(Stream.empty())
.filter(
allocationRange ->
allocationRange.getLength()
> 0) // filter out 0% allocation ranges
.map(Range::getBucketValue)
.min(Integer::compareTo) // find the minimum bucket value
.flatMap(
minActiveBucketValue ->
Optional.ofNullable(getTestBucket(minActiveBucketValue)))
.map(minActiveBucket -> new Result(minActiveBucket, null))
.orElse(Result.EMPTY); // skip choosing a test bucket if failed to find the
// minimum active bucket
}
return result;
}
default Payload validateForcePayload(final Payload currentPayload, final Payload forcePayload) {
// check if force payload exists and has the same payload type as the current payload
if (forcePayload.sameType(currentPayload)) {
// Payload type json currently not supported
if (!Payload.hasType(forcePayload, PayloadType.JSON)) {
if (Payload.hasType(forcePayload, PayloadType.MAP)) {
return validateForcePayloadMap(currentPayload, forcePayload);
}
return forcePayload;
}
}
return currentPayload;
}
/*
* Validated Force Payload Map by checking that each forced key exists in the current payload and is of the same instance type. If forcePayload is invalid return currentPayload to not overwrite
*/
@Nullable
default Payload validateForcePayloadMap(
@Nullable final Payload currentPayload, @Nullable final Payload forcePayload) {
final Map currentPayloadMap = currentPayload.getMap();
final Map forcePayloadMap = forcePayload.getMap();
final ObjectMapper objectMapper = new ObjectMapper();
if (currentPayloadMap != null && forcePayloadMap != null) {
final Map validatedMap = new HashMap<>(currentPayloadMap);
for (final String keyString : forcePayloadMap.keySet()) {
if (currentPayloadMap.containsKey(keyString)) {
try {
final Object forcePayloadValue = forcePayloadMap.get(keyString);
// check current class of value and try to parse force value to it. force
// values are strings before validation
if (currentPayloadMap.get(keyString) instanceof Double) {
validatedMap.put(keyString, forcePayloadValue);
} else if (currentPayloadMap.get(keyString) instanceof Double[]) {
validatedMap.put(
keyString,
((ArrayList) forcePayloadValue).toArray(new Double[0]));
} else if (currentPayloadMap.get(keyString) instanceof Long) {
// ObjectMapper reads in as Object and automatically chooses Integer
// over Long this recasts to Long
validatedMap.put(keyString, Long.valueOf((Integer) forcePayloadValue));
} else if (currentPayloadMap.get(keyString) instanceof Long[]) {
// ObjectMapper reads in as Object and automatically chooses Integer[]
// over Long[] this recasts to Long[]
validatedMap.put(
keyString,
objectMapper.readValue(
objectMapper.writeValueAsString(forcePayloadValue),
Long[].class));
} else if (currentPayloadMap.get(keyString) instanceof String) {
validatedMap.put(keyString, forcePayloadValue);
} else if (currentPayloadMap.get(keyString) instanceof String[]) {
validatedMap.put(
keyString,
((ArrayList) forcePayloadValue).toArray(new String[0]));
} else {
return currentPayload;
}
} catch (final IllegalArgumentException
| ArrayStoreException
| ClassCastException
| IOException e) {
return currentPayload;
}
} else {
return currentPayload;
}
}
return new Payload(validatedMap);
}
return currentPayload;
}
/** Models a result of an assigned bucket and allocation by {@code TestChooser}. */
class Result {
/**
* Empty result (no chosen buckets or no chosen allocations) which is typically used when 1:
* all allocation rules aren't matched to a context, and 2: forcing to use a fallback
* bucket.
*/
public static final Result EMPTY = new Result(null, null);
@Nullable private final TestBucket testBucket;
@Nullable private final Allocation allocation;
Result(@Nullable final TestBucket testBucket, @Nullable final Allocation allocation) {
this.testBucket = testBucket;
this.allocation = allocation;
}
/**
* Returns a chosen test in {@code TestChooser}. Returns null if any bucket isn't chosen.
*/
@Nullable
public TestBucket getTestBucket() {
return testBucket;
}
/**
* Returns a matched allocation in {@code TestChooser}. Returns null if any rules isn't
* matched.
*/
@Nullable
public Allocation getAllocation() {
return allocation;
}
}
}