Skip to content

Commit f71a9b1

Browse files
authored
Merge pull request #1387 from guardian/tf-handle-missing-variants
Handle deleted variants in bandit data
2 parents a7aa51b + 6605689 commit f71a9b1

File tree

5 files changed

+41
-12
lines changed

5 files changed

+41
-12
lines changed

src/server/selection/banditData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async function getBanditSamplesForTest(
7777
return parsedResults.data;
7878
}
7979

80-
interface BanditVariantData {
80+
export interface BanditVariantData {
8181
variantName: string;
8282
mean: number;
8383
}

src/server/selection/epsilonGreedySelection.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,18 @@ describe('selectVariantWithHighestMean', () => {
126126
jest.spyOn(global.Math, 'random').mockReturnValue(0.8);
127127
expect(selectVariantWithHighestMean(banditData, epicTest)?.name).toEqual('v3');
128128
});
129+
130+
it('should ignore variants in bandit data that are not in the test configuration', () => {
131+
const banditData = {
132+
testName: 'example-1',
133+
sortedVariants: [
134+
{ variantName: 'ghost', mean: 2 }, // not in test.variants
135+
{ variantName: 'v1', mean: 1 },
136+
{ variantName: 'v2', mean: 0.5 },
137+
{ variantName: 'v3', mean: 0.3 },
138+
],
139+
};
140+
const result = selectVariantWithHighestMean(banditData, epicTest);
141+
expect(result?.name).toEqual('v1');
142+
});
129143
});

src/server/selection/epsilonGreedySelection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Test, Variant } from '../../shared/types';
22
import { putMetric } from '../utils/cloudwatch';
33
import { logError } from '../utils/logging';
44
import type { BanditData } from './banditData';
5-
import { selectRandomVariant } from './helpers';
5+
import { filterValidVariants, selectRandomVariant } from './helpers';
66

77
/**
88
* In general we select the best known variant, except with probability 'epsilon' when we select at random.
@@ -13,7 +13,8 @@ export function selectVariantWithHighestMean<V extends Variant, T extends Test<V
1313
testBanditData: BanditData,
1414
test: T,
1515
): V | undefined {
16-
const { sortedVariants } = testBanditData;
16+
const sortedVariants = filterValidVariants(testBanditData.sortedVariants, test);
17+
1718
// variants array is sorted by mean, use just the best variants
1819
const bestMean = sortedVariants[0].mean;
1920
const bestVariants = sortedVariants.findIndex((variant) => variant.mean < bestMean);

src/server/selection/helpers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import seedrandom from 'seedrandom';
22
import type { Test, Variant } from '../../shared/types';
33
import { putMetric } from '../utils/cloudwatch';
44
import { logError } from '../utils/logging';
5+
import type { BanditVariantData } from './banditData';
56

67
export function selectRandomVariant<V extends Variant, T extends Test<V>>(test: T): V | undefined {
78
const randomVariantIndex = Math.floor(Math.random() * test.variants.length);
@@ -20,3 +21,16 @@ export const getRandomNumber = (seed: string, mvtId: number | string = ''): numb
2021
const rng = seedrandom(mvtId + seed);
2122
return Math.abs(rng.int32());
2223
};
24+
25+
/**
26+
* It's possible for the variants in the bandit data to temporarily not match the variants in a test configuration.
27+
* This can happen if a variant is deleted in the RRCP, and the cached bandit data still references the deleted variant.
28+
* This function filters out any invalid variants.
29+
*/
30+
export const filterValidVariants = <V extends Variant, T extends Test<V>>(
31+
sortedVariantsData: BanditVariantData[],
32+
test: T,
33+
): BanditVariantData[] => {
34+
const validVariantNames = new Set(test.variants.map((v) => v.name));
35+
return sortedVariantsData.filter((v) => validVariantNames.has(v.variantName));
36+
};

src/server/selection/rouletteSelection.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Test, Variant } from '../../shared/types';
22
import type { BanditData } from './banditData';
3-
import { selectRandomVariant } from './helpers';
3+
import { filterValidVariants, selectRandomVariant } from './helpers';
44

55
export function selectVariantUsingRoulette<V extends Variant, T extends Test<V>>(
66
test: T,
@@ -11,20 +11,20 @@ export function selectVariantUsingRoulette<V extends Variant, T extends Test<V>>
1111
return selectRandomVariant(test);
1212
}
1313

14-
const sumOfMeans = testBanditData.sortedVariants.reduce((sum, v) => sum + v.mean, 0);
14+
const sortedVariants = filterValidVariants(testBanditData.sortedVariants, test);
15+
const sumOfMeans = sortedVariants.reduce((sum, v) => sum + v.mean, 0);
1516

1617
if (sumOfMeans <= 0) {
1718
return selectRandomVariant(test);
1819
}
1920

2021
const minWeight = 0.1; // Ensure no variant gets less than 10%
21-
const variantsWithWeights: Array<{ weight: number; variantName: string }> =
22-
testBanditData.sortedVariants
23-
.map(({ variantName, mean }) => ({
24-
variantName,
25-
weight: Math.max(mean / sumOfMeans, minWeight),
26-
}))
27-
.sort((a, b) => a.weight - b.weight);
22+
const variantsWithWeights: Array<{ weight: number; variantName: string }> = sortedVariants
23+
.map(({ variantName, mean }) => ({
24+
variantName,
25+
weight: Math.max(mean / sumOfMeans, minWeight),
26+
}))
27+
.sort((a, b) => a.weight - b.weight);
2828

2929
// The sum of the weights may be greater than 1, so we now need to normalise them
3030
const sumOfWeights = variantsWithWeights.reduce((sum, v) => sum + v.weight, 0);

0 commit comments

Comments
 (0)