Skip to content

Commit 2074d37

Browse files
mkomorskiMarcin Komorski
authored andcommitted
Currency Module: Adding auction delay handling (prebid#12364)
* Delay auction param on currency module * hookConfig change * test improvement * review fixes * introducing timeoutQueue * fix --------- Co-authored-by: Marcin Komorski <[email protected]>
1 parent 93fbb7d commit 2074d37

File tree

4 files changed

+110
-31
lines changed

4 files changed

+110
-31
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function timeoutQueue() {
2+
const queue = [];
3+
return {
4+
submit(timeout, onResume, onTimeout) {
5+
const item = [
6+
onResume,
7+
setTimeout(() => {
8+
queue.splice(queue.indexOf(item), 1);
9+
onTimeout();
10+
}, timeout)
11+
];
12+
queue.push(item);
13+
},
14+
resume() {
15+
while (queue.length) {
16+
const [onResume, timerId] = queue.shift();
17+
clearTimeout(timerId);
18+
onResume();
19+
}
20+
}
21+
}
22+
}

modules/currency.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {config} from '../src/config.js';
66
import {getHook} from '../src/hook.js';
77
import {defer} from '../src/utils/promise.js';
88
import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js';
9-
import {timedBidResponseHook} from '../src/utils/perfMetrics.js';
9+
import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js';
1010
import {on as onEvent, off as offEvent} from '../src/events.js';
11+
import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js';
1112

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

1517
let ratesURL;
1618
let bidResponseQueue = [];
@@ -26,6 +28,9 @@ let defaultRates;
2628

2729
export let responseReady = defer();
2830

31+
const delayedAuctions = timeoutQueue();
32+
let auctionDelay = 0;
33+
2934
/**
3035
* Configuration function for currency
3136
* @param {object} config
@@ -77,6 +82,7 @@ export function setConfig(config) {
7782
}
7883

7984
if (typeof config.adServerCurrency === 'string') {
85+
auctionDelay = config.auctionDelay;
8086
logInfo('enabling currency support', arguments);
8187

8288
adServerCurrency = config.adServerCurrency;
@@ -106,6 +112,7 @@ export function setConfig(config) {
106112
initCurrency();
107113
} else {
108114
// currency support is disabled, setting defaults
115+
auctionDelay = 0;
109116
logInfo('disabling currency support');
110117
resetCurrency();
111118
}
@@ -137,6 +144,7 @@ function loadRates() {
137144
conversionCache = {};
138145
currencyRatesLoaded = true;
139146
processBidResponseQueue();
147+
delayedAuctions.resume();
140148
} catch (e) {
141149
errorSettingsRates('Failed to parse currencyRates response: ' + response);
142150
}
@@ -145,6 +153,7 @@ function loadRates() {
145153
errorSettingsRates(...args);
146154
currencyRatesLoaded = true;
147155
processBidResponseQueue();
156+
delayedAuctions.resume();
148157
needToCallForCurrencyFile = true;
149158
}
150159
}
@@ -162,6 +171,7 @@ function initCurrency() {
162171
getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency);
163172
getHook('addBidResponse').before(addBidResponseHook, 100);
164173
getHook('responsesReady').before(responsesReadyHook);
174+
getHook('requestBids').before(requestBidsHook, 50);
165175
onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout);
166176
onEvent(EVENTS.AUCTION_INIT, loadRates);
167177
loadRates();
@@ -172,6 +182,7 @@ function resetCurrency() {
172182
if (currencySupportEnabled) {
173183
getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove();
174184
getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove();
185+
getHook('requestBids').getHooks({hook: requestBidsHook}).remove();
175186
offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout);
176187
offEvent(EVENTS.AUCTION_INIT, loadRates);
177188
delete getGlobal().convertCurrency;
@@ -335,3 +346,16 @@ export function setOrtbCurrency(ortbRequest, bidderRequest, context) {
335346
}
336347

337348
registerOrtbProcessor({type: REQUEST, name: 'currency', fn: setOrtbCurrency});
349+
350+
export const requestBidsHook = timedAuctionHook('currency', function requestBidsHook(fn, reqBidsConfigObj) {
351+
const continueAuction = ((that) => () => fn.call(that, reqBidsConfigObj))(this);
352+
353+
if (!currencyRatesLoaded && auctionDelay > 0) {
354+
delayedAuctions.submit(auctionDelay, continueAuction, () => {
355+
logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction ${reqBidsConfigObj.auctionId}`)
356+
continueAuction();
357+
});
358+
} else {
359+
continueAuction();
360+
}
361+
});

modules/priceFloors.js

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
deepAccess,
44
deepClone,
55
deepSetValue,
6-
generateUUID,
76
getParameterByName,
87
isNumber,
98
logError,
@@ -13,7 +12,8 @@ import {
1312
parseGPTSingleSizeArray,
1413
parseUrl,
1514
pick,
16-
deepEqual
15+
deepEqual,
16+
generateUUID
1717
} from '../src/utils.js';
1818
import {getGlobal} from '../src/prebidGlobal.js';
1919
import {config} from '../src/config.js';
@@ -30,6 +30,7 @@ import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.j
3030
import {adjustCpm} from '../src/utils/cpm.js';
3131
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';
3232
import {convertCurrency} from '../libraries/currencyUtils/currency.js';
33+
import { timeoutQueue } from '../libraries/timeoutQueue/timeoutQueue.js';
3334

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

7778
/**
7879
* @summary Each auction can have differing floors data depending on execution time or per adunit setup
@@ -440,17 +441,11 @@ export function createFloorsDataForAuction(adUnits, auctionId) {
440441
* @summary This is the function which will be called to exit our module and continue the auction.
441442
*/
442443
export function continueAuction(hookConfig) {
443-
// only run if hasExited
444444
if (!hookConfig.hasExited) {
445-
// if this current auction is still fetching, remove it from the _delayedAuctions
446-
_delayedAuctions = _delayedAuctions.filter(auctionConfig => auctionConfig.timer !== hookConfig.timer);
447-
448445
// We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves
449446
hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || generateUUID();
450-
451447
// now we do what we need to with adUnits and save the data object to be used for getFloor and enforcement calls
452448
_floorDataForAuction[hookConfig.reqBidsConfigObj.auctionId] = createFloorsDataForAuction(hookConfig.reqBidsConfigObj.adUnits || getGlobal().adUnits, hookConfig.reqBidsConfigObj.auctionId);
453-
454449
hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]);
455450
hookConfig.hasExited = true;
456451
}
@@ -581,36 +576,22 @@ export const requestBidsHook = timedAuctionHook('priceFloors', function requestB
581576
reqBidsConfigObj,
582577
context: this,
583578
nextFn: fn,
584-
haveExited: false,
579+
hasExited: false,
585580
timer: null
586581
};
587582

