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 }