-
Notifications
You must be signed in to change notification settings - Fork 2
/
client.go
149 lines (124 loc) · 3.45 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package forwardcache
import (
"hash/crc32"
"net/http"
"net/url"
"sync"
"github.com/mikegleasonjr/forwardcache/consistenthash"
)
const (
defaultPath = "/proxy"
defaultReplicas = 50
)
// Client represents a nonparticipating client in the pool. It delegates
// requests to the responsible peer.
type Client struct {
path string
replicas int
hashFn consistenthash.Hash
transport http.RoundTripper
peers []string
mu sync.RWMutex // guards peers
hashMap *consistenthash.Map
}
// NewClient creates a Client.
func NewClient(options ...func(*Client)) *Client {
c := &Client{
path: defaultPath,
replicas: defaultReplicas,
hashFn: crc32.ChecksumIEEE,
transport: http.DefaultTransport,
}
for _, option := range options {
option(c)
}
c.SetPool(c.peers...)
return c
}
// SetPool updates the client's peers list. Each peer should
// be a valid base URL, for example "http://example.net:8000".
func (c *Client) SetPool(peers ...string) {
c.mu.Lock()
defer c.mu.Unlock()
c.peers = peers
c.hashMap = consistenthash.New(c.replicas, c.hashFn)
c.hashMap.Add(c.peers...)
}
// HTTPClient returns an http.Client that uses the Client as its transport.
func (c *Client) HTTPClient() *http.Client {
cl := new(http.Client)
*cl = *http.DefaultClient
cl.Transport = c
return cl
}
// RoundTrip makes the request go through one of the peer. Since Client
// implements the Roundtripper interface, it can be used as a transport.
func (c *Client) RoundTrip(req *http.Request) (*http.Response, error) {
peer := c.choosePeer(req.URL.String())
return c.roundTripTo(peer, req)
}
func (c *Client) choosePeer(url string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.hashMap.Get(url)
}
func (c *Client) roundTripTo(peer string, req *http.Request) (*http.Response, error) {
query := c.peerHandlerURL(peer, req.URL.String())
cpy := clone(req) // per RoundTripper contract
cpy.URL = query
cpy.Host = query.Host
return c.transport.RoundTrip(cpy)
}
func (c *Client) peerHandlerURL(peer string, origin string) *url.URL {
u, _ := url.Parse(peer)
u.Path = c.path
u.RawQuery = "q=" + url.QueryEscape(origin)
return u
}
// WithPath specifies the HTTP path that will serve proxy requests.
// Defaults to "/proxy".
func WithPath(p string) func(*Client) {
return func(c *Client) {
c.path = p
}
}
// WithReplicas specifies the number of key replicas on the consistent hash.
// Defaults to 50.
func WithReplicas(r int) func(*Client) {
return func(c *Client) {
c.replicas = r
}
}
// WithHashFn specifies the hash function of the consistent hash.
// Defaults to crc32.ChecksumIEEE.
func WithHashFn(h consistenthash.Hash) func(*Client) {
return func(c *Client) {
c.hashFn = h
}
}
// WithClientTransport lets you configure a custom transport
// used between the local client and the proxies.
// Defaults to http.DefaultTransport.
func WithClientTransport(t http.RoundTripper) func(*Client) {
return func(c *Client) {
c.transport = t
}
}
// WithPool lets you configure the client's list of peers.
// Defaults to nil. See Client.SetPool(...).
func WithPool(peers ...string) func(*Client) {
return func(c *Client) {
c.peers = peers
}
}
// clones a request, credits goes to:
// https://github.com/golang/oauth2/blob/master/transport.go#L36
func clone(r *http.Request) *http.Request {
r2 := new(http.Request)
*r2 = *r
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}