// ExampleDiscover_refreshAsync updates the servers list asynchronously every // 100 milliseconds. func ExampleDiscover_refreshAsync() { discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br") // depending on where this examples run the retrieving time differs (DNS RTT), // so as we cannot sleep a deterministic period, to make this test more useful // we are creating a channel to alert the main go routine that we got an // answer from the network retrieved := make(chan bool) discovery.SetRetriever(dnsdisco.RetrieverFunc(func(service, proto, name string) (servers []*net.SRV, err error) { _, servers, err = net.LookupSRV(service, proto, name) retrieved <- true return })) // refresh the SRV records every 100 milliseconds stopRefresh := discovery.RefreshAsync(100 * time.Millisecond) <-retrieved // sleep for a short period only to allow the library to process the SRV // records retrieved from the network time.Sleep(100 * time.Millisecond) target, port := discovery.Choose() fmt.Printf("Target: %s\nPort: %d\n", target, port) close(stopRefresh) // Output: // Target: jabber.registro.br. // Port: 5269 }
func TestRefreshAsync(t *testing.T) { t.Parallel() for _, scenario := range refreshAsyncScenarios { t.Run(scenario.description, func(t *testing.T) { discovery := dnsdisco.NewDiscovery(scenario.service, scenario.proto, scenario.name) discovery.SetRetriever(scenario.retriever) discovery.SetHealthChecker(scenario.healthChecker) finish := discovery.RefreshAsync(scenario.refreshInterval) defer close(finish) time.Sleep(scenario.refreshInterval + (50 * time.Millisecond)) target, port := discovery.Choose() if target != scenario.expectedTarget { t.Errorf("mismatch targets. Expecting: “%s”; found “%s”", scenario.expectedTarget, target) } if port != scenario.expectedPort { t.Errorf("mismatch ports. Expecting: “%d”; found “%d”", scenario.expectedPort, port) } if errs := discovery.Errors(); !reflect.DeepEqual(errs, scenario.expectedErrors) { t.Errorf("mismatch errors. Expecting: “%#v”; found “%#v”", scenario.expectedErrors, errs) } }) } }
func TestDefaultLoadBalancer(t *testing.T) { t.Parallel() for _, scenario := range defaultLoadBalancerScenarios { t.Run(scenario.description, func(t *testing.T) { discovery := dnsdisco.NewDiscovery(scenario.service, scenario.proto, scenario.name) discovery.SetRetriever(scenario.retriever) discovery.SetHealthChecker(scenario.healthChecker) if err := discovery.Refresh(); err != nil { t.Errorf("unexpected error while retrieving DNS records. Details: %s", err) } var target string var port uint16 for j := 0; j <= scenario.rerun; j++ { target, port = discovery.Choose() } if target != scenario.expectedTarget { t.Errorf("mismatch targets. Expecting: “%s”; found “%s”", scenario.expectedTarget, target) } if port != scenario.expectedPort { t.Errorf("mismatch ports. Expecting: “%d”; found “%d”", scenario.expectedPort, port) } }) } }
func BenchmarkDefaultLoadBalancer(b *testing.B) { discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br") discovery.SetHealthChecker(dnsdisco.HealthCheckerFunc(func(target string, port uint16, proto string) (ok bool, err error) { return true, nil })) discovery.SetRetriever(dnsdisco.RetrieverFunc(func(service, proto, name string) ([]*net.SRV, error) { return []*net.SRV{ { Target: "server1.example.com.", Port: 1111, Weight: 10, Priority: 20, }, { Target: "server2.example.com.", Port: 2222, Weight: 70, Priority: 10, }, { Target: "server3.example.com.", Port: 3333, Weight: 100, Priority: 20, }, { Target: "server4.example.com.", Port: 4444, Weight: 1, Priority: 15, }, { Target: "server5.example.com.", Port: 5555, Weight: 40, Priority: 60, }, }, nil })) // Retrieve the servers if err := discovery.Refresh(); err != nil { fmt.Println(err) return } for i := 0; i < b.N; i++ { discovery.Choose() } }
// ExampleRetrieverFunc uses a specific resolver with custom timeouts. func ExampleRetrieverFunc() { discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br") discovery.SetRetriever(dnsdisco.RetrieverFunc(func(service, proto, name string) (servers []*net.SRV, err error) { client := dns.Client{ ReadTimeout: 2 * time.Second, WriteTimeout: 2 * time.Second, } name = strings.TrimRight(name, ".") z := fmt.Sprintf("_%s._%s.%s.", service, proto, name) var request dns.Msg request.SetQuestion(z, dns.TypeSRV) request.RecursionDesired = true response, _, err := client.Exchange(&request, "8.8.8.8:53") if err != nil { return nil, err } for _, rr := range response.Answer { if srv, ok := rr.(*dns.SRV); ok { servers = append(servers, &net.SRV{ Target: srv.Target, Port: srv.Port, Priority: srv.Priority, Weight: srv.Weight, }) } } return })) // Retrieve the servers if err := discovery.Refresh(); err != nil { fmt.Println(err) return } target, port := discovery.Choose() fmt.Printf("Target: %s\nPort: %d\n", target, port) // Output: // Target: jabber.registro.br. // Port: 5269 }
// Example_loadBalancer shows how it is possible to replace the default load // balancer algorithm with a new one following the round robin strategy // (https://en.wikipedia.org/wiki/Round-robin_scheduling). func Example_loadBalancer() { discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br") discovery.SetLoadBalancer(new(roundRobinLoadBalancer)) // Retrieve the servers if err := discovery.Refresh(); err != nil { fmt.Println(err) return } target, port := discovery.Choose() fmt.Printf("Target: %s\nPort: %d\n", target, port) // Output: // Target: jabber.registro.br. // Port: 5269 }
// ExampleHealthCheckerFunc tests HTTP fetching the homepage and checking the // HTTP status code. func ExampleHealthCheckerFunc() { discovery := dnsdisco.NewDiscovery("http", "tcp", "pantz.org") discovery.SetHealthChecker(dnsdisco.HealthCheckerFunc(func(target string, port uint16, proto string) (ok bool, err error) { response, err := http.Get("http://www.pantz.org") if err != nil { return false, err } return response.StatusCode == http.StatusOK, nil })) // Retrieve the servers if err := discovery.Refresh(); err != nil { fmt.Println(err) return } target, port := discovery.Choose() fmt.Printf("Target: %s\nPort: %d\n", target, port) // Output: // Target: www.pantz.org. // Port: 80 }
func TestDefaultHealthChecker(t *testing.T) { t.Parallel() ln, err := startTCPTestServer() if err != nil { t.Fatal(err) } defer ln.Close() testServerHost, p, err := net.SplitHostPort(ln.Addr().String()) if err != nil { t.Fatal(err) } testServerPort, err := strconv.ParseUint(p, 10, 16) if err != nil { t.Fatal(err) } defaultHealthCheckerScenarios := []struct { description string service string proto string name string retriever dnsdisco.RetrieverFunc loadBalancer loadBalacerMock expectedTarget string expectedPort uint16 expectedError error }{ { description: "it should identify a healthy server", service: "jabber", proto: "tcp", name: "registro.br", retriever: dnsdisco.RetrieverFunc(func(service, proto, name string) ([]*net.SRV, error) { return []*net.SRV{ { Target: testServerHost, Port: uint16(testServerPort), Priority: 10, Weight: 20, }, }, nil }), loadBalancer: func() loadBalacerMock { var savedServers []*net.SRV return loadBalacerMock{ MockChangeServers: func(servers []*net.SRV) { savedServers = servers }, MockLoadBalance: func() (target string, port uint16) { if len(savedServers) > 0 { return savedServers[0].Target, savedServers[0].Port } return "", 0 }, } }(), expectedTarget: testServerHost, expectedPort: uint16(testServerPort), }, { description: "it should fail when it's not a valid proto", service: "jabber", proto: "xxx", name: "registro.br", retriever: dnsdisco.RetrieverFunc(func(service, proto, name string) ([]*net.SRV, error) { return []*net.SRV{ { Target: testServerHost, Port: uint16(testServerPort), Priority: 10, Weight: 20, }, }, nil }), loadBalancer: func() loadBalacerMock { var savedServers []*net.SRV return loadBalacerMock{ MockChangeServers: func(servers []*net.SRV) { savedServers = servers }, MockLoadBalance: func() (target string, port uint16) { if len(savedServers) > 0 { return savedServers[0].Target, savedServers[0].Port } return "", 0 }, } }(), expectedTarget: "", expectedPort: 0, }, { description: "it should fail to connect to an unknown server", service: "jabber", proto: "tcp", name: "registro.br", retriever: dnsdisco.RetrieverFunc(func(service, proto, name string) ([]*net.SRV, error) { return []*net.SRV{ { Target: "idontexist.example.com.", Port: uint16(testServerPort), Priority: 10, Weight: 20, }, }, nil }), loadBalancer: func() loadBalacerMock { var savedServers []*net.SRV return loadBalacerMock{ MockChangeServers: func(servers []*net.SRV) { savedServers = servers }, MockLoadBalance: func() (target string, port uint16) { if len(savedServers) > 0 { return savedServers[0].Target, savedServers[0].Port } return "", 0 }, } }(), expectedTarget: "", expectedPort: 0, }, } for _, scenario := range defaultHealthCheckerScenarios { t.Run(scenario.description, func(t *testing.T) { discovery := dnsdisco.NewDiscovery(scenario.service, scenario.proto, scenario.name) discovery.SetRetriever(scenario.retriever) discovery.SetLoadBalancer(scenario.loadBalancer) if err := discovery.Refresh(); err != nil { t.Errorf("unexpected error while retrieving DNS records. Details: %s", err) } target, port := discovery.Choose() if target != scenario.expectedTarget { t.Errorf("mismatch targets. Expecting: “%s”; found “%s”", scenario.expectedTarget, target) } if port != scenario.expectedPort { t.Errorf("mismatch ports. Expecting: “%d”; found “%d”", scenario.expectedPort, port) } }) } }