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  }