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  }