147 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			147 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package gocache
 | |
| 
 | |
| import (
 | |
| 	"log"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// JanitorShiftTarget is the target number of expired keys to find during passive clean up duty
 | |
| 	// before pausing the passive expired keys eviction process
 | |
| 	JanitorShiftTarget = 25
 | |
| 
 | |
| 	// JanitorMaxIterationsPerShift is the maximum number of nodes to traverse before pausing
 | |
| 	//
 | |
| 	// This is to prevent the janitor from traversing the entire cache, which could take a long time
 | |
| 	// to complete depending on the size of the cache.
 | |
| 	//
 | |
| 	// By limiting it to a small number, we are effectively reducing the impact of passive eviction.
 | |
| 	JanitorMaxIterationsPerShift = 1000
 | |
| 
 | |
| 	// JanitorMinShiftBackOff is the minimum interval between each iteration of steps
 | |
| 	// defined by JanitorMaxIterationsPerShift
 | |
| 	JanitorMinShiftBackOff = 50 * time.Millisecond
 | |
| 
 | |
| 	// JanitorMaxShiftBackOff is the maximum interval between each iteration of steps
 | |
| 	// defined by JanitorMaxIterationsPerShift
 | |
| 	JanitorMaxShiftBackOff = 500 * time.Millisecond
 | |
| )
 | |
| 
 | |
| // StartJanitor starts the janitor on a different goroutine
 | |
| // The janitor's job is to delete expired keys in the background, in other words, it takes care of passive eviction.
 | |
| // It can be stopped by calling Cache.StopJanitor.
 | |
| // If you do not start the janitor, expired keys will only be deleted when they are accessed through Get, GetByKeys, or
 | |
| // GetAll.
 | |
| func (cache *Cache) StartJanitor() error {
 | |
| 	if cache.stopJanitor != nil {
 | |
| 		return ErrJanitorAlreadyRunning
 | |
| 	}
 | |
| 	cache.stopJanitor = make(chan bool)
 | |
| 	go func() {
 | |
| 		// rather than starting from the tail on every run, we can try to start from the last traversed entry
 | |
| 		var lastTraversedNode *Entry
 | |
| 		totalNumberOfExpiredKeysInPreviousRunFromTailToHead := 0
 | |
| 		backOff := JanitorMinShiftBackOff
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-time.After(backOff):
 | |
| 				// Passive clean up duty
 | |
| 				cache.mutex.Lock()
 | |
| 				if cache.tail != nil {
 | |
| 					start := time.Now()
 | |
| 					steps := 0
 | |
| 					expiredEntriesFound := 0
 | |
| 					current := cache.tail
 | |
| 					if lastTraversedNode != nil {
 | |
| 						// Make sure the lastTraversedNode is still in the cache, otherwise we might be traversing nodes that were already deleted.
 | |
| 						// Furthermore, we need to make sure that the entry from the cache has the same pointer as the lastTraversedNode
 | |
| 						// to verify that there isn't just a new cache entry with the same key (i.e. in case lastTraversedNode got evicted)
 | |
| 						if entryFromCache, isInCache := cache.get(lastTraversedNode.Key); isInCache && entryFromCache == lastTraversedNode {
 | |
| 							current = lastTraversedNode
 | |
| 						}
 | |
| 					}
 | |
| 					if current == cache.tail {
 | |
| 						if Debug {
 | |
| 							log.Printf("There are currently %d entries in the cache. The last walk resulted in finding %d expired keys", len(cache.entries), totalNumberOfExpiredKeysInPreviousRunFromTailToHead)
 | |
| 						}
 | |
| 						totalNumberOfExpiredKeysInPreviousRunFromTailToHead = 0
 | |
| 					}
 | |
| 					for current != nil {
 | |
| 						// since we're walking from the tail to the head, we get the previous reference
 | |
| 						var previous *Entry
 | |
| 						steps++
 | |
| 						if current.Expired() {
 | |
| 							expiredEntriesFound++
 | |
| 							// Because delete will remove the previous reference from the entry, we need to store the
 | |
| 							// previous reference before we delete it
 | |
| 							previous = current.previous
 | |
| 							cache.delete(current.Key)
 | |
| 							cache.stats.ExpiredKeys++
 | |
| 						}
 | |
| 						if current == cache.head {
 | |
| 							lastTraversedNode = nil
 | |
| 							break
 | |
| 						}
 | |
| 						// Travel to the current node's previous node only if no specific previous node has been specified
 | |
| 						if previous != nil {
 | |
| 							current = previous
 | |
| 						} else {
 | |
| 							current = current.previous
 | |
| 						}
 | |
| 						lastTraversedNode = current
 | |
| 						if steps == JanitorMaxIterationsPerShift || expiredEntriesFound >= JanitorShiftTarget {
 | |
| 							if expiredEntriesFound > 0 {
 | |
| 								backOff = JanitorMinShiftBackOff
 | |
| 							} else {
 | |
| 								if backOff*2 <= JanitorMaxShiftBackOff {
 | |
| 									backOff *= 2
 | |
| 								} else {
 | |
| 									backOff = JanitorMaxShiftBackOff
 | |
| 								}
 | |
| 							}
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 					if Debug {
 | |
| 						log.Printf("traversed %d nodes and found %d expired entries in %s before stopping\n", steps, expiredEntriesFound, time.Since(start))
 | |
| 					}
 | |
| 					totalNumberOfExpiredKeysInPreviousRunFromTailToHead += expiredEntriesFound
 | |
| 				} else {
 | |
| 					if backOff*2 < JanitorMaxShiftBackOff {
 | |
| 						backOff *= 2
 | |
| 					} else {
 | |
| 						backOff = JanitorMaxShiftBackOff
 | |
| 					}
 | |
| 				}
 | |
| 				cache.mutex.Unlock()
 | |
| 			case <-cache.stopJanitor:
 | |
| 				cache.stopJanitor <- true
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 	//if Debug {
 | |
| 	//	go func() {
 | |
| 	//		var m runtime.MemStats
 | |
| 	//		for {
 | |
| 	//			runtime.ReadMemStats(&m)
 | |
| 	//			log.Printf("Alloc=%vMB; HeapReleased=%vMB; Sys=%vMB; HeapInUse=%vMB; HeapObjects=%v; HeapObjectsFreed=%v; GC=%v; cache.memoryUsage=%vMB; cacheSize=%d\n", m.Alloc/1024/1024, m.HeapReleased/1024/1024, m.Sys/1024/1024, m.HeapInuse/1024/1024, m.HeapObjects, m.Frees, m.NumGC, cache.memoryUsage/1024/1024, cache.Count())
 | |
| 	//			time.Sleep(3 * time.Second)
 | |
| 	//		}
 | |
| 	//	}()
 | |
| 	//}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // StopJanitor stops the janitor
 | |
| func (cache *Cache) StopJanitor() {
 | |
| 	if cache.stopJanitor != nil {
 | |
| 		// Tell the janitor to stop, and then wait for the janitor to reply on the same channel that it's stopping
 | |
| 		// This may seem a bit odd, but this allows us to avoid a data race condition when trying to set
 | |
| 		// cache.stopJanitor to nil
 | |
| 		cache.stopJanitor <- true
 | |
| 		<-cache.stopJanitor
 | |
| 		cache.stopJanitor = nil
 | |
| 	}
 | |
| }
 |