From 18617bd4777b0306a813a4c0fe89adfbf851a15d Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 17 Feb 2022 10:47:09 -0600 Subject: [PATCH] Support limiting throttle settings by app name --- devicefilter/devicefilter.go | 2 +- redis/measured_reporter.go | 8 +++++- throttle/throttle.go | 54 ++++++++++++++++++++++++------------ throttle/throttle_test.go | 50 ++++++++++++++++++++------------- 4 files changed, 74 insertions(+), 40 deletions(-) diff --git a/devicefilter/devicefilter.go b/devicefilter/devicefilter.go index 9eb0129a..4c24ee9e 100644 --- a/devicefilter/devicefilter.go +++ b/devicefilter/devicefilter.go @@ -118,7 +118,7 @@ func (f *deviceFilterPre) Apply(cs *filters.ConnectionState, req *http.Request, return next(cs, req) } - settings, capOn := f.throttleConfig.SettingsFor(lanternDeviceID, u.CountryCode, req.Header.Get(common.PlatformHeader), req.Header[common.SupportedDataCaps]) + settings, capOn := f.throttleConfig.SettingsFor(lanternDeviceID, u.CountryCode, req.Header.Get(common.PlatformHeader), req.Header.Get(common.AppHeader), req.Header[common.SupportedDataCaps]) measuredCtx := map[string]interface{}{ "throttled": false, diff --git a/redis/measured_reporter.go b/redis/measured_reporter.go index 4536e90f..ebf3a521 100644 --- a/redis/measured_reporter.go +++ b/redis/measured_reporter.go @@ -130,12 +130,18 @@ func submit(countryLookup geo.CountryLookup, rc *redis.Client, scriptSHA string, platform = _platform.(string) } + var appName string + _appName, ok := sac.ctx["app"] + if ok { + appName = _appName.(string) + } + var supportedDataCaps []string _supportedDataCaps, ok := sac.ctx["supported_data_caps"] if ok { supportedDataCaps = _supportedDataCaps.([]string) } - throttleSettings, hasThrottleSettings := throttleConfig.SettingsFor(deviceID, countryCode, platform, supportedDataCaps) + throttleSettings, hasThrottleSettings := throttleConfig.SettingsFor(deviceID, countryCode, platform, appName, supportedDataCaps) pl := rc.Pipeline() throttleCohort := "" diff --git a/throttle/throttle.go b/throttle/throttle.go index d54cb485..6624cb7c 100644 --- a/throttle/throttle.go +++ b/throttle/throttle.go @@ -45,6 +45,9 @@ type Settings struct { // Label uniquely identifies this set of settings for reporting purposes Label string + // AppName constrains this setting to a particular application name. Leave blank to apply to all applications. + AppName string + // DeviceFloor is an optional number between 0 and 1 that sets the floor (inclusive) of devices included in the cohort that gets these settings DeviceFloor float64 @@ -85,7 +88,7 @@ type Config interface { // (country and platform) this should fall back to default values if a specific value isn't provided. // supportedDataCaps identifies which cap intervals the client supports ("daily", "weekly" or "monthly"). // If this list is empty, the client is assumed to support "monthly" (legacy clients). - SettingsFor(deviceID string, countryCode string, platform string, supportedDataCaps []string) (settings *Settings, ok bool) + SettingsFor(deviceID, countryCode, platform, appName string, supportedDataCaps []string) (settings *Settings, ok bool) } // NewForcedConfig returns a new Config that uses the forced threshold, rate and TTL @@ -104,7 +107,7 @@ type forcedConfig struct { Settings } -func (cfg *forcedConfig) SettingsFor(deviceID string, countryCode string, platform string, supportedDataCaps []string) (settings *Settings, ok bool) { +func (cfg *forcedConfig) SettingsFor(deviceID, countryCode, platform, appName string, supportedDataCaps []string) (settings *Settings, ok bool) { return &cfg.Settings, true } @@ -186,25 +189,25 @@ func (cfg *redisConfig) refreshSettings() { cfg.mx.Unlock() } -func (cfg *redisConfig) SettingsFor(deviceID string, countryCode string, platform string, supportedDataCaps []string) (*Settings, bool) { +func (cfg *redisConfig) SettingsFor(deviceID, countryCode, platform, appName string, supportedDataCaps []string) (*Settings, bool) { cfg.mx.RLock() settings := cfg.settings cfg.mx.RUnlock() - platformSettings, _ := settings[strings.ToLower(countryCode)] + platformSettings := settings[strings.ToLower(countryCode)] if platformSettings == nil { log.Tracef("No settings found for country %v, use default", countryCode) - platformSettings, _ = settings["default"] + platformSettings = settings["default"] if platformSettings == nil { log.Trace("No settings for default country, not throttling") return nil, false } } - constrainedSettings, _ := platformSettings[strings.ToLower(platform)] + constrainedSettings := platformSettings[strings.ToLower(platform)] if len(constrainedSettings) == 0 { log.Tracef("No settings found for platform %v, use default", platform) - constrainedSettings, _ = platformSettings["default"] + constrainedSettings = platformSettings["default"] if len(constrainedSettings) == 0 { log.Trace("No settings for default platform, not throttling") return nil, false @@ -229,21 +232,36 @@ func (cfg *redisConfig) SettingsFor(deviceID string, countryCode string, platfor hashOfDeviceID := hash.Sum64() const scale = 1000000 // do not change this, as it will result in users being segmented differently than they were before segment := float64((hashOfDeviceID % scale)) / float64(scale) - for _, candidateSettings := range constrainedSettings { - if clientSupportsInterval(candidateSettings.CapResets) { - if candidateSettings.DeviceFloor <= segment && (candidateSettings.DeviceCeil > segment || (candidateSettings.DeviceCeil == 1 && segment == 1)) { - return candidateSettings, true + + settingsForAppName := func(checkAppName string) *Settings { + for _, candidateSettings := range constrainedSettings { + if clientSupportsInterval(candidateSettings.CapResets) { + appMatches := candidateSettings.AppName == checkAppName + deviceMatches := candidateSettings.DeviceFloor <= segment && (candidateSettings.DeviceCeil > segment || (candidateSettings.DeviceCeil == 1 && segment == 1)) + if appMatches && deviceMatches { + return candidateSettings + } } } - } - log.Tracef("No setting for segment %v, using first supported in list", segment) - for _, candidateSettings := range constrainedSettings { - if clientSupportsInterval(candidateSettings.CapResets) { - return candidateSettings, true + log.Tracef("No setting for segment %v, using first supported in list", segment) + for _, candidateSettings := range constrainedSettings { + if clientSupportsInterval(candidateSettings.CapResets) { + appMatches := candidateSettings.AppName == checkAppName + if appMatches { + return candidateSettings + } + } } + + return nil + } + + result := settingsForAppName(appName) + if result == nil && appName != "" { + log.Tracef("No applicable settings found for app name %v, trying with no app name", appName) + result = settingsForAppName("") } - log.Trace("No cap available, don't throttle") - return nil, false + return result, result != nil } diff --git a/throttle/throttle_test.go b/throttle/throttle_test.go index b7ceb9f1..a0044816 100644 --- a/throttle/throttle_test.go +++ b/throttle/throttle_test.go @@ -43,12 +43,18 @@ const ( {"label": "cohort 8", "deviceFloor": 0.5, "deviceCeil": 1.0, "threshold": 4100, "rate": 410, "capResets": "monthly"}, {"label": "cohort 8", "deviceFloor": 0.5, "deviceCeil": 1.0, "threshold": 4200, "rate": 420, "capResets": "legacy"} ] + }, + "ir": { + "default": [ + {"label": "capped", "threshold": 1000, "rate": 100, "capResets": "monthly"}, + {"label": "notcapped", "threshold": 1000000, "rate": 100, "capResets": "monthly", "appName": "specialapp"} + ] } }` ) -func doTest(t *testing.T, cfg Config, deviceID string, countryCode string, platform string, supportedDataCaps []string, expectedThreshold int64, expectedRate int64, expectedCapResets CapInterval, testCase string) { - settings, ok := cfg.SettingsFor(deviceID, countryCode, platform, supportedDataCaps) +func doTest(t *testing.T, cfg Config, deviceID, countryCode, platform, appName string, supportedDataCaps []string, expectedThreshold int64, expectedRate int64, expectedCapResets CapInterval, testCase string) { + settings, ok := cfg.SettingsFor(deviceID, countryCode, platform, appName, supportedDataCaps) require.True(t, ok, "valid config for "+testCase) require.NotNil(t, settings, "non-nil settings for "+testCase) require.Equal(t, expectedThreshold, settings.Threshold, "correct threshold for "+testCase) @@ -65,35 +71,39 @@ func TestThrottleConfig(t *testing.T) { // try a bad config first require.NoError(t, rc.Set(context.Background(), "_throttle", "blah I'm bad settings blah", 0).Err()) cfg := NewRedisConfig(rc, refreshInterval) - _, ok := cfg.SettingsFor(deviceIDInSegment1, "cn", "windows", []string{"monthly", "weekly"}) + _, ok := cfg.SettingsFor(deviceIDInSegment1, "cn", "windows", "lantern", []string{"monthly", "weekly"}) require.False(t, ok, "Loading throttle settings from bad config should fail") // now do a good config require.NoError(t, rc.Set(context.Background(), "_throttle", goodSettings, 0).Err()) cfg = NewRedisConfig(rc, refreshInterval) - doTest(t, cfg, deviceIDInSegment1, "cn", "windows", []string{"monthly", "weekly"}, 4000, 400, "weekly", "known country, known platform, segment 1") - doTest(t, cfg, deviceIDInSegment2, "cn", "windows", []string{"monthly", "weekly"}, 4100, 410, "monthly", "known country, known platform, segment 2") - doTest(t, cfg, deviceIDInSegment1, "cn", "windows", []string{"monthly", "weekly"}, 4000, 400, "weekly", "known country, known platform, unknown segment") - doTest(t, cfg, deviceIDInSegment1, "cn", "windows", nil, 4200, 420, "legacy", "known country, known platform, segment 1, legacy client") + doTest(t, cfg, deviceIDInSegment1, "cn", "windows", "lantern", []string{"monthly", "weekly"}, 4000, 400, "weekly", "known country, known platform, segment 1") + doTest(t, cfg, deviceIDInSegment2, "cn", "windows", "lantern", []string{"monthly", "weekly"}, 4100, 410, "monthly", "known country, known platform, segment 2") + doTest(t, cfg, deviceIDInSegment1, "cn", "windows", "lantern", []string{"monthly", "weekly"}, 4000, 400, "weekly", "known country, known platform, unknown segment") + doTest(t, cfg, deviceIDInSegment1, "cn", "windows", "lantern", nil, 4200, 420, "legacy", "known country, known platform, segment 1, legacy client") + + doTest(t, cfg, deviceIDInSegment1, "cn", "", "lantern", []string{"monthly", "weekly"}, 3000, 300, "weekly", "known country, unknown platform, segment 1") + doTest(t, cfg, deviceIDInSegment2, "cn", "", "lantern", []string{"monthly", "weekly"}, 3100, 310, "monthly", "known country, unknown platform, segment 2") + doTest(t, cfg, deviceIDInSegment1, "cn", "", "lantern", []string{"monthly", "weekly"}, 3000, 300, "weekly", "known country, unknown platform, unknown segment") - doTest(t, cfg, deviceIDInSegment1, "cn", "", []string{"monthly", "weekly"}, 3000, 300, "weekly", "known country, unknown platform, segment 1") - doTest(t, cfg, deviceIDInSegment2, "cn", "", []string{"monthly", "weekly"}, 3100, 310, "monthly", "known country, unknown platform, segment 2") - doTest(t, cfg, deviceIDInSegment1, "cn", "", []string{"monthly", "weekly"}, 3000, 300, "weekly", "known country, unknown platform, unknown segment") + doTest(t, cfg, deviceIDInSegment1, "de", "windows", "lantern", []string{"monthly", "weekly"}, 2000, 200, "weekly", "unknown country, known platform, segment 1") + doTest(t, cfg, deviceIDInSegment2, "de", "windows", "lantern", []string{"monthly", "weekly"}, 2100, 210, "monthly", "unknown country, known platform, segment 2") + doTest(t, cfg, deviceIDInSegment1, "de", "windows", "lantern", []string{"monthly", "weekly"}, 2000, 200, "weekly", "unknown country, known platform, unknown segment") - doTest(t, cfg, deviceIDInSegment1, "de", "windows", []string{"monthly", "weekly"}, 2000, 200, "weekly", "unknown country, known platform, segment 1") - doTest(t, cfg, deviceIDInSegment2, "de", "windows", []string{"monthly", "weekly"}, 2100, 210, "monthly", "unknown country, known platform, segment 2") - doTest(t, cfg, deviceIDInSegment1, "de", "windows", []string{"monthly", "weekly"}, 2000, 200, "weekly", "unknown country, known platform, unknown segment") + doTest(t, cfg, deviceIDInSegment1, "de", "", "lantern", []string{"monthly", "weekly"}, 1000, 100, "weekly", "unknown country, unknown platform, segment 1") + doTest(t, cfg, deviceIDInSegment2, "de", "", "lantern", []string{"monthly", "weekly"}, 1100, 110, "monthly", "unknown country, unknown platform, segment 2") + doTest(t, cfg, deviceIDInSegment1, "de", "", "lantern", []string{"monthly", "weekly"}, 1000, 100, "weekly", "unknown country, unknown platform, unknown segment") - doTest(t, cfg, deviceIDInSegment1, "de", "", []string{"monthly", "weekly"}, 1000, 100, "weekly", "unknown country, unknown platform, segment 1") - doTest(t, cfg, deviceIDInSegment2, "de", "", []string{"monthly", "weekly"}, 1100, 110, "monthly", "unknown country, unknown platform, segment 2") - doTest(t, cfg, deviceIDInSegment1, "de", "", []string{"monthly", "weekly"}, 1000, 100, "weekly", "unknown country, unknown platform, unknown segment") + doTest(t, cfg, deviceIDInSegment1, "ir", "", "lantern", []string{"monthly", "weekly"}, 1000, 100, "monthly", "capped named app") + doTest(t, cfg, deviceIDInSegment1, "ir", "", "", []string{"monthly", "weekly"}, 1000, 100, "monthly", "capped unnamed app") + doTest(t, cfg, deviceIDInSegment1, "ir", "", "specialapp", []string{"monthly", "weekly"}, 1000000, 100, "monthly", "uncapped app") // update settings require.NoError(t, rc.Set(context.Background(), "_throttle", strings.ReplaceAll(goodSettings, "4", "5"), 0).Err()) time.Sleep(refreshInterval * 2) - doTest(t, cfg, deviceIDInSegment1, "cn", "windows", []string{"monthly", "weekly"}, 5000, 500, "weekly", "known country, known platform, segment 1, after update") + doTest(t, cfg, deviceIDInSegment1, "cn", "windows", "lantern", []string{"monthly", "weekly"}, 5000, 500, "weekly", "known country, known platform, segment 1, after update") } func TestForcedConfig(t *testing.T) { @@ -101,7 +111,7 @@ func TestForcedConfig(t *testing.T) { defer stopCapture() cfg := NewForcedConfig(1024, 512, "weekly") - doTest(t, cfg, deviceIDInSegment1, "", "", []string{"monthly", "weekly"}, 1024, 512, "weekly", "forced config") + doTest(t, cfg, deviceIDInSegment1, "", "", "lantern", []string{"monthly", "weekly"}, 1024, 512, "weekly", "forced config") } func TestFailToConnectRedis(t *testing.T) { @@ -114,7 +124,7 @@ func TestFailToConnectRedis(t *testing.T) { }) cfg := NewRedisConfig(bogusClient, refreshInterval) - _, ok := cfg.SettingsFor(deviceIDInSegment1, "cn", "windows", []string{"monthly", "weekly"}) + _, ok := cfg.SettingsFor(deviceIDInSegment1, "cn", "windows", "lantern", []string{"monthly", "weekly"}) require.False(t, ok, "Loading throttle settings when unable to contact redis should fail") redisClient := testutil.TestRedis(t) @@ -123,5 +133,5 @@ func TestFailToConnectRedis(t *testing.T) { time.Sleep(refreshInterval * 2) // Should load the config when Redis is back up online - doTest(t, cfg, deviceIDInSegment1, "cn", "windows", []string{"monthly", "weekly"}, 4000, 400, "weekly", "known country, known platform, segment 1, redis back online") + doTest(t, cfg, deviceIDInSegment1, "cn", "windows", "lantern", []string{"monthly", "weekly"}, 4000, 400, "weekly", "known country, known platform, segment 1, redis back online") }