go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/rpcquota/module.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package rpcquota 16 17 import ( 18 "context" 19 "flag" 20 21 "go.chromium.org/luci/common/errors" 22 luciflag "go.chromium.org/luci/common/flag" 23 "go.chromium.org/luci/common/logging" 24 "go.chromium.org/luci/config" 25 "go.chromium.org/luci/config/server/cfgmodule" 26 "go.chromium.org/luci/server/module" 27 quota "go.chromium.org/luci/server/quotabeta" 28 "go.chromium.org/luci/server/quotabeta/quotaconfig" 29 "go.chromium.org/luci/server/quotabeta/quotaconfig/configservice" 30 "go.chromium.org/luci/server/redisconn" 31 ) 32 33 var quotaTrackOnlyKey = "go.chromium.org/luci/resultdb/internal/rpcquota:quotaModeKey" 34 35 var ModuleName = module.RegisterName("go.chromium.org/luci/resultdb/internal/rpcquota") 36 37 var _ module.Module = &rpcquotaModule{} 38 39 // rpcquotaModule implements module.Module. 40 type rpcquotaModule struct { 41 opts *ModuleOptions 42 } 43 44 const ( 45 // Disabled: Quota is fully off. No quotaconfig loaded, no quota 46 // amounts deducted in redis, no UnaryServerInterceptor registered. 47 RPCQuotaModeDisabled = "disabled" 48 49 // Track only: deduct quota but don't fail requests. Intended for dark 50 // launch. 51 RPCQuotaModeTrackOnly = "track-only" 52 53 // RPC quota fully enabled. Requests that exceed quota will be failed. 54 RPCQuotaModeEnforce = "enforce" 55 ) 56 57 type ModuleOptions struct { 58 // RPCQuotaMode determines whether quota is off, active (but not 59 // enforced), or active and enforced. 60 RPCQuotaMode string 61 } 62 63 func (o *ModuleOptions) Register(fs *flag.FlagSet) { 64 if o.RPCQuotaMode == "" { 65 o.RPCQuotaMode = RPCQuotaModeDisabled 66 } 67 fs.Var( 68 luciflag.NewChoice(&o.RPCQuotaMode, RPCQuotaModeDisabled, RPCQuotaModeTrackOnly, RPCQuotaModeEnforce), 69 "rpcquota-mode", 70 "RPC quota mode. Options are: disabled, track-only, enforce.") 71 } 72 73 // NewModule returns a module.Module for the rpcquota library initialized from 74 // the given *ModuleOptions. 75 func NewModule(opts *ModuleOptions) module.Module { 76 return &rpcquotaModule{ 77 opts: opts, 78 } 79 } 80 81 // NewModuleFromFlags returns a module.Module for the rpcquota library which 82 // can be initialized from command line flags. 83 func NewModuleFromFlags() module.Module { 84 opts := &ModuleOptions{} 85 opts.Register(flag.CommandLine) 86 return NewModule(opts) 87 } 88 89 // Dependencies returns required and optional dependencies for this module. 90 // Implements module.Module. 91 func (*rpcquotaModule) Dependencies() []module.Dependency { 92 return []module.Dependency{ 93 module.RequiredDependency(redisconn.ModuleName), 94 // NOTE: cfgmodule and rpcquota must be initialized before 95 // quota, so it declaring quota.ModuleName as a dependency 96 // doesn't work. Instead rpcquota users must pass rpcquota 97 // directly to MainWithModules to ensure it gets initialized 98 // first. 99 module.RequiredDependency(cfgmodule.ModuleName), 100 } 101 } 102 103 // Name returns the module.Name for this module. 104 // Implements module.Module. 105 func (*rpcquotaModule) Name() module.Name { 106 return ModuleName 107 } 108 109 var ErrInvalidOptions = errors.New("Invalid options") 110 111 // Initialize initializes this module by adding a quota.Use implementation to 112 // the serving context, according to the RPCQuotaMode in the ModuleOptions. 113 func (m *rpcquotaModule) Initialize(ctx context.Context, host module.Host, opts module.HostOptions) (context.Context, error) { 114 switch m.opts.RPCQuotaMode { 115 case RPCQuotaModeDisabled: 116 // Install a stub config so that luci/server/quota Initialize 117 // does not panic, but otherwise there's nothing to do. 118 cfg, err := quotaconfig.NewMemory(ctx, nil) 119 if err != nil { 120 return nil, err 121 } 122 return quota.Use(ctx, cfg), nil 123 case RPCQuotaModeTrackOnly: 124 // Leave a flag in the context that will be checked by 125 // UpdateUserQuota before returning ErrInsufficientQuota. 126 // Note that this is not the common path: we expect that 127 // 1. nearly all requests will be within quota 128 // 2. this option is only set during the "dark" launch phase. 129 ctx = context.WithValue(ctx, "aTrackOnlyKey, true) 130 case RPCQuotaModeEnforce: 131 default: 132 return nil, ErrInvalidOptions 133 } 134 if !opts.Prod { 135 logging.Warningf(ctx, "RPC quota not disabled, but not running in prod.") 136 } 137 138 ss, err := config.ServiceSet("luci-resultdb") 139 if err != nil { 140 return nil, err 141 } 142 cfgsvc := configservice.New(ctx, ss, "rpcquota.cfg") 143 if err := cfgsvc.Refresh(ctx); err != nil { 144 return nil, err 145 } 146 ctx = quota.Use(ctx, cfgsvc) 147 148 host.RegisterUnaryServerInterceptors(quotaCheckInterceptor()) 149 return ctx, nil 150 }