588583
// If auction delay > 0 AND we are fetching -> Then wait until it finishes
589584
if (_floorsConfig.auctionDelay > 0 && fetching) {
590-
hookConfig.timer = setTimeout(() => {
585+
_delayedAuctions.submit(_floorsConfig.auctionDelay, () => continueAuction(hookConfig), () => {
591586
logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction`);
592587
_floorsConfig.fetchStatus = 'timeout';
593588
continueAuction(hookConfig);
594-
}, _floorsConfig.auctionDelay);
595-
_delayedAuctions.push(hookConfig);
589+
});
596590
} else {
597591
continueAuction(hookConfig);
598592
}
599593
});
600594

601-
/**
602-
* @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume
603-
* those delayed auctions when delay is hit or success return or fail return
604-
*/
605-
function resumeDelayedAuctions() {
606-
_delayedAuctions.forEach(auctionConfig => {
607-
// clear the timeout
608-
clearTimeout(auctionConfig.timer);
609-
continueAuction(auctionConfig);
610-
});
611-
_delayedAuctions = [];
612-
}
613-
614595
/**
615596
* This function handles the ajax response which comes from the user set URL to fetch floors data from
616597
* @param {object} fetchResponse The floors data response which came back from the url configured in config.floors
@@ -635,7 +616,7 @@ export function handleFetchResponse(fetchResponse) {
635616
}
636617

637618
// if any auctions are waiting for fetch to finish, we need to continue them!
638-
resumeDelayedAuctions();
619+
_delayedAuctions.resume();
639620
}
640621

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

646627
// if any auctions are waiting for fetch to finish, we need to continue them!
647-
resumeDelayedAuctions();
628+
_delayedAuctions.resume();
648629
}
649630

650631
/**

test/spec/modules/currency_spec.js

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
getCurrencyRates
44
} from 'test/fixtures/fixtures.js';
55

6-
import { getGlobal } from 'src/prebidGlobal.js';
6+
import {getGlobal} from 'src/prebidGlobal.js';
77

88
import {
99
setConfig,
@@ -13,9 +13,11 @@ import {
1313
responseReady
1414
} from 'modules/currency.js';
1515
import {createBid} from '../../../src/bidfactory.js';
16-
import { EVENTS, STATUS, REJECTION_REASON } from '../../../src/constants.js';
16+
import * as utils from 'src/utils.js';
17+
import {EVENTS, STATUS, REJECTION_REASON} from '../../../src/constants.js';
1718
import {server} from '../../mocks/xhr.js';
1819
import * as events from 'src/events.js';
20+
import {requestBidsHook} from '../../../modules/currency.js';
1921

2022
var assert = require('chai').assert;
2123
var expect = require('chai').expect;
@@ -522,4 +524,54 @@ describe('currency', function () {
522524
expect(innerBid.currency).to.equal('CNY');
523525
});
524526
});
527+
528+
describe('auctionDelay param', () => {
529+
const continueAuction = sinon.stub();
530+
let logWarnSpy;
531+
532+
beforeEach(function() {
533+
sandbox = sinon.sandbox.create();
534+
clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z
535+
logWarnSpy = sinon.spy(utils, 'logWarn');
536+
});
537+
538+
afterEach(function () {
539+
clock.runAll();
540+
sandbox.restore();
541+
clock.restore();
542+
utils.logWarn.restore();
543+
continueAuction.resetHistory();
544+
});
545+
546+
it('should delay auction start when auctionDelay set in module config', () => {
547+
setConfig({auctionDelay: 2000, adServerCurrency: 'USD'});
548+
const reqBidsConfigObj = {
549+
auctionId: '128937'
550+
};
551+
requestBidsHook(continueAuction, reqBidsConfigObj);
552+
clock.tick(1000);
553+
expect(continueAuction.notCalled).to.be.true;
554+
});
555+
556+
it('should start auction when auctionDelay time passed', () => {
557+
setConfig({auctionDelay: 2000, adServerCurrency: 'USD'});
558+
const reqBidsConfigObj = {
559+
auctionId: '128937'
560+
};
561+
requestBidsHook(continueAuction, reqBidsConfigObj);
562+
clock.tick(3000);
563+
expect(logWarnSpy.calledOnce).to.equal(true);
564+
expect(continueAuction.calledOnce).to.be.true;
565+
});
566+
567+
it('should run auction if rates were fetched before auctionDelay time', () => {
568+
setConfig({auctionDelay: 3000, adServerCurrency: 'USD'});
569+
const reqBidsConfigObj = {
570+
auctionId: '128937'
571+
};
572+
fakeCurrencyFileServer.respond();
573+
requestBidsHook(continueAuction, reqBidsConfigObj);
574+
expect(continueAuction.calledOnce).to.be.true;
575+
});
576+
});
525577
});

0 commit comments

Comments
 (0)