~egtann/lanhttp

fc1b74b7b8f835242afe934bcd36c7cc9dde01a1 — Evan Tann 1 year, 2 months ago 62daf19
swap to random load distribution algorithm
3 files changed, 40 insertions(+), 53 deletions(-)

M README.md
M lanhttp.go
M lanhttp_test.go
M README.md => README.md +2 -0
@@ 1,3 1,5 @@
lanhttp wraps http.Client to automatically route internal traffic (denoted by
*.internal) URLs to internal endpoints, and route other traffic normally over
the internet. This is an alternative to consul and other DNS-level routing.

It distributes traffic randomly among the internal IPs.

M lanhttp.go => lanhttp.go +16 -29
@@ 4,6 4,7 @@ import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"net/http"
	"net/url"


@@ 35,12 36,7 @@ type HTTPClient interface {
// provide some more control.
type Logger interface{ Printf(string, ...interface{}) }

type Routes map[string]*backend

type backend struct {
	IPs   []string
	Index int
}
type Routes map[string][]string

type logger struct {
	l  Logger


@@ 140,11 136,7 @@ func (c *Client) first(urls []string, timeout time.Duration) Routes {
		go update(uri)
	}
	select {
	case backends := <-ch:
		routes := make(Routes, len(backends))
		for host, ips := range backends {
			routes[host] = &backend{IPs: ips}
		}
	case routes := <-ch:
		return routes
	case <-ctx.Done():
		// Default to keeping our existing routes, so a slowdown from


@@ 220,29 212,24 @@ func (c *Client) getIP(host string) string {
	c.mu.RLock()
	defer c.mu.RUnlock()

	backend, ok := c.backends[host]
	ips, ok := c.backends[host]
	if !ok {
		return ""
	}
	if len(backend.IPs) == 0 {
	if len(ips) == 0 {
		return ""
	}
	backend.Index = (backend.Index + 1) % len(backend.IPs)
	return backend.IPs[backend.Index]
	return ips[rand.Intn(len(ips))]
}

// Routes returns a copy of all live backend IPs and their current round-robin
// indexes.
// Routes returns a copy of all live backend IPs.
func (c *Client) Routes() Routes {
	c.mu.RLock()
	defer c.mu.RUnlock()

	r := make(Routes, len(c.backends))
	for host, b := range c.backends {
		r[host] = &backend{
			IPs:   append([]string{}, b.IPs...),
			Index: b.Index,
		}
	for host, ips := range c.backends {
		r[host] = append([]string{}, ips...)
	}
	return r
}


@@ 258,22 245,22 @@ func diff(a, b Routes) bool {
		if (a[key] == nil) != (b[key] == nil) {
			return true
		}
		if len(a[key].IPs) != len(b[key].IPs) {
		if len(a[key]) != len(b[key]) {
			return true
		}

		// Sort the live backends to get better performance when
		// diffing them
		sort.Slice(a[key].IPs, func(i, j int) bool {
			return a[key].IPs[i] < a[key].IPs[j]
		sort.Slice(a[key], func(i, j int) bool {
			return a[key][i] < a[key][j]
		})
		sort.Slice(b[key].IPs, func(i, j int) bool {
			return b[key].IPs[i] < b[key].IPs[j]
		sort.Slice(b[key], func(i, j int) bool {
			return b[key][i] < b[key][j]
		})

		// Compare two and exit on the first different string
		for i, ip := range a[key].IPs {
			if b[key].IPs[i] != ip {
		for i, ip := range a[key] {
			if b[key][i] != ip {
				return true
			}
		}

M lanhttp_test.go => lanhttp_test.go +22 -24
@@ 1,6 1,7 @@
package lanhttp

import (
	"math/rand"
	"testing"
)



@@ 19,48 20,43 @@ func TestDiff(t *testing.T) {
			want:  false,
		},
		"same content": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a"}}},
			haveB: Routes{"a": &backend{IPs: []string{"a"}}},
			haveA: Routes{"a": []string{"a"}},
			haveB: Routes{"a": []string{"a"}},
			want:  false,
		},
		"sort": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a", "b"}}},
			haveB: Routes{"a": &backend{IPs: []string{"b", "a"}}},
			want:  false,
		},
		"index ignored": testcase{
			haveA: Routes{"a": &backend{Index: 0}},
			haveB: Routes{"a": &backend{Index: 1}},
			haveA: Routes{"a": []string{"a", "b"}},
			haveB: Routes{"a": []string{"b", "a"}},
			want:  false,
		},
		"a > b": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a"}}},
			haveA: Routes{"a": []string{"a"}},
			haveB: Routes{},
			want:  true,
		},
		"b > a": testcase{
			haveA: Routes{},
			haveB: Routes{"a": &backend{IPs: []string{"a"}}},
			haveB: Routes{"a": []string{"a"}},
			want:  true,
		},
		"a > b ips": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a", "b", "c"}}},
			haveB: Routes{"a": &backend{IPs: []string{"a", "c"}}},
			haveA: Routes{"a": []string{"a", "b", "c"}},
			haveB: Routes{"a": []string{"a", "c"}},
			want:  true,
		},
		"b > a ips": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a"}}},
			haveB: Routes{"a": &backend{IPs: []string{"a", "b"}}},
			haveA: Routes{"a": []string{"a"}},
			haveB: Routes{"a": []string{"a", "b"}},
			want:  true,
		},
		"a != b": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a"}}},
			haveB: Routes{"b": &backend{IPs: []string{"a"}}},
			haveA: Routes{"a": []string{"a"}},
			haveB: Routes{"b": []string{"a"}},
			want:  true,
		},
		"a != b ips": testcase{
			haveA: Routes{"a": &backend{IPs: []string{"a"}}},
			haveB: Routes{"a": &backend{IPs: []string{"b"}}},
			haveA: Routes{"a": []string{"a"}},
			haveB: Routes{"a": []string{"b"}},
			want:  true,
		},
	}


@@ 77,19 73,21 @@ func TestDiff(t *testing.T) {
}

func TestGetIP(t *testing.T) {
	// Set a seed to ensure our results below are consistent
	rand.Seed(16)
	c := NewClient(nil).WithRoutes(Routes{
		"a.internal": &backend{IPs: []string{"1", "2"}},
		"a.internal": []string{"1", "2"},
	})
	if got := c.getIP("a.internal"); got != "2" {
		t.Fatal("expected 2 (1st)")
	}
	if got := c.getIP("a.internal"); got != "1" {
		t.Fatal("expected 1 (1st)")
	}
	if got := c.getIP("a.internal"); got != "2" {
		t.Fatal("expected 2 (2nd)")
		t.Fatal("expected 2 (1st)")
	}
	if got := c.getIP("a.internal"); got != "1" {
		t.Fatal("expected 1 (2nd)")
	}
	if got := c.getIP("a.internal"); got != "2" {
		t.Fatal("expected 2 (2nd)")
	}
}