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 xdsclient 20 21import ( 22 "bytes" 23 "encoding/json" 24 "fmt" 25 "sync" 26 "time" 27 28 "google.golang.org/grpc/xds/internal/xdsclient/bootstrap" 29) 30 31const defaultWatchExpiryTimeout = 15 * time.Second 32 33// This is the Client returned by New(). It contains one client implementation, 34// and maintains the refcount. 35var singletonClient = &clientRefCounted{} 36 37// To override in tests. 38var bootstrapNewConfig = bootstrap.NewConfig 39 40// clientRefCounted is ref-counted, and to be shared by the xds resolver and 41// balancer implementations, across multiple ClientConns and Servers. 42type clientRefCounted struct { 43 *clientImpl 44 45 // This mu protects all the fields, including the embedded clientImpl above. 46 mu sync.Mutex 47 refCount int 48} 49 50// New returns a new xdsClient configured by the bootstrap file specified in env 51// variable GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG. 52// 53// The returned xdsClient is a singleton. This function creates the xds client 54// if it doesn't already exist. 55// 56// Note that the first invocation of New() or NewWithConfig() sets the client 57// singleton. The following calls will return the singleton xds client without 58// checking or using the config. 59func New() (XDSClient, error) { 60 // This cannot just return newRefCounted(), because in error cases, the 61 // returned nil is a typed nil (*clientRefCounted), which may cause nil 62 // checks fail. 63 c, err := newRefCounted() 64 if err != nil { 65 return nil, err 66 } 67 return c, nil 68} 69 70func newRefCounted() (*clientRefCounted, error) { 71 singletonClient.mu.Lock() 72 defer singletonClient.mu.Unlock() 73 // If the client implementation was created, increment ref count and return 74 // the client. 75 if singletonClient.clientImpl != nil { 76 singletonClient.refCount++ 77 return singletonClient, nil 78 } 79 80 // Create the new client implementation. 81 config, err := bootstrapNewConfig() 82 if err != nil { 83 return nil, fmt.Errorf("xds: failed to read bootstrap file: %v", err) 84 } 85 c, err := newWithConfig(config, defaultWatchExpiryTimeout) 86 if err != nil { 87 return nil, err 88 } 89 90 singletonClient.clientImpl = c 91 singletonClient.refCount++ 92 return singletonClient, nil 93} 94 95// NewWithConfig returns a new xdsClient configured by the given config. 96// 97// The returned xdsClient is a singleton. This function creates the xds client 98// if it doesn't already exist. 99// 100// Note that the first invocation of New() or NewWithConfig() sets the client 101// singleton. The following calls will return the singleton xds client without 102// checking or using the config. 103// 104// This function is internal only, for c2p resolver and testing to use. DO NOT 105// use this elsewhere. Use New() instead. 106func NewWithConfig(config *bootstrap.Config) (XDSClient, error) { 107 singletonClient.mu.Lock() 108 defer singletonClient.mu.Unlock() 109 // If the client implementation was created, increment ref count and return 110 // the client. 111 if singletonClient.clientImpl != nil { 112 singletonClient.refCount++ 113 return singletonClient, nil 114 } 115 116 // Create the new client implementation. 117 c, err := newWithConfig(config, defaultWatchExpiryTimeout) 118 if err != nil { 119 return nil, err 120 } 121 122 singletonClient.clientImpl = c 123 singletonClient.refCount++ 124 return singletonClient, nil 125} 126 127// Close closes the client. It does ref count of the xds client implementation, 128// and closes the gRPC connection to the management server when ref count 129// reaches 0. 130func (c *clientRefCounted) Close() { 131 c.mu.Lock() 132 defer c.mu.Unlock() 133 c.refCount-- 134 if c.refCount == 0 { 135 c.clientImpl.Close() 136 // Set clientImpl back to nil. So if New() is called after this, a new 137 // implementation will be created. 138 c.clientImpl = nil 139 } 140} 141 142// NewWithConfigForTesting is exported for testing only. 143// 144// Note that this function doesn't set the singleton, so that the testing states 145// don't leak. 146func NewWithConfigForTesting(config *bootstrap.Config, watchExpiryTimeout time.Duration) (XDSClient, error) { 147 cl, err := newWithConfig(config, watchExpiryTimeout) 148 if err != nil { 149 return nil, err 150 } 151 return &clientRefCounted{clientImpl: cl, refCount: 1}, nil 152} 153 154// NewClientWithBootstrapContents returns an xds client for this config, 155// separate from the global singleton. This should be used for testing 156// purposes only. 157func NewClientWithBootstrapContents(contents []byte) (XDSClient, error) { 158 // Normalize the contents 159 buf := bytes.Buffer{} 160 err := json.Indent(&buf, contents, "", "") 161 if err != nil { 162 return nil, fmt.Errorf("xds: error normalizing JSON: %v", err) 163 } 164 contents = bytes.TrimSpace(buf.Bytes()) 165 166 clientsMu.Lock() 167 defer clientsMu.Unlock() 168 if c := clients[string(contents)]; c != nil { 169 c.mu.Lock() 170 // Since we don't remove the *Client from the map when it is closed, we 171 // need to recreate the impl if the ref count dropped to zero. 172 if c.refCount > 0 { 173 c.refCount++ 174 c.mu.Unlock() 175 return c, nil 176 } 177 c.mu.Unlock() 178 } 179 180 bcfg, err := bootstrap.NewConfigFromContents(contents) 181 if err != nil { 182 return nil, fmt.Errorf("xds: error with bootstrap config: %v", err) 183 } 184 185 cImpl, err := newWithConfig(bcfg, defaultWatchExpiryTimeout) 186 if err != nil { 187 return nil, err 188 } 189 190 c := &clientRefCounted{clientImpl: cImpl, refCount: 1} 191 clients[string(contents)] = c 192 return c, nil 193} 194 195var ( 196 clients = map[string]*clientRefCounted{} 197 clientsMu sync.Mutex 198) 199