Skip to content

Commit

Permalink
Support limiting throttle settings by app name
Browse files Browse the repository at this point in the history
  • Loading branch information
oxtoacart committed Feb 17, 2022
1 parent b98bfca commit 18617bd
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 40 deletions.
2 changes: 1 addition & 1 deletion devicefilter/devicefilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion redis/measured_reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ""
Expand Down
54 changes: 36 additions & 18 deletions throttle/throttle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
50 changes: 30 additions & 20 deletions throttle/throttle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -65,43 +71,47 @@ 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) {
stopCapture := testlog.Capture(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) {
Expand All @@ -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)
Expand All @@ -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")
}

0 comments on commit 18617bd

Please sign in to comment.