github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/prxrev.go (about) 1 // Package ais provides core functionality for the AIStore object storage. 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package ais 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "net/http" 12 "net/http/httputil" 13 "net/url" 14 "sync" 15 16 "github.com/NVIDIA/aistore/api/apc" 17 "github.com/NVIDIA/aistore/cmn" 18 "github.com/NVIDIA/aistore/cmn/cos" 19 "github.com/NVIDIA/aistore/cmn/debug" 20 "github.com/NVIDIA/aistore/cmn/nlog" 21 "github.com/NVIDIA/aistore/core/meta" 22 ) 23 24 type ( 25 reverseProxy struct { 26 cloud *httputil.ReverseProxy // unmodified GET requests => storage.googleapis.com 27 nodes sync.Map // map of reverse proxies keyed by node DaemonIDs 28 primary struct { 29 rp *httputil.ReverseProxy 30 url string 31 sync.Mutex 32 } 33 } 34 singleRProxy struct { 35 rp *httputil.ReverseProxy 36 u *url.URL 37 } 38 ) 39 40 // forward control plane request to the current primary proxy 41 // return: forf (forwarded or failed) where forf = true means exactly that: forwarded or failed 42 func (p *proxy) forwardCP(w http.ResponseWriter, r *http.Request, msg *apc.ActMsg, s string, origBody ...[]byte) (forf bool) { 43 var ( 44 body []byte 45 smap = p.owner.smap.get() 46 ) 47 if !smap.isValid() { 48 errmsg := fmt.Sprintf("%s must be starting up: cannot execute", p.si) 49 if msg != nil { 50 p.writeErrStatusf(w, r, http.StatusServiceUnavailable, "%s %s: %s", errmsg, msg.Action, s) 51 } else { 52 p.writeErrStatusf(w, r, http.StatusServiceUnavailable, "%s %q", errmsg, s) 53 } 54 return true 55 } 56 if p.settingNewPrimary.Load() { 57 p.writeErrStatusf(w, r, http.StatusServiceUnavailable, 58 "%s is in transition, cannot process the request", p.si) 59 return true 60 } 61 if smap.isPrimary(p.si) { 62 return 63 } 64 // We must **not** send any request body when doing HEAD request. 65 // Otherwise, the request can be rejected and terminated. 66 if r.Method != http.MethodHead { 67 if len(origBody) > 0 && len(origBody[0]) > 0 { 68 body = origBody[0] 69 } else if msg != nil { 70 body = cos.MustMarshal(msg) 71 } 72 } 73 primary := &p.rproxy.primary 74 primary.Lock() 75 if primary.url != smap.Primary.PubNet.URL { 76 primary.url = smap.Primary.PubNet.URL 77 uparsed, err := url.Parse(smap.Primary.PubNet.URL) 78 cos.AssertNoErr(err) 79 config := cmn.GCO.Get() 80 primary.rp = httputil.NewSingleHostReverseProxy(uparsed) 81 primary.rp.Transport = rpTransport(config) 82 primary.rp.ErrorHandler = p.rpErrHandler 83 } 84 primary.Unlock() 85 if len(body) > 0 { 86 debug.AssertFunc(func() bool { 87 l, _ := io.Copy(io.Discard, r.Body) 88 return l == 0 89 }) 90 91 r.Body = io.NopCloser(bytes.NewBuffer(body)) 92 r.ContentLength = int64(len(body)) // Directly setting `Content-Length` header. 93 } 94 if cmn.Rom.FastV(5, cos.SmoduleAIS) { 95 pname := smap.Primary.StringEx() 96 if msg != nil { 97 nlog.Infof("%s: forwarding \"%s:%s\" to the primary %s", p, msg.Action, s, pname) 98 } else { 99 nlog.Infof("%s: forwarding %q to the primary %s", p, s, pname) 100 } 101 } 102 primary.rp.ServeHTTP(w, r) 103 return true 104 } 105 106 func rpTransport(config *cmn.Config) *http.Transport { 107 var ( 108 err error 109 transport = cmn.NewTransport(cmn.TransportArgs{Timeout: config.Client.Timeout.D()}) 110 ) 111 if config.Net.HTTP.UseHTTPS { 112 transport.TLSClientConfig, err = cmn.NewTLS(config.Net.HTTP.ToTLS()) 113 if err != nil { 114 cos.ExitLog(err) 115 } 116 } 117 return transport 118 } 119 120 // Based on default error handler `defaultErrorHandler` in `httputil/reverseproxy.go`. 121 func (p *proxy) rpErrHandler(w http.ResponseWriter, r *http.Request, err error) { 122 var ( 123 smap = p.owner.smap.get() 124 si = smap.PubNet2Node(r.URL.Host) // assuming pub 125 dst = r.URL.Host 126 ) 127 if si != nil { 128 dst = "node " + si.StringEx() 129 } 130 if cos.IsErrConnectionRefused(err) { 131 nlog.Errorf("%s: %s is unreachable (%s %s)", p, dst, r.Method, r.URL.Path) 132 } else { 133 nlog.Errorf("%s rproxy to %s (%s %s): %v", p, dst, r.Method, r.URL.Path, err) 134 } 135 w.WriteHeader(http.StatusBadGateway) 136 } 137 138 func (p *proxy) reverseNodeRequest(w http.ResponseWriter, r *http.Request, si *meta.Snode) { 139 parsedURL, err := url.Parse(si.URL(cmn.NetPublic)) 140 debug.AssertNoErr(err) 141 p.reverseRequest(w, r, si.ID(), parsedURL) 142 } 143 144 func (p *proxy) reverseRequest(w http.ResponseWriter, r *http.Request, nodeID string, parsedURL *url.URL) { 145 rproxy := p.rproxy.loadOrStore(nodeID, parsedURL, p.rpErrHandler) 146 rproxy.ServeHTTP(w, r) 147 } 148 149 func (p *proxy) reverseRemAis(w http.ResponseWriter, r *http.Request, msg *apc.ActMsg, bck *cmn.Bck, query url.Values) (err error) { 150 var ( 151 backend = cmn.BackendConfAIS{} 152 aliasOrUUID = bck.Ns.UUID 153 config = cmn.GCO.Get() 154 v = config.Backend.Get(apc.AIS) 155 ) 156 if v == nil { 157 p.writeErrMsg(w, r, "no remote ais clusters attached") 158 return err 159 } 160 161 cos.MustMorphMarshal(v, &backend) 162 urls, exists := backend[aliasOrUUID] 163 if !exists { 164 var refreshed bool 165 if p.remais.Ver == 0 { 166 p._remais(&config.ClusterConfig, true) 167 refreshed = true 168 } 169 ml: 170 p.remais.mu.RLock() 171 for _, remais := range p.remais.A { 172 if remais.Alias == aliasOrUUID || remais.UUID == aliasOrUUID { 173 urls = []string{remais.URL} 174 exists = true 175 break 176 } 177 } 178 p.remais.mu.RUnlock() 179 if !exists && !refreshed { 180 p._remais(&config.ClusterConfig, true) 181 refreshed = true 182 goto ml 183 } 184 } 185 if !exists { 186 err = cos.NewErrNotFound(p, "remote UUID/alias "+aliasOrUUID) 187 p.writeErr(w, r, err) 188 return err 189 } 190 191 debug.Assert(len(urls) > 0) 192 u, err := url.Parse(urls[0]) 193 if err != nil { 194 p.writeErr(w, r, err) 195 return err 196 } 197 if msg != nil { 198 body := cos.MustMarshal(msg) 199 r.ContentLength = int64(len(body)) 200 r.Body = io.NopCloser(bytes.NewReader(body)) 201 } 202 203 bck.Ns.UUID = "" 204 query = cmn.DelBckFromQuery(query) 205 query = bck.AddToQuery(query) 206 r.URL.RawQuery = query.Encode() 207 p.reverseRequest(w, r, aliasOrUUID, u) 208 return nil 209 } 210 211 ////////////////// 212 // reverseProxy // 213 ////////////////// 214 215 func (rp *reverseProxy) init() { 216 rp.cloud = &httputil.ReverseProxy{ 217 Director: func(_ *http.Request) {}, 218 Transport: rpTransport(cmn.GCO.Get()), 219 } 220 } 221 222 func (rp *reverseProxy) loadOrStore(uuid string, u *url.URL, 223 errHdlr func(w http.ResponseWriter, r *http.Request, err error)) *httputil.ReverseProxy { 224 revProxyIf, exists := rp.nodes.Load(uuid) 225 if exists { 226 shrp := revProxyIf.(*singleRProxy) 227 if shrp.u.Host == u.Host { 228 return shrp.rp 229 } 230 } 231 rproxy := httputil.NewSingleHostReverseProxy(u) 232 rproxy.Transport = rpTransport(cmn.GCO.Get()) 233 rproxy.ErrorHandler = errHdlr 234 235 // NOTE: races are rare probably happen only when storing an entry for the first time or when URL changes. 236 // Also, races don't impact the correctness as we always have latest entry for `uuid`, `URL` pair (see: L3917). 237 rp.nodes.Store(uuid, &singleRProxy{rproxy, u}) 238 return rproxy 239 }