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  }