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

Introduce SeatNonBid in hookoutcome #3416

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

ashishshinde-pubm
Copy link
Contributor

Currently, bids getting rejected in module stage are not captured in seatNonBid.
This PR introduce the SeatNonBid in HookStageOutcome so that each module stage can return rejected bids.
In auction/amp_auction, we will collect the SeatNonBid from all stageOutcomes (stageOutcomes whose execution-status is success)

Changes in opertb_ext package -

  1. To remove the cyclic dependency, moved the seat_non_bid.go and test files in openrtb_ext package.
  2. Removed the dependency from 'entities.PbsOrtbBid' structure and introduced 'NonBidParams' struct inside openrtb_ext package.
  3. Introduced Append() method in seat_non_bid.go which will append nonBids passed in input argument.
  4. Each stageOutcome will contain seatNonBid object and we will collect and add in the bidResponseExt just before sending the response.

Changes in exchange.go -

  1. Exchange will keep its own copy of SeatNonBid to collect bids rejected in HoldAuction.
  2. Since the SeatNonBid is not concurrent-safe, each go-routine (getallbids function) will have to return the SeatNonBid and main thread will use Append() method to collect all rejected bids.
  3. This SeatNonBid will be returned in the AuctionResponse.
  4. Exchange will not add the SeatNonBids under the bidresponse.ext.prebid.seatnonbid because auctionresponsehook gets executed after holdAuction function in the auction/amp-auction, hence auction will take of adding all seatnonbids in bidresponseExt.

Changes in auction.go/amp_auction.go -

  1. For each request, we will maintain SeatNonBid to collect rejected bids.
  2. We will append the SeatNonBid returned by exchange (holdAuction).
  3. In happy path, we will collect the SeatNonBid from all stageOutcomes and will append it to SeatNonBid got from exchange.
  4. Based on flag request.ext.prebid.returnallbidstatus will decide and add the SeatNonBid in the bidResponseExt.
  5. In case of critical error, ao.AuctionResponse/ao.Response would be nil hence we will collect SeatNonBid from stageOutcomes in the defer statement.

Changes in auction.go/video_auction.go -

  1. Currently, Hook execution does not take place in the video_auction hence we will get SeatNonBid only from exchange (holdAuction).

Changes for unit-test cases -

  1. Introduced mockSeatNonBidHook so that we can get the SeatNonBids from hookStageOutcomes for unit-test cases.
  2. Add unit test cases wherever required either by extending existing function or by adding new one.

