Skip to content

Commit 5cb4bcb

Browse files
authored
Core & currency module: fix bug where bid responses are collected after auction ends (prebid#10558)
* Core & currency module: fix bug where bid responses are collected after auction ends * hook fn does not need to be a hook
1 parent 1155fb9 commit 5cb4bcb

File tree

4 files changed

+60
-150
lines changed

4 files changed

+60
-150
lines changed

modules/currency.js

+9-14
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,7 @@ export var currencyRates = {};
2222
var bidderCurrencyDefault = {};
2323
var defaultRates;
2424

25-
export const ready = (() => {
26-
let ctl;
27-
function reset() {
28-
ctl = defer();
29-
}
30-
reset();
31-
return {done: () => ctl.resolve(), reset, promise: () => ctl.promise}
32-
})();
25+
export let responseReady = defer();
3326

3427
/**
3528
* Configuration function for currency
@@ -137,6 +130,7 @@ function initCurrency(url) {
137130
// Adding conversion function to prebid global for external module and on page use
138131
getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency);
139132
getHook('addBidResponse').before(addBidResponseHook, 100);
133+
getHook('responsesReady').before(responsesReadyHook);
140134

141135
// call for the file if we haven't already
142136
if (needToCallForCurrencyFile) {
@@ -150,26 +144,23 @@ function initCurrency(url) {
150144
conversionCache = {};
151145
currencyRatesLoaded = true;
152146
processBidResponseQueue();
153-
ready.done();
154147
} catch (e) {
155148
errorSettingsRates('Failed to parse currencyRates response: ' + response);
156149
}
157150
},
158151
error: function (...args) {
159152
errorSettingsRates(...args);
160-
ready.done();
161153
}
162154
}
163155
);
164-
} else {
165-
ready.done();
166156
}
167157
}
168158

169159
function resetCurrency() {
170160
logInfo('Uninstalling addBidResponse decorator for currency module', arguments);
171161

172162
getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove();
163+
getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove();
173164
delete getGlobal().convertCurrency;
174165

175166
adServerCurrency = 'USD';
@@ -179,6 +170,11 @@ function resetCurrency() {
179170
needToCallForCurrencyFile = true;
180171
currencyRates = {};
181172
bidderCurrencyDefault = {};
173+
responseReady = defer();
174+
}
175+
176+
function responsesReadyHook(next, ready) {
177+
next(ready.then(() => responseReady.promise));
182178
}
183179

184180
export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid, reject) {
@@ -215,15 +211,14 @@ export const addBidResponseHook = timedBidResponseHook('currency', function addB
215211
bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid, reject]));
216212
if (!currencySupportEnabled || currencyRatesLoaded) {
217213
processBidResponseQueue();
218-
} else {
219-
fn.untimed.bail(ready.promise());
220214
}
221215
});
222216

223217
function processBidResponseQueue() {
224218
while (bidResponseQueue.length > 0) {
225219
(bidResponseQueue.shift())();
226220
}
221+
responseReady.resolve()
227222
}
228223

229224
function wrapFunction(fn, context, params) {

src/auction.js

+11-30
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,14 @@ export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) {
410410
this.dispatch.call(null, adUnitCode, bid);
411411
}, 'addBidResponse');
412412

413+
/**
414+
* Delay hook for adapter responses.
415+
*
416+
* `ready` is a promise; auctions wait for it to resolve before closing. Modules can hook into this
417+
* to delay the end of auctions while they perform initialization that does not need to delay their start.
418+
*/
419+
export const responsesReady = hook('sync', (ready) => ready, 'responsesReady');
420+
413421
export const addBidderRequests = hook('sync', function(bidderRequests) {
414422
this.dispatch.call(this.context, bidderRequests);
415423
}, 'addBidderRequests');
@@ -425,32 +433,6 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM
425433
let allAdapterCalledDone = false;
426434
let bidderRequestsDone = new Set();
427435
let bidResponseMap = {};
428-
const ready = {};
429-
430-
function waitFor(requestId, result) {
431-
if (ready[requestId] == null) {
432-
ready[requestId] = GreedyPromise.resolve();
433-
}
434-
ready[requestId] = ready[requestId].then(() => GreedyPromise.resolve(result).catch(() => {}))
435-
}
436-
437-
function guard(bidderRequest, fn) {
438-
let timeout = bidderRequest.timeout;
439-
if (timeout == null || timeout > auctionInstance.getTimeout()) {
440-
timeout = auctionInstance.getTimeout();
441-
}
442-
const timeRemaining = auctionInstance.getAuctionStart() + timeout - Date.now();
443-
const wait = ready[bidderRequest.bidderRequestId];
444-
const orphanWait = ready['']; // also wait for "orphan" responses that are not associated with any request
445-
if ((wait != null || orphanWait != null) && timeRemaining > 0) {
446-
GreedyPromise.race([
447-
GreedyPromise.timeout(timeRemaining),
448-
GreedyPromise.resolve(orphanWait).then(() => wait)
449-
]).then(fn);
450-
} else {
451-
fn();
452-
}
453-
}
454436

455437
function afterBidAdded() {
456438
outstandingBidsAdded--;
@@ -525,8 +507,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM
525507
return {
526508
addBidResponse: (function () {
527509
function addBid(adUnitCode, bid) {
528-
const bidderRequest = index.getBidderRequest(bid);
529-
waitFor((bidderRequest && bidderRequest.bidderRequestId) || '', addBidResponse.call({
510+
addBidResponse.call({
530511
dispatch: acceptBidResponse,
531512
}, adUnitCode, bid, (() => {
532513
let rejected = false;
@@ -536,13 +517,13 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM
536517
rejected = true;
537518
}
538519
}
539-
})()));
520+
})())
540521
}
541522
addBid.reject = rejectBidResponse;
542523
return addBid;
543524
})(),
544525
adapterDone: function () {
545-
guard(this, adapterDone.bind(this))
526+
responsesReady(GreedyPromise.resolve()).finally(() => adapterDone.call(this));
546527
}
547528
}
548529
}

