Skip to content

Commit

Permalink
Currency Module: Adding auction delay handling (prebid#12364)
Browse files Browse the repository at this point in the history
* Delay auction param on currency module

* hookConfig change

* test improvement

* review fixes

* introducing timeoutQueue

* fix

---------

Co-authored-by: Marcin Komorski <[email protected]>
  • Loading branch information
mkomorski and Marcin Komorski authored Dec 3, 2024
1 parent a8dccf5 commit a7fb4ad
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 31 deletions.
22 changes: 22 additions & 0 deletions libraries/timeoutQueue/timeoutQueue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function timeoutQueue() {
const queue = [];
return {
submit(timeout, onResume, onTimeout) {
const item = [
onResume,
setTimeout(() => {
queue.splice(queue.indexOf(item), 1);
onTimeout();
}, timeout)
];
queue.push(item);
},
resume() {
while (queue.length) {
const [onResume, timerId] = queue.shift();
clearTimeout(timerId);
onResume();
}
}
}
}
26 changes: 25 additions & 1 deletion modules/currency.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {config} from '../src/config.js';
import {getHook} from '../src/hook.js';
import {defer} from '../src/utils/promise.js';
import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js';
import {timedBidResponseHook} from '../src/utils/perfMetrics.js';
import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js';
import {on as onEvent, off as offEvent} from '../src/events.js';
import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js';

const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$';
const CURRENCY_RATE_PRECISION = 4;
const MODULE_NAME = 'currency';

let ratesURL;
let bidResponseQueue = [];
Expand All @@ -26,6 +28,9 @@ let defaultRates;

export let responseReady = defer();

const delayedAuctions = timeoutQueue();
let auctionDelay = 0;

/**
* Configuration function for currency
* @param {object} config
Expand Down Expand Up @@ -77,6 +82,7 @@ export function setConfig(config) {
}

if (typeof config.adServerCurrency === 'string') {
auctionDelay = config.auctionDelay;
logInfo('enabling currency support', arguments);

adServerCurrency = config.adServerCurrency;
Expand Down Expand Up @@ -106,6 +112,7 @@ export function setConfig(config) {
initCurrency();
} else {
// currency support is disabled, setting defaults
auctionDelay = 0;
logInfo('disabling currency support');
resetCurrency();
}
Expand Down Expand Up @@ -137,6 +144,7 @@ function loadRates() {
conversionCache = {};
currencyRatesLoaded = true;
processBidResponseQueue();
delayedAuctions.resume();
} catch (e) {
errorSettingsRates('Failed to parse currencyRates response: ' + response);
}
Expand All @@ -145,6 +153,7 @@ function loadRates() {
errorSettingsRates(...args);
currencyRatesLoaded = true;
processBidResponseQueue();
delayedAuctions.resume();
needToCallForCurrencyFile = true;
}
}
Expand All @@ -162,6 +171,7 @@ function initCurrency() {
getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency);
getHook('addBidResponse').before(addBidResponseHook, 100);
getHook('responsesReady').before(responsesReadyHook);
getHook('requestBids').before(requestBidsHook, 50);
onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout);
onEvent(EVENTS.AUCTION_INIT, loadRates);
loadRates();
Expand All @@ -172,6 +182,7 @@ function resetCurrency() {
if (currencySupportEnabled) {
getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove();
getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove();
getHook('requestBids').getHooks({hook: requestBidsHook}).remove();
offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout);
offEvent(EVENTS.AUCTION_INIT, loadRates);
delete getGlobal().convertCurrency;
Expand Down Expand Up @@ -335,3 +346,16 @@ export function setOrtbCurrency(ortbRequest, bidderRequest, context) {
}

registerOrtbProcessor({type: REQUEST, name: 'currency', fn: setOrtbCurrency});

export const requestBidsHook = timedAuctionHook('currency', function requestBidsHook(fn, reqBidsConfigObj) {
const continueAuction = ((that) => () => fn.call(that, reqBidsConfigObj))(this);

if (!currencyRatesLoaded && auctionDelay > 0) {
delayedAuctions.submit(auctionDelay, continueAuction, () => {
logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction ${reqBidsConfigObj.auctionId}`)
continueAuction();
});
} else {
continueAuction();
}
});
37 changes: 9 additions & 28 deletions modules/priceFloors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
deepAccess,
deepClone,
deepSetValue,
generateUUID,
getParameterByName,
isNumber,
logError,
Expand All @@ -13,7 +12,8 @@ import {
parseGPTSingleSizeArray,
parseUrl,
pick,
deepEqual
deepEqual,
generateUUID
} from '../src/utils.js';
import {getGlobal} from '../src/prebidGlobal.js';
import {config} from '../src/config.js';
Expand All @@ -30,6 +30,7 @@ import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.j
import {adjustCpm} from '../src/utils/cpm.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';
import {convertCurrency} from '../libraries/currencyUtils/currency.js';
import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js';

export const FLOOR_SKIPPED_REASON = {
NOT_FOUND: 'not_found',
Expand Down Expand Up @@ -72,7 +73,7 @@ let _floorsConfig = {};
/**
* @summary If a auction is to be delayed by an ongoing fetch we hold it here until it can be resumed
*/
let _delayedAuctions = [];
const _delayedAuctions = timeoutQueue();

/**
* @summary Each auction can have differing floors data depending on execution time or per adunit setup
Expand Down Expand Up @@ -440,17 +441,11 @@ export function createFloorsDataForAuction(adUnits, auctionId) {
* @summary This is the function which will be called to exit our module and continue the auction.
*/
export function continueAuction(hookConfig) {
// only run if hasExited
if (!hookConfig.hasExited) {
// if this current auction is still fetching, remove it from the _delayedAuctions
_delayedAuctions = _delayedAuctions.filter(auctionConfig => auctionConfig.timer !== hookConfig.timer);

// We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves
hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || generateUUID();

// now we do what we need to with adUnits and save the data object to be used for getFloor and enforcement calls
_floorDataForAuction[hookConfig.reqBidsConfigObj.auctionId] = createFloorsDataForAuction(hookConfig.reqBidsConfigObj.adUnits || getGlobal().adUnits, hookConfig.reqBidsConfigObj.auctionId);

hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]);
hookConfig.hasExited = true;
}
Expand Down Expand Up @@ -581,36 +576,22 @@ export const requestBidsHook = timedAuctionHook('priceFloors', function requestB
reqBidsConfigObj,
context: this,
nextFn: fn,
haveExited: false,
hasExited: false,
timer: null
};

