github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ext/dload/dispatcher.go (about) 1 // Package dload implements functionality to download resources into AIS cluster from external source. 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package dload 6 7 import ( 8 "context" 9 "fmt" 10 "net/http" 11 "sort" 12 "sync" 13 "time" 14 15 "github.com/NVIDIA/aistore/cmn" 16 "github.com/NVIDIA/aistore/cmn/atomic" 17 "github.com/NVIDIA/aistore/cmn/cos" 18 "github.com/NVIDIA/aistore/cmn/debug" 19 "github.com/NVIDIA/aistore/cmn/kvdb" 20 "github.com/NVIDIA/aistore/cmn/nlog" 21 "github.com/NVIDIA/aistore/core" 22 "github.com/NVIDIA/aistore/core/meta" 23 "github.com/NVIDIA/aistore/fs" 24 "github.com/NVIDIA/aistore/stats" 25 "github.com/NVIDIA/aistore/xact/xreg" 26 "golang.org/x/sync/errgroup" 27 ) 28 29 // Dispatcher serves as middle layer between receiving download requests 30 // and serving them to joggers which actually download objects from a remote location. 31 32 type ( 33 dispatcher struct { 34 xdl *Xact 35 startupSema startupSema // Semaphore which synchronizes goroutines at dispatcher startup. 36 joggers map[string]*jogger // mpath -> jogger 37 mtx sync.RWMutex // Protects map defined below. 38 abortJob map[string]*cos.StopCh // jobID -> abort job chan 39 workCh chan jobif 40 stopCh *cos.StopCh 41 config *cmn.Config 42 } 43 44 startupSema struct { 45 started atomic.Bool 46 } 47 48 global struct { 49 tstats stats.Tracker 50 db kvdb.Driver 51 store *infoStore 52 53 // Downloader selects one of the two clients (below) by the destination URL. 54 // Certification check is disabled for now and does not depend on cluster settings. 55 clientH *http.Client 56 clientTLS *http.Client 57 } 58 ) 59 60 var g global 61 62 func Init(tstats stats.Tracker, db kvdb.Driver, clientConf *cmn.ClientConf) { 63 g.clientH, g.clientTLS = cmn.NewDefaultClients(clientConf.TimeoutLong.D()) 64 65 if db == nil { // unit tests only 66 return 67 } 68 { 69 g.tstats = tstats 70 g.db = db 71 g.store = newInfoStore(db) 72 } 73 xreg.RegNonBckXact(&factory{}) 74 } 75 76 //////////////// 77 // dispatcher // 78 //////////////// 79 80 func newDispatcher(xdl *Xact) *dispatcher { 81 return &dispatcher{ 82 xdl: xdl, 83 startupSema: startupSema{}, 84 joggers: make(map[string]*jogger, 8), 85 workCh: make(chan jobif), 86 stopCh: cos.NewStopCh(), 87 abortJob: make(map[string]*cos.StopCh, 100), 88 config: cmn.GCO.Get(), 89 } 90 } 91 92 func (d *dispatcher) run() (err error) { 93 var ( 94 // limit the number of concurrent job dispatches (goroutines) 95 sema = cos.NewSemaphore(5 * fs.NumAvail()) 96 group, ctx = errgroup.WithContext(context.Background()) 97 ) 98 availablePaths := fs.GetAvail() 99 for mpath := range availablePaths { 100 d.addJogger(mpath) 101 } 102 // allow other goroutines to run 103 d.startupSema.markStarted() 104 105 nlog.Infoln(d.xdl.Name(), "started, cnt:", len(availablePaths)) 106 mloop: 107 for { 108 select { 109 case <-d.xdl.IdleTimer(): 110 nlog.Infoln(d.xdl.Name(), "idle timeout") 111 break mloop 112 case errCause := <-d.xdl.ChanAbort(): 113 nlog.Infoln(d.xdl.Name(), "aborted:", errCause) 114 break mloop 115 case <-ctx.Done(): 116 break mloop 117 case job := <-d.workCh: 118 // Start dispatching each job in new goroutine to make sure that 119 // all joggers are busy downloading the tasks (jobs with limits 120 // may not saturate the full downloader throughput). 121 d.mtx.Lock() 122 d.abortJob[job.ID()] = cos.NewStopCh() 123 d.mtx.Unlock() 124 125 select { 126 case <-d.xdl.IdleTimer(): 127 nlog.Infoln(d.xdl.Name(), "idle timeout") 128 break mloop 129 case errCause := <-d.xdl.ChanAbort(): 130 nlog.Infoln(d.xdl.Name(), "aborted:", errCause) 131 break mloop 132 case <-ctx.Done(): 133 break mloop 134 case <-sema.TryAcquire(): 135 group.Go(func() error { 136 defer sema.Release() 137 if !d.dispatchDownload(job) { 138 return cmn.NewErrAborted(job.String(), "download", nil) 139 } 140 return nil 141 }) 142 } 143 } 144 } 145 146 d.stop() 147 return group.Wait() 148 } 149 150 // stop running joggers 151 // no need to cleanup maps, dispatcher should not be used after stop() 152 func (d *dispatcher) stop() { 153 d.stopCh.Close() 154 for _, jogger := range d.joggers { 155 jogger.stop() 156 } 157 } 158 159 func (d *dispatcher) addJogger(mpath string) { 160 _, ok := d.joggers[mpath] 161 debug.Assert(!ok) 162 j := newJogger(d, mpath) 163 go j.jog() 164 d.joggers[mpath] = j 165 } 166 167 func (d *dispatcher) cleanupJob(jobID string) { 168 d.mtx.Lock() 169 if ch, exists := d.abortJob[jobID]; exists { 170 ch.Close() 171 delete(d.abortJob, jobID) 172 } 173 d.mtx.Unlock() 174 } 175 176 func (d *dispatcher) finish(job jobif) { 177 verbose := cmn.Rom.FastV(4, cos.SmoduleDload) 178 if verbose { 179 nlog.Infof("Waiting for job %q", job.ID()) 180 } 181 d.waitFor(job.ID()) 182 if verbose { 183 nlog.Infof("Job %q finished waiting for all tasks", job.ID()) 184 } 185 d.cleanupJob(job.ID()) 186 if verbose { 187 nlog.Infof("Job %q cleaned up", job.ID()) 188 } 189 job.cleanup() 190 if verbose { 191 nlog.Infof("Job %q has finished", job.ID()) 192 } 193 } 194 195 // forward request to designated jogger 196 func (d *dispatcher) dispatchDownload(job jobif) (ok bool) { 197 defer d.finish(job) 198 199 if aborted := d.checkAborted(); aborted || d.checkAbortedJob(job) { 200 return !aborted 201 } 202 203 diffResolver := NewDiffResolver(&defaultDiffResolverCtx{}) 204 go diffResolver.Start() 205 206 // In case of `!job.Sync()` we don't want to traverse entire bucket. 207 // We just want to download requested objects and to find out which 208 // objects must be checked (latest version-wise). Therefore, "walk" 209 // bucket only when we need to sync the objects. 210 if job.Sync() { 211 go diffResolver.walk(job) 212 } 213 214 go diffResolver.push(job, d) 215 216 for { 217 result, err := diffResolver.Next() 218 if err != nil { 219 return false 220 } 221 switch result.Action { 222 case DiffResolverRecv, DiffResolverSkip, DiffResolverErr, DiffResolverDelete: 223 var obj dlObj 224 if dst := result.Dst; dst != nil { 225 obj = dlObj{ 226 objName: dst.ObjName, 227 link: dst.Link, 228 fromRemote: dst.Link == "", 229 } 230 } else { 231 src := result.Src 232 debug.Assertf(result.Action == DiffResolverDelete, "%d vs %d", result.Action, DiffResolverDelete) 233 obj = dlObj{ 234 objName: src.ObjName, 235 link: "", 236 fromRemote: true, 237 } 238 } 239 240 g.store.incScheduled(job.ID()) 241 242 if result.Action == DiffResolverSkip { 243 g.store.incSkipped(job.ID()) 244 continue 245 } 246 247 task := &singleTask{xdl: d.xdl, obj: obj, job: job} 248 if result.Action == DiffResolverErr { 249 task.markFailed(result.Err.Error()) 250 continue 251 } 252 253 if result.Action == DiffResolverDelete { 254 requiresSync := job.Sync() 255 debug.Assert(requiresSync) 256 if _, err := core.T.EvictObject(result.Src); err != nil { 257 task.markFailed(err.Error()) 258 } else { 259 g.store.incFinished(job.ID()) 260 } 261 continue 262 } 263 264 ok, err := d.doSingle(task) 265 if err != nil { 266 nlog.Errorln(job.String(), "failed to download", obj.objName+":", err) 267 g.store.setAborted(job.ID()) // TODO -- FIXME: pass (report, handle) error, here and elsewhere 268 return ok 269 } 270 if !ok { 271 g.store.setAborted(job.ID()) 272 return false 273 } 274 case DiffResolverSend: 275 requiresSync := job.Sync() 276 debug.Assert(requiresSync) 277 case DiffResolverEOF: 278 g.store.setAllDispatched(job.ID(), true) 279 return true 280 } 281 } 282 } 283 284 func (d *dispatcher) jobAbortedCh(jobID string) *cos.StopCh { 285 d.mtx.RLock() 286 defer d.mtx.RUnlock() 287 if abCh, ok := d.abortJob[jobID]; ok { 288 return abCh 289 } 290 291 // Channel always sending something if entry in the map is missing. 292 abCh := cos.NewStopCh() 293 abCh.Close() 294 return abCh 295 } 296 297 func (d *dispatcher) checkAbortedJob(job jobif) bool { 298 select { 299 case <-d.jobAbortedCh(job.ID()).Listen(): 300 return true 301 default: 302 return false 303 } 304 } 305 306 func (d *dispatcher) checkAborted() bool { 307 select { 308 case <-d.stopCh.Listen(): 309 return true 310 default: 311 return false 312 } 313 } 314 315 // returns false if dispatcher encountered hard error, true otherwise 316 func (d *dispatcher) doSingle(task *singleTask) (ok bool, err error) { 317 bck := meta.CloneBck(task.job.Bck()) 318 if err := bck.Init(core.T.Bowner()); err != nil { 319 return true, err 320 } 321 322 mi, _, err := fs.Hrw(bck.MakeUname(task.obj.objName)) 323 if err != nil { 324 return false, err 325 } 326 jogger, ok := d.joggers[mi.Path] 327 if !ok { 328 err := fmt.Errorf("no jogger for mpath %s exists", mi.Path) 329 return false, err 330 } 331 332 // NOTE: Throttle job before making jogger busy - we don't want to clog the 333 // jogger as other tasks from other jobs can be already ready to download. 334 select { 335 case <-task.job.throttler().tryAcquire(): 336 break 337 case <-d.jobAbortedCh(task.job.ID()).Listen(): 338 return true, nil 339 } 340 341 // Secondly, try to push the new task into queue. 342 select { 343 // TODO -- FIXME: currently, dispatcher halts if any given jogger is "full" but others available 344 case jogger.putCh(task) <- task: 345 return true, nil 346 case <-d.jobAbortedCh(task.job.ID()).Listen(): 347 task.job.throttler().release() 348 return true, nil 349 case <-d.stopCh.Listen(): 350 task.job.throttler().release() 351 return false, nil 352 } 353 } 354 355 func (d *dispatcher) adminReq(req *request) (resp any, statusCode int, err error) { 356 if cmn.Rom.FastV(4, cos.SmoduleDload) { 357 nlog.Infof("Admin request (id: %q, action: %q, onlyActive: %t)", req.id, req.action, req.onlyActive) 358 } 359 // Need to make sure that the dispatcher has fully initialized and started, 360 // and it's ready for processing the requests. 361 d.startupSema.waitForStartup() 362 363 switch req.action { 364 case actStatus: 365 d.handleStatus(req) 366 case actAbort: 367 d.handleAbort(req) 368 case actRemove: 369 d.handleRemove(req) 370 default: 371 debug.Assertf(false, "%v; %v", req, req.action) 372 } 373 r := req.response 374 return r.value, r.statusCode, r.err 375 } 376 377 func (*dispatcher) handleRemove(req *request) { 378 dljob, err := g.store.checkExists(req) 379 if err != nil { 380 return 381 } 382 job := dljob.clone() 383 if job.JobRunning() { 384 req.errRsp(fmt.Errorf("job %q is still running", dljob.id), http.StatusBadRequest) 385 return 386 } 387 g.store.delJob(req.id) 388 req.okRsp(nil) 389 } 390 391 func (d *dispatcher) handleAbort(req *request) { 392 if _, err := g.store.checkExists(req); err != nil { 393 return 394 } 395 d.jobAbortedCh(req.id).Close() 396 for _, j := range d.joggers { 397 j.abortJob(req.id) 398 } 399 g.store.setAborted(req.id) 400 req.okRsp(nil) 401 } 402 403 func (d *dispatcher) handleStatus(req *request) { 404 var ( 405 finishedTasks []TaskDlInfo 406 dlErrors []TaskErrInfo 407 ) 408 dljob, err := g.store.checkExists(req) 409 if err != nil { 410 return 411 } 412 413 currentTasks := d.activeTasks(req.id) 414 if !req.onlyActive { 415 finishedTasks, err = g.store.getTasks(req.id) 416 if err != nil { 417 req.errRsp(err, http.StatusInternalServerError) 418 return 419 } 420 421 dlErrors, err = g.store.getErrors(req.id) 422 if err != nil { 423 req.errRsp(err, http.StatusInternalServerError) 424 return 425 } 426 sort.Sort(TaskErrByName(dlErrors)) 427 } 428 429 req.okRsp(&StatusResp{ 430 Job: dljob.clone(), 431 CurrentTasks: currentTasks, 432 FinishedTasks: finishedTasks, 433 Errs: dlErrors, 434 }) 435 } 436 437 func (d *dispatcher) activeTasks(reqID string) []TaskDlInfo { 438 currentTasks := make([]TaskDlInfo, 0, len(d.joggers)) 439 for _, j := range d.joggers { 440 task := j.getTask(reqID) 441 if task != nil { 442 currentTasks = append(currentTasks, task.ToTaskDlInfo()) 443 } 444 } 445 446 sort.Sort(TaskInfoByName(currentTasks)) 447 return currentTasks 448 } 449 450 // pending returns `true` if any joggers has pending tasks for a given `reqID`, 451 // `false` otherwise. 452 func (d *dispatcher) pending(jobID string) bool { 453 for _, j := range d.joggers { 454 if j.pending(jobID) { 455 return true 456 } 457 } 458 return false 459 } 460 461 // PRECONDITION: All tasks should be dispatched. 462 func (d *dispatcher) waitFor(jobID string) { 463 for ; ; time.Sleep(time.Second) { 464 if !d.pending(jobID) { 465 return 466 } 467 } 468 } 469 470 ///////////////// 471 // startupSema // 472 ///////////////// 473 474 func (ss *startupSema) markStarted() { ss.started.Store(true) } 475 476 func (ss *startupSema) waitForStartup() { 477 const ( 478 sleep = 500 * time.Millisecond 479 timeout = 10 * time.Second 480 errmsg = "FATAL: dispatcher takes too much time to start" 481 ) 482 if ss.started.Load() { 483 return 484 } 485 for total := time.Duration(0); !ss.started.Load(); total += sleep { 486 time.Sleep(sleep) 487 // should never happen even on slowest machines 488 debug.Assert(total < timeout, errmsg) 489 if total >= timeout && total < timeout+sleep*2 { 490 nlog.Errorln(errmsg) 491 } 492 } 493 }