github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/prxdl.go (about)

     1  // Package ais provides core functionality for the AIStore object storage.
     2  /*
     3   * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package ais
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strconv"
    13  	"time"
    14  
    15  	"github.com/NVIDIA/aistore/api/apc"
    16  	"github.com/NVIDIA/aistore/cmn"
    17  	"github.com/NVIDIA/aistore/cmn/cos"
    18  	"github.com/NVIDIA/aistore/cmn/debug"
    19  	"github.com/NVIDIA/aistore/core/meta"
    20  	"github.com/NVIDIA/aistore/ext/dload"
    21  	"github.com/NVIDIA/aistore/nl"
    22  	jsoniter "github.com/json-iterator/go"
    23  )
    24  
    25  // [METHOD] /v1/download
    26  func (p *proxy) downloadHandler(w http.ResponseWriter, r *http.Request) {
    27  	if !p.ClusterStarted() {
    28  		w.WriteHeader(http.StatusServiceUnavailable)
    29  		return
    30  	}
    31  	switch r.Method {
    32  	case http.MethodGet, http.MethodDelete:
    33  		p.httpdladm(w, r)
    34  	case http.MethodPost:
    35  		p.httpdlpost(w, r)
    36  	default:
    37  		cmn.WriteErr405(w, r, http.MethodDelete, http.MethodGet, http.MethodPost)
    38  	}
    39  }
    40  
    41  // httpDownloadAdmin is meant for aborting, removing and getting status updates for downloads.
    42  // GET /v1/download?id=...
    43  // DELETE /v1/download/{abort, remove}?id=...
    44  func (p *proxy) httpdladm(w http.ResponseWriter, r *http.Request) {
    45  	if !p.ClusterStarted() {
    46  		w.WriteHeader(http.StatusServiceUnavailable)
    47  		return
    48  	}
    49  	msg := &dload.AdminBody{}
    50  	if err := cmn.ReadJSON(w, r, &msg); err != nil {
    51  		return
    52  	}
    53  	if err := msg.Validate(r.Method == http.MethodDelete); err != nil {
    54  		p.writeErr(w, r, err)
    55  		return
    56  	}
    57  
    58  	if r.Method == http.MethodDelete {
    59  		items, err := cmn.ParseURL(r.URL.Path, apc.URLPathDownload.L, 1, false)
    60  		if err != nil {
    61  			p.writeErr(w, r, err)
    62  			return
    63  		}
    64  
    65  		if items[0] != apc.Abort && items[0] != apc.Remove {
    66  			p.writeErrAct(w, r, items[0])
    67  			return
    68  		}
    69  	}
    70  	if msg.ID != "" && p.ic.redirectToIC(w, r) {
    71  		return
    72  	}
    73  	resp, statusCode, err := p.dladm(r.Method, r.URL.Path, msg)
    74  	if err != nil {
    75  		p.writeErr(w, r, err, statusCode)
    76  	} else {
    77  		w.Header().Set(cos.HdrContentLength, strconv.Itoa(len(resp)))
    78  		w.Write(resp)
    79  	}
    80  }
    81  
    82  // POST /v1/download
    83  func (p *proxy) httpdlpost(w http.ResponseWriter, r *http.Request) {
    84  	if _, err := p.parseURL(w, r, apc.URLPathDownload.L, 0, false); err != nil {
    85  		return
    86  	}
    87  
    88  	jobID := dload.PrefixJobID + cos.GenUUID() // prefix to visually differentiate vs. xaction IDs
    89  
    90  	body, err := io.ReadAll(r.Body)
    91  	if err != nil {
    92  		p.writeErrStatusf(w, r, http.StatusInternalServerError, "failed to receive download request: %v", err)
    93  		return
    94  	}
    95  	dlb, dlBase, ok := p.validateDownload(w, r, body)
    96  	if !ok {
    97  		return
    98  	}
    99  
   100  	var progressInterval = dload.DownloadProgressInterval
   101  	if dlBase.ProgressInterval != "" {
   102  		ival, err := time.ParseDuration(dlBase.ProgressInterval)
   103  		if err != nil {
   104  			p.writeErrf(w, r, "%s: invalid progress interval %q: %v", p, dlBase.ProgressInterval, err)
   105  			return
   106  		}
   107  		progressInterval = ival
   108  	}
   109  
   110  	xid := cos.GenUUID()
   111  	if ecode, err := p.dlstart(r, xid, jobID, body); err != nil {
   112  		p.writeErrStatusf(w, r, ecode, "Error starting download: %v", err)
   113  		return
   114  	}
   115  	smap := p.owner.smap.get()
   116  	nl := dload.NewDownloadNL(jobID, string(dlb.Type), &smap.Smap, progressInterval)
   117  	nl.SetOwner(equalIC)
   118  	p.ic.registerEqual(regIC{nl: nl, smap: smap})
   119  
   120  	b := cos.MustMarshal(dload.DlPostResp{ID: jobID})
   121  	w.Header().Set(cos.HdrContentType, cos.ContentJSON)
   122  	w.Header().Set(cos.HdrContentLength, strconv.Itoa(len(b)))
   123  	w.Write(b)
   124  }
   125  
   126  func (p *proxy) dladm(method, path string, msg *dload.AdminBody) ([]byte, int, error) {
   127  	config := cmn.GCO.Get()
   128  	if msg.ID != "" && method == http.MethodGet && msg.OnlyActive {
   129  		nl := p.notifs.entry(msg.ID)
   130  		if nl != nil {
   131  			return p.dlstatus(nl, config)
   132  		}
   133  	}
   134  	var (
   135  		body        = cos.MustMarshal(msg)
   136  		args        = allocBcArgs()
   137  		xid         = cos.GenUUID()
   138  		q           = url.Values{apc.QparamUUID: []string{xid}}
   139  		notFoundCnt int
   140  	)
   141  	args.req = cmn.HreqArgs{Method: method, Path: path, Body: body, Query: q}
   142  	args.timeout = config.Timeout.MaxHostBusy.D()
   143  	results := p.bcastGroup(args)
   144  	defer freeBcastRes(results)
   145  	freeBcArgs(args)
   146  	respCnt := len(results)
   147  	if respCnt == 0 {
   148  		smap := p.owner.smap.get()
   149  		if smap.CountActiveTs() < 1 {
   150  			return nil, http.StatusBadRequest, cmn.NewErrNoNodes(apc.Target, smap.CountTargets())
   151  		}
   152  		err := fmt.Errorf("%s: target(s) temporarily unavailable? (%s)", p, smap.StringEx())
   153  		return nil, http.StatusInternalServerError, err
   154  	}
   155  
   156  	var (
   157  		validResponses = make([]*callResult, 0, respCnt) // TODO: avoid allocation
   158  		err            error
   159  	)
   160  	for _, res := range results {
   161  		if res.status == http.StatusOK {
   162  			validResponses = append(validResponses, res)
   163  			continue
   164  		}
   165  		if res.status != http.StatusNotFound {
   166  			return nil, res.status, res.err
   167  		}
   168  		notFoundCnt++
   169  		err = res.err
   170  	}
   171  	if notFoundCnt == respCnt { // All responded with 404.
   172  		return nil, http.StatusNotFound, err
   173  	}
   174  
   175  	switch method {
   176  	case http.MethodGet:
   177  		if msg.ID == "" {
   178  			// If ID is empty, return the list of downloads
   179  			aggregate := make(map[string]*dload.Job)
   180  			for _, resp := range validResponses {
   181  				if len(resp.bytes) == 0 {
   182  					continue
   183  				}
   184  				var parsedResp map[string]*dload.Job
   185  				if err := jsoniter.Unmarshal(resp.bytes, &parsedResp); err != nil {
   186  					return nil, http.StatusInternalServerError, err
   187  				}
   188  				for k, v := range parsedResp {
   189  					if oldMetric, ok := aggregate[k]; ok {
   190  						v.Aggregate(oldMetric)
   191  					}
   192  					aggregate[k] = v
   193  				}
   194  			}
   195  
   196  			listDownloads := make(dload.JobInfos, 0, len(aggregate))
   197  			for _, v := range aggregate {
   198  				listDownloads = append(listDownloads, v)
   199  			}
   200  			result := cos.MustMarshal(listDownloads)
   201  			return result, http.StatusOK, nil
   202  		}
   203  
   204  		var stResp *dload.StatusResp
   205  		for _, resp := range validResponses {
   206  			status := dload.StatusResp{}
   207  			if err := jsoniter.Unmarshal(resp.bytes, &status); err != nil {
   208  				return nil, http.StatusInternalServerError, err
   209  			}
   210  			stResp = stResp.Aggregate(&status)
   211  		}
   212  		body := cos.MustMarshal(stResp)
   213  		return body, http.StatusOK, nil
   214  	case http.MethodDelete:
   215  		res := validResponses[0]
   216  		return res.bytes, res.status, res.err
   217  	default:
   218  		debug.Assert(false, method)
   219  		return nil, http.StatusInternalServerError, nil
   220  	}
   221  }
   222  
   223  func (p *proxy) dlstatus(nl nl.Listener, config *cmn.Config) ([]byte, int, error) {
   224  	// bcast
   225  	p.notifs.bcastGetStats(nl, config.Periodic.NotifTime.D())
   226  	stats := nl.NodeStats()
   227  
   228  	var resp *dload.StatusResp
   229  	stats.Range(func(_ string, status any) bool {
   230  		var (
   231  			dlStatus *dload.StatusResp
   232  			ok       bool
   233  		)
   234  		if dlStatus, ok = status.(*dload.StatusResp); !ok {
   235  			dlStatus = &dload.StatusResp{}
   236  			if err := cos.MorphMarshal(status, dlStatus); err != nil {
   237  				debug.AssertNoErr(err)
   238  				return false
   239  			}
   240  		}
   241  		resp = resp.Aggregate(dlStatus)
   242  		return true
   243  	})
   244  
   245  	respJSON := cos.MustMarshal(resp)
   246  	return respJSON, http.StatusOK, nil
   247  }
   248  
   249  func (p *proxy) dlstart(r *http.Request, xid, jobID string, body []byte) (ecode int, err error) {
   250  	var (
   251  		config = cmn.GCO.Get()
   252  		query  = make(url.Values, 2)
   253  		args   = allocBcArgs()
   254  	)
   255  	query.Set(apc.QparamUUID, xid)
   256  	query.Set(apc.QparamJobID, jobID)
   257  	args.req = cmn.HreqArgs{Method: http.MethodPost, Path: r.URL.Path, Body: body, Query: query}
   258  	args.timeout = config.Timeout.MaxHostBusy.D()
   259  
   260  	results := p.bcastGroup(args)
   261  	freeBcArgs(args)
   262  
   263  	ecode = http.StatusOK
   264  	for _, res := range results {
   265  		if res.err != nil {
   266  			ecode, err = res.status, res.err
   267  			break
   268  		}
   269  	}
   270  	freeBcastRes(results)
   271  	return
   272  }
   273  
   274  func (p *proxy) validateDownload(w http.ResponseWriter, r *http.Request, body []byte) (dlb dload.Body, dlBase dload.Base, ok bool) {
   275  	if err := jsoniter.Unmarshal(body, &dlb); err != nil {
   276  		err = fmt.Errorf(cmn.FmtErrUnmarshal, p, "download request", cos.BHead(body), err)
   277  		p.writeErr(w, r, err)
   278  		return
   279  	}
   280  	if err := jsoniter.Unmarshal(dlb.RawMessage, &dlBase); err != nil {
   281  		err = fmt.Errorf(cmn.FmtErrUnmarshal, p, "download message", cos.BHead(dlb.RawMessage), err)
   282  		p.writeErr(w, r, err)
   283  		return
   284  	}
   285  	bck := meta.CloneBck(&dlBase.Bck)
   286  	args := bctx{p: p, w: w, r: r, reqBody: body, bck: bck, perms: apc.AccessRW}
   287  	args.createAIS = true
   288  	if _, err := args.initAndTry(); err == nil {
   289  		ok = true
   290  	}
   291  	return
   292  }