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 }