Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stored bid response #2160

Closed
wants to merge 0 commits into from
Closed

Stored bid response #2160

wants to merge 0 commits into from

Conversation

VeronikaSolovei9
Copy link
Contributor

@VeronikaSolovei9 VeronikaSolovei9 commented Feb 16, 2022

Stored bid responses implementation.
-Depends on stored auction response branch, PR #2132
-No unit tests

Code flow:
auction.go -> func parseRequest: added functionality to extract all stored bid responses and loads them from data store.
returns stored bid responses: imp id to bidder to stored response body
impBidderToStoredBidResponse := make(map[string]map[string]json.RawMessage)
if request is valid, data successfully loaded - sets StoredBidResponses: storedBidResponses to AuctionRequest and passes it to HoldAuction

utils.go -> func cleanOpenRTBRequests: builds list of Bidder Requests. In order to support stored bid responses, where in the same request impressions may have real bidder data and others may have stored bid responses I added BidderStoredResponses map[string]json.RawMessage to BidderRequest. One BidderRequest may have both real request and/or BidderStoredResponses initialized.

exchange/bidder.go -> func requestBid: executes real and fake requests to adapters
case if len(bidderRequest.BidRequest.Imp) > 0 { handles real request fetching, executes MakeRequests, sends real http requests
case if len(bidderRequest.BidderStoredResponses) > 0 { handles stored bid responses, doesn't execute MakeRequests
both of these cases sets response data to channel responseChannel. Stored bid response in this case looks like real bidder response, so it goes through usual processing including exercising bidder related code - func MakeBids

@bsardo bsardo requested review from bsardo and AlexBVolcy February 28, 2022 18:12
@VeronikaSolovei9 VeronikaSolovei9 force-pushed the stored_bid_response branch 2 times, most recently from 1030a5b to 193e126 Compare March 1, 2022 08:23
@VeronikaSolovei9
Copy link
Contributor Author

Merged dependent branch for single stored response and rebased on top of latest upstream

@VeronikaSolovei9 VeronikaSolovei9 marked this pull request as ready for review March 2, 2022 06:49
@@ -218,7 +221,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
var liveAdapters []openrtb_ext.BidderName

if len(r.StoredAuctionResponses) > 0 {
adapterBids, liveAdapters, err = buildStoredAuctionResponse(r.StoredAuctionResponses)
adapterBids, liveAdapters, err = buildStoreAuctionResponse(r.StoredAuctionResponses)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a spelling error, do we want this to say buildStoredAuctionResponse instead of buildStoreAuctionResponse

Copy link
Contributor Author

@VeronikaSolovei9 VeronikaSolovei9 Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed. It was fixed in Stored Single Response PR, but I rebased it incorrectly. Good catch!


adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid, 0)
liveAdapters := make([]openrtb_ext.BidderName, 0)
for impId, storedResp := range storedAuctionResponses {
for impId, storedResp := range storedActionResponses {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wondering if this is a spelling error where you have the word Action instead of Auction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed

@@ -3885,7 +3885,7 @@ func TestBuildStoredAuctionResponses(t *testing.T) {
}
for _, test := range testCases {

bids, adapters, err := buildStoredAuctionResponse(test.in.StoredAuctionResponses)
bids, adapters, err := buildStoreAuctionResponse(test.in.StoredAuctionResponses)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about potential spelling error Store vs. Stored

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed

if len(req.StoredBidResponses) > 0 {
// delete imps with stored bid resp
imps := req.BidRequest.Imp
req.BidRequest.Imp = nil //or new slice?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment //or new slice? still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, no need of this comment

for bidderName, storedResp := range storedData {
if _, ok := bidderToBidderResponse[openrtb_ext.BidderName(bidderName)]; !ok {
//new bidder with stored bid responses
//!!! check if bidder is valid/exists
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still needed, or if it is, could those !!! be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed and added this validation to 'processStoredAuctionResponses'

Copy link
Contributor

@AlexBVolcy AlexBVolcy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a few initial comments related to spelling and comments. Could you update the PR's description, to better describe what this PR is trying to accomplish? Is this trying to accomplish the issue outlined here #861

@VeronikaSolovei9
Copy link
Contributor Author

Good comments, addressed all of them, added description of changes to PR

Copy link
Contributor

@AlexBVolcy AlexBVolcy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I tested code coverage for this PR, and I left comments for which new code additions aren't yet covered by tests. Could you update this?

if _, isValid := deps.bidderMap[bidderResp.Bidder]; !isValid {
return nil, nil, []error{fmt.Errorf("request.imp[impId: %s].ext contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impId, bidderResp.Bidder)}
}
//assuming bidder is unique per one bid stored response
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment necessary? Just checking :)

Copy link
Contributor Author

@VeronikaSolovei9 VeronikaSolovei9 Mar 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 90% sure this should be unique, but I need to check with Bret. I'll remove the comment when I get the answer.
Discussion: #861 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion with Bret we don't need to support this. I'll remove a comment on my next commit.

@@ -66,6 +70,12 @@ func cleanOpenRTBRequests(ctx context.Context,
allBidderRequests, errs = getAuctionBidderRequests(req, requestExt, bidderToSyncerKey, impsByBidder, aliases)

if len(allBidderRequests) == 0 {
if len(bidderToBidderResponse) > 0 {
//all imps have stored bid responses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add test coverage for this block?

@@ -150,10 +159,25 @@ func cleanOpenRTBRequests(ctx context.Context,

if bidRequestAllowed {
privacyEnforcement.Apply(bidderRequest.BidRequest)

if bidderWithStoredBidResponses, ok := bidderToBidderResponse[bidderRequest.BidderName]; ok {
//this bidder has real imps and imps with stored bid response
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the above if statement explains the comment here. So i think this could be removed!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this code block from lines 164-177, aren't covered by tests.

impIdToRespId[impId] = impData.ImpExtPrebid.StoredAuctionResponse.ID

}
if len(impData.ImpExtPrebid.StoredBidResponse) > 0 {

bidderStoredRespId := make(map[string]string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add tests for these new changes (lines 1620-1633), I ran a code coverage test and see these aren't covered.

impInfo, errs := parseImpInfo(requestJson)
if len(errs) > 0 {
return nil, errs
return nil, nil, errs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you a test case that covers this error case?

}
if len(storedAuctionResponseIds) > 0 {
storedAuctionResponses, errs := deps.storedRespFetcher.FetchResponses(ctx, storedAuctionResponseIds)
if len(errs) > 0 {
return nil, errs
return nil, nil, errs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add test coverage for this error case

}
for impId, respId := range impIdToRespId {
impIdToStoredResp[impId] = storedAuctionResponses[respId]
}
return impIdToStoredResp, nil

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add test coverage for these additions (Lines 1646-1652)

go func(data *adapters.RequestData) {
responseChannel <- bidder.doRequest(ctx, data)
}(oneReqData) // Method arg avoids a race condition on oneReqData
if len(bidderRequest.BidderStoredResponses) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add test coverage for this code block (177-198)

@@ -243,6 +276,15 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb2.B
}
}

if len(bidderRequest.BidderStoredResponses) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add test coverage for this code block (line 279-287)

@@ -741,3 +765,40 @@ func applyFPD(fpd *firstpartydata.ResolvedFirstPartyData, bidReq *openrtb2.BidRe
bidReq.User = fpd.User
}
}

func buildBidderRequestsWithBidResponses(req AuctionRequest, aliases map[string]string) map[openrtb_ext.BidderName]BidderRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add tests for this function

@VeronikaSolovei9
Copy link
Contributor Author

When unit tests are added I'll make sure it's all covered.

@@ -1582,44 +1583,76 @@ func getJsonSyntaxError(testJSON []byte) (bool, string) {
// Note that processStoredAuctionResponses must be called after processStoredRequests
// because stored imps and stored requests can contain stored auction responses
// so the stored requests/imps have to be merged into the incoming request prior to processing stored auction responses.
func (deps *endpointDeps) processStoredAuctionResponses(ctx context.Context, requestJson []byte) (map[string]json.RawMessage, []error) {
func (deps *endpointDeps) processStoredAuctionResponses(ctx context.Context, requestJson []byte) (map[string]json.RawMessage, map[string]map[string]json.RawMessage, []error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is doing a bit too much IMO. Perhaps we can split this into a couple of functions each with a single responsibility?

Copy link
Contributor Author

@VeronikaSolovei9 VeronikaSolovei9 Mar 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function extracts storedResponses and storedBidResponses from raw request. We use the same fetcher for both of them. Because of parsing a raw request we extract all ids, get stored data and put this data to useful format for future use: return impIdToStoredResp, impBidderToStoredBidResponse, nil. What do you think would be a good split?

Comment on lines 49 to 52
impsByBidder, err := splitImps(req.BidRequest.Imp)
if err != nil {
errs = []error{err}
aliases, errs := parseAliases(req.BidRequest)
if len(errs) > 0 {
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any implications reversing the order in which aliases and imps are processed? I imagine it is just a matter of which error is reported if both splitting imps and parsing aliases result in errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved it to the first statement of the cleanOpenRTBRequests function, because aliases are needed in buildBidderRequestsWithBidResponses. buildBidderRequestsWithBidResponses should be executed before everything else, because it modifies request - removes all imps with stored bid responses from imp array.

        aliases, errs := parseAliases(req.BidRequest)
	if len(errs) > 0 {
		return
	}

	allowedBidderRequests = make([]BidderRequest, 0, 0)

	bidderToBidderResponse := buildBidderRequestsWithBidResponses(req, aliases)

       impsByBidder, err := splitImps(req.BidRequest.Imp)
...

Comment on lines 56 to 49
bidderToBidderResponse := buildBidderRequestsWithBidResponses(req, aliases)

impsByBidder, err := splitImps(req.BidRequest.Imp)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're building bidderToBidderResponse before splitting imps because it is easier to process stored bid responses before splitting right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildBidderRequestsWithBidResponses function removes "fake" imps from request. Split imps works with "real" impressions only. Please my previous comment - it has more context on this.


func buildBidderRequestsWithBidResponses(req AuctionRequest, aliases map[string]string) map[openrtb_ext.BidderName]BidderRequest {
var bidderToBidderResponse map[openrtb_ext.BidderName]BidderRequest
if len(req.StoredBidResponses) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function looks good. Can we flatten it a bit by using an early return here?

if len(req.StoredBidResponses) <= 0 {
	return bidderToBidderResponse
}

Comment on lines 772 to 780
// delete imps with stored bid resp
imps := req.BidRequest.Imp
req.BidRequest.Imp = nil
for _, imp := range imps {
if _, ok := req.StoredBidResponses[imp.ID]; !ok {
//add real imp back to request
req.BidRequest.Imp = append(req.BidRequest.Imp, imp)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, after looking at the calling code, I feel like this logic in this function should probably be split up a bit into separate functions so that we can appropriately name the different steps involved.
I was particularly confused by this code that follows the call to buildBidderRequestsWithBidResponses:

if len(allBidderRequests) == 0 {
		if len(bidderToBidderResponse) > 0 {

This leads me to think that it would benefit the reader if the part of this function that deletes all imps and then re-adds those that don't have a stored bid response were in its own function with some name like Strip/RemoveImpsWithStoredResponses.
The following call could be to the rest of this function that just focuses on building the bidder to stored responses map.
Both of these could be called in succession prior to executing that nested conditional I referenced here.

This kind of refactor should also reduce functional complexity resulting in simpler tests.

Perhaps we should have a stored responses package so its core logic is all in one place. Something like:
BuildStoredBidResponses --> returns the map
ExtractStoredBidResponse --> removes imps from the bid request that have a stored bid response
Prepare/SendStoredResponses --> prepares a request with its stored response to be sent on the channel
I might have missed some other functions here that are needed. This is just off the top of my head.

Comment on lines 178 to 156
//if stored bid responses are present - replace impIds and add them as is to responseChannel <- stored responses
if responseChannel == nil {
dataLen = dataLen + len(bidderRequest.BidderStoredResponses)
responseChannel = make(chan *httpCallInfo, dataLen)
}
for impId, bidresp := range bidderRequest.BidderStoredResponses {
//always one element in reqData because stored response is mapped to single imp
reqDataForStoredResp := adapters.RequestData{
Method: "POST",
Uri: "",
Body: []byte(impId), //use it to pass imp id for stored resp
}
respData := &httpCallInfo{
request: &reqDataForStoredResp,
response: &adapters.ResponseData{
StatusCode: 200,
Body: bidresp,
},
err: nil,
}
go func() { responseChannel <- respData }()
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like another possible function that can be extracted into a stored responses package to reduce some of the complexity here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants