├── dnscache.go ├── dnscache_test.go ├── license.txt └── readme.md /dnscache.go: -------------------------------------------------------------------------------- 1 | package dnscache 2 | // Package dnscache caches DNS lookups 3 | 4 | import ( 5 | "net" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type Resolver struct { 11 | lock sync.RWMutex 12 | cache map[string][]net.IP 13 | } 14 | 15 | func New(refreshRate time.Duration) *Resolver { 16 | resolver := &Resolver { 17 | cache: make(map[string][]net.IP, 64), 18 | } 19 | if refreshRate > 0 { 20 | go resolver.autoRefresh(refreshRate) 21 | } 22 | return resolver 23 | } 24 | 25 | func (r *Resolver) Fetch(address string) ([]net.IP, error) { 26 | r.lock.RLock() 27 | ips, exists := r.cache[address] 28 | r.lock.RUnlock() 29 | if exists { return ips, nil } 30 | 31 | return r.Lookup(address) 32 | } 33 | 34 | func (r *Resolver) FetchOne(address string) (net.IP, error) { 35 | ips, err := r.Fetch(address) 36 | if err != nil || len(ips) == 0 { return nil, err} 37 | return ips[0], nil 38 | } 39 | 40 | func (r *Resolver) FetchOneString(address string) (string, error) { 41 | ip, err := r.FetchOne(address) 42 | if err != nil || ip == nil { return "", err } 43 | return ip.String(), nil 44 | } 45 | 46 | func (r *Resolver) Refresh() { 47 | i := 0 48 | r.lock.RLock() 49 | addresses := make([]string, len(r.cache)) 50 | for key, _ := range r.cache { 51 | addresses[i] = key 52 | i++ 53 | } 54 | r.lock.RUnlock() 55 | 56 | for _, address := range addresses { 57 | r.Lookup(address) 58 | time.Sleep(time.Second * 2) 59 | } 60 | } 61 | 62 | func (r *Resolver) Lookup(address string) ([]net.IP, error) { 63 | ips, err := net.LookupIP(address) 64 | if err != nil { return nil, err } 65 | 66 | r.lock.Lock() 67 | r.cache[address] = ips 68 | r.lock.Unlock() 69 | return ips, nil 70 | } 71 | 72 | func (r *Resolver) autoRefresh(rate time.Duration) { 73 | for { 74 | time.Sleep(rate) 75 | r.Refresh() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /dnscache_test.go: -------------------------------------------------------------------------------- 1 | package dnscache 2 | 3 | import ( 4 | "net" 5 | "sort" 6 | "time" 7 | "testing" 8 | ) 9 | 10 | func TestFetchReturnsAndErrorOnInvalidLookup(t *testing.T) { 11 | ips, err := New(0).Lookup("invalid.viki.io") 12 | if ips != nil { 13 | t.Errorf("Expecting nil ips, got %v", ips) 14 | } 15 | expected := "lookup invalid.viki.io: no such host" 16 | if err.Error() != expected { 17 | t.Errorf("Expecting %q error, got %q", expected, err.Error()) 18 | } 19 | } 20 | 21 | func TestFetchReturnsAListOfIps(t *testing.T) { 22 | ips, _ := New(0).Lookup("dnscache.go.test.viki.io") 23 | assertIps(t, ips, []string{"1.123.58.13", "31.85.32.110"}) 24 | } 25 | 26 | func TestCallingLookupAddsTheItemToTheCache(t *testing.T) { 27 | r := New(0) 28 | r.Lookup("dnscache.go.test.viki.io") 29 | assertIps(t, r.cache["dnscache.go.test.viki.io"], []string{"1.123.58.13", "31.85.32.110"}) 30 | } 31 | 32 | func TestFetchLoadsValueFromTheCache(t *testing.T) { 33 | r := New(0) 34 | r.cache["invalid.viki.io"] = []net.IP{net.ParseIP("1.1.2.3")} 35 | ips, _ := r.Fetch("invalid.viki.io") 36 | assertIps(t, ips, []string{"1.1.2.3"}) 37 | } 38 | 39 | func TestFetchOneLoadsTheFirstValue(t *testing.T) { 40 | r := New(0) 41 | r.cache["something.viki.io"] = []net.IP{net.ParseIP("1.1.2.3"), net.ParseIP("100.100.102.103")} 42 | ip, _ := r.FetchOne("something.viki.io") 43 | assertIps(t, []net.IP{ip}, []string{"1.1.2.3"}) 44 | } 45 | 46 | func TestFetchOneStringLoadsTheFirstValue(t *testing.T) { 47 | r := New(0) 48 | r.cache["something.viki.io"] = []net.IP{net.ParseIP("100.100.102.103"), net.ParseIP("100.100.102.104")} 49 | ip, _ := r.FetchOneString("something.viki.io") 50 | if ip != "100.100.102.103" { 51 | t.Errorf("expected 100.100.102.103 but got %v", ip) 52 | } 53 | } 54 | 55 | func TestFetchLoadsTheIpAndCachesIt(t *testing.T) { 56 | r := New(0) 57 | ips, _ := r.Fetch("dnscache.go.test.viki.io") 58 | assertIps(t, ips, []string{"1.123.58.13", "31.85.32.110"}) 59 | assertIps(t, r.cache["dnscache.go.test.viki.io"], []string{"1.123.58.13", "31.85.32.110"}) 60 | } 61 | 62 | func TestItReloadsTheIpsAtAGivenInterval(t *testing.T) { 63 | r := New(1) 64 | r.cache["dnscache.go.test.viki.io"] = nil 65 | time.Sleep(time.Second * 2) 66 | assertIps(t, r.cache["dnscache.go.test.viki.io"], []string{"1.123.58.13", "31.85.32.110"}) 67 | } 68 | 69 | func assertIps(t *testing.T, actuals []net.IP, expected []string) { 70 | if len(actuals) != len(expected) { 71 | t.Errorf("Expecting %d ips, got %d", len(expected), len(actuals)) 72 | } 73 | sort.Strings(expected) 74 | for _, ip := range actuals { 75 | if sort.SearchStrings(expected, ip.String()) == -1 { 76 | t.Errorf("Got an unexpected ip: %v:", actuals[0]) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Viki Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### A DNS cache for Go 2 | CGO is used to lookup domain names. Given enough concurrent requests and the slightest hiccup in name resolution, it's quite easy to end up with blocked/leaking goroutines. 3 | 4 | The issue is documented at 5 | 6 | The Go team's singleflight solution (which isn't in stable yet) is rather elegant. However, it only eliminates concurrent lookups (thundering herd problems). Many systems can live with slightly stale resolve names, which means we can cacne DNS lookups and refresh them in the background. 7 | 8 | ### Installation 9 | Install using the "go get" command: 10 | 11 | go get github.com/viki-org/dnscache 12 | 13 | ### Usage 14 | The cache is thread safe. Create a new instance by specifying how long each entry should be cached (in seconds). Items will be refreshed in the background. 15 | 16 | //refresh items every 5 minutes 17 | resolver := dnscache.New(time.Minute * 5) 18 | 19 | //get an array of net.IP 20 | ips, _ := resolver.Fetch("api.viki.io") 21 | 22 | //get the first net.IP 23 | ip, _ := resolver.FetchOne("api.viki.io") 24 | 25 | //get the first net.IP as string 26 | ip, _ := resolver.FetchOneString("api.viki.io") 27 | 28 | If you are using an `http.Transport`, you can use this cache by speficifying a 29 | `Dial` function: 30 | 31 | transport := &http.Transport { 32 | MaxIdleConnsPerHost: 64, 33 | Dial: func(network string, address string) (net.Conn, error) { 34 | separator := strings.LastIndex(address, ":") 35 | ip, _ := dnscache.FetchString(address[:separator]) 36 | return net.Dial("tcp", ip + address[separator:]) 37 | }, 38 | } 39 | --------------------------------------------------------------------------------