github.com/prebid/prebid-server/v2@v2.18.0/stored_requests/fetcher.go (about) 1 package stored_requests 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 8 "github.com/prebid/prebid-server/v2/metrics" 9 ) 10 11 // Fetcher knows how to fetch Stored Request data by id. 12 // 13 // Implementations must be safe for concurrent access by multiple goroutines. 14 // Callers are expected to share a single instance as much as possible. 15 type Fetcher interface { 16 // FetchRequests fetches the stored requests for the given IDs. 17 // 18 // The first return value will be the Stored Request data, or nil if it doesn't exist. 19 // If requestID is an empty string, then this value will always be nil. 20 // 21 // The second return value will be a map from Stored Imp data. It will have a key for every ID 22 // in the impIDs list, unless errors exist. 23 // 24 // The returned objects can only be read from. They may not be written to. 25 FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) 26 FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) 27 } 28 29 type AccountFetcher interface { 30 // FetchAccount fetches the host account configuration for a publisher 31 FetchAccount(ctx context.Context, accountDefaultJSON json.RawMessage, accountID string) (json.RawMessage, []error) 32 } 33 34 type CategoryFetcher interface { 35 // FetchCategories fetches the ad-server/publisher specific category for the given IAB category 36 FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) 37 } 38 39 // AllFetcher is an interface that encapsulates both the original Fetcher and the CategoryFetcher 40 type AllFetcher interface { 41 Fetcher 42 AccountFetcher 43 CategoryFetcher 44 } 45 46 // NotFoundError is an error type to flag that an ID was not found by the Fetcher. 47 // This was added to support Multifetcher and any other case where we might expect 48 // that all IDs would not be found, and want to disentangle those errors from the others. 49 type NotFoundError struct { 50 ID string 51 DataType string 52 } 53 54 type Category struct { 55 Id string 56 Name string 57 } 58 59 func (e NotFoundError) Error() string { 60 return fmt.Sprintf(`Stored %s with ID="%s" not found.`, e.DataType, e.ID) 61 } 62 63 // Cache is an intermediate layer which can be used to create more complex Fetchers by composition. 64 // Implementations must be safe for concurrent access by multiple goroutines. 65 // To add a Cache layer in front of a Fetcher, see WithCache() 66 type Cache struct { 67 Requests CacheJSON 68 Imps CacheJSON 69 Responses CacheJSON 70 Accounts CacheJSON 71 } 72 type CacheJSON interface { 73 // Get works much like Fetcher.FetchRequests, with a few exceptions: 74 // 75 // 1. Any (actionable) errors should be logged by the implementation, rather than returned. 76 // 2. The returned maps _may_ be written to. 77 // 3. The returned maps must _not_ contain keys unless they were present in the argument ID list. 78 // 4. Callers _should not_ assume that the returned maps contain key for every argument id. 79 // The returned map will miss entries for keys which don't exist in the cache. 80 // 81 // Nil slices and empty strings are treated as "no ops". That is, a nil requestID will always produce a nil 82 // "stored request data" in the response. 83 Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) 84 85 // Invalidate will ensure that all values associated with the given IDs 86 // are no longer returned by the cache until new values are saved via Update 87 Invalidate(ctx context.Context, ids []string) 88 89 // Save will add or overwrite the data in the cache at the given keys 90 Save(ctx context.Context, data map[string]json.RawMessage) 91 } 92 93 // ComposedCache creates an interface to treat a slice of caches as a single cache 94 type ComposedCache []CacheJSON 95 96 // Get will attempt to Get from the caches in the order in which they are in the slice, 97 // stopping as soon as a value is found (or when all caches have been exhausted) 98 func (c ComposedCache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) { 99 data = make(map[string]json.RawMessage, len(ids)) 100 101 remainingIDs := ids 102 103 for _, cache := range c { 104 cachedData := cache.Get(ctx, remainingIDs) 105 data, remainingIDs = updateFromCache(data, remainingIDs, cachedData) 106 107 // finish early if all ids filled 108 if len(remainingIDs) == 0 { 109 break 110 } 111 } 112 113 return 114 } 115 116 func updateFromCache(data map[string]json.RawMessage, ids []string, newData map[string]json.RawMessage) (map[string]json.RawMessage, []string) { 117 remainingIDs := ids 118 119 if len(newData) > 0 { 120 remainingIDs = make([]string, 0, len(ids)) 121 122 for _, id := range ids { 123 if config, ok := newData[id]; ok { 124 data[id] = config 125 } else { 126 remainingIDs = append(remainingIDs, id) 127 } 128 } 129 } 130 131 return data, remainingIDs 132 } 133 134 // Invalidate will propagate invalidations to all underlying caches 135 func (c ComposedCache) Invalidate(ctx context.Context, ids []string) { 136 for _, cache := range c { 137 cache.Invalidate(ctx, ids) 138 } 139 } 140 141 // Save will propagate saves to all underlying caches 142 func (c ComposedCache) Save(ctx context.Context, data map[string]json.RawMessage) { 143 for _, cache := range c { 144 cache.Save(ctx, data) 145 } 146 } 147 148 type fetcherWithCache struct { 149 fetcher AllFetcher 150 cache Cache 151 metricsEngine metrics.MetricsEngine 152 } 153 154 // WithCache returns a Fetcher which uses the given Caches before delegating to the original. 155 // This can be called multiple times to compose Cache layers onto the backing Fetcher, though 156 // it is usually more desirable to first compose caches with Compose, ensuring propagation of updates 157 // and invalidations through all cache layers. 158 func WithCache(fetcher AllFetcher, cache Cache, metricsEngine metrics.MetricsEngine) AllFetcher { 159 return &fetcherWithCache{ 160 cache: cache, 161 fetcher: fetcher, 162 metricsEngine: metricsEngine, 163 } 164 } 165 166 func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) { 167 168 requestData = f.cache.Requests.Get(ctx, requestIDs) 169 impData = f.cache.Imps.Get(ctx, impIDs) 170 171 // Fixes #311 172 leftoverImps := findLeftovers(impIDs, impData) 173 leftoverReqs := findLeftovers(requestIDs, requestData) 174 175 // Record cache hits for stored requests and stored imps 176 f.metricsEngine.RecordStoredReqCacheResult(metrics.CacheHit, len(requestIDs)-len(leftoverReqs)) 177 f.metricsEngine.RecordStoredImpCacheResult(metrics.CacheHit, len(impIDs)-len(leftoverImps)) 178 // Record cache misses for stored requests and stored imps 179 f.metricsEngine.RecordStoredReqCacheResult(metrics.CacheMiss, len(leftoverReqs)) 180 f.metricsEngine.RecordStoredImpCacheResult(metrics.CacheMiss, len(leftoverImps)) 181 182 if len(leftoverReqs) > 0 || len(leftoverImps) > 0 { 183 fetcherReqData, fetcherImpData, fetcherErrs := f.fetcher.FetchRequests(ctx, leftoverReqs, leftoverImps) 184 errs = fetcherErrs 185 186 f.cache.Requests.Save(ctx, fetcherReqData) 187 f.cache.Imps.Save(ctx, fetcherImpData) 188 189 requestData = mergeData(requestData, fetcherReqData) 190 impData = mergeData(impData, fetcherImpData) 191 } 192 193 return 194 } 195 196 func (f *fetcherWithCache) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) { 197 data = f.cache.Responses.Get(ctx, ids) 198 199 leftoverResp := findLeftovers(ids, data) 200 201 if len(leftoverResp) > 0 { 202 fetcherRespData, fetcherErrs := f.fetcher.FetchResponses(ctx, leftoverResp) 203 errs = fetcherErrs 204 205 f.cache.Responses.Save(ctx, fetcherRespData) 206 207 data = mergeData(data, fetcherRespData) 208 } 209 210 return 211 } 212 213 func (f *fetcherWithCache) FetchAccount(ctx context.Context, acccountDefaultJSON json.RawMessage, accountID string) (account json.RawMessage, errs []error) { 214 accountData := f.cache.Accounts.Get(ctx, []string{accountID}) 215 // TODO: add metrics 216 if account, ok := accountData[accountID]; ok { 217 f.metricsEngine.RecordAccountCacheResult(metrics.CacheHit, 1) 218 return account, errs 219 } else { 220 f.metricsEngine.RecordAccountCacheResult(metrics.CacheMiss, 1) 221 } 222 account, errs = f.fetcher.FetchAccount(ctx, acccountDefaultJSON, accountID) 223 if len(errs) == 0 { 224 f.cache.Accounts.Save(ctx, map[string]json.RawMessage{accountID: account}) 225 } 226 return account, errs 227 } 228 229 func (f *fetcherWithCache) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { 230 return "", nil 231 } 232 233 func findLeftovers(ids []string, data map[string]json.RawMessage) (leftovers []string) { 234 leftovers = make([]string, 0, len(ids)-len(data)) 235 for _, id := range ids { 236 if _, ok := data[id]; !ok { 237 leftovers = append(leftovers, id) 238 } 239 } 240 return 241 } 242 243 func mergeData(cachedData map[string]json.RawMessage, fetchedData map[string]json.RawMessage) (mergedData map[string]json.RawMessage) { 244 mergedData = cachedData 245 if mergedData == nil { 246 mergedData = fetchedData 247 } else { 248 for key, value := range fetchedData { 249 mergedData[key] = value 250 } 251 } 252 253 return 254 }