test/spec/auctionmanager_spec.js

+29-91
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
adjustBids,
66
getMediaTypeGranularity,
77
getPriceByGranularity,
8-
addBidResponse, resetAuctionState
8+
addBidResponse, resetAuctionState, responsesReady
99
} from 'src/auction.js';
1010
import CONSTANTS from 'src/constants.json';
1111
import * as auctionModule from 'src/auction.js';
@@ -1803,116 +1803,54 @@ describe('auctionmanager.js', function () {
18031803
sinon.assert.calledWith(auction.addBidReceived, sinon.match({cpm: 1.23}));
18041804
})
18051805

1806-
describe('when addBidResponse hook returns promises', () => {
1807-
let resolvers, callbacks, bids;
1806+
describe('when responsesReady defers', () => {
1807+
let resolve, reject, promise, callbacks, bids;
18081808

1809-
function hook(next, ...args) {
1810-
next.bail(new Promise((resolve, reject) => {
1811-
resolvers.resolve.push(resolve);
1812-
resolvers.reject.push(reject);
1813-
}).finally(() => next(...args)));
1809+
function hook(next, ready) {
1810+
next(ready.then(() => promise));
18141811
}
18151812

1816-
function invokeCallbacks() {
1817-
bids.forEach((bid) => callbacks.addBidResponse(ADUNIT_CODE, bid));
1818-
bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest));
1819-
}
1813+
before(() => {
1814+
responsesReady.before(hook);
1815+
});
18201816

1821-
function delay(ms = 0) {
1822-
return new Promise((resolve) => {
1823-
setTimeout(resolve, ms)
1824-
});
1825-
}
1817+
after(() => {
1818+
responsesReady.getHooks({hook}).remove();
1819+
});
18261820

18271821
beforeEach(() => {
1822+
// eslint-disable-next-line promise/param-names
1823+
promise = new Promise((rs, rj) => {
1824+
resolve = rs;
1825+
reject = rj;
1826+
});
18281827
bids = [
18291828
mockBid({bidderCode: BIDDER_CODE1}),
18301829
mockBid({bidderCode: BIDDER_CODE})
18311830
]
18321831
bidRequests = bids.map((b) => mockBidRequest(b));
1833-
resolvers = {resolve: [], reject: []};
1834-
addBidResponse.before(hook);
18351832
callbacks = auctionCallbacks(doneSpy, auction);
18361833
Object.assign(auction, {
18371834
addNoBid: sinon.spy()
18381835
});
18391836
});
18401837

1841-
afterEach(() => {
1842-
addBidResponse.getHooks({hook: hook}).remove();
1843-
});
1844-
1845-
it('should wait for bids without a request bids before calling auctionDone', () => {
1846-
callbacks.addBidResponse(ADUNIT_CODE, Object.assign(mockBid(), {requestId: null}));
1847-
invokeCallbacks();
1848-
resolvers.resolve.slice(1, 3).forEach((fn) => fn());
1849-
return delay().then(() => {
1850-
expect(doneSpy.called).to.be.false;
1851-
resolvers.resolve[0]();
1852-
return delay();
1853-
}).then(() => {
1854-
expect(doneSpy.called).to.be.true;
1855-
});
1856-
});
1857-
18581838
Object.entries({
1859-
'all succeed': ['resolve', 'resolve'],
1860-
'some fail': ['resolve', 'reject'],
1861-
'all fail': ['reject', 'reject']
1862-
}).forEach(([test, results]) => {
1863-
describe(`(and ${test})`, () => {
1864-
it('should wait for them to complete before calling auctionDone', () => {
1865-
invokeCallbacks();
1866-
return delay().then(() => {
1867-
expect(doneSpy.called).to.be.false;
1868-
expect(auction.addNoBid.called).to.be.false;
1869-
resolvers[results[0]][0]();
1870-
return delay();
1871-
}).then(() => {
1872-
expect(doneSpy.called).to.be.false;
1873-
expect(auction.addNoBid.called).to.be.false;
1874-
resolvers[results[1]][1]();
1875-
return delay();
1876-
}).then(() => {
1877-
expect(doneSpy.called).to.be.true;
1878-
});
1879-
});
1839+
'resolve': () => resolve(),
1840+
'reject': () => reject(),
1841+
}).forEach(([t, resolver]) => {
1842+
it(`should wait for responsesReady to ${t} before calling auctionDone`, (done) => {
1843+
bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest));
1844+
setTimeout(() => {
1845+
sinon.assert.notCalled(doneSpy);
1846+
resolver();
1847+
setTimeout(() => {
1848+
sinon.assert.called(doneSpy);
1849+
done();
1850+
})
1851+
})
18801852
});
18811853
});
1882-
1883-
Object.entries({
1884-
bidder: (timeout) => {
1885-
bidRequests.forEach((r) => r.timeout = timeout);
1886-
auction.getTimeout = () => timeout + 10000
1887-
},
1888-
auction: (timeout) => {
1889-
auction.getTimeout = () => timeout;
1890-
bidRequests.forEach((r) => r.timeout = timeout + 10000)
1891-
}
1892-
}).forEach(([test, setTimeout]) => {
1893-
it(`should respect ${test} timeout if they never complete`, () => {
1894-
const start = Date.now() - 2900;
1895-
auction.getAuctionStart = () => start;
1896-
setTimeout(3000);
1897-
invokeCallbacks();
1898-
return delay().then(() => {
1899-
expect(doneSpy.called).to.be.false;
1900-
return delay(100);
1901-
}).then(() => {
1902-
expect(doneSpy.called).to.be.true;
1903-
});
1904-
});
1905-
1906-
it(`should not wait if ${test} has already timed out`, () => {
1907-
const start = Date.now() - 2000;
1908-
auction.getAuctionStart = () => start;
1909-
setTimeout(1000);
1910-
invokeCallbacks();
1911-
return delay().then(() => {
1912-
expect(doneSpy.called).to.be.true;
1913-
});
1914-
});
1915-
})
19161854
});
19171855

19181856
describe('when bids are rejected', () => {

test/spec/modules/currency_spec.js

+11-15
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
addBidResponseHook,
1111
currencySupportEnabled,
1212
currencyRates,
13-
ready
13+
responseReady
1414
} from 'modules/currency.js';
1515
import {createBid} from '../../../src/bidfactory.js';
1616
import CONSTANTS from '../../../src/constants.json';
@@ -32,7 +32,6 @@ describe('currency', function () {
3232

3333
beforeEach(function () {
3434
fakeCurrencyFileServer = server;
35-
ready.reset();
3635
});
3736

3837
afterEach(function () {
@@ -321,29 +320,26 @@ describe('currency', function () {
321320

322321
fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates()));
323322

324-
var bid = { 'cpm': 1, 'currency': 'USD' };
323+
const bid = { 'cpm': 1, 'currency': 'USD' };
325324

326325
setConfig({ 'adServerCurrency': 'JPY' });
327326

328-
var marker = false;
329-
let promiseResolved = false;
327+
let responseAdded = false;
328+
let isReady = false;
329+
responseReady.promise.then(() => { isReady = true });
330+
330331
addBidResponseHook(Object.assign(function() {
331-
marker = true;
332-
}, {
333-
bail: function (promise) {
334-
promise.then(() => promiseResolved = true);
335-
}
332+
responseAdded = true;
336333
}), 'elementId', bid);
337334

338-
expect(marker).to.equal(false);
339-
340335
setTimeout(() => {
341-
expect(promiseResolved).to.be.false;
336+
expect(responseAdded).to.equal(false);
337+
expect(isReady).to.equal(false);
342338
fakeCurrencyFileServer.respond();
343339

344340
setTimeout(() => {
345-
expect(marker).to.equal(true);
346-
expect(promiseResolved).to.be.true;
341+
expect(responseAdded).to.equal(true);
342+
expect(isReady).to.equal(true);
347343
done();
348344
});
349345
});

0 commit comments

Comments
 (0)