github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/xact/xs/lso.go (about)

     1  // Package xs is a collection of eXtended actions (xactions), including multi-object
     2  // operations, list-objects, (cluster) rebalance and (target) resilver, ETL, and more.
     3  /*
     4   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     5   */
     6  package xs
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"path"
    14  	"path/filepath"
    15  	"runtime"
    16  	"sort"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/NVIDIA/aistore/api/apc"
    21  	"github.com/NVIDIA/aistore/cmn"
    22  	"github.com/NVIDIA/aistore/cmn/archive"
    23  	"github.com/NVIDIA/aistore/cmn/cos"
    24  	"github.com/NVIDIA/aistore/cmn/debug"
    25  	"github.com/NVIDIA/aistore/cmn/nlog"
    26  	"github.com/NVIDIA/aistore/core"
    27  	"github.com/NVIDIA/aistore/core/meta"
    28  	"github.com/NVIDIA/aistore/fs"
    29  	"github.com/NVIDIA/aistore/hk"
    30  	"github.com/NVIDIA/aistore/memsys"
    31  	"github.com/NVIDIA/aistore/transport"
    32  	"github.com/NVIDIA/aistore/transport/bundle"
    33  	"github.com/NVIDIA/aistore/xact/xreg"
    34  	"github.com/tinylib/msgp/msgp"
    35  )
    36  
    37  // `on-demand` per list-objects request
    38  type (
    39  	lsoFactory struct {
    40  		msg *apc.LsoMsg
    41  		hdr http.Header
    42  		streamingF
    43  	}
    44  	LsoXact struct {
    45  		msg       *apc.LsoMsg
    46  		msgCh     chan *apc.LsoMsg // incoming requests
    47  		respCh    chan *LsoRsp     // responses - next pages
    48  		remtCh    chan *LsoRsp     // remote paging by the responsible target
    49  		stopCh    cos.StopCh       // to stop xaction
    50  		token     string           // continuation token -> last responded page
    51  		nextToken string           // next continuation token -> next pages
    52  		lastPage  cmn.LsoEntries   // last page (contents)
    53  		walk      struct {
    54  			pageCh       chan *cmn.LsoEnt // channel to accumulate listed object entries
    55  			stopCh       *cos.StopCh      // to abort bucket walk
    56  			wi           *walkInfo        // walking context and state
    57  			wg           sync.WaitGroup   // wait until this walk finishes
    58  			done         bool             // done walking (indication)
    59  			wor          bool             // wantOnlyRemote
    60  			dontPopulate bool             // when listing remote obj-s: don't include local MD (in re: LsDonAddRemote)
    61  			this         bool             // r.msg.SID == core.T.SID(): true when this target does remote paging
    62  		}
    63  		streamingX
    64  		lensgl int64
    65  		ctx    *core.LsoInvCtx
    66  	}
    67  	LsoRsp struct {
    68  		Err    error
    69  		Lst    *cmn.LsoRes
    70  		Status int
    71  	}
    72  )
    73  
    74  const (
    75  	pageChSize     = 128
    76  	remtPageChSize = 16
    77  )
    78  
    79  var (
    80  	errStopped = errors.New("stopped")
    81  	ErrGone    = errors.New("gone")
    82  )
    83  
    84  // interface guard
    85  var (
    86  	_ core.Xact      = (*LsoXact)(nil)
    87  	_ xreg.Renewable = (*lsoFactory)(nil)
    88  )
    89  
    90  func (*lsoFactory) New(args xreg.Args, bck *meta.Bck) xreg.Renewable {
    91  	custom := args.Custom.(*xreg.LsoArgs)
    92  	p := &lsoFactory{
    93  		streamingF: streamingF{RenewBase: xreg.RenewBase{Args: args, Bck: bck}, kind: apc.ActList},
    94  		msg:        custom.Msg,
    95  		hdr:        custom.Hdr,
    96  	}
    97  	return p
    98  }
    99  
   100  func (p *lsoFactory) Start() (err error) {
   101  	r := &LsoXact{
   102  		streamingX: streamingX{p: &p.streamingF, config: cmn.GCO.Get()},
   103  		msg:        p.msg,
   104  		msgCh:      make(chan *apc.LsoMsg), // unbuffered
   105  		respCh:     make(chan *LsoRsp),     // ditto: one caller-requested page at a time
   106  	}
   107  	if err = cmn.ValidatePrefix(p.msg.Prefix); err != nil {
   108  		return err
   109  	}
   110  
   111  	r.lastPage = allocLsoEntries()
   112  	r.stopCh.Init()
   113  
   114  	// idle timeout vs delayed next-page request
   115  	// see also: resetIdle()
   116  	r.DemandBase.Init(p.UUID(), apc.ActList, p.Bck, r.config.Timeout.MaxHostBusy.D())
   117  
   118  	// NOTE: is set by the first message, never changes
   119  	r.walk.wor = r.msg.WantOnlyRemoteProps()
   120  	r.walk.this = r.msg.SID == core.T.SID()
   121  
   122  	// true iff the bucket was not added - not initialized
   123  	r.walk.dontPopulate = r.walk.wor && p.Bck.Props == nil
   124  	debug.Assert(!r.walk.dontPopulate || p.msg.IsFlagSet(apc.LsDontAddRemote))
   125  
   126  	if r.listRemote() {
   127  		// begin streams
   128  		if !r.walk.wor {
   129  			nt := core.T.Sowner().Get().CountActiveTs()
   130  			if nt > 1 {
   131  				// NOTE streams
   132  				if err = p.beginStreams(r); err != nil {
   133  					return err
   134  				}
   135  			}
   136  		}
   137  		// NOTE alternative flow _this_ target will execute:
   138  		// - nextpage =>
   139  		// -     backend.GetBucketInv() =>
   140  		// -        while { backend.ListObjectsInv }
   141  		if cos.IsParseBool(p.hdr.Get(apc.HdrInventory)) && r.walk.this {
   142  			r.ctx = &core.LsoInvCtx{Name: p.hdr.Get(apc.HdrInvName), ID: p.hdr.Get(apc.HdrInvID)}
   143  		}
   144  	}
   145  
   146  	p.xctn = r
   147  	return nil
   148  }
   149  
   150  func (p *lsoFactory) beginStreams(r *LsoXact) (err error) {
   151  	if !r.walk.this {
   152  		r.remtCh = make(chan *LsoRsp, remtPageChSize) // <= by selected target (selected to page remote bucket)
   153  	}
   154  	trname := "lso-" + p.UUID()
   155  	dmxtra := bundle.Extra{Multiplier: 1, Config: r.config}
   156  	p.dm, err = bundle.NewDataMover(trname, r.recv, cmn.OwtPut, dmxtra)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	debug.Assert(p.dm != nil)
   161  	if err = p.dm.RegRecv(); err != nil {
   162  		if p.msg.ContinuationToken != "" {
   163  			err = fmt.Errorf("%s: late continuation [%s,%s], DM: %v", core.T,
   164  				p.msg.UUID, p.msg.ContinuationToken, err)
   165  		}
   166  		nlog.Errorln(err)
   167  		return err
   168  	}
   169  	p.dm.SetXact(r)
   170  	p.dm.Open()
   171  	return nil
   172  }
   173  
   174  /////////////
   175  // LsoXact //
   176  /////////////
   177  
   178  func (r *LsoXact) Run(wg *sync.WaitGroup) {
   179  	wg.Done()
   180  
   181  	if !r.listRemote() {
   182  		r.initWalk()
   183  	}
   184  loop:
   185  	for {
   186  		select {
   187  		case msg := <-r.msgCh:
   188  			// Copy only the values that can change between calls
   189  			debug.Assert(r.msg.UUID == msg.UUID && r.msg.Prefix == msg.Prefix && r.msg.Flags == msg.Flags)
   190  			r.msg.ContinuationToken = msg.ContinuationToken
   191  			r.msg.PageSize = msg.PageSize
   192  
   193  			// cannot change
   194  			debug.Assert((r.msg.SID == core.T.SID()) == r.walk.this)
   195  			debug.Assert(r.walk.wor == r.msg.WantOnlyRemoteProps())
   196  
   197  			r.IncPending()
   198  			resp := r.doPage()
   199  			r.DecPending()
   200  			if resp.Err == nil {
   201  				// report heterogeneous stats (x-list is an exception)
   202  				r.ObjsAdd(len(resp.Lst.Entries), 0)
   203  			}
   204  			r.respCh <- resp
   205  		case <-r.IdleTimer():
   206  			break loop
   207  		case <-r.ChanAbort():
   208  			break loop
   209  		}
   210  	}
   211  
   212  	r.stop()
   213  }
   214  
   215  func (r *LsoXact) stop() {
   216  	r.stopCh.Close()
   217  	if r.listRemote() {
   218  		if r.DemandBase.Finished() {
   219  			// must be aborted
   220  			if !r.walk.wor {
   221  				r.p.dm.Close(r.Err())
   222  				r.p.dm.UnregRecv()
   223  			}
   224  		} else {
   225  			r.DemandBase.Stop()
   226  			r.Finish()
   227  			r.lastmsg()
   228  
   229  			if !r.walk.wor {
   230  				r.p.dm.Close(r.Err())
   231  
   232  				if r.walk.this {
   233  					debug.Assert(r.remtCh == nil)
   234  					close(r.msgCh)
   235  					r.p.dm.UnregRecv()
   236  				} else if r.p.dm != nil {
   237  					// postpone unreg
   238  					hk.Reg(r.ID()+hk.NameSuffix, r.fcleanup, r.config.Timeout.MaxKeepalive.D())
   239  				}
   240  			}
   241  		}
   242  	} else {
   243  		r.DemandBase.Stop()
   244  		r.walk.stopCh.Close()
   245  		r.walk.wg.Wait()
   246  		r.lastmsg()
   247  		r.Finish()
   248  	}
   249  
   250  	if r.lastPage != nil {
   251  		freeLsoEntries(r.lastPage)
   252  		r.lastPage = nil
   253  	}
   254  	if r.ctx != nil {
   255  		if r.ctx.Lom != nil {
   256  			cos.Close(r.ctx.Lmfh)
   257  			r.ctx.Lom.Unlock(false) // NOTE: see GetBucketInv() "returns" comment in aws.go
   258  			core.FreeLOM(r.ctx.Lom)
   259  			r.ctx.Lom = nil
   260  		}
   261  		if r.ctx.SGL != nil {
   262  			if r.ctx.SGL.Len() > 0 {
   263  				nlog.Errorln(r.String(), "non-paginated leftover upon exit (bytes)", r.ctx.SGL.Len())
   264  			}
   265  			r.ctx.SGL.Free()
   266  			r.ctx.SGL = nil
   267  		}
   268  		r.ctx = nil
   269  	}
   270  }
   271  
   272  func (r *LsoXact) lastmsg() {
   273  	select {
   274  	case <-r.msgCh:
   275  		r.respCh <- &LsoRsp{Err: ErrGone}
   276  	default:
   277  		break
   278  	}
   279  	close(r.respCh)
   280  }
   281  
   282  // upon listing last page
   283  func (r *LsoXact) resetIdle() {
   284  	r.DemandBase.Reset(max(r.config.Timeout.MaxKeepalive.D(), 2*time.Second))
   285  }
   286  
   287  func (r *LsoXact) fcleanup() (d time.Duration) {
   288  	if cnt := r.wiCnt.Load(); cnt > 0 {
   289  		d = time.Second
   290  	} else {
   291  		d = hk.UnregInterval
   292  		if r.remtCh != nil {
   293  			close(r.remtCh)
   294  		}
   295  		close(r.msgCh)
   296  		r.p.dm.UnregRecv()
   297  	}
   298  	return
   299  }
   300  
   301  // skip on-demand idleness check
   302  func (r *LsoXact) Abort(err error) (ok bool) {
   303  	if ok = r.Base.Abort(err); ok {
   304  		r.Finish()
   305  	}
   306  	return
   307  }
   308  
   309  func (r *LsoXact) listRemote() bool { return r.p.Bck.IsRemote() && !r.msg.IsFlagSet(apc.LsObjCached) }
   310  
   311  // Start `fs.WalkBck`, so that by the time we read the next page `r.pageCh` is already populated.
   312  func (r *LsoXact) initWalk() {
   313  	r.walk.pageCh = make(chan *cmn.LsoEnt, pageChSize)
   314  	r.walk.done = false
   315  	r.walk.stopCh = cos.NewStopCh()
   316  	r.walk.wg.Add(1)
   317  
   318  	go r.doWalk(r.msg.Clone())
   319  	runtime.Gosched()
   320  }
   321  
   322  func (r *LsoXact) Do(msg *apc.LsoMsg) *LsoRsp {
   323  	// The guarantee here is that we either put something on the channel and our
   324  	// request will be processed (since the `msgCh` is unbuffered) or we receive
   325  	// message that the xaction has been stopped.
   326  	select {
   327  	case r.msgCh <- msg:
   328  		return <-r.respCh
   329  	case <-r.stopCh.Listen():
   330  		return &LsoRsp{Err: ErrGone}
   331  	}
   332  }
   333  
   334  func (r *LsoXact) doPage() *LsoRsp {
   335  	if r.listRemote() {
   336  		if r.msg.ContinuationToken == "" || r.msg.ContinuationToken != r.token {
   337  			// can't extract the next-to-list object name from the remotely generated
   338  			// continuation token, keeping and returning the entire last page
   339  			r.token = r.msg.ContinuationToken
   340  			if err := r.nextPageR(); err != nil {
   341  				return &LsoRsp{Status: http.StatusInternalServerError, Err: err}
   342  			}
   343  		}
   344  		page := &cmn.LsoRes{UUID: r.msg.UUID, Entries: r.lastPage, ContinuationToken: r.nextToken}
   345  		return &LsoRsp{Lst: page, Status: http.StatusOK}
   346  	}
   347  
   348  	if r.msg.ContinuationToken == "" || r.msg.ContinuationToken != r.token {
   349  		r.nextPageA()
   350  	}
   351  	var (
   352  		cnt  = r.msg.PageSize
   353  		idx  = r.findToken(r.msg.ContinuationToken)
   354  		lst  = r.lastPage[idx:]
   355  		page *cmn.LsoRes
   356  	)
   357  	debug.Assert(int64(len(lst)) >= cnt || r.walk.done)
   358  	if int64(len(lst)) >= cnt {
   359  		entries := lst[:cnt]
   360  		page = &cmn.LsoRes{UUID: r.msg.UUID, Entries: entries, ContinuationToken: entries[cnt-1].Name}
   361  	} else {
   362  		page = &cmn.LsoRes{UUID: r.msg.UUID, Entries: lst}
   363  	}
   364  	return &LsoRsp{Lst: page, Status: http.StatusOK}
   365  }
   366  
   367  // `ais show job` will report the sum of non-replicated obj numbers and
   368  // sum of obj sizes - for all visited objects
   369  // Returns the index of the first object in the page that follows the continuation `token`
   370  func (r *LsoXact) findToken(token string) int {
   371  	if r.listRemote() && r.token == token {
   372  		return 0
   373  	}
   374  	return sort.Search(len(r.lastPage), func(i int) bool { // TODO: revisit
   375  		return !cmn.TokenGreaterEQ(token, r.lastPage[i].Name)
   376  	})
   377  }
   378  
   379  func (r *LsoXact) havePage(token string, cnt int64) bool {
   380  	if r.walk.done {
   381  		return true
   382  	}
   383  	idx := r.findToken(token)
   384  	return idx+int(cnt) < len(r.lastPage)
   385  }
   386  
   387  func (r *LsoXact) nextPageR() (err error) {
   388  	var (
   389  		page *cmn.LsoRes
   390  		npg  = newNpgCtx(r.p.Bck, r.msg, r.LomAdd, r.ctx)
   391  		smap = core.T.Sowner().Get()
   392  		tsi  = smap.GetActiveNode(r.msg.SID)
   393  	)
   394  	if tsi == nil {
   395  		err = fmt.Errorf("%s: \"paging\" %s is down or inactive, %s", r, meta.Tname(r.msg.SID), smap)
   396  		goto ex
   397  	}
   398  	r.wiCnt.Inc()
   399  
   400  	// TODO -- FIXME: not counting/sizing (locally) present objects that are missing (deleted?) remotely
   401  	if r.walk.this {
   402  		nentries := allocLsoEntries()
   403  		page, err = npg.nextPageR(nentries, !r.walk.dontPopulate)
   404  		if !r.walk.wor && !r.IsAborted() {
   405  			if err == nil {
   406  				// bcast page
   407  				err = r.bcast(page)
   408  			} else {
   409  				r.sendTerm(r.msg.UUID, nil, err)
   410  			}
   411  		}
   412  	} else {
   413  		debug.Assert(!r.msg.WantOnlyRemoteProps() && /*same*/ !r.walk.wor)
   414  		select {
   415  		case rsp := <-r.remtCh:
   416  			if rsp == nil {
   417  				err = ErrGone
   418  			} else if rsp.Err != nil {
   419  				err = rsp.Err
   420  			} else {
   421  				page = rsp.Lst
   422  				err = npg.populate(page)
   423  			}
   424  		case <-r.stopCh.Listen():
   425  			err = ErrGone
   426  		}
   427  	}
   428  
   429  	r.wiCnt.Dec()
   430  ex:
   431  	if err != nil {
   432  		r.nextToken = ""
   433  		r.AddErr(err)
   434  		return err
   435  	}
   436  	if page.ContinuationToken == "" {
   437  		r.walk.done = true
   438  		r.resetIdle()
   439  	}
   440  	freeLsoEntries(r.lastPage)
   441  	r.lastPage = page.Entries
   442  	r.nextToken = page.ContinuationToken
   443  	return
   444  }
   445  
   446  func (r *LsoXact) bcast(page *cmn.LsoRes) (err error) {
   447  	if r.p.dm == nil { // single target
   448  		return nil
   449  	}
   450  	var (
   451  		mm        = core.T.PageMM()
   452  		siz       = max(r.lensgl, memsys.DefaultBufSize)
   453  		buf, slab = mm.AllocSize(siz)
   454  		sgl       = mm.NewSGL(siz, slab.Size())
   455  		mw        = msgp.NewWriterBuf(sgl, buf)
   456  	)
   457  	if err = page.EncodeMsg(mw); err == nil {
   458  		err = mw.Flush()
   459  	}
   460  	slab.Free(buf)
   461  	r.lensgl = sgl.Len()
   462  	if err != nil {
   463  		sgl.Free()
   464  		return err
   465  	}
   466  
   467  	o := transport.AllocSend()
   468  	{
   469  		o.Hdr.Bck = r.p.Bck.Clone()
   470  		o.Hdr.ObjName = r.Name()
   471  		o.Hdr.Opaque = cos.UnsafeB(r.p.UUID())
   472  		o.Hdr.ObjAttrs.Size = sgl.Len()
   473  	}
   474  	o.Callback, o.CmplArg = r.sentCb, sgl // cleanup
   475  	o.Reader = sgl
   476  	roc := memsys.NewReader(sgl)
   477  	r.p.dm.Bcast(o, roc)
   478  	return nil
   479  }
   480  
   481  func (r *LsoXact) sentCb(hdr *transport.ObjHdr, _ io.ReadCloser, arg any, err error) {
   482  	if err == nil {
   483  		// using generic out-counter to count broadcast pages
   484  		r.OutObjsAdd(1, hdr.ObjAttrs.Size)
   485  	} else if cmn.Rom.FastV(4, cos.SmoduleXs) || !cos.IsRetriableConnErr(err) {
   486  		nlog.Infof("Warning: %s: failed to send [%+v]: %v", core.T, hdr, err)
   487  	}
   488  	sgl, ok := arg.(*memsys.SGL)
   489  	debug.Assertf(ok, "%T", arg)
   490  	sgl.Free()
   491  }
   492  
   493  func (r *LsoXact) gcLastPage(from, to int) {
   494  	for i := from; i < to; i++ {
   495  		r.lastPage[i] = nil
   496  	}
   497  }
   498  
   499  func (r *LsoXact) nextPageA() {
   500  	if r.token > r.msg.ContinuationToken {
   501  		// restart traversing the bucket (TODO: cache more and try to scroll back)
   502  		r.walk.stopCh.Close()
   503  		r.walk.wg.Wait()
   504  		r.initWalk()
   505  		r.gcLastPage(0, len(r.lastPage))
   506  		r.lastPage = r.lastPage[:0]
   507  	} else {
   508  		if r.walk.done {
   509  			return
   510  		}
   511  		r.shiftLastPage(r.msg.ContinuationToken)
   512  	}
   513  	r.token = r.msg.ContinuationToken
   514  
   515  	if r.havePage(r.token, r.msg.PageSize) {
   516  		return
   517  	}
   518  	for cnt := int64(0); cnt < r.msg.PageSize; {
   519  		obj, ok := <-r.walk.pageCh
   520  		if !ok {
   521  			r.walk.done = true
   522  			r.resetIdle()
   523  			break
   524  		}
   525  		// Skip until the requested continuation token (TODO: revisit)
   526  		if cmn.TokenGreaterEQ(r.token, obj.Name) {
   527  			continue
   528  		}
   529  		cnt++
   530  		r.lastPage = append(r.lastPage, obj)
   531  	}
   532  }
   533  
   534  // Removes entries that were already sent to clients.
   535  // Is used only for AIS buckets and (cached == true) requests.
   536  func (r *LsoXact) shiftLastPage(token string) {
   537  	if token == "" || len(r.lastPage) == 0 {
   538  		return
   539  	}
   540  	j := r.findToken(token)
   541  	// the page is "after" the token - keep it all
   542  	if j == 0 {
   543  		return
   544  	}
   545  	l := len(r.lastPage)
   546  
   547  	// (all sent)
   548  	if j == l {
   549  		r.gcLastPage(0, l)
   550  		r.lastPage = r.lastPage[:0]
   551  		return
   552  	}
   553  
   554  	// otherwise, shift the not-yet-transmitted entries and fix the slice
   555  	copy(r.lastPage[0:], r.lastPage[j:])
   556  	r.gcLastPage(l-j, l)
   557  	r.lastPage = r.lastPage[:l-j]
   558  }
   559  
   560  func (r *LsoXact) doWalk(msg *apc.LsoMsg) {
   561  	r.walk.wi = newWalkInfo(msg, r.LomAdd)
   562  	opts := &fs.WalkBckOpts{
   563  		WalkOpts: fs.WalkOpts{CTs: []string{fs.ObjectType}, Callback: r.cb, Prefix: msg.Prefix, Sorted: true},
   564  	}
   565  	opts.WalkOpts.Bck.Copy(r.Bck().Bucket())
   566  	opts.ValidateCb = r.validateCb
   567  	if err := fs.WalkBck(opts); err != nil {
   568  		if err != filepath.SkipDir && err != errStopped {
   569  			r.AddErr(err, 0)
   570  		}
   571  	}
   572  	close(r.walk.pageCh)
   573  	r.walk.wg.Done()
   574  }
   575  
   576  func (r *LsoXact) validateCb(fqn string, de fs.DirEntry) error {
   577  	if !de.IsDir() {
   578  		return nil
   579  	}
   580  	err := r.walk.wi.processDir(fqn)
   581  	if err != nil {
   582  		return err
   583  	}
   584  	if !r.walk.wi.msg.IsFlagSet(apc.LsNoRecursion) {
   585  		return nil
   586  	}
   587  
   588  	// no recursion: check the level of nesting, add virtual dir-s
   589  
   590  	ct, err := core.NewCTFromFQN(fqn, nil)
   591  	if err != nil {
   592  		return nil
   593  	}
   594  	entry, err := cmn.HandleNoRecurs(r.walk.wi.msg.Prefix, ct.ObjectName())
   595  	if entry != nil {
   596  		select {
   597  		case r.walk.pageCh <- entry:
   598  		case <-r.walk.stopCh.Listen():
   599  			return errStopped
   600  		}
   601  	}
   602  	return err
   603  }
   604  
   605  func (r *LsoXact) cb(fqn string, de fs.DirEntry) error {
   606  	entry, err := r.walk.wi.callback(fqn, de)
   607  	if err != nil || entry == nil {
   608  		return err
   609  	}
   610  	msg := r.walk.wi.lsmsg()
   611  	if entry.Name <= msg.StartAfter {
   612  		return nil
   613  	}
   614  
   615  	select {
   616  	case r.walk.pageCh <- entry:
   617  		/* do nothing */
   618  	case <-r.walk.stopCh.Listen():
   619  		return errStopped
   620  	}
   621  
   622  	if !msg.IsFlagSet(apc.LsArchDir) {
   623  		return nil
   624  	}
   625  
   626  	// ls arch
   627  	// looking only at the file extension - not reading ("detecting") file magic (TODO: add lsmsg flag)
   628  	archList, err := archive.List(fqn)
   629  	if err != nil {
   630  		if archive.IsErrUnknownFileExt(err) {
   631  			// skip and keep going
   632  			err = nil
   633  		}
   634  		return err
   635  	}
   636  	entry.Flags |= apc.EntryIsArchive // the parent archive
   637  	for _, archEntry := range archList {
   638  		e := &cmn.LsoEnt{
   639  			Name:  path.Join(entry.Name, archEntry.Name),
   640  			Flags: entry.Flags | apc.EntryInArch,
   641  			Size:  archEntry.Size,
   642  		}
   643  		select {
   644  		case r.walk.pageCh <- e:
   645  			/* do nothing */
   646  		case <-r.walk.stopCh.Listen():
   647  			return errStopped
   648  		}
   649  	}
   650  	return nil
   651  }
   652  
   653  func (r *LsoXact) Snap() (snap *core.Snap) {
   654  	snap = &core.Snap{}
   655  	r.ToSnap(snap)
   656  
   657  	snap.IdleX = r.IsIdle()
   658  	return
   659  }
   660  
   661  //
   662  // streaming receive: remote pages
   663  //
   664  
   665  func (r *LsoXact) recv(hdr *transport.ObjHdr, objReader io.Reader, err error) error {
   666  	debug.Assert(r.listRemote())
   667  
   668  	if hdr.Opcode == opcodeAbrt {
   669  		err = errors.New(hdr.ObjName) // definitely see `streamingX.sendTerm()`
   670  	}
   671  	if err != nil && !cos.IsEOF(err) {
   672  		nlog.Errorln(core.T.String(), r.String(), len(r.remtCh), err)
   673  		r.remtCh <- &LsoRsp{Status: http.StatusInternalServerError, Err: err}
   674  		return err
   675  	}
   676  
   677  	debug.Assert(hdr.Opcode == 0)
   678  	r.IncPending()
   679  	buf, slab := core.T.PageMM().AllocSize(cmn.MsgpLsoBufSize)
   680  
   681  	err = r._recv(hdr, objReader, buf)
   682  
   683  	slab.Free(buf)
   684  	r.DecPending()
   685  	transport.DrainAndFreeReader(objReader)
   686  	return err
   687  }
   688  
   689  func (r *LsoXact) _recv(hdr *transport.ObjHdr, objReader io.Reader, buf []byte) (err error) {
   690  	var (
   691  		page = &cmn.LsoRes{}
   692  		mr   = msgp.NewReaderBuf(objReader, buf)
   693  	)
   694  	err = page.DecodeMsg(mr)
   695  	if err == nil {
   696  		r.remtCh <- &LsoRsp{Lst: page, Status: http.StatusOK}
   697  		// using generic in-counter to count received pages
   698  		r.InObjsAdd(1, hdr.ObjAttrs.Size)
   699  	} else {
   700  		nlog.Errorf("%s: failed to recv [%s: %s] num=%d from %s (%s, %s): %v",
   701  			core.T, page.UUID, page.ContinuationToken, len(page.Entries),
   702  			hdr.SID, hdr.Bck.Cname(""), string(hdr.Opaque), err)
   703  		r.remtCh <- &LsoRsp{Status: http.StatusInternalServerError, Err: err}
   704  	}
   705  	return
   706  }