go.uber.org/yarpc@v1.72.1/yarpcconfig/chooser.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package yarpcconfig 22 23 import ( 24 "fmt" 25 "sort" 26 "strings" 27 28 "go.uber.org/yarpc/api/peer" 29 "go.uber.org/yarpc/internal/config" 30 peerbind "go.uber.org/yarpc/peer" 31 ) 32 33 // PeerChooser facilitates decoding and building peer choosers. A peer chooser 34 // combines a peer list (which implements the peer selection strategy) and a 35 // peer list updater (which informs the peer list about different peers), 36 // allowing transports to rely on these two pieces for peer selection and load 37 // balancing. 38 // 39 // Format 40 // 41 // Peer chooser configuration may define only one of the following keys: 42 // `peer`, `with`, or the name of any registered PeerListSpec. 43 // 44 // `peer` indicates that requests must be sent to a single peer. 45 // 46 // http: 47 // peer: 127.0.0.1:8080 48 // 49 // Note that how this string is interpreted is transport-dependent. 50 // 51 // `with` specifies that a named peer chooser preset defined by the transport 52 // should be used rather than defining one by hand in the configuration. 53 // 54 // # Given a dev-proxy preset on your TransportSpec, 55 // http: 56 // with: dev-proxy 57 // 58 // If the name of a registered PeerListSpec is the key, an object specifying 59 // the configuration parameters for the PeerListSpec is expected along with 60 // the name of a known peer list updater and its configuration. 61 // 62 // # cfg.RegisterPeerList(roundrobin.Spec()) 63 // round-robin: 64 // peers: 65 // - 127.0.0.1:8080 66 // - 127.0.0.1:8081 67 // 68 // In the example above, there are no configuration parameters for the round 69 // robin peer list. The only remaining key is the name of the peer list 70 // updater: `peers` which is just a static list of peers. 71 // 72 // Integration 73 // 74 // To integrate peer choosers with your transport, embed this struct into your 75 // outbound configuration. 76 // 77 // type myOutboundConfig struct { 78 // config.PeerChooser 79 // 80 // Address string 81 // } 82 // 83 // Then in your Build*Outbound function, use the PeerChooser.BuildPeerChooser 84 // method to retrieve a peer chooser for your outbound. The following example 85 // only works if your transport implements the peer.Transport interface. 86 // 87 // func buildOutbound(cfg *myOutboundConfig, t transport.Transport, k *config.Kit) (transport.UnaryOutbound, error) { 88 // myTransport := t.(*MyTransport) 89 // peerChooser, err := cfg.BuildPeerChooser(myTransport, hostport.Identify, k) 90 // if err != nil { 91 // return nil, err 92 // } 93 // return myTransport.NewOutbound(peerChooser), nil 94 // } 95 // 96 // The *config.Kit received by the Build*Outbound function MUST be passed to 97 // the BuildPeerChooser function as-is. 98 // 99 // Note that the keys for the PeerChooser: peer, with, and any peer list name, 100 // share the namespace with the attributes of your outbound configuration. 101 type PeerChooser struct { 102 peerChooser 103 } 104 105 // peerChooser is the private representation of PeerChooser that captures 106 // decoded configuration without revealing it on the public type. 107 type peerChooser struct { 108 Peer string `config:"peer,interpolate"` 109 Preset string `config:"with,interpolate"` 110 Etc config.AttributeMap `config:",squash"` 111 } 112 113 // Empty returns true if the PeerChooser is empty, i.e., it does not have any 114 // keys defined. 115 // 116 // This allows Build*Outbound functions to handle the case where the peer 117 // configuration is specified in a different way than the standard peer 118 // configuration. 119 func (pc PeerChooser) Empty() bool { 120 return pc.Peer == "" && pc.Preset == "" && len(pc.Etc) == 0 121 } 122 123 // BuildPeerChooser translates the decoded configuration into a peer.Chooser. 124 // 125 // The identify function informs us how to convert string-based peer names 126 // into peer identifiers for the transport. 127 // 128 // The Kit received by the Build*Outbound function MUST be passed to 129 // BuildPeerChooser as-is. 130 func (pc PeerChooser) BuildPeerChooser(transport peer.Transport, identify func(string) peer.Identifier, kit *Kit) (peer.Chooser, error) { 131 // Establish a peer selection strategy. 132 switch { 133 case pc.Peer != "": 134 // myoutbound: 135 // outboundopt1: ... 136 // outboundopt2: ... 137 // peer: 127.0.0.1:8080 138 if len(pc.Etc) > 0 { 139 return nil, fmt.Errorf("unrecognized attributes in outbound config: %+v", pc.Etc) 140 } 141 return peerbind.NewSingle(identify(pc.Peer), transport), nil 142 case pc.Preset != "": 143 // myoutbound: 144 // outboundopt1: ... 145 // outboundopt2: ... 146 // with: somepreset 147 if len(pc.Etc) > 0 { 148 return nil, fmt.Errorf("unrecognized attributes in outbound config: %+v", pc.Etc) 149 } 150 151 preset, err := kit.peerChooserPreset(pc.Preset) 152 if err != nil { 153 return nil, err 154 } 155 156 return preset.Build(transport, kit) 157 default: 158 // myoutbound: 159 // outboundopt1: ... 160 // outboundopt2: ... 161 // my-peer-list: 162 // ... 163 return pc.buildPeerChooser(transport, identify, kit) 164 } 165 } 166 167 func (pc PeerChooser) buildPeerChooser(transport peer.Transport, identify func(string) peer.Identifier, kit *Kit) (peer.Chooser, error) { 168 peerChooserName, peerChooserConfig, err := getPeerListInfo(pc.Etc, kit) 169 if err != nil { 170 return nil, err 171 } 172 173 if peerChooserSpec := kit.maybePeerChooserSpec(peerChooserName); peerChooserSpec != nil { 174 chooserBuilder, err := peerChooserSpec.PeerChooser.Decode(peerChooserConfig, config.InterpolateWith(kit.resolver)) 175 if err != nil { 176 return nil, err 177 } 178 result, err := chooserBuilder.Build(transport, kit) 179 if err != nil { 180 return nil, err 181 } 182 return result.(peer.Chooser), nil 183 } 184 185 // if there was no chooser registered, we assume we have a peer list registered 186 peerListSpec, err := kit.peerListSpec(peerChooserName) 187 if err != nil { 188 return nil, err 189 } 190 191 // This builds the peer list updater and also removes its entry from the 192 // map. Given, 193 // 194 // least-pending: 195 // failurePenalty: 5s 196 // dns: 197 // .. 198 // 199 // We will be left with only failurePenalty in the map so that we can simply 200 // decode it into the peer list configuration type. 201 peerListUpdater, err := buildPeerListUpdater(peerChooserConfig, identify, kit) 202 if err != nil { 203 return nil, err 204 } 205 206 listBuilder, err := peerListSpec.PeerList.Decode(peerChooserConfig, config.InterpolateWith(kit.resolver)) 207 if err != nil { 208 return nil, err 209 } 210 result, err := listBuilder.Build(transport, kit) 211 if err != nil { 212 return nil, err 213 } 214 peerChooser := result.(peer.ChooserList) 215 216 return peerbind.Bind(peerChooser, peerListUpdater), nil 217 } 218 219 // getPeerListInfo extracts the peer list entry from the given attribute map. It 220 // must be the only remaining entry. 221 // 222 // For example, in 223 // 224 // myoutbound: 225 // outboundopt1: ... 226 // outboundopt2: ... 227 // my-peer-list: 228 // ... 229 // 230 // By the time getPeerListInfo is called, the map must only be, 231 // 232 // my-peer-list: 233 // ... 234 // 235 // The name of the peer list (my-peer-list) is returned with the attributes 236 // specified under that entry. 237 func getPeerListInfo(etc config.AttributeMap, kit *Kit) (name string, config config.AttributeMap, err error) { 238 names := etc.Keys() 239 switch len(names) { 240 case 0: 241 err = fmt.Errorf("no peer list or chooser provided in config, need one of: %+v", kit.peerChooserAndListSpecNames()) 242 case 1: 243 name = names[0] 244 _, err = etc.Pop(name, &config) 245 default: 246 err = fmt.Errorf("unrecognized attributes in outbound config: %+v", etc) 247 } 248 return 249 } 250 251 // buildPeerListUpdater builds the peer list updater given the peer list 252 // configuration map. For example, we might get, 253 // 254 // least-pending: 255 // failurePenalty: 5s 256 // dns: 257 // name: myservice.example.com 258 // record: A 259 func buildPeerListUpdater(c config.AttributeMap, identify func(string) peer.Identifier, kit *Kit) (peer.Binder, error) { 260 // Special case for explicit list of peers. 261 var peers []string 262 if _, err := c.Pop("peers", &peers); err != nil { 263 return nil, err 264 } 265 if len(peers) > 0 { 266 return peerbind.BindPeers(identifyAll(identify, peers)), nil 267 } 268 // TODO: Make peers a separate peer list updater that is registered by 269 // default instead of special casing here. 270 271 var ( 272 // The peer list updater config is in the same namespace as the 273 // attributes for the peer list config. We want to ensure that there is 274 // exactly one peer list updater in the config. 275 foundUpdaters []string 276 277 // The peer list updater spec we'll actually use. 278 peerListUpdaterSpec *compiledPeerListUpdaterSpec 279 ) 280 281 for name := range c { 282 spec := kit.peerListUpdaterSpec(name) 283 if spec != nil { 284 peerListUpdaterSpec = spec 285 foundUpdaters = append(foundUpdaters, name) 286 } 287 } 288 289 switch len(foundUpdaters) { 290 case 0: 291 updaterSpecNames := kit.peerListUpdaterSpecNames() 292 reason := "no peer list updaters are registered" 293 if len(updaterSpecNames) > 0 { 294 reason = "need one of " + strings.Join(updaterSpecNames, ", ") 295 } 296 return nil, fmt.Errorf( 297 "no recognized peer list updater in config: got %s; %s", 298 strings.Join(configNames(c), ", "), 299 reason, 300 ) 301 case 1: 302 // fall through to logic below 303 default: 304 sort.Strings(foundUpdaters) // deterministic error message 305 return nil, fmt.Errorf( 306 "found too many peer list updaters in config: got %s", 307 strings.Join(foundUpdaters, ", ")) 308 } 309 310 var peerListUpdaterConfig config.AttributeMap 311 if _, err := c.Pop(foundUpdaters[0], &peerListUpdaterConfig); err != nil { 312 return nil, err 313 } 314 315 // This decodes all attributes on the peer list updater block, including the 316 // field with the name of the peer list updater. 317 peerListUpdaterBuilder, err := peerListUpdaterSpec.PeerListUpdater.Decode(peerListUpdaterConfig, config.InterpolateWith(kit.resolver)) 318 if err != nil { 319 return nil, err 320 } 321 322 result, err := peerListUpdaterBuilder.Build(kit) 323 if err != nil { 324 return nil, err 325 } 326 327 return result.(peer.Binder), nil 328 } 329 330 func identifyAll(identify func(string) peer.Identifier, peers []string) []peer.Identifier { 331 pids := make([]peer.Identifier, len(peers)) 332 for i, p := range peers { 333 pids[i] = identify(p) 334 } 335 return pids 336 } 337 338 func configNames(c config.AttributeMap) (names []string) { 339 for name := range c { 340 names = append(names, name) 341 } 342 sort.Strings(names) 343 return 344 }