// If auction delay > 0 AND we are fetching -> Then wait until it finishes
if (_floorsConfig.auctionDelay > 0 && fetching) {
hookConfig.timer = setTimeout(() => {
_delayedAuctions.submit(_floorsConfig.auctionDelay, () => continueAuction(hookConfig), () => {
logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction`);
_floorsConfig.fetchStatus = 'timeout';
continueAuction(hookConfig);
}, _floorsConfig.auctionDelay);
_delayedAuctions.push(hookConfig);
});
} else {
continueAuction(hookConfig);
}
});

/**
* @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume
* those delayed auctions when delay is hit or success return or fail return
*/
function resumeDelayedAuctions() {
_delayedAuctions.forEach(auctionConfig => {
// clear the timeout
clearTimeout(auctionConfig.timer);
continueAuction(auctionConfig);
});
_delayedAuctions = [];
}

/**
* This function handles the ajax response which comes from the user set URL to fetch floors data from
* @param {object} fetchResponse The floors data response which came back from the url configured in config.floors
Expand All @@ -635,7 +616,7 @@ export function handleFetchResponse(fetchResponse) {
}

// if any auctions are waiting for fetch to finish, we need to continue them!
resumeDelayedAuctions();
_delayedAuctions.resume();
}

function handleFetchError(status) {
Expand All @@ -644,7 +625,7 @@ function handleFetchError(status) {
logError(`${MODULE_NAME}: Fetch errored with: `, status);

// if any auctions are waiting for fetch to finish, we need to continue them!
resumeDelayedAuctions();
_delayedAuctions.resume();
}

/**
Expand Down
56 changes: 54 additions & 2 deletions test/spec/modules/currency_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
getCurrencyRates
} from 'test/fixtures/fixtures.js';

import { getGlobal } from 'src/prebidGlobal.js';
import {getGlobal} from 'src/prebidGlobal.js';

import {
setConfig,
Expand All @@ -13,9 +13,11 @@ import {
responseReady
} from 'modules/currency.js';
import {createBid} from '../../../src/bidfactory.js';
import { EVENTS, STATUS, REJECTION_REASON } from '../../../src/constants.js';
import * as utils from 'src/utils.js';
import {EVENTS, STATUS, REJECTION_REASON} from '../../../src/constants.js';
import {server} from '../../mocks/xhr.js';
import * as events from 'src/events.js';
import {requestBidsHook} from '../../../modules/currency.js';

var assert = require('chai').assert;
var expect = require('chai').expect;
Expand Down Expand Up @@ -522,4 +524,54 @@ describe('currency', function () {
expect(innerBid.currency).to.equal('CNY');
});
});

describe('auctionDelay param', () => {
const continueAuction = sinon.stub();
let logWarnSpy;

beforeEach(function() {
sandbox = sinon.sandbox.create();
clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z
logWarnSpy = sinon.spy(utils, 'logWarn');
});

afterEach(function () {
clock.runAll();
sandbox.restore();
clock.restore();
utils.logWarn.restore();
continueAuction.resetHistory();
});

it('should delay auction start when auctionDelay set in module config', () => {
setConfig({auctionDelay: 2000, adServerCurrency: 'USD'});
const reqBidsConfigObj = {
auctionId: '128937'
};
requestBidsHook(continueAuction, reqBidsConfigObj);
clock.tick(1000);
expect(continueAuction.notCalled).to.be.true;
});

it('should start auction when auctionDelay time passed', () => {
setConfig({auctionDelay: 2000, adServerCurrency: 'USD'});
const reqBidsConfigObj = {
auctionId: '128937'
};
requestBidsHook(continueAuction, reqBidsConfigObj);
clock.tick(3000);
expect(logWarnSpy.calledOnce).to.equal(true);
expect(continueAuction.calledOnce).to.be.true;
});

it('should run auction if rates were fetched before auctionDelay time', () => {
setConfig({auctionDelay: 3000, adServerCurrency: 'USD'});
const reqBidsConfigObj = {
auctionId: '128937'
};
fakeCurrencyFileServer.respond();
requestBidsHook(continueAuction, reqBidsConfigObj);
expect(continueAuction.calledOnce).to.be.true;
});
});
});

0 comments on commit a7fb4ad

Please sign in to comment.