1/*
2 *
3 * Copyright 2020 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19package client
20
21import (
22	"fmt"
23	"sync"
24	"time"
25
26	"google.golang.org/grpc/xds/internal/client/bootstrap"
27)
28
29const defaultWatchExpiryTimeout = 15 * time.Second
30
31// This is the Client returned by New(). It contains one client implementation,
32// and maintains the refcount.
33var singletonClient = &Client{}
34
35// To override in tests.
36var bootstrapNewConfig = bootstrap.NewConfig
37
38// Client is a full fledged gRPC client which queries a set of discovery APIs
39// (collectively termed as xDS) on a remote management server, to discover
40// various dynamic resources.
41//
42// The xds client is a singleton. It will be shared by the xds resolver and
43// balancer implementations, across multiple ClientConns and Servers.
44type Client struct {
45	*clientImpl
46
47	// This mu protects all the fields, including the embedded clientImpl above.
48	mu       sync.Mutex
49	refCount int
50}
51
52// New returns a new xdsClient configured by the bootstrap file specified in env
53// variable GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG.
54//
55// The returned xdsClient is a singleton. This function creates the xds client
56// if it doesn't already exist.
57//
58// Note that the first invocation of New() or NewWithConfig() sets the client
59// singleton. The following calls will return the singleton xds client without
60// checking or using the config.
61func New() (*Client, error) {
62	singletonClient.mu.Lock()
63	defer singletonClient.mu.Unlock()
64	// If the client implementation was created, increment ref count and return
65	// the client.
66	if singletonClient.clientImpl != nil {
67		singletonClient.refCount++
68		return singletonClient, nil
69	}
70
71	// Create the new client implementation.
72	config, err := bootstrapNewConfig()
73	if err != nil {
74		return nil, fmt.Errorf("xds: failed to read bootstrap file: %v", err)
75	}
76	c, err := newWithConfig(config, defaultWatchExpiryTimeout)
77	if err != nil {
78		return nil, err
79	}
80
81	singletonClient.clientImpl = c
82	singletonClient.refCount++
83	return singletonClient, nil
84}
85
86// NewWithConfig returns a new xdsClient configured by the given config.
87//
88// The returned xdsClient is a singleton. This function creates the xds client
89// if it doesn't already exist.
90//
91// Note that the first invocation of New() or NewWithConfig() sets the client
92// singleton. The following calls will return the singleton xds client without
93// checking or using the config.
94//
95// This function is internal only, for c2p resolver to use. DO NOT use this
96// elsewhere. Use New() instead.
97func NewWithConfig(config *bootstrap.Config) (*Client, error) {
98	singletonClient.mu.Lock()
99	defer singletonClient.mu.Unlock()
100	// If the client implementation was created, increment ref count and return
101	// the client.
102	if singletonClient.clientImpl != nil {
103		singletonClient.refCount++
104		return singletonClient, nil
105	}
106
107	// Create the new client implementation.
108	c, err := newWithConfig(config, defaultWatchExpiryTimeout)
109	if err != nil {
110		return nil, err
111	}
112
113	singletonClient.clientImpl = c
114	singletonClient.refCount++
115	return singletonClient, nil
116}
117
118// Close closes the client. It does ref count of the xds client implementation,
119// and closes the gRPC connection to the management server when ref count
120// reaches 0.
121func (c *Client) Close() {
122	c.mu.Lock()
123	defer c.mu.Unlock()
124	c.refCount--
125	if c.refCount == 0 {
126		c.clientImpl.Close()
127		// Set clientImpl back to nil. So if New() is called after this, a new
128		// implementation will be created.
129		c.clientImpl = nil
130	}
131}
132
133// NewWithConfigForTesting is exported for testing only.
134//
135// Note that this function doesn't set the singleton, so that the testing states
136// don't leak.
137func NewWithConfigForTesting(config *bootstrap.Config, watchExpiryTimeout time.Duration) (*Client, error) {
138	cl, err := newWithConfig(config, watchExpiryTimeout)
139	if err != nil {
140		return nil, err
141	}
142	return &Client{clientImpl: cl, refCount: 1}, nil
143}
144