diff --git a/clientutil/backend.go b/clientutil/backend.go new file mode 100644 index 0000000000000000000000000000000000000000..cd06a19a0371eebe0f8f89a2806d798938b976aa --- /dev/null +++ b/clientutil/backend.go @@ -0,0 +1,115 @@ +package clientutil + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "sync" + "time" +) + +// BackendConfig specifies the configuration to access a service. +// +// Services with multiple backends can be replicated or partitioned, +// depending on a configuration switch, making it a deployment-time +// decision. Clients are expected to compute their own sharding +// function (either by database lookup or other methods), and expose a +// 'shard' parameter on their APIs. +type BackendConfig struct { + URL string `yaml:"url"` + Sharded bool `yaml:"sharded"` + TLSConfig *TLSClientConfig `yaml:"tls_config"` +} + +// Backend is a runtime class that provides http Clients for use with +// a specific service backend. If the service can't be partitioned, +// pass an empty string to the Client method. +type Backend interface { + // URL for the service for a specific shard. + URL(string) string + + // Client that can be used to make a request to the service. + Client(string) *http.Client +} + +// NewBackend returns a new Backend with the given config. +func NewBackend(config *BackendConfig) (Backend, error) { + u, err := url.Parse(config.URL) + if err != nil { + return nil, err + } + + var tlsConfig *tls.Config + if config.TLSConfig != nil { + tlsConfig, err = config.TLSConfig.TLSConfig() + if err != nil { + return nil, err + } + } + + if config.Sharded { + return &replicatedClient{newHTTPClient(u, tlsConfig)}, nil + } + return &shardedClient{ + baseURL: u, + tlsConfig: tlsConfig, + shards: make(map[string]*http.Client), + }, nil +} + +type replicatedClient struct { + c *http.Client + u *url.URL +} + +func (r *replicatedClient) Client(_ string) *http.Client { return r.c } +func (r *replicatedClient) URL(_ string) string { return r.u.String() } + +type shardedClient struct { + baseURL *url.URL + tlsConfig *tls.Config + mx sync.Mutex + urls map[string]*url.URL + shards map[string]*http.Client +} + +func (s *shardedClient) getShardURL(shard string) *url.URL { + if shard == "" { + return s.baseURL + } + u, ok := s.urls[shard] + if !ok { + var tmp = *s.baseURL + tmp.Host = fmt.Sprintf("%s.%s", shard, tmp.Host) + u = &tmp + s.urls[shard] = u + } + return u +} + +func (s *shardedClient) URL(shard string) string { + s.mx.Lock() + defer s.mx.Unlock() + return s.getShardURL(shard).String() +} + +func (s *shardedClient) Client(shard string) *http.Client { + s.mx.Lock() + defer s.mx.Unlock() + + client, ok := s.shards[shard] + if !ok { + u := s.getShardURL(shard) + client = newHTTPClient(u, s.tlsConfig) + s.shards[shard] = client + } + return client +} + +func newHTTPClient(u *url.URL, tlsConfig *tls.Config) *http.Client { + return &http.Client{ + Transport: NewTransport([]string{u.Host}, tlsConfig, nil), + Timeout: 30 * time.Second, + } +}