github.com/prebid/prebid-server/v2@v2.18.0/config/stored_requests.go (about)

     1  package config
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/golang/glog"
     9  )
    10  
    11  // DataType constants
    12  type DataType string
    13  
    14  const (
    15  	RequestDataType    DataType = "Request"
    16  	CategoryDataType   DataType = "Category"
    17  	VideoDataType      DataType = "Video"
    18  	AMPRequestDataType DataType = "AMP Request"
    19  	AccountDataType    DataType = "Account"
    20  	ResponseDataType   DataType = "Response"
    21  )
    22  
    23  // Section returns the config section this type is defined in
    24  func (dataType DataType) Section() string {
    25  	return map[DataType]string{
    26  		RequestDataType:    "stored_requests",
    27  		CategoryDataType:   "categories",
    28  		VideoDataType:      "stored_video_req",
    29  		AMPRequestDataType: "stored_amp_req",
    30  		AccountDataType:    "accounts",
    31  		ResponseDataType:   "stored_responses",
    32  	}[dataType]
    33  }
    34  
    35  // Section returns the config section
    36  func (sr *StoredRequests) Section() string {
    37  	return sr.dataType.Section()
    38  }
    39  
    40  // DataType returns the DataType associated with this config
    41  func (sr *StoredRequests) DataType() DataType {
    42  	return sr.dataType
    43  }
    44  
    45  // SetDataType sets the DataType on this config. Needed for tests.
    46  func (sr *StoredRequests) SetDataType(dataType DataType) {
    47  	sr.dataType = dataType
    48  }
    49  
    50  // StoredRequests struct defines options for stored requests for each data type
    51  // including some amp stored_requests options
    52  type StoredRequests struct {
    53  	// dataType is a tag pushed from upstream indicating the type of object fetched here
    54  	dataType DataType
    55  	// Files should be used if Stored Requests should be loaded from the filesystem.
    56  	// Fetchers are in stored_requests/backends/file_system/fetcher.go
    57  	Files FileFetcherConfig `mapstructure:"filesystem"`
    58  	// Database configures Fetchers and EventProducers which read from a Database.
    59  	// Fetchers are in stored_requests/backends/db_fetcher/fetcher.go
    60  	// EventProducers are in stored_requests/events/database
    61  	Database DatabaseConfig `mapstructure:"database"`
    62  	// HTTP configures an instance of stored_requests/backends/http/http_fetcher.go.
    63  	// If non-nil, Stored Requests will be fetched from the endpoint described there.
    64  	HTTP HTTPFetcherConfig `mapstructure:"http"`
    65  	// InMemoryCache configures an instance of stored_requests/caches/memory/cache.go.
    66  	// If non-nil, Stored Requests will be saved in an in-memory cache.
    67  	InMemoryCache InMemoryCache `mapstructure:"in_memory_cache"`
    68  	// CacheEvents configures an instance of stored_requests/events/api/api.go.
    69  	// This is a sub-object containing the endpoint name to use for this API endpoint.
    70  	CacheEvents CacheEventsConfig `mapstructure:"cache_events"`
    71  	// HTTPEvents configures an instance of stored_requests/events/http/http.go.
    72  	// If non-nil, the server will use those endpoints to populate and update the cache.
    73  	HTTPEvents HTTPEventsConfig `mapstructure:"http_events"`
    74  }
    75  
    76  // HTTPEventsConfig configures stored_requests/events/http/http.go
    77  type HTTPEventsConfig struct {
    78  	Endpoint    string `mapstructure:"endpoint"`
    79  	RefreshRate int64  `mapstructure:"refresh_rate_seconds"`
    80  	Timeout     int    `mapstructure:"timeout_ms"`
    81  	AmpEndpoint string `mapstructure:"amp_endpoint"`
    82  }
    83  
    84  func (cfg HTTPEventsConfig) TimeoutDuration() time.Duration {
    85  	return time.Duration(cfg.Timeout) * time.Millisecond
    86  }
    87  
    88  func (cfg HTTPEventsConfig) RefreshRateDuration() time.Duration {
    89  	return time.Duration(cfg.RefreshRate) * time.Second
    90  }
    91  
    92  // CacheEventsConfig configured stored_requests/events/api/api.go
    93  type CacheEventsConfig struct {
    94  	// Enabled should be true to enable the events api endpoint
    95  	Enabled bool `mapstructure:"enabled"`
    96  	// Endpoint is the url path exposed for this stored requests events api
    97  	Endpoint string `mapstructure:"endpoint"`
    98  }
    99  
   100  // FileFetcherConfig configures a stored_requests/backends/file_fetcher/fetcher.go
   101  type FileFetcherConfig struct {
   102  	// Enabled should be true if Stored Requests should be loaded from the filesystem.
   103  	Enabled bool `mapstructure:"enabled"`
   104  	// Path to the directory this file fetcher gets data from.
   105  	Path string `mapstructure:"directorypath"`
   106  }
   107  
   108  // HTTPFetcherConfig configures a stored_requests/backends/http_fetcher/fetcher.go
   109  type HTTPFetcherConfig struct {
   110  	Endpoint    string `mapstructure:"endpoint"`
   111  	AmpEndpoint string `mapstructure:"amp_endpoint"`
   112  }
   113  
   114  // Migrate combined stored_requests+amp configuration to separate simple config sections
   115  func resolvedStoredRequestsConfig(cfg *Configuration) {
   116  	sr := &cfg.StoredRequests
   117  	amp := &cfg.StoredRequestsAMP
   118  
   119  	sr.CacheEvents.Endpoint = "/storedrequests/openrtb2" // why is this here and not SetDefault ?
   120  
   121  	// Amp uses the same config but some fields get replaced by Amp* version of similar fields
   122  	cfg.StoredRequestsAMP = cfg.StoredRequests
   123  	amp.Database.FetcherQueries.QueryTemplate = sr.Database.FetcherQueries.AmpQueryTemplate
   124  	amp.Database.CacheInitialization.Query = sr.Database.CacheInitialization.AmpQuery
   125  	amp.Database.PollUpdates.Query = sr.Database.PollUpdates.AmpQuery
   126  	amp.HTTP.Endpoint = sr.HTTP.AmpEndpoint
   127  	amp.CacheEvents.Endpoint = "/storedrequests/amp"
   128  	amp.HTTPEvents.Endpoint = sr.HTTPEvents.AmpEndpoint
   129  
   130  	// Set data types for each section
   131  	cfg.StoredRequests.dataType = RequestDataType
   132  	cfg.StoredRequestsAMP.dataType = AMPRequestDataType
   133  	cfg.StoredVideo.dataType = VideoDataType
   134  	cfg.CategoryMapping.dataType = CategoryDataType
   135  	cfg.Accounts.dataType = AccountDataType
   136  	cfg.StoredResponses.dataType = ResponseDataType
   137  }
   138  
   139  func (cfg *StoredRequests) validate(errs []error) []error {
   140  	if cfg.DataType() == AccountDataType && cfg.Database.ConnectionInfo.Database != "" {
   141  		errs = append(errs, fmt.Errorf("%s.database: retrieving accounts via database not available, use accounts.files", cfg.Section()))
   142  	} else {
   143  		errs = cfg.Database.validate(cfg.DataType(), errs)
   144  	}
   145  
   146  	// Categories do not use cache so none of the following checks apply
   147  	if cfg.DataType() == CategoryDataType {
   148  		return errs
   149  	}
   150  
   151  	if cfg.InMemoryCache.Type == "none" {
   152  		if cfg.CacheEvents.Enabled {
   153  			errs = append(errs, fmt.Errorf("%s: cache_events must be disabled if in_memory_cache=none", cfg.Section()))
   154  		}
   155  
   156  		if cfg.HTTPEvents.RefreshRate != 0 {
   157  			errs = append(errs, fmt.Errorf("%s: http_events.refresh_rate_seconds must be 0 if in_memory_cache=none", cfg.Section()))
   158  		}
   159  
   160  		if cfg.Database.PollUpdates.Query != "" {
   161  			errs = append(errs, fmt.Errorf("%s: database.poll_for_updates.query must be empty if in_memory_cache=none", cfg.Section()))
   162  		}
   163  		if cfg.Database.CacheInitialization.Query != "" {
   164  			errs = append(errs, fmt.Errorf("%s: database.initialize_caches.query must be empty if in_memory_cache=none", cfg.Section()))
   165  		}
   166  	}
   167  	errs = cfg.InMemoryCache.validate(cfg.DataType(), errs)
   168  	return errs
   169  }
   170  
   171  // DatabaseConfig configures the Stored Request ecosystem to use Database. This must include a Fetcher,
   172  // and may optionally include some EventProducers to populate and refresh the caches.
   173  type DatabaseConfig struct {
   174  	ConnectionInfo      DatabaseConnection       `mapstructure:"connection"`
   175  	FetcherQueries      DatabaseFetcherQueries   `mapstructure:"fetcher"`
   176  	CacheInitialization DatabaseCacheInitializer `mapstructure:"initialize_caches"`
   177  	PollUpdates         DatabaseUpdatePolling    `mapstructure:"poll_for_updates"`
   178  }
   179  
   180  func (cfg *DatabaseConfig) validate(dataType DataType, errs []error) []error {
   181  	if cfg.ConnectionInfo.Database == "" {
   182  		return errs
   183  	}
   184  
   185  	errs = cfg.CacheInitialization.validate(dataType, errs)
   186  	errs = cfg.PollUpdates.validate(dataType, errs)
   187  	return errs
   188  }
   189  
   190  // DatabaseConnection has options which put types to the Database Connection string. See:
   191  // https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
   192  type DatabaseConnection struct {
   193  	Driver      string `mapstructure:"driver"`
   194  	Database    string `mapstructure:"dbname"`
   195  	Host        string `mapstructure:"host"`
   196  	Port        int    `mapstructure:"port"`
   197  	Username    string `mapstructure:"user"`
   198  	Password    string `mapstructure:"password"`
   199  	QueryString string `mapstructure:"query_string"`
   200  	TLS         TLS    `mapstructure:"tls"`
   201  }
   202  
   203  type TLS struct {
   204  	RootCert   string `mapstructure:"root_cert"`
   205  	ClientCert string `mapstructure:"client_cert"`
   206  	ClientKey  string `mapstructure:"client_key"`
   207  }
   208  
   209  type DatabaseFetcherQueries struct {
   210  	// QueryTemplate is the Database Query which can be used to fetch configs from the database.
   211  	// It is a Template, rather than a full Query, because a single HTTP request may reference multiple Stored Requests.
   212  	//
   213  	// In the simplest case, this could be something like:
   214  	//   SELECT id, requestData, 'request' as type
   215  	//     FROM stored_requests
   216  	//     WHERE id in $REQUEST_ID_LIST
   217  	//     UNION ALL
   218  	//   SELECT id, impData, 'imp' as type
   219  	//     FROM stored_imps
   220  	//     WHERE id in $IMP_ID_LIST
   221  	//
   222  	// The MakeQuery function will transform this query into:
   223  	//   SELECT id, requestData, 'request' as type
   224  	//     FROM stored_requests
   225  	//     WHERE id in ($1)
   226  	//     UNION ALL
   227  	//   SELECT id, impData, 'imp' as type
   228  	//     FROM stored_imps
   229  	//     WHERE id in ($2, $3, $4, ...)
   230  	//
   231  	// ... where the number of "$x" args depends on how many IDs are nested within the HTTP request.
   232  	QueryTemplate string `mapstructure:"query"`
   233  
   234  	// AmpQueryTemplate is the same as QueryTemplate, but used in the `/openrtb2/amp` endpoint.
   235  	AmpQueryTemplate string `mapstructure:"amp_query"`
   236  }
   237  
   238  type DatabaseCacheInitializer struct {
   239  	Timeout int `mapstructure:"timeout_ms"`
   240  	// Query should be something like:
   241  	//
   242  	// SELECT id, requestData, 'request' AS type FROM stored_requests
   243  	// UNION ALL
   244  	// SELECT id, impData, 'imp' AS type FROM stored_imps
   245  	//
   246  	// This query will be run once on startup to fetch _all_ known Stored Request data from the database.
   247  	//
   248  	// For more details on the expected format of requestData and impData, see stored_requests/events/database/database.go
   249  	Query string `mapstructure:"query"`
   250  	// AmpQuery is just like Query, but for AMP Stored Requests
   251  	AmpQuery string `mapstructure:"amp_query"`
   252  }
   253  
   254  func (cfg *DatabaseCacheInitializer) validate(dataType DataType, errs []error) []error {
   255  	section := dataType.Section()
   256  	if cfg.Query == "" {
   257  		return errs
   258  	}
   259  	if cfg.Timeout <= 0 {
   260  		errs = append(errs, fmt.Errorf("%s: database.initialize_caches.timeout_ms must be positive", section))
   261  	}
   262  	if strings.Contains(cfg.Query, "$") {
   263  		errs = append(errs, fmt.Errorf("%s: database.initialize_caches.query should not contain any wildcards denoted by $ (e.g. $LAST_UPDATED)", section))
   264  	}
   265  	return errs
   266  }
   267  
   268  type DatabaseUpdatePolling struct {
   269  	// RefreshRate determines how frequently the Query and AmpQuery are run.
   270  	RefreshRate int `mapstructure:"refresh_rate_seconds"`
   271  
   272  	// Timeout is the amount of time before a call to the database is aborted.
   273  	Timeout int `mapstructure:"timeout_ms"`
   274  
   275  	// An example UpdateQuery is:
   276  	//
   277  	// SELECT id, requestData, 'request' AS type
   278  	//   FROM stored_requests
   279  	//   WHERE last_updated > $LAST_UPDATED
   280  	// UNION ALL
   281  	// SELECT id, impData, 'imp' AS type
   282  	//   FROM stored_imps
   283  	//   WHERE last_updated > $LAST_UPDATED
   284  	//
   285  	// The code will be run periodically to fetch updates from the database.
   286  	Query string `mapstructure:"query"`
   287  	// AmpQuery is the same as Query, but used for the `/openrtb2/amp` endpoint.
   288  	AmpQuery string `mapstructure:"amp_query"`
   289  }
   290  
   291  func (cfg *DatabaseUpdatePolling) validate(dataType DataType, errs []error) []error {
   292  	section := dataType.Section()
   293  	if cfg.Query == "" {
   294  		return errs
   295  	}
   296  
   297  	if cfg.RefreshRate <= 0 {
   298  		errs = append(errs, fmt.Errorf("%s: database.poll_for_updates.refresh_rate_seconds must be > 0", section))
   299  	}
   300  
   301  	if cfg.Timeout <= 0 {
   302  		errs = append(errs, fmt.Errorf("%s: database.poll_for_updates.timeout_ms must be > 0", section))
   303  	}
   304  	if !strings.Contains(cfg.Query, "$LAST_UPDATED") {
   305  		errs = append(errs, fmt.Errorf("%s: database.poll_for_updates.query must contain $LAST_UPDATED parameter", section))
   306  	}
   307  
   308  	return errs
   309  }
   310  
   311  type InMemoryCache struct {
   312  	// Identify the type of memory cache. "none", "unbounded", "lru"
   313  	Type string `mapstructure:"type"`
   314  	// TTL is the maximum number of seconds that an unused value will stay in the cache.
   315  	// TTL <= 0 can be used for "no ttl". Elements will still be evicted based on the Size.
   316  	TTL int `mapstructure:"ttl_seconds"`
   317  	// Size is the max total cache size allowed for single caches
   318  	Size int `mapstructure:"size_bytes"`
   319  	// RequestCacheSize is the max number of bytes allowed in the cache for Stored Requests. Values <= 0 will have no limit
   320  	RequestCacheSize int `mapstructure:"request_cache_size_bytes"`
   321  	// ImpCacheSize is the max number of bytes allowed in the cache for Stored Imps. Values <= 0 will have no limit
   322  	ImpCacheSize int `mapstructure:"imp_cache_size_bytes"`
   323  	// ResponsesCacheSize is the max number of bytes allowed in the cache for Stored Responses. Values <= 0 will have no limit
   324  	RespCacheSize int `mapstructure:"resp_cache_size_bytes"`
   325  }
   326  
   327  func (cfg *InMemoryCache) validate(dataType DataType, errs []error) []error {
   328  	section := dataType.Section()
   329  	switch cfg.Type {
   330  	case "none":
   331  		// No errors for no config options
   332  	case "unbounded":
   333  		if cfg.TTL != 0 {
   334  			errs = append(errs, fmt.Errorf("%s: in_memory_cache.ttl_seconds is not supported for unbounded caches. Got %d", section, cfg.TTL))
   335  		}
   336  		if dataType == AccountDataType {
   337  			// single cache
   338  			if cfg.Size != 0 {
   339  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.size_bytes is not supported for unbounded caches. Got %d", section, cfg.Size))
   340  			}
   341  		} else {
   342  			// dual (request and imp) caches
   343  			if cfg.RequestCacheSize != 0 {
   344  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes is not supported for unbounded caches. Got %d", section, cfg.RequestCacheSize))
   345  			}
   346  			if cfg.ImpCacheSize != 0 {
   347  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes is not supported for unbounded caches. Got %d", section, cfg.ImpCacheSize))
   348  			}
   349  			if cfg.RespCacheSize != 0 {
   350  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.resp_cache_size_bytes is not supported for unbounded caches. Got %d", section, cfg.RespCacheSize))
   351  			}
   352  		}
   353  	case "lru":
   354  		if dataType == AccountDataType {
   355  			// single cache
   356  			if cfg.Size <= 0 {
   357  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.Size))
   358  			}
   359  			if cfg.RequestCacheSize > 0 || cfg.ImpCacheSize > 0 || cfg.RespCacheSize > 0 {
   360  				glog.Warningf("%s: in_memory_cache.request_cache_size_bytes, imp_cache_size_bytes and resp_cache_size_bytes do not apply to this section and will be ignored", section)
   361  			}
   362  		} else {
   363  			// dual (request and imp) caches
   364  			if cfg.RequestCacheSize <= 0 {
   365  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.RequestCacheSize))
   366  			}
   367  			if cfg.ImpCacheSize <= 0 {
   368  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.ImpCacheSize))
   369  			}
   370  			if cfg.RespCacheSize <= 0 {
   371  				errs = append(errs, fmt.Errorf("%s: in_memory_cache.resp_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.RespCacheSize))
   372  			}
   373  			if cfg.Size > 0 {
   374  				glog.Warningf("%s: in_memory_cache.size_bytes does not apply in this section and will be ignored", section)
   375  			}
   376  		}
   377  	default:
   378  		errs = append(errs, fmt.Errorf("%s: in_memory_cache.type %s is invalid", section, cfg.Type))
   379  	}
   380  	return errs
   381  }