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 response fetcher (with new func for stored resp) #2099

Merged
merged 9 commits into from
Jan 10, 2022

Conversation

VeronikaSolovei9
Copy link
Contributor

@VeronikaSolovei9 VeronikaSolovei9 commented Dec 1, 2021

Stored response fetcher implementation:
-Added new FetchResponses function to Fetcher interface and all implementations
-Added new config object to store fetch responses configuration, assuming all stored responses will be stored in a new table.
-Functionality in this PR will be used in further stored responses feature implementation, so for now stored response fetcher does nothing - just exists and never gets called. Future places to call storedRespFetcher.FetchResponses are auction and amp endpoints.
-Initialization of Stored response fetcher happens in router.go -> func New -> storedRequestsConf.NewStoredRequests
-There are 2 actual implementations of FetchResponses function (others are just stubs to satisfy interface contract):
-- in db_fetcher/fetcher.go -> func (fetcher *dbFetcher) FetchResponses - place where stored responses downloads from DataBase
-- in stored_requests/fetcher.go -> func (f *fetcherWithCache) FetchResponses - place where stored responses get and set to in memory cache
-Polling functionality update - download newly created stored responses to in-memory cache: database.go -> fetchDelta() -> sendEvents (this is another place where data downloads from DB)

This PR is a first step to implement Prebid server stored responses feature

TODO: add metrics for stored responses - will be done in separate PR

Note: this code was tested using local Database. In order to enable it in prod new table should be created.

Fetch

@AlexBVolcy
Copy link
Contributor

AlexBVolcy commented Dec 8, 2021

Hey @VeronikaSolovei9, I was wondering if you could update the description of this PR to be a little more detailed about what this PR accomplishes? I think it would be helpful for me while reviewing :)

@VeronikaSolovei9 VeronikaSolovei9 marked this pull request as ready for review December 9, 2021 02:19

rows, err := fetcher.db.QueryContext(ctx, query, idInterfaces...)
if err != nil {
fmt.Println(err.Error(), query)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to be printing the error to console, instead of just returning this error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, updated :)

var data []byte
var dataType string

// Fixes #338
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick, i don't think we need this comment here for the merge?

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

storedData[id] = data
}

// Fixes #338
Copy link
Contributor

Choose a reason for hiding this comment

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

Same point as above, i think this comment may not be necessary.

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


// Fixes #338
if rows.Err() != nil {
fmt.Println(rows.Err())
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need to print the error to terminal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, removed

expected := buildQueryResponses(sampleResponsesQueryTemplate, 0)
assertStringsEqual(t, query, expected)
}

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 nitpick, but I see these tests from line 46-64, are all testing the same function but with different cases (i.e. No IDs vs Many IDs). This can be refactored to be one table driven test with many test cases within it, which is something I prefer, but of course if you'd like to keep it as it is then no worries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I followed the existing pattern, but yes, agree, this should be a table drive test. Updated.

"github.com/lib/pq"

"github.com/golang/glog"
"github.com/prebid/prebid-server/stored_requests"
)

