Merge pull request #18018 from overleaf/revert-17906-ab-split-test-assignments-optim-pt2

Revert "[web] Store anonymous users split test assignments in new format in session"

GitOrigin-RevId: 2c1a95031a9d1d99b9dfef54eb4b80264a32ba0d
This commit is contained in:
Alexandre Bourdin 2024-04-18 19:08:33 +02:00 committed by Copybot
parent bfe75c7d31
commit 14bbc65e99
5 changed files with 169 additions and 163 deletions

View file

@ -12,7 +12,9 @@ class SplitTestCache extends CacheLoader {
async load() { async load() {
Metrics.inc('split_test_get_split_test_from_mongo', 1, {}) Metrics.inc('split_test_get_split_test_from_mongo', 1, {})
const splitTests = await SplitTestManager.getRuntimeTests() const splitTests = await SplitTestManager.getRuntimeTests()
return new Map(splitTests.map(splitTest => [splitTest.name, splitTest])) return new Map(
splitTests.map(splitTest => [splitTest.name, splitTest.toObject()])
)
} }
serialize(value) { serialize(value) {

View file

@ -492,10 +492,7 @@ function _getNonSaasAssignment(splitTestName) {
async function _getSplitTest(name) { async function _getSplitTest(name) {
const splitTests = await SplitTestCache.get('') const splitTests = await SplitTestCache.get('')
const splitTest = splitTests?.get(name) return splitTests?.get(name)
if (splitTest && !splitTest.archived) {
return splitTest
}
} }
module.exports = { module.exports = {

View file

@ -60,7 +60,9 @@ async function getSplitTests({ name, phase, type, active, archived }) {
async function getRuntimeTests() { async function getRuntimeTests() {
try { try {
return SplitTest.find({}).lean().exec() return await SplitTest.find({
archived: { $ne: true },
}).exec()
} catch (error) { } catch (error) {
throw OError.tag(error, 'Failed to get active split tests list') throw OError.tag(error, 'Failed to get active split tests list')
} }

View file

@ -9,35 +9,29 @@ const SplitTestUtils = require('./SplitTestUtils')
const SplitTestUserGetter = require('./SplitTestUserGetter') const SplitTestUserGetter = require('./SplitTestUserGetter')
const CACHE_TOMBSTONE_SPLIT_TEST_NOT_ACTIVE_FOR_USER = null const CACHE_TOMBSTONE_SPLIT_TEST_NOT_ACTIVE_FOR_USER = null
const TOKEN_SEP = ';'
// this is safe to use as a separator adjacent to a base64 string because Mongo object IDs
// do not generate any padding when converted (24 hex digits = 12 bytes => multiple of 6),
// thus do not contain any trailing `=`
const KEY_VALUE_SEP = '='
const ID_VERSION_SEP = '_'
const VARIANT_DATE_SEP = ':'
async function getAssignments(session) { async function getAssignments(session) {
await _convertAnonymousAssignmentsIfNeeded(session) if (!session.splitTests && !session.sta) {
if (!session.sta) {
return undefined return undefined
} }
const assignments = {} // await _convertAnonymousAssignmentsIfNeeded(session)
const tokens = session.sta.split(TOKEN_SEP) const assignments = _.clone(session.splitTests || {})
if (session.sta) {
const tokens = session.sta.split(';')
const splitTests = Array.from((await SplitTestCache.get('')).values()) const splitTests = Array.from((await SplitTestCache.get('')).values())
for (const token of tokens) { for (const token of tokens) {
try { try {
if (!token.length) { if (!token.length) {
continue continue
} }
const [splitTestNameVersion, info] = token.split(KEY_VALUE_SEP) const [splitTestNameVersion, info] = token.split('=')
const [splitTestId64, versionStr] = const [splitTestId64, versionStr] = splitTestNameVersion.split('_')
splitTestNameVersion.split(ID_VERSION_SEP)
const splitTest = splitTests.find( const splitTest = splitTests.find(
test => splitTestId64 === _convertIdToBase64(test._id) test =>
test._id.toString() ===
new ObjectId(Buffer.from(splitTestId64, 'base64')).toString()
) )
if (!splitTest) { if (!splitTest) {
continue continue
@ -45,7 +39,7 @@ async function getAssignments(session) {
const splitTestName = splitTest.name const splitTestName = splitTest.name
const versionNumber = parseInt(versionStr) const versionNumber = parseInt(versionStr)
const [variantChar, timestampStr36] = info.split(VARIANT_DATE_SEP) const [variantChar, timestampStr36] = info.split(':')
const assignedAt = new Date(parseInt(timestampStr36, 36) * 1000) const assignedAt = new Date(parseInt(timestampStr36, 36) * 1000)
let variantName let variantName
if (variantChar === 'd') { if (variantChar === 'd') {
@ -69,32 +63,46 @@ async function getAssignments(session) {
} catch (error) { } catch (error) {
logger.error( logger.error(
{ err: error, token }, { err: error, token },
'Failed to resolve cached anonymous split test assignments from session' 'Failed to resolve anonymous split test assignment from session'
) )
} }
} }
}
return assignments return assignments
} }
async function appendAssignment(session, assignment) { async function appendAssignment(session, assignment) {
await _convertAnonymousAssignmentsIfNeeded(session) // await _convertAnonymousAssignmentsIfNeeded(session)
if ( if (!session.splitTests) {
!_hasExistingAssignment( session.splitTests = {}
session,
assignment.splitTestId,
assignment.versionNumber
)
) {
if (!session.sta) {
session.sta = ''
} }
const splitTests = await SplitTestCache.get('') if (!session.splitTests[assignment.splitTestName]) {
const splitTest = splitTests.get(assignment.splitTestName) session.splitTests[assignment.splitTestName] = []
const assignmentString = _buildAssignmentString(splitTest, assignment) }
const separator = session.sta.length > 0 ? TOKEN_SEP : ''
session.sta += `${separator}${assignmentString}` const assignments = await getAssignments(session)
if (
!_.find(assignments[assignment.splitTestName], {
variantName: assignment.variantName,
versionNumber: assignment.versionNumber,
})
) {
// if (!session.sta) {
// session.sta = ''
// }
// const splitTests = await SplitTestCache.get('')
// const splitTest = splitTests.get(assignment.splitTestName)
// const assignmentString = _buildAssignmentString(splitTest, assignment)
// const separator = session.sta.length > 0 ? ';' : ''
// session.sta += `${separator}${assignmentString}`
session.splitTests[assignment.splitTestName].push({
variantName: assignment.variantName,
versionNumber: assignment.versionNumber,
phase: assignment.phase,
assignedAt: assignment.assignedAt,
})
} }
} }
@ -174,63 +182,62 @@ function collectSessionStats(session) {
JSON.stringify(session.cachedSplitTestAssignments).length JSON.stringify(session.cachedSplitTestAssignments).length
) )
} }
if (session.sta) { if (session.splitTests) {
Metrics.summary( Metrics.summary(
'split_test_session_storage_count', 'split_test_session_storage_count',
(session.sta || '').split(';').length (session.sta || '').split(';').length +
Object.keys(session.splitTests).length
) )
Metrics.summary( Metrics.summary(
'split_test_session_storage_size', 'split_test_session_storage_size',
(session.sta || '').length (session.sta || '').length + JSON.stringify(session.splitTests).length
) )
} }
} }
async function _convertAnonymousAssignmentsIfNeeded(session) { // async function _convertAnonymousAssignmentsIfNeeded(session) {
if (session.splitTests) { // if (typeof session.splitTests === 'object') {
const splitTests = await SplitTestCache.get('') // const sessionAssignments = session.splitTests
if (!session.sta) { // const splitTests = await SplitTestCache.get('')
session.sta = '' // session.splitTests = ''
} // for (const [splitTestName, assignments] of Object.entries(
for (const [splitTestName, assignments] of Object.entries( // sessionAssignments
session.splitTests // )) {
)) { // const splitTest = splitTests.get(splitTestName)
const splitTest = splitTests.get(splitTestName) // for (const assignment of assignments) {
for (const assignment of assignments) { // const assignmentString = _buildAssignmentString(splitTest, assignment)
const assignmentString = _buildAssignmentString(splitTest, assignment) // const separator = session.splitTests.length > 0 ? ';' : ''
const separator = session.sta.length > 0 ? TOKEN_SEP : '' // session.splitTests += `${separator}${assignmentString}`
session.sta += `${separator}${assignmentString}` // }
} // }
} // }
delete session.splitTests // }
}
}
function _hasExistingAssignment(session, splitTest, versionNumber) { // function _hasExistingAssignment(session, splitTest, versionNumber) {
if (!session.sta) { // if (!session.sta) {
return false // return false
} // }
const index = session.sta.indexOf( // const index = session.sta.indexOf(
`${_convertIdToBase64(splitTest._id)}${ID_VERSION_SEP}${versionNumber}=` // `${_convertIdToBase64(splitTest._id)}_${versionNumber}=`
) // )
return index >= 0 // return index >= 0
} // }
function _buildAssignmentString(splitTest, assignment) { // function _buildAssignmentString(splitTest, assignment) {
const { versionNumber, variantName, assignedAt } = assignment // const { versionNumber, variantName, assignedAt } = assignment
const variants = SplitTestUtils.getCurrentVersion(splitTest).variants // const variants = SplitTestUtils.getCurrentVersion(splitTest).variants
const splitTestId = _convertIdToBase64(splitTest._id) // const splitTestId = _convertIdToBase64(splitTest._id)
const variantChar = // const variantChar =
variantName === 'default' // variantName === 'default'
? 'd' // ? 'd'
: _.findIndex(variants, { name: variantName }) // : _.findIndex(variants, { name: variantName })
const timestamp = Math.floor(assignedAt.getTime() / 1000).toString(36) // const timestamp = Math.floor(assignedAt.getTime() / 1000).toString(36)
return `${splitTestId}${ID_VERSION_SEP}${versionNumber}${KEY_VALUE_SEP}${variantChar}${VARIANT_DATE_SEP}${timestamp}` // return `${splitTestId}_${versionNumber}=${variantChar}:${timestamp}`
} // }
function _convertIdToBase64(id) { // function _convertIdToBase64(id) {
return new ObjectId(id).toString('base64') // return new ObjectId(id).toString('base64')
} // }
module.exports = { module.exports = {
getAssignments: callbackify(getAssignments), getAssignments: callbackify(getAssignments),

View file

@ -15,45 +15,6 @@ describe('SplitTestSessionHandler', function () {
} }
this.SplitTestUserGetter = {} this.SplitTestUserGetter = {}
this.Metrics = {} this.Metrics = {}
this.SplitTestCache.get = sinon.stub().resolves(
new Map(
Object.entries({
'anon-test-1': {
_id: '661f92a4669764bb03f73e37',
name: 'anon-test-1',
versions: [
{
versionNumber: 1,
variants: [
{
name: 'enabled',
},
],
},
],
},
'anon-test-2': {
_id: '661f92a9d68ea711d6bf2df4',
name: 'anon-test-2',
versions: [
{
versionNumber: 1,
variants: [
{
name: 'v-1',
},
{
name: 'v-2',
},
],
},
],
},
})
)
)
this.SplitTestSessionHandler = SandboxedModule.require(MODULE_PATH, { this.SplitTestSessionHandler = SandboxedModule.require(MODULE_PATH, {
requires: { requires: {
'./SplitTestCache': this.SplitTestCache, './SplitTestCache': this.SplitTestCache,
@ -191,6 +152,43 @@ describe('SplitTestSessionHandler', function () {
}) })
it('should merge assignments from both splitTests and sta fields', async function () { it('should merge assignments from both splitTests and sta fields', async function () {
this.SplitTestCache.get = sinon.stub().resolves(
new Map(
Object.entries({
'anon-test-1': {
_id: '661f92a4669764bb03f73e37',
name: 'anon-test-1',
versions: [
{
versionNumber: 1,
variants: [
{
name: 'enabled',
},
],
},
],
},
'anon-test-2': {
_id: '661f92a9d68ea711d6bf2df4',
name: 'anon-test-2',
versions: [
{
versionNumber: 1,
variants: [
{
name: 'v-1',
},
{
name: 'v-2',
},
],
},
],
},
})
)
)
const session = { const session = {
splitTests: { splitTests: {
'anon-test-1': [ 'anon-test-1': [
@ -225,18 +223,18 @@ describe('SplitTestSessionHandler', function () {
}, },
], ],
'anon-test-2': [ 'anon-test-2': [
{
variantName: 'v-2',
versionNumber: 2,
phase: 'release',
assignedAt: new Date(1712858400000),
},
{ {
variantName: 'default', variantName: 'default',
versionNumber: 1, versionNumber: 1,
phase: 'release', phase: 'release',
assignedAt: new Date(1712307600000), assignedAt: new Date(1712307600000),
}, },
{
variantName: 'v-2',
versionNumber: 2,
phase: 'release',
assignedAt: new Date(1712858400000),
},
], ],
}) })
}) })