Miki Tebeka avatar Miki Tebeka committed acfbf28

Many more tests, docs ...

Comments (0)

Files changed (4)

+2012-09-06 version 0.2.0
+    * Support multiple backends [issue #3]
+
 2012-09-03 version 0.1.2
     * Synchronize backend change [issue #2] (Thanks Jonathan Amsterdam)
 
 .. _here: https://bitbucket.org/tebeka/seamless
 
 
-Old API
-=======
-`seamless` version < 0.2.0 supported only one backend. The old API is still
-supported.
-
-/switch?backend=host:port
-    equivalent to `/set?backends=host:port`
-
-/current
-    equivalent to `/get`
-
-
 LICENSE
 =======
 MIT_
 // Sync backend changes
 var backendsLock sync.RWMutex
 
+// backend regular expression
 var backendRe *regexp.Regexp = regexp.MustCompile("^[^:]+:[0-9]+$")
 
-// isValidBackend returns true if backend is in "host:port" format.
+// isValidBackend returns true if backend is in "host:port" format
 func isValidBackend(backend string) bool {
 	return backendRe.MatchString(backend)
 }
 
-// nextBackend returns the next backend to use.
-// (Uses backendsLock.RLock)
+// nextBackend returns the next backend to use (uses backendsLock.RLock)
 func nextBackend() (string, error) {
 	backendsLock.RLock()
 	defer backendsLock.RUnlock()
 		return "", fmt.Errorf("No backends")
 	}
 
+	currentBackend = (currentBackend + 1) % len(backends)
 	backend := backends[currentBackend]
-	currentBackend = (currentBackend + 1) % len(backends)
 	return backend, nil
 }
 
+// parseBackends parses string in format "host:port,host:port" and return list of backends
 func parseBackends(str string) ([]string, error) {
 	backends := strings.Split(str, ",")
 	if len(backends) == 0 {
 	http.HandleFunc("/add", addHandler)
 	http.HandleFunc("/remove", removeHandler)
 
-	// Support pre 0.2.0 API
-	http.HandleFunc("/current", getHandler)
-	http.HandleFunc("/switch", switchHandler)
-
 	return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
 }
 
-func setBackends(param string, w http.ResponseWriter, req *http.Request) {
-	newBackends, err := parseBackends(req.FormValue(param))
-	if err != nil {
-		msg := fmt.Sprintf("error: %s", err)
-		log.Println(msg)
-		http.Error(w, msg, http.StatusBadRequest)
-		return
-	}
-
-	backendsLock.Lock()
-	defer backendsLock.Unlock()
-	backends = newBackends
-	getHandler(w, req)
-}
-
-// switchHandler handler /switch and switches backend
-func switchHandler(w http.ResponseWriter, req *http.Request) {
-	setBackends("backend", w, req)
-}
-
-// setHandler handler /switch and switches backend
-func setHandler(w http.ResponseWriter, req *http.Request) {
-	setBackends("backends", w, req)
-}
-
 // getHandler handles /current and return the current backend
 func getHandler(w http.ResponseWriter, req *http.Request) {
 	w.Header().Set("Content-Type", "text/plain")
 	fmt.Fprintf(w, "%s\n", strings.Join(backends, ","))
 }
 
+// setBackends sets the current list of backends and sets currentBackend to 0
+func setBackends(newBackends []string) {
+	backendsLock.Lock()
+	defer backendsLock.Unlock()
+
+	backends = newBackends
+	currentBackend = 0
+}
+
+// setHandler handler /set and sets backends
+func setHandler(w http.ResponseWriter, req *http.Request) {
+	newBackends, err := parseBackends(req.FormValue("backends"))
+	if err != nil {
+		msg := fmt.Sprintf("error: %s", err)
+		log.Println(msg)
+		http.Error(w, msg, http.StatusBadRequest)
+		return
+	}
+
+	setBackends(newBackends)
+	getHandler(w, req)
+}
+
+// addHandler handles /add to add a new backend
 func addHandler(w http.ResponseWriter, req *http.Request) {
 	backend := req.FormValue("backend")
 	if len(backend) == 0 {
 	return items
 }
 
+// removeHandler handles /remove and remove a backend
 func removeHandler(w http.ResponseWriter, req *http.Request) {
 	err := ""
 
 	backends = newBackends
 }
 
+// seamless launches the HTTP API and then start proxying
+func seamless(localAddr string, apiPort int, backends []string, out chan error) {
+	local, err := net.Listen("tcp", localAddr)
+	if local == nil {
+		out <- fmt.Errorf("cannot listen: %v", err)
+		return
+	}
+
+	go func() {
+		if err := startHttpServer(apiPort); err != nil {
+			out <- fmt.Errorf("cannot listen on %d: %v", apiPort, err)
+		}
+	}()
+
+	for {
+		conn, err := local.Accept()
+		if conn == nil {
+			die("accept failed: %v", err)
+		}
+		backend, err := nextBackend()
+		if err != nil {
+			log.Printf("error: can't get next backend %v\n", err)
+			conn.Close()
+		}
+		go forward(conn, backend)
+	}
+}
+
 func main() {
 	flag.Usage = func() {
 		fmt.Fprintf(os.Stderr, "usage: seamless LISTEN_PORT BACKENDS\n")
 		os.Exit(1)
 	}
 	localAddr := fmt.Sprintf(":%s", flag.Arg(0))
+
 	var err error
 	backends, err = parseBackends(flag.Arg(1))
 	if err != nil {
 		die(fmt.Sprintf("%s", err))
 	}
 
-	local, err := net.Listen("tcp", localAddr)
-	if local == nil {
-		die("cannot listen: %v", err)
-	}
+	out := make(chan error)
+	go seamless(localAddr, *port, backends, out)
 
-	go func() {
-		if err := startHttpServer(*port); err != nil {
-			die("cannot listen on %d: %v", *port, err)
-		}
-	}()
-
-	for {
-		conn, err := local.Accept()
-		if conn == nil {
-			die("accept failed: %v", err)
-		}
-		backend, err := nextBackend()
-		if err != nil {
-			log.Printf("error: can't get next backend %v\n", err)
-			conn.Close()
-		}
-		go forward(conn, backend)
+	err = <-out
+	if err != nil {
+		die("%s", err)
 	}
 }
 	"time"
 )
 
-var port int = 6777
+var apiPort int = 6777
+var numBackends int = 3
+var proxyPort = 6888
+
+type testHandler int
+
+func (h testHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	w.Header().Set("Content-Type", "text/plain")
+	fmt.Fprintf(w, "%d", h)
+}
+
+func startBackend(i int) {
+	handler := testHandler(i)
+	port := 6700 + i
+	server := http.Server{Handler: handler, Addr: fmt.Sprintf(":%d", port)}
+	go server.ListenAndServe()
+}
 
 func init() {
-	backends = []string{"localhost:8888"}
-	go startHttpServer(port)
+	for i := 0; i < numBackends; i++ {
+		startBackend(i)
+	}
+
+	out := make(chan error)
+	go seamless(fmt.Sprintf(":%d", proxyPort), apiPort, backends, out)
+
 	time.Sleep(1 * time.Second)
 }
 
-func callAPI(suffix string) (string, error) {
-	url := fmt.Sprintf("http://localhost:%d/%s", port, suffix)
-	resp, err := http.Get(url)
+func call(url string) (string, error) {
+	// We really don't want keep alive or caching :)
+	client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
+	resp, err := client.Get(url)
 	if err != nil {
-		return "", fmt.Errorf("error connecting to /current: %v\n", err)
+		return "", fmt.Errorf("can't GET %s: %v\n", url, err)
 	}
 	defer resp.Body.Close()
 
 	return string(reply), nil
 }
 
-func getTest(suffix string, t *testing.T) {
-	reply, err := callAPI(suffix)
+func callAPI(suffix string) (string, error) {
+	url := fmt.Sprintf("http://localhost:%d/%s", apiPort, suffix)
+	return call(url)
+}
+
+func TestHTTPGet(t *testing.T) {
+	setBackends([]string{"localhost:8080"})
+	reply, err := callAPI("get")
 	if err != nil {
 		t.Fatalf("%s", err)
 	}
 	}
 }
 
-func TestHttpOldAPI(t *testing.T) {
-	getTest("current", t)
-}
-
-func TestHTTPGet(t *testing.T) {
-	getTest("get", t)
-}
-
 func TestHTTPAdd(t *testing.T) {
-	backends = []string{"localhost:8888"}
+	setBackends([]string{"localhost:8888"})
 	backend := "localhost:8887"
 
 	reply, err := callAPI(fmt.Sprintf("add?backend=%s", backend))
 
 func TestHTTPRemove(t *testing.T) {
 	backend1, backend2 := "localhost:8888", "localhost:8887"
-	backends = []string{backend1, backend2}
+	setBackends([]string{backend1, backend2})
 	reply, err := callAPI(fmt.Sprintf("remove?backend=%s", backend1))
 	if err != nil {
 		t.Fatalf("%s", err)
 
 func Test_nextBackend(t *testing.T) {
 	backend1, backend2 := "localhost:8888", "localhost:8887"
-	backends = []string{backend1, backend2}
-	currentBackend = 0
+	setBackends([]string{backend1, backend2})
 
-	for i, expected := range []string{backend1, backend2, backend1} {
+	for i, expected := range []string{backend2, backend1, backend2} {
 		next, _ := nextBackend()
 		if next != expected {
 			t.Fatalf("backend should be %s at %d (was %s)", expected, i, next)
 			t.Fatalf("go %v for %s (expected %v)", value, c.backends, c.expected)
 		}
 
-
 	}
 }
+
+func backendAddr(i int) string {
+	return fmt.Sprintf("localhost:%d", 6700+i)
+}
+
+func callProxy() (string, error) {
+	url := fmt.Sprintf("http://localhost:%d", proxyPort)
+	return call(url)
+}
+
+func TestProxy(t *testing.T) {
+	setBackends([]string{backendAddr(0), backendAddr(1)})
+
+	for i := 0; i < 7; i++ {
+		reply, err := callProxy()
+		if err != nil {
+			t.Fatalf("can't call proxy - %v", err)
+		}
+		expected := fmt.Sprintf("%d", (i+1)%len(backends))
+		if reply != expected {
+			t.Fatalf("bad backend for i=%d: got %s instead of %s", i, reply, expected)
+		}
+	}
+}
+
+func TestProxyRemove(t *testing.T) {
+	setBackends([]string{backendAddr(0), backendAddr(1)})
+	suffix := fmt.Sprintf("remove?backend=%s", backendAddr(0))
+	if _, err := callAPI(suffix); err != nil {
+		t.Fatalf("can't remove %s - %s", backendAddr(0), err)
+	}
+
+	for i := 0; i < 7; i++ {
+		reply, err := callProxy()
+		if err != nil {
+			t.Fatalf("can't call proxy - %v", err)
+		}
+		if reply != "1" {
+			t.Fatalf("bad reply %s (expected 1)", reply)
+		}
+	}
+}
+
+func TestProxyAdd(t *testing.T) {
+	setBackends([]string{backendAddr(0), backendAddr(1)})
+
+	suffix := fmt.Sprintf("add?backend=%s", backendAddr(2))
+	if _, err := callAPI(suffix); err != nil {
+		t.Fatalf("can't remove %s - %s", backendAddr(0), err)
+	}
+
+	for i := 0; i < 7; i++ {
+		reply, err := callProxy()
+		if err != nil {
+			t.Fatalf("can't call proxy - %v", err)
+		}
+		expected := fmt.Sprintf("%d", (i+1)%len(backends))
+		if reply != expected {
+			t.Fatalf("bad reply %s (expected %s)", reply, expected)
+		}
+	}
+}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.