func NewFetcher(db *sql.DB, queryMaker func(int, int) string) stored_requests.AllFetcher {
func NewFetcher(db *sql.DB, queryMaker func(int, int) string, responseQueryMaker func(int) string) stored_requests.AllFetcher {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just for my knowledge, what is happening here in this function signature where I read func(int) in the function signature. Is another function being called in relation to responseQueryMaker in the NewFetcher function signature?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. There are two different functions to build queries.
Please refer to the config/stored_requests.go file.

func (cfg *PostgresFetcherQueries) MakeQuery(numReqs int, numImps int) (query string) {
	return resolve(cfg.QueryTemplate, numReqs, numImps)
}

func (cfg *PostgresFetcherQueries) MakeQueryResponses(numIds int) (query string) {
	return resolveQueryResponses(cfg.QueryTemplate, numIds)
}

func resolve(template string, numReqs int, numImps int) (query string) {
	numReqs = ensureNonNegative("Request", numReqs)
	numImps = ensureNonNegative("Imp", numImps)

	query = strings.Replace(template, "%REQUEST_ID_LIST%", makeIdList(0, numReqs), -1)
	query = strings.Replace(query, "%IMP_ID_LIST%", makeIdList(numReqs, numImps), -1)
	return
}

func resolveQueryResponses(template string, numIds int) (query string) {
	numIds = ensureNonNegative("Response", numIds)

	query = strings.Replace(template, "%ID_LIST%", makeIdList(0, numIds), -1)
	return
}

There are two different resolvers for db queries. We need them to support stored responses and stored requests db fetch functionality.
For stored requests there is a query that fetches both "Request" and "Imp" in one run using union and it has two placeholders: "%REQUEST_ID_LIST%" and "%IMP_ID_LIST%".
It's complicated and messy to reuse it for responses.
For responses there is another query that fetches just one type of data in one run, it's simpler and eventually we plan to replace complicated query with union to a simple query like we have for responses. Also there is no bundle to data type like REQUEST_ID_LIST or IMP_ID_LIST. For now I can see this is the cleanest way to support it and make code and configs more or less readable.

assertHasData(t, storedResponses, "resp-id-1", `{"resp":false,"value":1}`)
assertHasData(t, storedResponses, "resp-id-2", `{"resp":true,"value":2}`)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment to above, where I think the TestGoodRepsonseFetchResponses, TestPartialResponse, and TestEmptyResponse tests could be consolidated into one table driven test.

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

assertMapLength(t, 1, storedResponses)
assertHasData(t, storedResponses, "stored-resp-id", "{}")
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment to above, where I think the TestGoodRepsonseFetchResponses, TestPartialResponse, and TestEmptyResponse tests could be consolidated into one table driven test.

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

assertErrorCount(t, 0, errs)
assertMapLength(t, 0, storedResponses)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment to above, where I think the TestGoodRepsonseFetchResponses, TestPartialResponse, and TestEmptyResponse tests could be consolidated into one table driven test.

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

// },
// "responses": {
// "resp1": { ... stored data for resp1 ... },
// "resp2": { ... stored data for resp2 ... },
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 commented out code 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.

Yes, this is a valid comment.

// Record cache hits for stored responses
//f.metricsEngine.RecordStoredReqCacheResult(metrics.CacheHit, len(ids)-len(leftoverResp))
// Record cache misses for stored responses
//f.metricsEngine.RecordStoredImpCacheResult(metrics.CacheMiss, len(leftoverResp))
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 commented out code 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

Copy link
Collaborator

@bsardo bsardo left a comment

Choose a reason for hiding this comment

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

This is looking good. I've gone through all of the logic changes and have just a few comments. I still need to finish going through a few more tests to complete the review which I will do shortly.

Comment on lines +20 to +22
if responseQueryMaker == nil {
glog.Fatalf("The Postgres Stored Response Fetcher requires a responseQueryMaker function. Please report this as a bug.")
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems odd to me that the responseQueryMaker is required to be not nil here because this function is not only used to create a database-backed fetcher for the stored response fetcher, but also the stored requests fetcher.
From what I can tell, if you're in a test environment where you want stored responses, the host would configure the stored responses fetcher but not the stored requests fetcher and they would define both the requests and response queries for the response fetcher. If you're in a prod environment, the host would configured the stored requests fetcher but not the stored response fetcher, and in that case, the host should only be required to configure the requests query but not a response query. Does that make sense? Was that how you envisioned the configuration working? If so, then I think a change is needed here because I think this will result in a fatal error in the production scenario and I don't think we want to require the host to define a response query.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code is not clear enough and needs to be separate in future.
Now it works with any kind of configuration because function is hardcoded and logic in upstream function determines what kind of data we want to fetch:
/stored_requests/config/config.go -> func newFetcher :

//stored imps, req, videos, responses. If config exists each of them will have a separate fetcher. 
if cfg.Postgres.FetcherQueries.QueryTemplate != "" { 

      idList = append(idList, db_fetcher.NewFetcher(db, cfg.Postgres.FetcherQueries.MakeQuery, cfg.Postgres.FetcherQueries.MakeQueryResponses))
       
}

If stored response config is not defined - this part of code will be skipped.
Hope this answers your question.
But Idk why do we need to check if queryMaker and responseQueryMaker are defined, they are hardcoded upstream. I followed existing code pattern.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah gotcha. I see that they are always not nil.

@@ -401,6 +412,8 @@ type InMemoryCache struct {
RequestCacheSize int `mapstructure:"request_cache_size_bytes"`
// ImpCacheSize is the max number of bytes allowed in the cache for Stored Imps. Values <= 0 will have no limit
ImpCacheSize int `mapstructure:"imp_cache_size_bytes"`
// ResponsesCacheSize is the max number of bytes allowed in the cache for Stored Responses. Values <= 0 will have no limit
RespCacheSize int `mapstructure:"resp_cache_size_bytes"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add defaults for this field in config/config.go for each fetcher similar to what we do for request_cache_size_bytes and imp_cache_size_bytes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, added

Accounts: memory.NewCache(256*1024, -1, "Account"),
Requests: memory.NewCache(256*1024, -1, "Request"),
Imps: memory.NewCache(256*1024, -1, "Imp"),
Responses: memory.NewCache(256*1024, -1, "Responseds"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Super nitpick: typo should be "Responses"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooof, modified!

Copy link
Collaborator

@bsardo bsardo left a comment

Choose a reason for hiding this comment

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

The tests look good, just a couple of comments.

}

for _, test := range testCases {
query := buildQueryResponses(sampleResponsesQueryTemplate, test.inputRespNumber)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Super nitpick and maybe more of a matter of personal preference: I don't see the value in having a level of indirection in this test by introducing a function buildQueryResponses unless you're planning to reuse that function in some other test. Why not just keep everything inline here and instead write:

// build query response
cfg := PostgresFetcherQueries{QueryTemplate:  sampleResponsesQueryTemplate}
query := cfg.MakeQueryResponses(test.inputRespNumber)

I'm ok if you want to keep it as is. I just think that tests are easier to understand if they have as little indirection as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Modified. It was a copy-paste from stored requests tests

Comment on lines 155 to 160
assertErrsExist(t, (&InMemoryCache{
Type: "lru",
RequestCacheSize: 1000,
ImpCacheSize: 0,
RespCacheSize: 0,
}).validate(RequestDataType, nil))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it make more sense to isolate the error to the ImpCacheSize and RespCacheSize here so instead of this assertErrsExist where both ImpCacheSize and RespCacheSize are 0, we introduce two assertErrsExists where each has only one of them set to 0 like so:

assertErrsExist(t, (&InMemoryCache{
		Type:             "lru",
		RequestCacheSize: 1000,
		ImpCacheSize:     0,
		RespCacheSize:    1000,
	}).validate(RequestDataType, nil))
assertErrsExist(t, (&InMemoryCache{
		Type:             "lru",
		RequestCacheSize: 1000,
		ImpCacheSize:     1000,
		RespCacheSize:    0,
	}).validate(RequestDataType, nil))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I understand this. Can we discuss it offline?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Sorry I was overthinking about this :)

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 just have a couple more small comments. This is looking good!

accountsFetcher stored_requests.AccountFetcher,
categoriesFetcher stored_requests.CategoryFetcher,
videoFetcher stored_requests.Fetcher,
storedRespFetcher stored_requests.Fetcher) {
// TODO: Switch this to be set in config defaults
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these comments still needed 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.

Deleted

}
}()

storedData := make(map[string]json.RawMessage, len(ids))
Copy link
Contributor

Choose a reason for hiding this comment

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

You use len(ids) a few times in this function, lines 101, 105, 106, and here 121. Is the length of the ids array changing at all here? If no, you could just extract len(ids) to a variable, and use that variable instead, just to improve some efficiency 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.

len(arr) is just a memory reading operation. Array already has this value and it doesn't need to be re-evaluated.

@AlexBVolcy AlexBVolcy merged commit ec1df96 into master Jan 10, 2022
wwwyyy pushed a commit to wwwyyy/prebid-server that referenced this pull request Jan 18, 2022
* 'master' of https://github.com/wwwyyy/prebid-server:
  GDPR: pass geo based on special feature 1 opt-in (prebid#2122)
  Adyoulike: Fix adapter errors if no content (prebid#2134)
  Update adyoulike.yaml (prebid#2131)
  Add ORTB 2.4 schain support (prebid#2108)
  Stored response fetcher (with new func for stored resp) (prebid#2099)
  Adot: Add OpenRTB macros resolution (prebid#2118)
  Rubicon adapter: accept accountId, siteId, zoneId as strings (prebid#2110)
  Operaads: support multiformat request (prebid#2121)
  Update go-gdpr to v1.11.0 (prebid#2125)
  Adgeneration: Update request to include device and app related info in headers and query params (prebid#2109)
  Adnuntius: Read device information from ortb Request (prebid#2112)
  New Adapter: apacdex and Remove Adapter: valueimpression (prebid#2097)
  New Adapter: Compass (prebid#2102)
  Orbidder: bidfloor currency handling (prebid#2093)
ramyferjaniadot pushed a commit to adotmob/prebid-server that referenced this pull request Feb 2, 2022
* Stored resp fetcher

* Stored resp fetcher unit tests

* Rebase on upstream

* Code clean up

* Unit tests clean up

* Unit tests clean up

* Code clean up

* Minor unit test fix

* Deleted old comment
jorgeluisrocha pushed a commit to jwplayer/prebid-server that referenced this pull request Sep 28, 2022
* Stored resp fetcher

* Stored resp fetcher unit tests

* Rebase on upstream

* Code clean up

* Unit tests clean up

* Unit tests clean up

* Code clean up

* Minor unit test fix

* Deleted old comment
shunj-nb pushed a commit to ParticleMedia/prebid-server that referenced this pull request Nov 8, 2022
* Stored resp fetcher

* Stored resp fetcher unit tests

* Rebase on upstream

* Code clean up

* Unit tests clean up

* Unit tests clean up

* Code clean up

* Minor unit test fix

* Deleted old comment
@SyntaxNode SyntaxNode deleted the stored_resp_fetcher_v2 branch March 13, 2023 13:55
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