github.com/google/cloudprober@v0.11.3/targets/resolver/resolver.go (about) 1 // Copyright 2017 The Cloudprober Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package resolver provides a caching, non-blocking DNS resolver. All requests 16 // for cached resources are returned immediately and if cache has expired, an 17 // offline goroutine is fired to update it. 18 package resolver 19 20 import ( 21 "errors" 22 "fmt" 23 "net" 24 "sync" 25 "time" 26 ) 27 28 // The max age and the timeout for resolving a target. 29 const defaultMaxAge = 5 * time.Minute 30 31 type cacheRecord struct { 32 ip4 net.IP 33 ip6 net.IP 34 lastUpdatedAt time.Time 35 err error 36 mu sync.Mutex 37 updateInProgress bool 38 callInit sync.Once 39 } 40 41 // Resolver provides an asynchronous caching DNS resolver. 42 type Resolver struct { 43 cache map[string]*cacheRecord 44 mu sync.Mutex 45 DefaultMaxAge time.Duration 46 resolve func(string) ([]net.IP, error) // used for testing 47 } 48 49 // ipVersion tells if an IP address is IPv4 or IPv6. 50 func ipVersion(ip net.IP) int { 51 if len(ip.To4()) == net.IPv4len { 52 return 4 53 } 54 if len(ip) == net.IPv6len { 55 return 6 56 } 57 return 0 58 } 59 60 // resolveOrTimeout tries to resolve, but times out and returns an error if it 61 // takes more than defaultMaxAge. 62 // Has the potential of creating a bunch of pending goroutines if backend 63 // resolve call has a tendency of indefinitely hanging. 64 func (r *Resolver) resolveOrTimeout(name string) ([]net.IP, error) { 65 var ips []net.IP 66 var err error 67 doneChan := make(chan struct{}) 68 69 go func() { 70 ips, err = r.resolve(name) 71 close(doneChan) 72 }() 73 74 select { 75 case <-doneChan: 76 return ips, err 77 case <-time.After(defaultMaxAge): 78 return nil, fmt.Errorf("timed out after %v", defaultMaxAge) 79 } 80 } 81 82 // Resolve returns IP address for a name. 83 // Issues an update call for the cache record if it's older than defaultMaxAge. 84 func (r *Resolver) Resolve(name string, ipVer int) (net.IP, error) { 85 maxAge := r.DefaultMaxAge 86 if maxAge == 0 { 87 maxAge = defaultMaxAge 88 } 89 return r.resolveWithMaxAge(name, ipVer, maxAge, nil) 90 } 91 92 // getCacheRecord returns the cache record for the target. 93 // It must be kept light, as it blocks the main mutex of the map. 94 func (r *Resolver) getCacheRecord(name string) *cacheRecord { 95 r.mu.Lock() 96 defer r.mu.Unlock() 97 cr := r.cache[name] 98 if cr == nil { 99 cr = &cacheRecord{ 100 err: errors.New("cache record not initialized yet"), 101 } 102 r.cache[name] = cr 103 } 104 return cr 105 } 106 107 // resolveWithMaxAge returns IP address for a name, issuing an update call for 108 // the cache record if it's older than the argument maxAge. 109 // refreshed channel is primarily used for testing. Method pushes true to 110 // refreshed channel once and if the value is refreshed, or false, if it 111 // doesn't need refreshing. 112 func (r *Resolver) resolveWithMaxAge(name string, ipVer int, maxAge time.Duration, refreshed chan<- bool) (net.IP, error) { 113 cr := r.getCacheRecord(name) 114 cr.refreshIfRequired(name, r.resolveOrTimeout, maxAge, refreshed) 115 cr.mu.Lock() 116 defer cr.mu.Unlock() 117 118 var ip net.IP 119 120 switch ipVer { 121 case 0: 122 if cr.ip4 != nil { 123 ip = cr.ip4 124 } else if cr.ip6 != nil { 125 ip = cr.ip6 126 } 127 case 4: 128 ip = cr.ip4 129 case 6: 130 ip = cr.ip6 131 default: 132 return nil, fmt.Errorf("unknown IP version: %d", ipVer) 133 } 134 135 if ip == nil && cr.err == nil { 136 return nil, fmt.Errorf("found no IP%d IP for %s", ipVer, name) 137 } 138 return ip, cr.err 139 } 140 141 // refresh refreshes the cacheRecord by making a call to the provided "resolve" function. 142 func (cr *cacheRecord) refresh(name string, resolve func(string) ([]net.IP, error), refreshed chan<- bool) { 143 // Note that we call backend's resolve outside of the mutex locks and take the lock again 144 // to update the cache record once we have the results from the backend. 145 ips, err := resolve(name) 146 147 cr.mu.Lock() 148 defer cr.mu.Unlock() 149 if refreshed != nil { 150 refreshed <- true 151 } 152 cr.err = err 153 cr.lastUpdatedAt = time.Now() 154 cr.updateInProgress = false 155 if err != nil { 156 return 157 } 158 cr.ip4 = nil 159 cr.ip6 = nil 160 for _, ip := range ips { 161 switch ipVersion(ip) { 162 case 4: 163 cr.ip4 = ip 164 case 6: 165 cr.ip6 = ip 166 } 167 } 168 } 169 170 // refreshIfRequired does most of the work. Overall goal is to minimize the 171 // lock period of the cache record. To that end, if the cache record needs 172 // updating, we do that with the mutex unlocked. 173 // 174 // If cache record is new, blocks until it's resolved for the first time. 175 // If cache record needs updating, kicks off refresh asynchronously. 176 // If cache record is already being updated or fresh enough, returns immediately. 177 func (cr *cacheRecord) refreshIfRequired(name string, resolve func(string) ([]net.IP, error), maxAge time.Duration, refreshed chan<- bool) { 178 cr.callInit.Do(func() { cr.refresh(name, resolve, refreshed) }) 179 cr.mu.Lock() 180 defer cr.mu.Unlock() 181 182 // Cache record is old and no update in progress, issue a request to update. 183 if !cr.updateInProgress && time.Since(cr.lastUpdatedAt) >= maxAge { 184 cr.updateInProgress = true 185 go cr.refresh(name, resolve, refreshed) 186 } else if refreshed != nil { 187 refreshed <- false 188 } 189 } 190 191 // NewWithResolve returns a new Resolver with the given backend resolver. 192 // This is useful for testing. 193 func NewWithResolve(resolveFunc func(string) ([]net.IP, error)) *Resolver { 194 return &Resolver{ 195 cache: make(map[string]*cacheRecord), 196 resolve: resolveFunc, 197 DefaultMaxAge: defaultMaxAge, 198 } 199 } 200 201 // New returns a new Resolver. 202 func New() *Resolver { 203 return NewWithResolve(net.LookupIP) 204 }