1// Copyright 2015 The etcd Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package store 16 17import ( 18 "encoding/json" 19 "fmt" 20 "path" 21 "strconv" 22 "strings" 23 "sync" 24 "time" 25 26 etcdErr "github.com/coreos/etcd/error" 27 "github.com/coreos/etcd/pkg/types" 28 "github.com/jonboulle/clockwork" 29) 30 31// The default version to set when the store is first initialized. 32const defaultVersion = 2 33 34var minExpireTime time.Time 35 36func init() { 37 minExpireTime, _ = time.Parse(time.RFC3339, "2000-01-01T00:00:00Z") 38} 39 40type Store interface { 41 Version() int 42 Index() uint64 43 44 Get(nodePath string, recursive, sorted bool) (*Event, error) 45 Set(nodePath string, dir bool, value string, expireOpts TTLOptionSet) (*Event, error) 46 Update(nodePath string, newValue string, expireOpts TTLOptionSet) (*Event, error) 47 Create(nodePath string, dir bool, value string, unique bool, 48 expireOpts TTLOptionSet) (*Event, error) 49 CompareAndSwap(nodePath string, prevValue string, prevIndex uint64, 50 value string, expireOpts TTLOptionSet) (*Event, error) 51 Delete(nodePath string, dir, recursive bool) (*Event, error) 52 CompareAndDelete(nodePath string, prevValue string, prevIndex uint64) (*Event, error) 53 54 Watch(prefix string, recursive, stream bool, sinceIndex uint64) (Watcher, error) 55 56 Save() ([]byte, error) 57 Recovery(state []byte) error 58 59 Clone() Store 60 SaveNoCopy() ([]byte, error) 61 62 JsonStats() []byte 63 DeleteExpiredKeys(cutoff time.Time) 64 65 HasTTLKeys() bool 66} 67 68type TTLOptionSet struct { 69 ExpireTime time.Time 70 Refresh bool 71} 72 73type store struct { 74 Root *node 75 WatcherHub *watcherHub 76 CurrentIndex uint64 77 Stats *Stats 78 CurrentVersion int 79 ttlKeyHeap *ttlKeyHeap // need to recovery manually 80 worldLock sync.RWMutex // stop the world lock 81 clock clockwork.Clock 82 readonlySet types.Set 83} 84 85// New creates a store where the given namespaces will be created as initial directories. 86func New(namespaces ...string) Store { 87 s := newStore(namespaces...) 88 s.clock = clockwork.NewRealClock() 89 return s 90} 91 92func newStore(namespaces ...string) *store { 93 s := new(store) 94 s.CurrentVersion = defaultVersion 95 s.Root = newDir(s, "/", s.CurrentIndex, nil, Permanent) 96 for _, namespace := range namespaces { 97 s.Root.Add(newDir(s, namespace, s.CurrentIndex, s.Root, Permanent)) 98 } 99 s.Stats = newStats() 100 s.WatcherHub = newWatchHub(1000) 101 s.ttlKeyHeap = newTtlKeyHeap() 102 s.readonlySet = types.NewUnsafeSet(append(namespaces, "/")...) 103 return s 104} 105 106// Version retrieves current version of the store. 107func (s *store) Version() int { 108 return s.CurrentVersion 109} 110 111// Index retrieves the current index of the store. 112func (s *store) Index() uint64 { 113 s.worldLock.RLock() 114 defer s.worldLock.RUnlock() 115 return s.CurrentIndex 116} 117 118// Get returns a get event. 119// If recursive is true, it will return all the content under the node path. 120// If sorted is true, it will sort the content by keys. 121func (s *store) Get(nodePath string, recursive, sorted bool) (*Event, error) { 122 var err *etcdErr.Error 123 124 s.worldLock.RLock() 125 defer s.worldLock.RUnlock() 126 127 defer func() { 128 if err == nil { 129 s.Stats.Inc(GetSuccess) 130 if recursive { 131 reportReadSuccess(GetRecursive) 132 } else { 133 reportReadSuccess(Get) 134 } 135 return 136 } 137 138 s.Stats.Inc(GetFail) 139 if recursive { 140 reportReadFailure(GetRecursive) 141 } else { 142 reportReadFailure(Get) 143 } 144 }() 145 146 n, err := s.internalGet(nodePath) 147 if err != nil { 148 return nil, err 149 } 150 151 e := newEvent(Get, nodePath, n.ModifiedIndex, n.CreatedIndex) 152 e.EtcdIndex = s.CurrentIndex 153 e.Node.loadInternalNode(n, recursive, sorted, s.clock) 154 155 return e, nil 156} 157 158// Create creates the node at nodePath. Create will help to create intermediate directories with no ttl. 159// If the node has already existed, create will fail. 160// If any node on the path is a file, create will fail. 161func (s *store) Create(nodePath string, dir bool, value string, unique bool, expireOpts TTLOptionSet) (*Event, error) { 162 var err *etcdErr.Error 163 164 s.worldLock.Lock() 165 defer s.worldLock.Unlock() 166 167 defer func() { 168 if err == nil { 169 s.Stats.Inc(CreateSuccess) 170 reportWriteSuccess(Create) 171 return 172 } 173 174 s.Stats.Inc(CreateFail) 175 reportWriteFailure(Create) 176 }() 177 178 e, err := s.internalCreate(nodePath, dir, value, unique, false, expireOpts.ExpireTime, Create) 179 if err != nil { 180 return nil, err 181 } 182 183 e.EtcdIndex = s.CurrentIndex 184 s.WatcherHub.notify(e) 185 186 return e, nil 187} 188 189// Set creates or replace the node at nodePath. 190func (s *store) Set(nodePath string, dir bool, value string, expireOpts TTLOptionSet) (*Event, error) { 191 var err *etcdErr.Error 192 193 s.worldLock.Lock() 194 defer s.worldLock.Unlock() 195 196 defer func() { 197 if err == nil { 198 s.Stats.Inc(SetSuccess) 199 reportWriteSuccess(Set) 200 return 201 } 202 203 s.Stats.Inc(SetFail) 204 reportWriteFailure(Set) 205 }() 206 207 // Get prevNode value 208 n, getErr := s.internalGet(nodePath) 209 if getErr != nil && getErr.ErrorCode != etcdErr.EcodeKeyNotFound { 210 err = getErr 211 return nil, err 212 } 213 214 if expireOpts.Refresh { 215 if getErr != nil { 216 err = getErr 217 return nil, err 218 } else { 219 value = n.Value 220 } 221 } 222 223 // Set new value 224 e, err := s.internalCreate(nodePath, dir, value, false, true, expireOpts.ExpireTime, Set) 225 if err != nil { 226 return nil, err 227 } 228 e.EtcdIndex = s.CurrentIndex 229 230 // Put prevNode into event 231 if getErr == nil { 232 prev := newEvent(Get, nodePath, n.ModifiedIndex, n.CreatedIndex) 233 prev.Node.loadInternalNode(n, false, false, s.clock) 234 e.PrevNode = prev.Node 235 } 236 237 if !expireOpts.Refresh { 238 s.WatcherHub.notify(e) 239 } else { 240 e.SetRefresh() 241 s.WatcherHub.add(e) 242 } 243 244 return e, nil 245} 246 247// returns user-readable cause of failed comparison 248func getCompareFailCause(n *node, which int, prevValue string, prevIndex uint64) string { 249 switch which { 250 case CompareIndexNotMatch: 251 return fmt.Sprintf("[%v != %v]", prevIndex, n.ModifiedIndex) 252 case CompareValueNotMatch: 253 return fmt.Sprintf("[%v != %v]", prevValue, n.Value) 254 default: 255 return fmt.Sprintf("[%v != %v] [%v != %v]", prevValue, n.Value, prevIndex, n.ModifiedIndex) 256 } 257} 258 259func (s *store) CompareAndSwap(nodePath string, prevValue string, prevIndex uint64, 260 value string, expireOpts TTLOptionSet) (*Event, error) { 261 262 var err *etcdErr.Error 263 264 s.worldLock.Lock() 265 defer s.worldLock.Unlock() 266 267 defer func() { 268 if err == nil { 269 s.Stats.Inc(CompareAndSwapSuccess) 270 reportWriteSuccess(CompareAndSwap) 271 return 272 } 273 274 s.Stats.Inc(CompareAndSwapFail) 275 reportWriteFailure(CompareAndSwap) 276 }() 277 278 nodePath = path.Clean(path.Join("/", nodePath)) 279 // we do not allow the user to change "/" 280 if s.readonlySet.Contains(nodePath) { 281 return nil, etcdErr.NewError(etcdErr.EcodeRootROnly, "/", s.CurrentIndex) 282 } 283 284 n, err := s.internalGet(nodePath) 285 if err != nil { 286 return nil, err 287 } 288 if n.IsDir() { // can only compare and swap file 289 err = etcdErr.NewError(etcdErr.EcodeNotFile, nodePath, s.CurrentIndex) 290 return nil, err 291 } 292 293 // If both of the prevValue and prevIndex are given, we will test both of them. 294 // Command will be executed, only if both of the tests are successful. 295 if ok, which := n.Compare(prevValue, prevIndex); !ok { 296 cause := getCompareFailCause(n, which, prevValue, prevIndex) 297 err = etcdErr.NewError(etcdErr.EcodeTestFailed, cause, s.CurrentIndex) 298 return nil, err 299 } 300 301 if expireOpts.Refresh { 302 value = n.Value 303 } 304 305 // update etcd index 306 s.CurrentIndex++ 307 308 e := newEvent(CompareAndSwap, nodePath, s.CurrentIndex, n.CreatedIndex) 309 e.EtcdIndex = s.CurrentIndex 310 e.PrevNode = n.Repr(false, false, s.clock) 311 eNode := e.Node 312 313 // if test succeed, write the value 314 n.Write(value, s.CurrentIndex) 315 n.UpdateTTL(expireOpts.ExpireTime) 316 317 // copy the value for safety 318 valueCopy := value 319 eNode.Value = &valueCopy 320 eNode.Expiration, eNode.TTL = n.expirationAndTTL(s.clock) 321 322 if !expireOpts.Refresh { 323 s.WatcherHub.notify(e) 324 } else { 325 e.SetRefresh() 326 s.WatcherHub.add(e) 327 } 328 329 return e, nil 330} 331 332// Delete deletes the node at the given path. 333// If the node is a directory, recursive must be true to delete it. 334func (s *store) Delete(nodePath string, dir, recursive bool) (*Event, error) { 335 var err *etcdErr.Error 336 337 s.worldLock.Lock() 338 defer s.worldLock.Unlock() 339 340 defer func() { 341 if err == nil { 342 s.Stats.Inc(DeleteSuccess) 343 reportWriteSuccess(Delete) 344 return 345 } 346 347 s.Stats.Inc(DeleteFail) 348 reportWriteFailure(Delete) 349 }() 350 351 nodePath = path.Clean(path.Join("/", nodePath)) 352 // we do not allow the user to change "/" 353 if s.readonlySet.Contains(nodePath) { 354 return nil, etcdErr.NewError(etcdErr.EcodeRootROnly, "/", s.CurrentIndex) 355 } 356 357 // recursive implies dir 358 if recursive { 359 dir = true 360 } 361 362 n, err := s.internalGet(nodePath) 363 if err != nil { // if the node does not exist, return error 364 return nil, err 365 } 366 367 nextIndex := s.CurrentIndex + 1 368 e := newEvent(Delete, nodePath, nextIndex, n.CreatedIndex) 369 e.EtcdIndex = nextIndex 370 e.PrevNode = n.Repr(false, false, s.clock) 371 eNode := e.Node 372 373 if n.IsDir() { 374 eNode.Dir = true 375 } 376 377 callback := func(path string) { // notify function 378 // notify the watchers with deleted set true 379 s.WatcherHub.notifyWatchers(e, path, true) 380 } 381 382 err = n.Remove(dir, recursive, callback) 383 if err != nil { 384 return nil, err 385 } 386 387 // update etcd index 388 s.CurrentIndex++ 389 390 s.WatcherHub.notify(e) 391 392 return e, nil 393} 394 395func (s *store) CompareAndDelete(nodePath string, prevValue string, prevIndex uint64) (*Event, error) { 396 var err *etcdErr.Error 397 398 s.worldLock.Lock() 399 defer s.worldLock.Unlock() 400 401 defer func() { 402 if err == nil { 403 s.Stats.Inc(CompareAndDeleteSuccess) 404 reportWriteSuccess(CompareAndDelete) 405 return 406 } 407 408 s.Stats.Inc(CompareAndDeleteFail) 409 reportWriteFailure(CompareAndDelete) 410 }() 411 412 nodePath = path.Clean(path.Join("/", nodePath)) 413 414 n, err := s.internalGet(nodePath) 415 if err != nil { // if the node does not exist, return error 416 return nil, err 417 } 418 if n.IsDir() { // can only compare and delete file 419 return nil, etcdErr.NewError(etcdErr.EcodeNotFile, nodePath, s.CurrentIndex) 420 } 421 422 // If both of the prevValue and prevIndex are given, we will test both of them. 423 // Command will be executed, only if both of the tests are successful. 424 if ok, which := n.Compare(prevValue, prevIndex); !ok { 425 cause := getCompareFailCause(n, which, prevValue, prevIndex) 426 return nil, etcdErr.NewError(etcdErr.EcodeTestFailed, cause, s.CurrentIndex) 427 } 428 429 // update etcd index 430 s.CurrentIndex++ 431 432 e := newEvent(CompareAndDelete, nodePath, s.CurrentIndex, n.CreatedIndex) 433 e.EtcdIndex = s.CurrentIndex 434 e.PrevNode = n.Repr(false, false, s.clock) 435 436 callback := func(path string) { // notify function 437 // notify the watchers with deleted set true 438 s.WatcherHub.notifyWatchers(e, path, true) 439 } 440 441 err = n.Remove(false, false, callback) 442 if err != nil { 443 return nil, err 444 } 445 446 s.WatcherHub.notify(e) 447 448 return e, nil 449} 450 451func (s *store) Watch(key string, recursive, stream bool, sinceIndex uint64) (Watcher, error) { 452 s.worldLock.RLock() 453 defer s.worldLock.RUnlock() 454 455 key = path.Clean(path.Join("/", key)) 456 if sinceIndex == 0 { 457 sinceIndex = s.CurrentIndex + 1 458 } 459 // WatcherHub does not know about the current index, so we need to pass it in 460 w, err := s.WatcherHub.watch(key, recursive, stream, sinceIndex, s.CurrentIndex) 461 if err != nil { 462 return nil, err 463 } 464 465 return w, nil 466} 467 468// walk walks all the nodePath and apply the walkFunc on each directory 469func (s *store) walk(nodePath string, walkFunc func(prev *node, component string) (*node, *etcdErr.Error)) (*node, *etcdErr.Error) { 470 components := strings.Split(nodePath, "/") 471 472 curr := s.Root 473 var err *etcdErr.Error 474 475 for i := 1; i < len(components); i++ { 476 if len(components[i]) == 0 { // ignore empty string 477 return curr, nil 478 } 479 480 curr, err = walkFunc(curr, components[i]) 481 if err != nil { 482 return nil, err 483 } 484 } 485 486 return curr, nil 487} 488 489// Update updates the value/ttl of the node. 490// If the node is a file, the value and the ttl can be updated. 491// If the node is a directory, only the ttl can be updated. 492func (s *store) Update(nodePath string, newValue string, expireOpts TTLOptionSet) (*Event, error) { 493 var err *etcdErr.Error 494 495 s.worldLock.Lock() 496 defer s.worldLock.Unlock() 497 498 defer func() { 499 if err == nil { 500 s.Stats.Inc(UpdateSuccess) 501 reportWriteSuccess(Update) 502 return 503 } 504 505 s.Stats.Inc(UpdateFail) 506 reportWriteFailure(Update) 507 }() 508 509 nodePath = path.Clean(path.Join("/", nodePath)) 510 // we do not allow the user to change "/" 511 if s.readonlySet.Contains(nodePath) { 512 return nil, etcdErr.NewError(etcdErr.EcodeRootROnly, "/", s.CurrentIndex) 513 } 514 515 currIndex, nextIndex := s.CurrentIndex, s.CurrentIndex+1 516 517 n, err := s.internalGet(nodePath) 518 if err != nil { // if the node does not exist, return error 519 return nil, err 520 } 521 if n.IsDir() && len(newValue) != 0 { 522 // if the node is a directory, we cannot update value to non-empty 523 return nil, etcdErr.NewError(etcdErr.EcodeNotFile, nodePath, currIndex) 524 } 525 526 if expireOpts.Refresh { 527 newValue = n.Value 528 } 529 530 e := newEvent(Update, nodePath, nextIndex, n.CreatedIndex) 531 e.EtcdIndex = nextIndex 532 e.PrevNode = n.Repr(false, false, s.clock) 533 eNode := e.Node 534 535 n.Write(newValue, nextIndex) 536 537 if n.IsDir() { 538 eNode.Dir = true 539 } else { 540 // copy the value for safety 541 newValueCopy := newValue 542 eNode.Value = &newValueCopy 543 } 544 545 // update ttl 546 n.UpdateTTL(expireOpts.ExpireTime) 547 548 eNode.Expiration, eNode.TTL = n.expirationAndTTL(s.clock) 549 550 if !expireOpts.Refresh { 551 s.WatcherHub.notify(e) 552 } else { 553 e.SetRefresh() 554 s.WatcherHub.add(e) 555 } 556 557 s.CurrentIndex = nextIndex 558 559 return e, nil 560} 561 562func (s *store) internalCreate(nodePath string, dir bool, value string, unique, replace bool, 563 expireTime time.Time, action string) (*Event, *etcdErr.Error) { 564 565 currIndex, nextIndex := s.CurrentIndex, s.CurrentIndex+1 566 567 if unique { // append unique item under the node path 568 nodePath += "/" + fmt.Sprintf("%020s", strconv.FormatUint(nextIndex, 10)) 569 } 570 571 nodePath = path.Clean(path.Join("/", nodePath)) 572 573 // we do not allow the user to change "/" 574 if s.readonlySet.Contains(nodePath) { 575 return nil, etcdErr.NewError(etcdErr.EcodeRootROnly, "/", currIndex) 576 } 577 578 // Assume expire times that are way in the past are 579 // This can occur when the time is serialized to JS 580 if expireTime.Before(minExpireTime) { 581 expireTime = Permanent 582 } 583 584 dirName, nodeName := path.Split(nodePath) 585 586 // walk through the nodePath, create dirs and get the last directory node 587 d, err := s.walk(dirName, s.checkDir) 588 589 if err != nil { 590 s.Stats.Inc(SetFail) 591 reportWriteFailure(action) 592 err.Index = currIndex 593 return nil, err 594 } 595 596 e := newEvent(action, nodePath, nextIndex, nextIndex) 597 eNode := e.Node 598 599 n, _ := d.GetChild(nodeName) 600 601 // force will try to replace an existing file 602 if n != nil { 603 if replace { 604 if n.IsDir() { 605 return nil, etcdErr.NewError(etcdErr.EcodeNotFile, nodePath, currIndex) 606 } 607 e.PrevNode = n.Repr(false, false, s.clock) 608 609 n.Remove(false, false, nil) 610 } else { 611 return nil, etcdErr.NewError(etcdErr.EcodeNodeExist, nodePath, currIndex) 612 } 613 } 614 615 if !dir { // create file 616 // copy the value for safety 617 valueCopy := value 618 eNode.Value = &valueCopy 619 620 n = newKV(s, nodePath, value, nextIndex, d, expireTime) 621 622 } else { // create directory 623 eNode.Dir = true 624 625 n = newDir(s, nodePath, nextIndex, d, expireTime) 626 } 627 628 // we are sure d is a directory and does not have the children with name n.Name 629 d.Add(n) 630 631 // node with TTL 632 if !n.IsPermanent() { 633 s.ttlKeyHeap.push(n) 634 635 eNode.Expiration, eNode.TTL = n.expirationAndTTL(s.clock) 636 } 637 638 s.CurrentIndex = nextIndex 639 640 return e, nil 641} 642 643// InternalGet gets the node of the given nodePath. 644func (s *store) internalGet(nodePath string) (*node, *etcdErr.Error) { 645 nodePath = path.Clean(path.Join("/", nodePath)) 646 647 walkFunc := func(parent *node, name string) (*node, *etcdErr.Error) { 648 649 if !parent.IsDir() { 650 err := etcdErr.NewError(etcdErr.EcodeNotDir, parent.Path, s.CurrentIndex) 651 return nil, err 652 } 653 654 child, ok := parent.Children[name] 655 if ok { 656 return child, nil 657 } 658 659 return nil, etcdErr.NewError(etcdErr.EcodeKeyNotFound, path.Join(parent.Path, name), s.CurrentIndex) 660 } 661 662 f, err := s.walk(nodePath, walkFunc) 663 664 if err != nil { 665 return nil, err 666 } 667 return f, nil 668} 669 670// DeleteExpiredKeys will delete all expired keys 671func (s *store) DeleteExpiredKeys(cutoff time.Time) { 672 s.worldLock.Lock() 673 defer s.worldLock.Unlock() 674 675 for { 676 node := s.ttlKeyHeap.top() 677 if node == nil || node.ExpireTime.After(cutoff) { 678 break 679 } 680 681 s.CurrentIndex++ 682 e := newEvent(Expire, node.Path, s.CurrentIndex, node.CreatedIndex) 683 e.EtcdIndex = s.CurrentIndex 684 e.PrevNode = node.Repr(false, false, s.clock) 685 686 callback := func(path string) { // notify function 687 // notify the watchers with deleted set true 688 s.WatcherHub.notifyWatchers(e, path, true) 689 } 690 691 s.ttlKeyHeap.pop() 692 node.Remove(true, true, callback) 693 694 reportExpiredKey() 695 s.Stats.Inc(ExpireCount) 696 697 s.WatcherHub.notify(e) 698 } 699 700} 701 702// checkDir will check whether the component is a directory under parent node. 703// If it is a directory, this function will return the pointer to that node. 704// If it does not exist, this function will create a new directory and return the pointer to that node. 705// If it is a file, this function will return error. 706func (s *store) checkDir(parent *node, dirName string) (*node, *etcdErr.Error) { 707 node, ok := parent.Children[dirName] 708 709 if ok { 710 if node.IsDir() { 711 return node, nil 712 } 713 714 return nil, etcdErr.NewError(etcdErr.EcodeNotDir, node.Path, s.CurrentIndex) 715 } 716 717 n := newDir(s, path.Join(parent.Path, dirName), s.CurrentIndex+1, parent, Permanent) 718 719 parent.Children[dirName] = n 720 721 return n, nil 722} 723 724// Save saves the static state of the store system. 725// It will not be able to save the state of watchers. 726// It will not save the parent field of the node. Or there will 727// be cyclic dependencies issue for the json package. 728func (s *store) Save() ([]byte, error) { 729 b, err := json.Marshal(s.Clone()) 730 if err != nil { 731 return nil, err 732 } 733 734 return b, nil 735} 736 737func (s *store) SaveNoCopy() ([]byte, error) { 738 b, err := json.Marshal(s) 739 if err != nil { 740 return nil, err 741 } 742 743 return b, nil 744} 745 746func (s *store) Clone() Store { 747 s.worldLock.Lock() 748 749 clonedStore := newStore() 750 clonedStore.CurrentIndex = s.CurrentIndex 751 clonedStore.Root = s.Root.Clone() 752 clonedStore.WatcherHub = s.WatcherHub.clone() 753 clonedStore.Stats = s.Stats.clone() 754 clonedStore.CurrentVersion = s.CurrentVersion 755 756 s.worldLock.Unlock() 757 return clonedStore 758} 759 760// Recovery recovers the store system from a static state 761// It needs to recover the parent field of the nodes. 762// It needs to delete the expired nodes since the saved time and also 763// needs to create monitoring go routines. 764func (s *store) Recovery(state []byte) error { 765 s.worldLock.Lock() 766 defer s.worldLock.Unlock() 767 err := json.Unmarshal(state, s) 768 769 if err != nil { 770 return err 771 } 772 773 s.ttlKeyHeap = newTtlKeyHeap() 774 775 s.Root.recoverAndclean() 776 return nil 777} 778 779func (s *store) JsonStats() []byte { 780 s.Stats.Watchers = uint64(s.WatcherHub.count) 781 return s.Stats.toJson() 782} 783 784func (s *store) HasTTLKeys() bool { 785 s.worldLock.RLock() 786 defer s.worldLock.RUnlock() 787 return s.ttlKeyHeap.Len() != 0 788} 789