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  }