import { config, isUnitTest } from "../../utils/config"; import { EndFunction, BucketSet } from "../metric.service"; import AbstractScoreHashMap, { NormStats } from "./abstractScoreHashMap.service"; import { ChoiceRequest } from "../../models"; import constants from "../../utils/constants"; import logger from "../../utils/logger"; const LOG_ID = "[CONTEXTHASHMAP_SERVICE]"; const RELOAD_SECTION_NAME: string = "context_hashmap_reload"; const GET_SCORE_SECTION_NAME: string = "context_hashmap_get_score"; const HB_DISPLAY_DEFAULT_SCORE_USAGE = "context_hashmap_hb_display_default_score_usage"; const CONTEXT_HASHMAP_FALLBACK_LEVEL = "context_hashmap_fallback_level"; interface DefaultAccScore { fromFallbacklevel: number; scoreValue: number; } export interface ContextFeatures { economicType?: string | null; format?: string | null; ssp?: string | null; campaignId: string; assetId?: string | null; assetUnit?: string | null; defaultAccScore?: DefaultAccScore; // new features for price model environmentType: string; priceModel: string; adUnitType?: string | null; // advertType: string; temporary remove since the value in not sync with stats apiKey?: string | null; } export class ContextHashMap extends AbstractScoreHashMap { private static instance: ContextHashMap; protected logId: string = LOG_ID; protected s3Bucket: string = `${config.S3_CONTEXT_BUCKET}`; protected s3Prefix: string = `${config.S3_CONTEXT_ACC_RATE_PREFIX}/${config.INVENTORY}`; protected maxRetries: number = parseInt(config.ACC_RATE_MAP_MAX_RETRIES); protected retryDelay: number = parseInt(config.ACC_RATE_MAP_RETRY_DELAY); protected refreshInterval: number = parseInt(config.ACC_RATE_MAP_REFRESH_INTERVAL); protected refreshMetricLabel: string = RELOAD_SECTION_NAME; private constructor() { super(); this.metricService.addSection(RELOAD_SECTION_NAME); this.metricService.addSection(GET_SCORE_SECTION_NAME, [], BucketSet.zeroTo5ms); this.metricService.addCounter( HB_DISPLAY_DEFAULT_SCORE_USAGE, "number of time default score for display campaign in header-bidding is used" ); this.metricService.addCounter( CONTEXT_HASHMAP_FALLBACK_LEVEL, "accomplish fallback level when get context score is used", ["level"] ); this.scheduleRefresh(); } public static getInstance(): ContextHashMap { if (!this.instance || isUnitTest()) { this.instance = new ContextHashMap(); } return this.instance; } private static buildLevelKey(features: ContextFeatures, level: number): string { if (config.INVENTORY === "internal") { const keyCommonPart = `${features.economicType}|${features.environmentType}|${features.priceModel}|${features.adUnitType}`; switch (level) { case 1: return `${keyCommonPart}|${features.campaignId}|${features.apiKey}|${features.assetUnit}`; case 2: return `${keyCommonPart}|${features.campaignId}|${features.apiKey}|${constants.FALLBACK_STR}`; case 3: return `${keyCommonPart}|${features.campaignId}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}`; case 4: return `${keyCommonPart}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}`; default: throw new Error("Error level key should be between 1 and 4 (included)"); } } else { switch (level) { case 1: return `${features.economicType}|${features.format}|${features.ssp}|${features.campaignId}|${features.assetId}|${features.assetUnit}`; case 2: return `${features.economicType}|${features.format}|${features.ssp}|${features.campaignId}|${features.assetId}|${constants.FALLBACK_STR}`; case 3: return `${features.economicType}|${features.format}|${features.ssp}|${features.campaignId}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}`; case 4: return `${features.economicType}|${features.format}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}`; default: throw new Error("Error level key should be between 1 and 4 (included)"); } } } /** * Get statistics by campaign. Fallback on economic type & format average if campaign entry do not exist * @param economicType * @param format * @param campaignId */ public getStatistics( economicType: string | null | undefined, format: string | null | undefined, campaignId: string ): NormStats { const statistics: NormStats | undefined = this.normMap.get( `${economicType}|${format}|${campaignId}` ); const fallbackStatistics: NormStats | undefined = this.normMap.get( `${economicType}|${format}|${constants.FALLBACK_STR}` ); return this.formatStatistics(statistics, fallbackStatistics); } /** * get score from hashMap using economicType, format, ssp, campaignId, appBundleId|domain and adUnitId|webPage with a fallback mechanism. * Lookup score using ssp, campaignId and app. * If no match lookup on using ssp and campaignId. * If no match lookup on using economicType and format. * @param {ContextFeatures} features * @param {ChoiceRequest} request * @return {number} score retrieved from the map. No score is retrieved return undefined value * @memberof ContextHashMap */ public getScore(features: ContextFeatures, request: ChoiceRequest): number | undefined { const end: EndFunction = this.metricService.startSection(GET_SCORE_SECTION_NAME); let score; let level = features.assetUnit ? 1 : 2; request.metadata.context_level = level; while (score === undefined && level < 5) { const key = ContextHashMap.buildLevelKey(features, level); logger.debug(() => `${LOG_ID} key level ${level}: ${key}`); score = this.scoresMap.get(key); request.metadata.context_level = level; level++; } this.metricService.incCounterWithReq(request, CONTEXT_HASHMAP_FALLBACK_LEVEL, 1, { level: request.metadata.context_level, }); end(true); return score; } }