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  }