github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/prxnotif.go (about) 1 // Package ais provides core functionality for the AIStore object storage. 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package ais 6 7 import ( 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "sync" 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/cmn/mono" 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/ext/dload" 24 "github.com/NVIDIA/aistore/hk" 25 "github.com/NVIDIA/aistore/nl" 26 "github.com/NVIDIA/aistore/xact" 27 "github.com/NVIDIA/aistore/xact/xreg" 28 jsoniter "github.com/json-iterator/go" 29 ) 30 31 // Notification "receiver" 32 33 const notifsName = "p-notifs" 34 35 type ( 36 listeners struct { 37 m map[string]nl.Listener // [UUID => NotifListener] 38 sync.RWMutex 39 } 40 41 notifs struct { 42 p *proxy 43 nls *listeners // running 44 fin *listeners // finished 45 46 added []nl.Listener // reusable slice of `nl` to add to `nls` 47 removed []nl.Listener // reusable slice of `nl` to remove from `nls` 48 finished []nl.Listener // reusable slice of `nl` to add to `fin` 49 50 smapVer int64 51 mu sync.Mutex 52 } 53 jsonNotifs struct { 54 Running []*notifListenMsg `json:"running"` 55 Finished []*notifListenMsg `json:"finished"` 56 } 57 58 nlFilter xreg.Flt 59 60 // receiver to start listening 61 notifListenMsg struct { 62 nl nl.Listener 63 } 64 jsonNL struct { 65 Type string `json:"type"` 66 NL jsoniter.RawMessage `json:"nl"` 67 } 68 ) 69 70 // interface guard 71 var ( 72 _ meta.Slistener = (*notifs)(nil) 73 74 _ json.Marshaler = (*notifs)(nil) 75 _ json.Unmarshaler = (*notifs)(nil) 76 77 _ json.Marshaler = (*notifListenMsg)(nil) 78 _ json.Unmarshaler = (*notifListenMsg)(nil) 79 ) 80 81 //////////// 82 // notifs // 83 //////////// 84 85 func (n *notifs) init(p *proxy) { 86 n.p = p 87 n.nls = newListeners() 88 n.fin = newListeners() 89 90 n.added = make([]nl.Listener, 16) 91 n.removed = make([]nl.Listener, 16) 92 n.finished = make([]nl.Listener, 16) 93 94 hk.Reg(notifsName+hk.NameSuffix, n.housekeep, hk.PruneActiveIval) 95 n.p.Sowner().Listeners().Reg(n) 96 } 97 98 // handle other nodes' notifications 99 // verb /v1/notifs/[progress|finished] - apc.Progress and apc.Finished, respectively 100 func (n *notifs) handler(w http.ResponseWriter, r *http.Request) { 101 var ( 102 notifMsg = &core.NotifMsg{} 103 nl nl.Listener 104 uuid string 105 tid = r.Header.Get(apc.HdrCallerID) // sender node ID 106 ) 107 if r.Method != http.MethodPost { 108 cmn.WriteErr405(w, r, http.MethodPost) 109 return 110 } 111 apiItems, err := n.p.parseURL(w, r, apc.URLPathNotifs.L, 1, false) 112 if err != nil { 113 return 114 } 115 116 if apiItems[0] != apc.Progress && apiItems[0] != apc.Finished { 117 n.p.writeErrf(w, r, "Invalid route /notifs/%s", apiItems[0]) 118 return 119 } 120 if cmn.ReadJSON(w, r, notifMsg) != nil { 121 return 122 } 123 124 // NOTE: the sender is asynchronous - ignores the response - 125 // which is why we consider `not-found`, `already-finished`, 126 // and `unknown-notifier` benign non-error conditions 127 uuid = notifMsg.UUID 128 if !withRetry(cmn.Rom.CplaneOperation(), func() bool { 129 nl = n.entry(uuid) 130 return nl != nil 131 }) { 132 return 133 } 134 135 var ( 136 srcs = nl.Notifiers() 137 tsi, ok = srcs[tid] 138 ) 139 if !ok { 140 return 141 } 142 // 143 // NotifListener and notifMsg must have the same type 144 // 145 nl.RLock() 146 if nl.HasFinished(tsi) { 147 n.p.writeErrSilentf(w, r, http.StatusBadRequest, 148 "%s: duplicate %s from %s, %s", n.p.si, notifMsg, tid, nl) 149 nl.RUnlock() 150 return 151 } 152 nl.RUnlock() 153 154 switch apiItems[0] { 155 case apc.Progress: 156 nl.Lock() 157 n._progress(nl, tsi, notifMsg) 158 nl.Unlock() 159 case apc.Finished: 160 n._finished(nl, tsi, notifMsg) 161 } // default not needed - cannot happen 162 } 163 164 func (*notifs) _progress(nl nl.Listener, tsi *meta.Snode, msg *core.NotifMsg) { 165 if msg.ErrMsg != "" { 166 nl.AddErr(errors.New(msg.ErrMsg)) 167 } 168 // when defined, `data must be valid encoded stats 169 if msg.Data != nil { 170 stats, _, _, err := nl.UnmarshalStats(msg.Data) 171 debug.AssertNoErr(err) 172 nl.SetStats(tsi.ID(), stats) 173 } 174 } 175 176 func (n *notifs) _finished(nl nl.Listener, tsi *meta.Snode, msg *core.NotifMsg) { 177 var ( 178 srcErr error 179 done bool 180 aborted = msg.AbortedX 181 ) 182 nl.Lock() 183 if msg.Data != nil { 184 // ditto 185 stats, _, abortedSnap, err := nl.UnmarshalStats(msg.Data) 186 debug.AssertNoErr(err) 187 nl.SetStats(tsi.ID(), stats) 188 189 if abortedSnap != msg.AbortedX && cmn.Rom.FastV(4, cos.SmoduleAIS) { 190 nlog.Infof("Warning: %s: %t vs %t [%s]", msg, abortedSnap, msg.AbortedX, nl.String()) 191 } 192 aborted = aborted || abortedSnap 193 } 194 if msg.ErrMsg != "" { 195 srcErr = errors.New(msg.ErrMsg) 196 } 197 done = n.markFinished(nl, tsi, srcErr, aborted) 198 nl.Unlock() 199 200 if done { 201 n.done(nl) 202 } 203 } 204 205 // start listening 206 func (n *notifs) add(nl nl.Listener) (err error) { 207 debug.Assert(xact.IsValidUUID(nl.UUID())) 208 if nl.ActiveCount() == 0 { 209 return fmt.Errorf("cannot add %q with no active notifiers", nl) 210 } 211 if exists := n.nls.add(nl, false /*locked*/); exists { 212 return 213 } 214 nl.SetAddedTime() 215 if cmn.Rom.FastV(5, cos.SmoduleAIS) { 216 nlog.Infoln("add", nl.Name()) 217 } 218 return 219 } 220 221 func (n *notifs) del(nl nl.Listener, locked bool) (ok bool) { 222 ok = n.nls.del(nl, locked /*locked*/) 223 if ok && cmn.Rom.FastV(5, cos.SmoduleAIS) { 224 nlog.Infoln("del", nl.Name()) 225 } 226 return 227 } 228 229 func (n *notifs) entry(uuid string) nl.Listener { 230 entry, exists := n.nls.entry(uuid) 231 if exists { 232 return entry 233 } 234 entry, exists = n.fin.entry(uuid) 235 if exists { 236 return entry 237 } 238 return nil 239 } 240 241 func (n *notifs) find(flt nlFilter) (nl nl.Listener) { 242 if flt.ID != "" { 243 return n.entry(flt.ID) 244 } 245 nl = n.nls.find(flt) 246 if nl != nil || (flt.OnlyRunning != nil && *flt.OnlyRunning) { 247 return nl 248 } 249 nl = n.fin.find(flt) 250 return nl 251 } 252 253 func (n *notifs) findAll(flt nlFilter) (nls []nl.Listener) { 254 if flt.ID != "" { 255 if nl := n.entry(flt.ID); nl != nil { 256 nls = append(nls, nl) 257 } 258 return 259 } 260 nls = n.nls.findAll(flt) 261 if flt.OnlyRunning != nil && *flt.OnlyRunning { 262 return 263 } 264 if s2 := n.fin.findAll(flt); len(s2) > 0 { 265 nls = append(nls, s2...) 266 } 267 return 268 } 269 270 func (n *notifs) size() (size int) { 271 n.nls.RLock() 272 n.fin.RLock() 273 size = n.nls.len() + n.fin.len() 274 n.fin.RUnlock() 275 n.nls.RUnlock() 276 return 277 } 278 279 // PRECONDITION: `nl` should be under lock. 280 func (*notifs) markFinished(nl nl.Listener, tsi *meta.Snode, srcErr error, aborted bool) (done bool) { 281 nl.MarkFinished(tsi) 282 if aborted { 283 nl.SetAborted() 284 if srcErr == nil { 285 detail := fmt.Sprintf("%s from %s", nl, tsi.StringEx()) 286 srcErr = cmn.NewErrAborted(nl.String(), detail, nil) 287 } 288 } 289 if srcErr != nil { 290 nl.AddErr(srcErr) 291 } 292 return nl.ActiveCount() == 0 || aborted 293 } 294 295 func (n *notifs) done(nl nl.Listener) { 296 if !n.del(nl, false /*locked*/) { 297 // `nl` already removed from active map 298 return 299 } 300 n.fin.add(nl, false /*locked*/) 301 302 if nl.Aborted() { 303 smap := n.p.owner.smap.get() 304 // abort via primary to eliminate redundant intra-cluster messaging-and-handling 305 // TODO: confirm & load-balance 306 doSend := true 307 if smap.Primary != nil { // nil in unit tests 308 debug.Assert(n.p.SID() != smap.Primary.ID() || smap.IsPrimary(n.p.si)) 309 doSend = smap.IsPrimary(n.p.si) || 310 !smap.IsIC(smap.Primary) // never happens but ok 311 } 312 if doSend { 313 // NOTE: we accept finished notifications even after 314 // `nl` is aborted. Handle locks carefully. 315 args := allocBcArgs() 316 args.req = abortReq(nl) 317 args.network = cmn.NetIntraControl 318 args.timeout = cmn.Rom.MaxKeepalive() 319 args.nodes = []meta.NodeMap{nl.Notifiers()} 320 args.nodeCount = len(args.nodes[0]) 321 args.smap = smap 322 args.async = true 323 _ = n.p.bcastNodes(args) // args.async: result is already discarded/freed 324 freeBcArgs(args) 325 } 326 } 327 nl.Callback(nl, time.Now().UnixNano()) 328 } 329 330 func abortReq(nl nl.Listener) cmn.HreqArgs { 331 if nl.Kind() == apc.ActDownload { 332 // downloader implements abort via http.MethodDelete 333 // and different messaging 334 return dload.AbortReq(nl.UUID() /*job ID*/) 335 } 336 msg := apc.ActMsg{ 337 Action: apc.ActXactStop, 338 Name: cmn.ErrXactICNotifAbort.Error(), 339 Value: xact.ArgsMsg{ID: nl.UUID() /*xid*/, Kind: nl.Kind()}, 340 } 341 args := cmn.HreqArgs{Method: http.MethodPut} 342 args.Body = cos.MustMarshal(msg) 343 args.Path = apc.URLPathXactions.S 344 return args 345 } 346 347 // 348 // housekeeping 349 // 350 351 func (n *notifs) housekeep() time.Duration { 352 now := time.Now().UnixNano() 353 n.fin.Lock() 354 for _, nl := range n.fin.m { 355 timeout := hk.DelOldIval 356 if nl.Kind() == apc.ActList { 357 timeout = hk.OldAgeLsoNotif 358 } 359 if time.Duration(now-nl.EndTime()) > timeout { 360 n.fin.del(nl, true /*locked*/) 361 } 362 } 363 n.fin.Unlock() 364 365 n.nls.RLock() // TODO: atomic instead 366 if n.nls.len() == 0 { 367 n.nls.RUnlock() 368 return hk.PruneActiveIval 369 } 370 tempn := make(map[string]nl.Listener, n.nls.len()) 371 for uuid, nl := range n.nls.m { 372 tempn[uuid] = nl 373 } 374 n.nls.RUnlock() 375 for _, nl := range tempn { 376 n.bcastGetStats(nl, hk.PruneActiveIval) 377 } 378 // cleanup temp cloned notifs 379 clear(tempn) 380 381 return hk.PruneActiveIval 382 } 383 384 // conditional: query targets iff they delayed updating 385 func (n *notifs) bcastGetStats(nl nl.Listener, dur time.Duration) { 386 var ( 387 config = cmn.GCO.Get() 388 progressInterval = config.Periodic.NotifTime.D() 389 done bool 390 ) 391 nl.RLock() 392 nodesTardy, syncRequired := nl.NodesTardy(dur) 393 nl.RUnlock() 394 if !syncRequired { 395 return 396 } 397 args := allocBcArgs() 398 args.network = cmn.NetIntraControl 399 args.timeout = config.Timeout.MaxHostBusy.D() 400 args.req = nl.QueryArgs() // nodes to fetch stats from 401 args.nodes = []meta.NodeMap{nodesTardy} 402 args.nodeCount = len(args.nodes[0]) 403 args.smap = n.p.owner.smap.get() 404 debug.Assert(args.nodeCount > 0) // Ensure that there is at least one node to fetch. 405 406 results := n.p.bcastNodes(args) 407 freeBcArgs(args) 408 for _, res := range results { 409 if res.err == nil { 410 stats, finished, aborted, err := nl.UnmarshalStats(res.bytes) 411 if err != nil { 412 nlog.Errorf("%s: failed to parse stats from %s: %v", n.p, res.si.StringEx(), err) 413 continue 414 } 415 nl.Lock() 416 if finished { 417 done = done || n.markFinished(nl, res.si, nil, aborted) 418 } 419 nl.SetStats(res.si.ID(), stats) 420 nl.Unlock() 421 } else if res.status == http.StatusNotFound { 422 if mono.Since(nl.AddedTime()) < progressInterval { 423 // likely didn't start yet - skipping 424 continue 425 } 426 err := fmt.Errorf("%s: %s not found at %s", n.p.si, nl, res.si.StringEx()) 427 nl.Lock() 428 done = done || n.markFinished(nl, res.si, err, true) // NOTE: not-found at one ==> all done 429 nl.Unlock() 430 } else if cmn.Rom.FastV(4, cos.SmoduleAIS) { 431 nlog.Errorf("%s: %s, node %s: %v", n.p, nl, res.si.StringEx(), res.unwrap()) 432 } 433 } 434 freeBcastRes(results) 435 if done { 436 n.done(nl) 437 } 438 } 439 440 func (n *notifs) getOwner(uuid string) (o string, exists bool) { 441 var nl nl.Listener 442 if nl = n.entry(uuid); nl != nil { 443 exists = true 444 o = nl.GetOwner() 445 } 446 return 447 } 448 449 // TODO: consider Smap versioning per NotifListener 450 func (n *notifs) ListenSmapChanged() { 451 if !n.p.ClusterStarted() { 452 return 453 } 454 smap := n.p.owner.smap.get() 455 if n.smapVer >= smap.Version { 456 return 457 } 458 n.smapVer = smap.Version 459 460 n.nls.RLock() 461 if n.nls.len() == 0 { 462 n.nls.RUnlock() 463 return 464 } 465 var ( 466 remnl = make(map[string]nl.Listener) 467 remid = make(cos.StrKVs) 468 ) 469 for uuid, nl := range n.nls.m { 470 nl.RLock() 471 for sid := range nl.ActiveNotifiers() { 472 if node := smap.GetActiveNode(sid); node == nil { 473 remnl[uuid] = nl 474 remid[uuid] = sid 475 break 476 } 477 } 478 nl.RUnlock() 479 } 480 n.nls.RUnlock() 481 if len(remnl) == 0 { 482 return 483 } 484 now := time.Now().UnixNano() 485 486 repeat: 487 for uuid, nl := range remnl { 488 sid := remid[uuid] 489 if nl.Kind() == apc.ActRebalance && nl.Cause() != "" { // for the cause, see ais/rebmeta 490 nlog.Infof("Warning: %s: %s is out, ignore 'smap-changed'", nl.String(), sid) 491 delete(remnl, uuid) 492 goto repeat 493 } 494 err := &errNodeNotFound{"abort " + nl.String() + " via 'smap-changed':", sid, n.p.si, smap} 495 nl.Lock() 496 nl.AddErr(err) 497 nl.SetAborted() 498 nl.Unlock() 499 } 500 if len(remnl) == 0 { 501 return 502 } 503 504 // cleanup and callback w/ nl.Err 505 n.fin.Lock() 506 for uuid, nl := range remnl { 507 debug.Assert(nl.UUID() == uuid) 508 n.fin.add(nl, true /*locked*/) 509 } 510 n.fin.Unlock() 511 n.nls.Lock() 512 for _, nl := range remnl { 513 n.del(nl, true /*locked*/) 514 } 515 n.nls.Unlock() 516 517 for _, nl := range remnl { 518 nl.Callback(nl, now) 519 } 520 // cleanup 521 clear(remnl) 522 clear(remid) 523 } 524 525 func (n *notifs) MarshalJSON() (data []byte, err error) { 526 t := jsonNotifs{} 527 n.nls.RLock() 528 n.fin.RLock() 529 if n.nls.len() == 0 && n.fin.len() == 0 { 530 n.fin.RUnlock() 531 n.nls.RUnlock() 532 return 533 } 534 t.Running = make([]*notifListenMsg, 0, n.nls.len()) 535 t.Finished = make([]*notifListenMsg, 0, n.fin.len()) 536 for _, nl := range n.nls.m { 537 t.Running = append(t.Running, newNLMsg(nl)) 538 } 539 n.nls.RUnlock() 540 541 for _, nl := range n.fin.m { 542 t.Finished = append(t.Finished, newNLMsg(nl)) 543 } 544 n.fin.RUnlock() 545 546 return jsoniter.Marshal(t) 547 } 548 549 func (n *notifs) UnmarshalJSON(data []byte) (err error) { 550 if len(data) == 0 { 551 return 552 } 553 t := jsonNotifs{} 554 if err = jsoniter.Unmarshal(data, &t); err != nil { 555 return 556 } 557 if len(t.Running) == 0 && len(t.Finished) == 0 { 558 return 559 } 560 n.mu.Lock() 561 n.apply(&t) 562 n.mu.Unlock() 563 return 564 } 565 566 // Identify the diff in ownership table and populate `added`, `removed` and `finished` slices 567 // (under lock) 568 func (n *notifs) apply(t *jsonNotifs) { 569 added, removed, finished := n.added[:0], n.removed[:0], n.finished[:0] 570 n.nls.RLock() 571 n.fin.RLock() 572 for _, m := range t.Running { 573 if n.fin.exists(m.nl.UUID()) || n.nls.exists(m.nl.UUID()) { 574 continue 575 } 576 added = append(added, m.nl) 577 } 578 579 for _, m := range t.Finished { 580 if n.fin.exists(m.nl.UUID()) { 581 continue 582 } 583 if n.nls.exists(m.nl.UUID()) { 584 removed = append(removed, m.nl) 585 } 586 finished = append(finished, m.nl) 587 } 588 n.fin.RUnlock() 589 n.nls.RUnlock() 590 591 if len(removed) == 0 && len(added) == 0 { 592 goto fin 593 } 594 595 // Add/Remove `nl` - `n.nls`. 596 n.nls.Lock() 597 for _, nl := range added { 598 n.nls.add(nl, true /*locked*/) 599 } 600 for _, nl := range removed { 601 n.nls.del(nl, true /*locked*/) 602 } 603 n.nls.Unlock() 604 605 fin: 606 if len(finished) == 0 { 607 return 608 } 609 610 n.fin.Lock() 611 // Add `nl` to `n.fin`. 612 for _, nl := range finished { 613 n.fin.add(nl, true /*locked*/) 614 } 615 n.fin.Unlock() 616 617 // Call the Callback for each `nl` marking it finished. 618 now := time.Now().UnixNano() 619 for _, nl := range finished { 620 nl.Callback(nl, now) 621 } 622 } 623 624 func (n *notifs) String() string { 625 l, f := n.nls.len(), n.fin.len() // not r-locking 626 return fmt.Sprintf("%s (nls=%d, fin=%d)", notifsName, l, f) 627 } 628 629 /////////////// 630 // listeners // 631 /////////////// 632 633 func newListeners() *listeners { return &listeners{m: make(map[string]nl.Listener, 64)} } 634 func (l *listeners) len() int { return len(l.m) } 635 636 func (l *listeners) entry(uuid string) (entry nl.Listener, exists bool) { 637 l.RLock() 638 entry, exists = l.m[uuid] 639 l.RUnlock() 640 return 641 } 642 643 func (l *listeners) add(nl nl.Listener, locked bool) (exists bool) { 644 if !locked { 645 l.Lock() 646 } 647 if _, exists = l.m[nl.UUID()]; !exists { 648 l.m[nl.UUID()] = nl 649 } 650 if !locked { 651 l.Unlock() 652 } 653 return 654 } 655 656 func (l *listeners) del(nl nl.Listener, locked bool) (ok bool) { 657 if !locked { 658 l.Lock() 659 } else { 660 debug.AssertRWMutexLocked(&l.RWMutex) 661 } 662 if _, ok = l.m[nl.UUID()]; ok { 663 delete(l.m, nl.UUID()) 664 } 665 if !locked { 666 l.Unlock() 667 } 668 return 669 } 670 671 // PRECONDITION: `l` should be under lock. 672 func (l *listeners) exists(uuid string) (ok bool) { 673 _, ok = l.m[uuid] 674 return 675 } 676 677 // Returns a listener that matches the filter condition. 678 // - returns the first one that's still running, if exists 679 // - otherwise, returns the one that finished most recently 680 // (compare with the below) 681 func (l *listeners) find(flt nlFilter) (nl nl.Listener) { 682 var ftime int64 683 l.RLock() 684 for _, listener := range l.m { 685 if !flt.match(listener) { 686 continue 687 } 688 et := listener.EndTime() 689 if ftime != 0 && et < ftime { 690 debug.Assert(listener.Finished()) 691 continue 692 } 693 nl = listener 694 if !listener.Finished() { 695 break 696 } 697 ftime = et 698 } 699 l.RUnlock() 700 return 701 } 702 703 // returns all matches 704 func (l *listeners) findAll(flt nlFilter) (nls []nl.Listener) { 705 l.RLock() 706 for _, listener := range l.m { 707 if flt.match(listener) { 708 nls = append(nls, listener) 709 } 710 } 711 l.RUnlock() 712 return 713 } 714 715 //////////////////// 716 // notifListenMsg // 717 //////////////////// 718 719 func newNLMsg(nl nl.Listener) *notifListenMsg { 720 return ¬ifListenMsg{nl: nl} 721 } 722 723 func (n *notifListenMsg) MarshalJSON() (data []byte, err error) { 724 n.nl.RLock() 725 msg := jsonNL{Type: n.nl.Kind(), NL: cos.MustMarshal(n.nl)} 726 n.nl.RUnlock() 727 return jsoniter.Marshal(msg) 728 } 729 730 func (n *notifListenMsg) UnmarshalJSON(data []byte) (err error) { 731 t := jsonNL{} 732 if err = jsoniter.Unmarshal(data, &t); err != nil { 733 return 734 } 735 if dload.IsType(t.Type) { 736 n.nl = &dload.NotifDownloadListerner{} 737 } else { 738 n.nl = &xact.NotifXactListener{} 739 } 740 return jsoniter.Unmarshal(t.NL, &n.nl) 741 } 742 743 // 744 // Notification listener filter (nlFilter) 745 // 746 747 func (nf *nlFilter) match(nl nl.Listener) bool { 748 if nl.UUID() == nf.ID { 749 return true 750 } 751 if nf.Kind == "" || nl.Kind() == nf.Kind { 752 if nf.Bck == nil || nf.Bck.IsEmpty() { 753 return true 754 } 755 for _, bck := range nl.Bcks() { 756 qbck := (*cmn.QueryBcks)(nf.Bck.Bucket()) 757 if qbck.Contains(bck) { 758 return true 759 } 760 } 761 } 762 return false 763 } 764 765 // yet another call-retrying utility (TODO: unify) 766 767 func withRetry(timeout time.Duration, cond func() bool) (ok bool) { 768 sleep := cos.ProbingFrequency(timeout) 769 for elapsed := time.Duration(0); elapsed < timeout; elapsed += sleep { 770 if ok = cond(); ok { 771 break 772 } 773 time.Sleep(sleep) 774 } 775 return 776 }