@ashishshinde-pubm ashishshinde-pubm marked this pull request as ready for review January 19, 2024 03:49
// NonBidsWrapper contains the map of seat with list of nonBids
type NonBidsWrapper struct {
seatNonBidsMap map[string][]NonBid
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We use the term 'wrapper" in this package for helper objects which assist with json encoding/decoding. This is a different usage. Please use a different term, such as something like NonBidCollection.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, changed it to NonBidCollection


// AddBid adds the nonBid into the map against the respective seat.
// Note: This function is not a thread safe.
func (snb *NonBidsWrapper) AddBid(bidParams NonBidParams) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This method has two responsibilities, to build a NonBid object and add it to the collection. I recommend to separate these responsibilities. A helper function named something like NewNonBid or BuildNonBid could be encapsulate building a NonBid from a combination of the bid, reason, seat, etc...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, introduced new function NewNonBid

ImpId: bidParams.Bid.ImpID,
StatusCode: bidParams.NonBidReason,
Ext: NonBidExt{
Prebid: ExtResponseNonBidPrebid{Bid: NonBidObject{
Copy link
Contributor

Choose a reason for hiding this comment

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

It's strange to see a type called NonBidObject instead of simply NonBid. I'd like the names to be clearer.

We often prepend Ext for structures contained within an extension field. However, Seat Non Bid is intended to be promoted to the top level response in the future so I support the existing names of SeatNonBid and NonBid. These names would remain the same when the structures are moved into the OpenRTB repo.

I suggest we rename NonBidExt to ExtNonBid to better align with the naming pattern used for other objects. ExtReponseNonBidPrebid can be shorted to ExtNonBidPrebid for improved readability and to enforce the relationship with it's parent ExtNonBid structure. Then the unclear name NonBidObject can be renamed to ExtNonBidPrebidBid which is a little wordy but very clear.

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 the variables like below
NonBidExt ------> ExtNonBid
ExtReponseNonBidPrebid ----> ExtNonBidPrebid
NonBidObject ---> ExtNonBidPrebidBid

@@ -138,6 +139,12 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
activityControl := privacy.ActivityControl{}

defer func() {
// if AmpObject.AuctionResponse is nil then collect nonbids from all stage outcomes and set it in the AmpObject.SeatNonBid
// Nil AmpObject.AuctionResponse indicates the occurrence of a fatal error.
if ao.AuctionResponse == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a more direct way for checking the occurrence of a fatal error instead of needing to explain the significance of an empty response in a comment? Hopefully one we can reuse in other parts of the code instead of copying/pasting this 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.

One direct way would be to call below 2 lines after every return statement.
seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes()))
ao.SeatNonBid = seatNonBid.Get()

But currently there are around 6/7 return statement in Auction() & AmpAuction() function. To avoid the repetitive code, added this block in defer statement.
Thoughts ??

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 defer block is a good place for this.
Can we rely on the errors in ao? Looks like fatal errors should be added in ao in case of every error in the endpoint, like: ao.Errors = append(ao.Errors, rejectErr)

Same for equivalent logic in "auction.go"

Copy link
Collaborator

Choose a reason for hiding this comment

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

You could perhaps have a foundErrors boolean flag that starts false, and then is set to true whenever errors are detected. Then use this flag in the defer rather than checking for nil, resulting in a more straightforward way to satisfy the conditional, and futureproofing against any possible change where either an error happens after ao.AuctionResponse is set or it stays nil despite no error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Used foundError instead of ao.Response nil check.

@@ -325,6 +340,7 @@ func sendAmpResponse(
labels metrics.Labels,
ao analytics.AmpObject,
errs []error,
seatNonBid *openrtb_ext.NonBidsWrapper,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this passed as a pointer / by reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need to pass it as pointer. corrected it.

@@ -352,6 +368,8 @@ func sendAmpResponse(
glog.Errorf("/openrtb2/amp Critical error unpacking targets: %v", err)
ao.Errors = append(ao.Errors, fmt.Errorf("Critical error while unpacking AMP targets: %v", err))
ao.Status = http.StatusInternalServerError
seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to my above comment, seatNonBid could be nil here.. leading to a confusing situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, made correction. Now passing the seatNonBid as a copy.

DebugMessages []string `json:"debug_messages,omitempty"`
Errors []string `json:"-"`
Warnings []string `json:"-"`
SeatNonBid openrtb_ext.NonBidsWrapper `json:"-"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this not a slice of SeatNonBid objects?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We believe, building the standard openrtb_ext.SeatNonBid is expensive because each nonbid needs to be mapped against respective seat and impid.
Similarly, obtaining standard openrtb_ext.SeatNonBid object from each stageOutcome and again building the final openrtb_ext.SeatNonBid would add additional computational overhead.
Example -
Assume, RawBidderResponse hook returns

SeatNonBid: []openrtb_ext.SeatNonBid{
{
	NonBid: []openrtb_ext.NonBid{
		{
			ImpId:      "imp1",
			StatusCode: 100,
		}
	},
	Seat: "pubmatic",
}

& AuctionResponseHook hook returns

SeatNonBid: []openrtb_ext.SeatNonBid{
{
	NonBid: []openrtb_ext.NonBid{
		{
			ImpId:      "imp2",
			StatusCode: 100,
		}
	},
	Seat: "pubmatic",
}

But we need to combine them for bidresponseExt.Prebid.SeatNonBid and auctionObject.SeatNonBid like

SeatNonBid: []openrtb_ext.SeatNonBid{
{
	NonBid: []openrtb_ext.NonBid{
                {
			ImpId:      "imp1",
			StatusCode: 100,
		},
		{
			ImpId:      "imp2",
			StatusCode: 100,
		}
	},
	Seat: "pubmatic",
}

But, if we allow hookOutcome to return openrtb_ext.NonBidsWrapper/openrtb_ext.NonBidCollection then it become easy to add non-bids by using SeatNonBid.AddBid() function.
To combine the SeatNonBids from multiple hookOutcomes, we can use SeatNonBid.Append() function.
Please refer - getNonBidsFromStageOutcomes function which does the same.

Copy link
Contributor

@VeronikaSolovei9 VeronikaSolovei9 left a comment

Choose a reason for hiding this comment

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

Added mostly questions for better understanding the context.

Comment on lines +299 to +304
if ao.AuctionResponse != nil {
// this check ensures that we collect nonBids from stageOutcomes only once.
// there could be a case where ao.AuctionResponse nil and reqWrapper.RebuildRequest returns error
seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes()))
ao.SeatNonBid = seatNonBid.Get()
}
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 needed here? It duplicates logic from defer block

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in defer, we are collecting non-bids only when ao.AuctionResponse is nil.
This is one place, where we can get the ao.AuctionResponse as non-nil and defer will not collect the non-bids.

@@ -138,6 +139,12 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
activityControl := privacy.ActivityControl{}

defer func() {
// if AmpObject.AuctionResponse is nil then collect nonbids from all stage outcomes and set it in the AmpObject.SeatNonBid
// Nil AmpObject.AuctionResponse indicates the occurrence of a fatal error.
if ao.AuctionResponse == nil {
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 defer block is a good place for this.
Can we rely on the errors in ao? Looks like fatal errors should be added in ao in case of every error in the endpoint, like: ao.Errors = append(ao.Errors, rejectErr)

Same for equivalent logic in "auction.go"

want want
}{
{
description: "request parsing failed, auctionObject should contain seatNonBid",
Copy link
Contributor

Choose a reason for hiding this comment

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

This part is not clear to me. parseRequest has nothing to do with bid responses. Do we still want to include seatNonBid to the auction response?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As part of the SeatNonBid proposal (#2367) , seatNonBid is not just related to bid-responses.
Please refer NBR codes (202: Error - bad input parameters, 116: No Bid - Incomplete SupplyChain) this needs to be capture inside parseRequest.

Comment on lines +381 to +383
stageOutcomes := hookExecutor.GetOutcomes()
seatNonBid.Append(getNonBidsFromStageOutcomes(stageOutcomes))
ao.SeatNonBid = seatNonBid.Get()
Copy link
Contributor

Choose a reason for hiding this comment

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

This code is also duplicating logic in defer. Func sendAuctionResponse executes right before the defer block. Do we need it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In sendAuctionResponse , we will always get the non-nil ao.Response.
In defer block, we are collecting nonBids from stageOutcomes only when ao.Response is nil.
We need it here, because we need to add the seatNonBids in resp.ext.prebid.seatnonbid

@@ -0,0 +1,80 @@
package openrtb_ext

import "github.com/prebid/openrtb/v19/openrtb2"
Copy link
Contributor

Choose a reason for hiding this comment

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

We updated the OpenRTB library to version 20. Please merge with the master branch and change these imports to "github.com/prebid/openrtb/v20/openrtb2"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@ashishshinde-pubm
Copy link
Contributor Author

ashishshinde-pubm commented Jan 31, 2024

@VeronikaSolovei9
We should call getNonBidsFromStageOutcomes only once.
We cannot rely on ao.Errors. As per current implementation, we are not capturing the errors (ao.errors) but we need to call getNonBidsFromStageOutcomes and add ao.SeatNonBid for following cases .

  1. https://github.com/ashishshinde-pubm/prebid-server/blob/SNBR_HookStage/endpoints/openrtb2/auction.go#L202
  2. https://github.com/ashishshinde-pubm/prebid-server/blob/SNBR_HookStage/endpoints/openrtb2/auction.go#L287

Also, there are following cases where ao.Errors is not-empty and we don't want to call getNonBidsFromStageOutcomes in defer. (in this cases, we have already called the getNonBidsFromStageOutcomes )

  1. https://github.com/ashishshinde-pubm/prebid-server/blob/SNBR_HookStage/endpoints/openrtb2/auction.go#L396
  2. https://github.com/ashishshinde-pubm/prebid-server/blob/SNBR_HookStage/endpoints/openrtb2/auction.go#L417

As per current implementation, my requirement for auction.go is to call getNonBidsFromStageOutcomes in defer only when request doesn't pass through sendAuctionResponse function.

For amp_auction.go , we can rely on ao.Errors but to I was thinking to make both auction & amp_auction rely on common thing which seems to be ao.response in this case.

Thoughts ??

@VeronikaSolovei9
Copy link
Contributor

@ashishshinde-pubm Thank you for your responses! We need to discuss something with the team and we will get back to you.

@bsardo bsardo assigned guscarreon and unassigned bsardo Feb 8, 2024
@ashishshinde-pubm
Copy link
Contributor Author

@SyntaxNode , @VeronikaSolovei9 Any updates on this ?

@bsardo
Copy link
Collaborator

bsardo commented Feb 22, 2024

@ashishshinde-pubm sorry for the delay. There are a few other high priority items ahead of this but we will revisit soon.

@bsardo bsardo assigned hhhjort and unassigned guscarreon Feb 26, 2024
@hhhjort
Copy link
Collaborator

hhhjort commented Mar 12, 2024

There are also some merge conflicts now.

@bsardo bsardo assigned bsardo and unassigned hhhjort Apr 8, 2024
@zhongshixi
Copy link
Contributor

zhongshixi commented Apr 15, 2024

@bsardo was looking at the PR, I also have a use case to construct SeatNonBid from our hook, i was more thinking of directly modifying the []SeatNonBid so hook that deals with openrtb.BidResponse can directly access ext.prebid.seatnobid to append bids that is rejected by hook ( since not all stages should construct []SeatNonBid, it should be handled after we receive bid from adapter ) , so i think we should provide an easy function/struct/interface to directly append []SeatNonBid in the original BidResponse which should save us a lot of trouble from modifying code in other places

e.g
Screenshot 2024-04-15 at 6 43 28 PM

we can then invoke the function in AddMutation function in the hook so we can append the seat non bid in concurrently safe way
Screenshot 2024-04-15 at 6 44 55 PM

let me know how the discussion goes so that you guys figure out a agreed workflow for the scenario

@ashishshinde-pubm
Copy link
Contributor Author

@zhongshixi As per the PRD (#2367) , the seatnonbid are not just related to bid response.
Example - SeatNonBid should be constructed for code-204 (Request Blocked - Privacy).
Refer - openrtb_ext/seat_non_bids.go, it provides easy functions (NewBid/AddBid) for adding seatNonBids in bidResponse.

@zhongshixi
Copy link
Contributor

@ashishshinde-pubm i see, thanks for the quick response, i read the PRD and i see the community's direction to include the bidder request as part of seat non bid. since this PR is only surfacing SeatNonBid in hookoutcome, i will post my thoughts on that PRD threads.

@zhongshixi
Copy link
Contributor

zhongshixi commented Apr 22, 2024

@ashishshinde-pubm - I read through your PR and i like the design of using openrtb_ext.NonBidCollection{}, since i need to implement the feature of collecting rejected bidder request, i will borrow your idea of openrtb_ext.NonBidCollection{} only and use it in my PR

if you want, we can work on it together to make the puzzle complete so that seatNonBid can collect information on critical decision points in prebid server that rejects bid request and bid response

@ashishshinde-pubm
Copy link
Contributor Author

@bsardo , @hhhjort , @VeronikaSolovei9 I've resolved the conflcts, Please have a look on this PR.

@ashishshinde-pubm
Copy link
Contributor Author

@bretg , @bsardo Any plan to review this PR ?

Comment on lines 6 to 8
type NonBidCollection struct {
seatNonBidsMap map[string][]NonBid
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we get rid of NonBidCollection and this entire file and use SeatNonBidBuilder from #2891?

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove NonBidCollection. But move SeatNonBidBuilder to openrtb_ext package from exchange package due to cyclic import.

Comment on lines 186 to 191
// if AuctionObject.Response is nil then collect nonbids from all stage outcomes and set it in the AuctionObject.
// Nil AuctionObject.Response indicates the occurrence of a fatal error.
if ao.Response == nil {
seatNonBid.Append(getNonBidsFromStageOutcomes(hookExecutor.GetOutcomes()))
ao.SeatNonBid = seatNonBid.Get()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

While I understand why you chose to put this logic in defer, I don't think we should get into the habit of putting such conditional and computational logic in here. Is there another place we can put this?

Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid adding this code at multiple places, added it in the defer block.

Comment on lines +314 to +316
if response.Ext == nil {
response.Ext = json.RawMessage(`{}`)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to do this? Is this to avoid an error when unmarshaling?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes.

ao.HookExecutionOutcome = stageOutcomes
err := setSeatNonBidRaw(ao.RequestWrapper, response, ao.SeatNonBid)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps this is necessary at the moment but it seems a little odd that we are using the request wrapper that is on the analytics object.

Copy link
Contributor

Choose a reason for hiding this comment

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

RequestWrapper is not being passed to sendAuctionResponse currently. We will have to change sendAuctionResponse definition to accept RequestWrapper as argument instead of BidRequest.

@Pubmatic-Dhruv-Sonone
Copy link
Contributor

@bsardo Addressed the review comments.

@bsardo
Copy link
Collaborator

bsardo commented Nov 4, 2024

Hi @Pubmatic-Dhruv-Sonone, can you please merge with master again to resolve conflicts? Thanks!

@Pubmatic-Dhruv-Sonone
Copy link
Contributor

@bsardo Merged master. Please review again.

if setSeatNonBid(respExt, request, auctionResponse) {
if respExtJson, err := jsonutil.Marshal(respExt); err == nil {
if setSeatNonBid(respExt, nonBids) {
if respExtJson, err := json.Marshal(respExt); err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please leave the usage of our jsonutil helper.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

@bsardo bsardo assigned hhhjort and unassigned VeronikaSolovei9 Dec 9, 2024
},
},
{
name: "returnallbistatus is true, update seatnonbid in nil responseExt",
Copy link
Collaborator

Choose a reason for hiding this comment

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

typo in the name, should be "returnallbidstatus is true..."

Copy link
Contributor

Choose a reason for hiding this comment

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

Fixed.

@Pubmatic-Dhruv-Sonone
Copy link
Contributor

@hhhjort Addressed the review comments. Please check.

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

Successfully merging this pull request may close these issues.

8 participants