From 44dee81ce86e92caae74c060511f43388b3f46af Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Wed, 20 Jun 2018 15:06:41 -0600 Subject: [PATCH] Max origin concurrent auctions (#2743) * initial attempt at limiting concurrenet auctions by origin * fix queueing of auctions for max origin * don't decrement on timeout as it is already called by onreadystatechange * move auction timer so it doesn't start until queued auction starts * set default max concurrent origin requests to 4 and make configurable * fix tests to not queue for auction.callBids * change MAX_REQUEST_PER_ORIGIN to local var --- src/adaptermanager.js | 12 +++- src/ajax.js | 84 ++++++++++---------------- src/auction.js | 104 ++++++++++++++++++++++++++++---- test/spec/unit/pbjs_api_spec.js | 1 + 4 files changed, 136 insertions(+), 65 deletions(-) diff --git a/src/adaptermanager.js b/src/adaptermanager.js index f6c0d5c421e3..3d5d56d6ee22 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -273,7 +273,7 @@ exports.checkBidRequestSizes = (adUnits) => { return adUnits; } -exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { +exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb, requestCallbacks) => { if (!bidRequests.length) { utils.logWarn('callBids executed with no bidRequests. Were they filtered by labels or sizing?'); return; @@ -285,7 +285,10 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { }, [[], []]); if (serverBidRequests.length) { - const s2sAjax = ajaxBuilder(serverBidRequests[0].timeout); + const s2sAjax = ajaxBuilder(serverBidRequests[0].timeout, requestCallbacks ? { + request: requestCallbacks.request.bind(null, 's2s'), + done: requestCallbacks.done + } : undefined); let adaptersServerSide = _s2sConfig.bidders; const s2sAdapter = _bidderRegistry[_s2sConfig.adapter]; let tid = serverBidRequests[0].tid; @@ -336,7 +339,6 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { } } - const ajax = (clientBidRequests.length) ? ajaxBuilder(clientBidRequests[0].timeout) : null; // handle client adapter requests clientBidRequests.forEach(bidRequest => { bidRequest.start = timestamp(); @@ -347,6 +349,10 @@ exports.callBids = (adUnits, bidRequests, addBidResponse, doneCb) => { events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); bidRequest.doneCbCallCount = 0; let done = doneCb(bidRequest.bidderRequestId); + let ajax = ajaxBuilder(clientBidRequests[0].timeout, requestCallbacks ? { + request: requestCallbacks.request.bind(null, bidRequest.bidderCode), + done: requestCallbacks.done + } : undefined); adapter.callBids(bidRequest, addBidResponse, done, ajax); } else { utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); diff --git a/src/ajax.js b/src/ajax.js index ded2f95f8a52..e17f782ac308 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -15,12 +15,13 @@ const XHR_DONE = 4; */ export const ajax = ajaxBuilder(); -export function ajaxBuilder(timeout = 3000) { +export function ajaxBuilder(timeout = 3000, {request, done} = {}) { return function(url, callback, data, options = {}) { try { let x; - let useXDomainRequest = false; let method = options.method || (data ? 'POST' : 'GET'); + let parser = document.createElement('a'); + parser.href = url; let callbacks = typeof callback === 'object' && callback !== null ? callback : { success: function() { @@ -35,46 +36,24 @@ export function ajaxBuilder(timeout = 3000) { callbacks.success = callback; } - if (!window.XMLHttpRequest) { - useXDomainRequest = true; - } else { - x = new window.XMLHttpRequest(); - if (x.responseType === undefined) { - useXDomainRequest = true; - } - } - - if (useXDomainRequest) { - x = new window.XDomainRequest(); - x.onload = function () { - callbacks.success(x.responseText, x); - }; + x = new window.XMLHttpRequest(); - // http://stackoverflow.com/questions/15786966/xdomainrequest-aborts-post-on-ie-9 - x.onerror = function () { - callbacks.error('error', x); - }; - x.ontimeout = function () { - callbacks.error('timeout', x); - }; - x.onprogress = function() { - utils.logMessage('xhr onprogress'); - }; - } else { - x.onreadystatechange = function () { - if (x.readyState === XHR_DONE) { - let status = x.status; - if ((status >= 200 && status < 300) || status === 304) { - callbacks.success(x.responseText, x); - } else { - callbacks.error(x.statusText, x); - } + x.onreadystatechange = function () { + if (x.readyState === XHR_DONE) { + if (typeof done === 'function') { + done(parser.origin); } - }; - x.ontimeout = function () { - utils.logError(' xhr timeout after ', x.timeout, 'ms'); - }; - } + let status = x.status; + if ((status >= 200 && status < 300) || status === 304) { + callbacks.success(x.responseText, x); + } else { + callbacks.error(x.statusText, x); + } + } + }; + x.ontimeout = function () { + utils.logError(' xhr timeout after ', x.timeout, 'ms'); + }; if (method === 'GET' && data) { let urlInfo = parseURL(url, options); @@ -86,18 +65,21 @@ export function ajaxBuilder(timeout = 3000) { // IE needs timoeut to be set after open - see #1410 x.timeout = timeout; - if (!useXDomainRequest) { - if (options.withCredentials) { - x.withCredentials = true; - } - utils._each(options.customHeaders, (value, header) => { - x.setRequestHeader(header, value); - }); - if (options.preflight) { - x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); + if (options.withCredentials) { + x.withCredentials = true; } + utils._each(options.customHeaders, (value, header) => { + x.setRequestHeader(header, value); + }); + if (options.preflight) { + x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); + + if (typeof request === 'function') { + request(parser.origin); + } + if (method === 'POST' && data) { x.send(data); } else { diff --git a/src/auction.js b/src/auction.js index 8992f16218e2..5722bcc6990e 100644 --- a/src/auction.js +++ b/src/auction.js @@ -74,6 +74,11 @@ events.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) { adjustBids(bid); }); +const MAX_REQUESTS_PER_ORIGIN = 4; +const outstandingRequests = {}; +const sourceInfo = {}; +const queuedCalls = []; + /** * Creates new auction instance * @@ -176,26 +181,103 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) } function callBids() { - startAuctionTimer(); _auctionStatus = AUCTION_STARTED; _auctionStart = Date.now(); - const auctionInit = { - timestamp: _auctionStart, - auctionId: _auctionId, - timeout: _timeout - }; - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, auctionInit); - let bidRequests = adaptermanager.makeBidRequests(_adUnits, _auctionStart, _auctionId, _timeout, _labels); utils.logInfo(`Bids Requested for Auction with id: ${_auctionId}`, bidRequests); bidRequests.forEach(bidRequest => { addBidRequests(bidRequest); }); - _auctionStatus = AUCTION_IN_PROGRESS; - adaptermanager.callBids(_adUnits, bidRequests, addBidResponse.bind(this), done.bind(this)); - }; + let requests = {}; + + let call = { + bidRequests, + run: () => { + startAuctionTimer(); + + _auctionStatus = AUCTION_IN_PROGRESS; + + const auctionInit = { + timestamp: _auctionStart, + auctionId: _auctionId, + timeout: _timeout + }; + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, auctionInit); + + adaptermanager.callBids(_adUnits, bidRequests, addBidResponse.bind(this), done.bind(this), { + request(source, origin) { + increment(outstandingRequests, origin); + increment(requests, source); + + if (!sourceInfo[source]) { + sourceInfo[source] = { + SRA: true, + origin + }; + } + if (requests[source] > 1) { + sourceInfo[source].SRA = false; + } + }, + done(origin) { + outstandingRequests[origin]--; + if (queuedCalls[0]) { + if (runIfOriginHasCapacity(queuedCalls[0])) { + queuedCalls.shift(); + } + } + } + }); + } + }; + + if (!runIfOriginHasCapacity(call)) { + utils.logWarn('queueing auction due to limited endpoint capacity'); + queuedCalls.push(call); + } + + function runIfOriginHasCapacity(call) { + let hasCapacity = true; + + let maxRequests = config.getConfig('maxRequestsPerOrigin') || MAX_REQUESTS_PER_ORIGIN; + + call.bidRequests.some(bidRequest => { + let requests = 1; + let source = (typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC) ? 's2s' + : bidRequest.bidderCode; + // if we have no previous info on this source just let them through + if (sourceInfo[source]) { + if (sourceInfo[source].SRA === false) { + // some bidders might use more than the MAX_REQUESTS_PER_ORIGIN in a single auction. In those cases + // set their request count to MAX_REQUESTS_PER_ORIGIN so the auction isn't permanently queued waiting + // for capacity for that bidder + requests = Math.min(bidRequest.bids.length, maxRequests); + } + if (outstandingRequests[sourceInfo[source].origin] + requests > maxRequests) { + hasCapacity = false; + } + } + // return only used for terminating this .some() iteration early if it is determined we don't have capacity + return !hasCapacity; + }); + + if (hasCapacity) { + call.run(); + } + + return hasCapacity; + } + + function increment(obj, prop) { + if (typeof obj[prop] === 'undefined') { + obj[prop] = 1 + } else { + obj[prop]++; + } + } + } return { addBidReceived, diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index ab5da55f4206..3156ea671f71 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1305,6 +1305,7 @@ describe('Unit: Prebid Module', function () { ] }]; adUnitCodes = ['adUnit-code']; + configObj.setConfig({maxRequestsPerOrigin: Number.MAX_SAFE_INTEGER || 99999999}); let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); spyCallBids = sinon.spy(adaptermanager, 'callBids'); createAuctionStub = sinon.stub(auctionModule, 'newAuction');