1/*
2Copyright 2013 Google Inc.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package groupcache
18
19import (
20	"context"
21	"errors"
22	"flag"
23	"log"
24	"net"
25	"net/http"
26	"os"
27	"os/exec"
28	"strconv"
29	"strings"
30	"sync"
31	"testing"
32	"time"
33)
34
35var (
36	peerAddrs = flag.String("test_peer_addrs", "", "Comma-separated list of peer addresses; used by TestHTTPPool")
37	peerIndex = flag.Int("test_peer_index", -1, "Index of which peer this child is; used by TestHTTPPool")
38	peerChild = flag.Bool("test_peer_child", false, "True if running as a child process; used by TestHTTPPool")
39)
40
41func TestHTTPPool(t *testing.T) {
42	if *peerChild {
43		beChildForTestHTTPPool()
44		os.Exit(0)
45	}
46
47	const (
48		nChild = 4
49		nGets  = 100
50	)
51
52	var childAddr []string
53	for i := 0; i < nChild; i++ {
54		childAddr = append(childAddr, pickFreeAddr(t))
55	}
56
57	var cmds []*exec.Cmd
58	var wg sync.WaitGroup
59	for i := 0; i < nChild; i++ {
60		cmd := exec.Command(os.Args[0],
61			"--test.run=TestHTTPPool",
62			"--test_peer_child",
63			"--test_peer_addrs="+strings.Join(childAddr, ","),
64			"--test_peer_index="+strconv.Itoa(i),
65		)
66		cmds = append(cmds, cmd)
67		wg.Add(1)
68		if err := cmd.Start(); err != nil {
69			t.Fatal("failed to start child process: ", err)
70		}
71		go awaitAddrReady(t, childAddr[i], &wg)
72	}
73	defer func() {
74		for i := 0; i < nChild; i++ {
75			if cmds[i].Process != nil {
76				cmds[i].Process.Kill()
77			}
78		}
79	}()
80	wg.Wait()
81
82	// Use a dummy self address so that we don't handle gets in-process.
83	p := NewHTTPPool("should-be-ignored")
84	p.Set(addrToURL(childAddr)...)
85
86	// Dummy getter function. Gets should go to children only.
87	// The only time this process will handle a get is when the
88	// children can't be contacted for some reason.
89	getter := GetterFunc(func(ctx context.Context, key string, dest Sink) error {
90		return errors.New("parent getter called; something's wrong")
91	})
92	g := NewGroup("httpPoolTest", 1<<20, getter)
93
94	for _, key := range testKeys(nGets) {
95		var value string
96		if err := g.Get(context.TODO(), key, StringSink(&value)); err != nil {
97			t.Fatal(err)
98		}
99		if suffix := ":" + key; !strings.HasSuffix(value, suffix) {
100			t.Errorf("Get(%q) = %q, want value ending in %q", key, value, suffix)
101		}
102		t.Logf("Get key=%q, value=%q (peer:key)", key, value)
103	}
104}
105
106func testKeys(n int) (keys []string) {
107	keys = make([]string, n)
108	for i := range keys {
109		keys[i] = strconv.Itoa(i)
110	}
111	return
112}
113
114func beChildForTestHTTPPool() {
115	addrs := strings.Split(*peerAddrs, ",")
116
117	p := NewHTTPPool("http://" + addrs[*peerIndex])
118	p.Set(addrToURL(addrs)...)
119
120	getter := GetterFunc(func(ctx context.Context, key string, dest Sink) error {
121		dest.SetString(strconv.Itoa(*peerIndex) + ":" + key)
122		return nil
123	})
124	NewGroup("httpPoolTest", 1<<20, getter)
125
126	log.Fatal(http.ListenAndServe(addrs[*peerIndex], p))
127}
128
129// This is racy. Another process could swoop in and steal the port between the
130// call to this function and the next listen call. Should be okay though.
131// The proper way would be to pass the l.File() as ExtraFiles to the child
132// process, and then close your copy once the child starts.
133func pickFreeAddr(t *testing.T) string {
134	l, err := net.Listen("tcp", "127.0.0.1:0")
135	if err != nil {
136		t.Fatal(err)
137	}
138	defer l.Close()
139	return l.Addr().String()
140}
141
142func addrToURL(addr []string) []string {
143	url := make([]string, len(addr))
144	for i := range addr {
145		url[i] = "http://" + addr[i]
146	}
147	return url
148}
149
150func awaitAddrReady(t *testing.T, addr string, wg *sync.WaitGroup) {
151	defer wg.Done()
152	const max = 1 * time.Second
153	tries := 0
154	for {
155		tries++
156		c, err := net.Dial("tcp", addr)
157		if err == nil {
158			c.Close()
159			return
160		}
161		delay := time.Duration(tries) * 25 * time.Millisecond
162		if delay > max {
163			delay = max
164		}
165		time.Sleep(delay)
166	}
167}
168