lrubank_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. package stringutil
  2. import (
  3. "fmt"
  4. "sync"
  5. "testing"
  6. "time"
  7. )
  8. func TestBasicLruEvict(t *testing.T) {
  9. lruBank := NewLruStringBank(3, 2*time.Second).(*lruStringBank)
  10. defer lruBank.Stop()
  11. lruBank.LoadOrStore("foo", "foo")
  12. time.Sleep(500 * time.Millisecond)
  13. lruBank.LoadOrStore("bar", "bar")
  14. time.Sleep(500 * time.Millisecond)
  15. lruBank.LoadOrStore("whaz", "whaz")
  16. time.Sleep(500 * time.Millisecond)
  17. // access foo, updating recency
  18. lruBank.LoadOrStore("foo", "foo")
  19. // should push bar out after eviction runs
  20. lruBank.LoadOrStore("test", "test")
  21. time.Sleep(time.Second)
  22. lruBank.lock.Lock()
  23. for _, v := range lruBank.m {
  24. t.Logf("Value: %s\n", v.value)
  25. if v.value == "bar" {
  26. t.Errorf("The 'bar' entry should've been replaced by 'test'")
  27. }
  28. }
  29. lruBank.lock.Unlock()
  30. }
  31. // ---------------------------------------------------------------------------
  32. // LoadOrStore
  33. // ---------------------------------------------------------------------------
  34. // A stored value must be retrievable and LoadOrStore must signal the hit/miss
  35. // correctly via the boolean return.
  36. func TestLoadOrStore_MissAndHit(t *testing.T) {
  37. bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
  38. defer bank.Stop()
  39. v, loaded := bank.LoadOrStore("hello", "hello")
  40. if loaded {
  41. t.Errorf("first LoadOrStore: expected loaded=false, got true")
  42. }
  43. if v != "hello" {
  44. t.Errorf("first LoadOrStore: expected value %q, got %q", "hello", v)
  45. }
  46. v, loaded = bank.LoadOrStore("hello", "world")
  47. if !loaded {
  48. t.Errorf("second LoadOrStore: expected loaded=true, got false")
  49. }
  50. // The original value must be returned on a hit, not the new candidate.
  51. if v != "hello" {
  52. t.Errorf("second LoadOrStore: expected cached value %q, got %q", "hello", v)
  53. }
  54. }
  55. // Hitting an existing entry must update its recency so it is not evicted ahead
  56. // of entries that were never touched again.
  57. func TestLoadOrStore_HitUpdateRecency(t *testing.T) {
  58. bank := NewLruStringBank(2, 500*time.Millisecond).(*lruStringBank)
  59. defer bank.Stop()
  60. bank.LoadOrStore("old", "old")
  61. time.Sleep(100 * time.Millisecond)
  62. bank.LoadOrStore("keep", "keep")
  63. time.Sleep(100 * time.Millisecond)
  64. // Re-touch "old" so it becomes the most-recently-used.
  65. bank.LoadOrStore("old", "old")
  66. time.Sleep(100 * time.Millisecond)
  67. // Adding a third entry exceeds capacity; "keep" should be the oldest now.
  68. bank.LoadOrStore("new", "new")
  69. // Wait for the eviction goroutine.
  70. time.Sleep(600 * time.Millisecond)
  71. bank.lock.Lock()
  72. defer bank.lock.Unlock()
  73. if _, ok := bank.m["keep"]; ok {
  74. t.Error("expected 'keep' to be evicted but it is still present")
  75. }
  76. if _, ok := bank.m["old"]; !ok {
  77. t.Error("expected 'old' to survive eviction after its recency was refreshed")
  78. }
  79. }
  80. // ---------------------------------------------------------------------------
  81. // LoadOrStoreFunc
  82. // ---------------------------------------------------------------------------
  83. // The factory function must only be called on a cache miss, not on a hit.
  84. func TestLoadOrStoreFunc_FactoryCalledOnMissOnly(t *testing.T) {
  85. bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
  86. defer bank.Stop()
  87. calls := 0
  88. factory := func() string {
  89. calls++
  90. return "k"
  91. }
  92. bank.LoadOrStoreFunc("k", factory)
  93. bank.LoadOrStoreFunc("k", factory)
  94. if calls != 1 {
  95. t.Errorf("factory should be called exactly once, got %d calls", calls)
  96. }
  97. }
  98. // ---------------------------------------------------------------------------
  99. // Capacity / eviction
  100. // ---------------------------------------------------------------------------
  101. // If the bank never exceeds capacity, nothing should be evicted.
  102. func TestEviction_BelowCapacityNoEviction(t *testing.T) {
  103. const capacity = 5
  104. bank := NewLruStringBank(capacity, 200*time.Millisecond).(*lruStringBank)
  105. defer bank.Stop()
  106. for i := 0; i < capacity; i++ {
  107. bank.LoadOrStore(fmt.Sprintf("v%d", i), fmt.Sprintf("v%d", i))
  108. }
  109. // Wait several eviction cycles.
  110. time.Sleep(600 * time.Millisecond)
  111. bank.lock.Lock()
  112. defer bank.lock.Unlock()
  113. if got := len(bank.m); got != capacity {
  114. t.Errorf("expected %d entries, got %d", capacity, got)
  115. }
  116. }
  117. // After eviction the map must be trimmed down to exactly capacity.
  118. func TestEviction_ExceedCapacityTrimsToCapacity(t *testing.T) {
  119. const capacity = 3
  120. bank := NewLruStringBank(capacity, 350*time.Millisecond).(*lruStringBank)
  121. defer bank.Stop()
  122. for i := 0; i < capacity+3; i++ {
  123. bank.LoadOrStore(fmt.Sprintf("v%d", i), fmt.Sprintf("v%d", i))
  124. time.Sleep(20 * time.Millisecond) // ensure distinct timestamps
  125. }
  126. // Wait for eviction.
  127. time.Sleep(500 * time.Millisecond)
  128. bank.lock.Lock()
  129. defer bank.lock.Unlock()
  130. if got := len(bank.m); got > capacity {
  131. t.Errorf("expected at most %d entries after eviction, got %d", capacity, got)
  132. }
  133. }
  134. // The most-recently-used entries must survive eviction.
  135. func TestEviction_MRUSurvives(t *testing.T) {
  136. const capacity = 2
  137. bank := NewLruStringBank(capacity, 300*time.Millisecond).(*lruStringBank)
  138. defer bank.Stop()
  139. bank.LoadOrStore("evict1", "evict1")
  140. time.Sleep(50 * time.Millisecond)
  141. bank.LoadOrStore("evict2", "evict2")
  142. time.Sleep(50 * time.Millisecond)
  143. // These two are the most recent; they must survive.
  144. bank.LoadOrStore("keep1", "keep1")
  145. time.Sleep(50 * time.Millisecond)
  146. bank.LoadOrStore("keep2", "keep2")
  147. time.Sleep(500 * time.Millisecond)
  148. bank.lock.Lock()
  149. defer bank.lock.Unlock()
  150. for _, must := range []string{"keep1", "keep2"} {
  151. if _, ok := bank.m[must]; !ok {
  152. t.Errorf("expected %q to survive eviction", must)
  153. }
  154. }
  155. }
  156. // ---------------------------------------------------------------------------
  157. // Clear
  158. // ---------------------------------------------------------------------------
  159. func TestClear_EmptiesMap(t *testing.T) {
  160. bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
  161. defer bank.Stop()
  162. for i := 0; i < 5; i++ {
  163. bank.LoadOrStore(fmt.Sprintf("v%d", i), fmt.Sprintf("v%d", i))
  164. }
  165. bank.Clear()
  166. bank.lock.Lock()
  167. defer bank.lock.Unlock()
  168. if len(bank.m) != 0 {
  169. t.Errorf("expected empty map after Clear, got %d entries", len(bank.m))
  170. }
  171. }
  172. // After a Clear, previously stored keys must not be found.
  173. func TestClear_PreviousKeysGone(t *testing.T) {
  174. bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
  175. defer bank.Stop()
  176. bank.LoadOrStore("hello", "world")
  177. bank.Clear()
  178. _, loaded := bank.LoadOrStore("hello", "new")
  179. if loaded {
  180. t.Error("expected key to be absent after Clear, but it was found")
  181. }
  182. }
  183. // ---------------------------------------------------------------------------
  184. // nOldest helper
  185. // ---------------------------------------------------------------------------
  186. func TestNOldest_ReturnsCorrectCount(t *testing.T) {
  187. now := time.Now()
  188. entries := []*lruEntry{
  189. {value: "a", used: now.Add(-4 * time.Second).UnixMilli()},
  190. {value: "b", used: now.Add(-3 * time.Second).UnixMilli()},
  191. {value: "c", used: now.Add(-2 * time.Second).UnixMilli()},
  192. {value: "d", used: now.Add(-1 * time.Second).UnixMilli()},
  193. {value: "e", used: now.UnixMilli()},
  194. }
  195. oldest := nOldest(entries, 2)
  196. if len(oldest) != 2 {
  197. t.Fatalf("expected 2 oldest entries, got %d", len(oldest))
  198. }
  199. values := map[string]bool{}
  200. for _, e := range oldest {
  201. values[e.value] = true
  202. }
  203. for _, must := range []string{"a", "b"} {
  204. if !values[must] {
  205. t.Errorf("expected %q in oldest set, got %v", must, values)
  206. }
  207. }
  208. }
  209. func TestNOldest_NGreaterThanLen(t *testing.T) {
  210. now := time.Now()
  211. entries := []*lruEntry{
  212. {value: "x", used: now.UnixMilli()},
  213. {value: "y", used: now.Add(-time.Second).UnixMilli()},
  214. }
  215. result := nOldest(entries, 10)
  216. if len(result) != 2 {
  217. t.Errorf("expected all %d entries when n >= len, got %d", 2, len(result))
  218. }
  219. }
  220. func TestNOldest_NEqualsLen(t *testing.T) {
  221. now := time.Now()
  222. entries := []*lruEntry{
  223. {value: "x", used: now.UnixMilli()},
  224. {value: "y", used: now.Add(-time.Second).UnixMilli()},
  225. }
  226. result := nOldest(entries, 2)
  227. if len(result) != 2 {
  228. t.Errorf("expected 2 entries when n == len, got %d", len(result))
  229. }
  230. }
  231. func TestNOldest_NIsZero(t *testing.T) {
  232. now := time.Now()
  233. entries := []*lruEntry{
  234. {value: "x", used: now.UnixMilli()},
  235. }
  236. result := nOldest(entries, 0)
  237. if len(result) != 0 {
  238. t.Errorf("expected 0 entries when n=0, got %d", len(result))
  239. }
  240. }
  241. // ---------------------------------------------------------------------------
  242. // Concurrency
  243. // ---------------------------------------------------------------------------
  244. // Concurrent LoadOrStore calls must not race or panic.
  245. func TestConcurrentLoadOrStore(t *testing.T) {
  246. bank := NewLruStringBank(50, 100*time.Millisecond).(*lruStringBank)
  247. defer bank.Stop()
  248. const goroutines = 20
  249. const opsEach = 100
  250. var wg sync.WaitGroup
  251. for i := 0; i < goroutines; i++ {
  252. g := i
  253. wg.Go(func() {
  254. for i := 0; i < opsEach; i++ {
  255. key := fmt.Sprintf("k%d", (g*opsEach+i)%30)
  256. bank.LoadOrStore(key, key)
  257. }
  258. })
  259. }
  260. waiter := func() chan struct{} {
  261. st := make(chan struct{})
  262. go func() {
  263. wg.Wait()
  264. close(st)
  265. }()
  266. return st
  267. }
  268. select {
  269. case <-waiter():
  270. t.Logf("Completed Successfully\n")
  271. case <-time.After(10 * time.Second):
  272. t.Logf("Timed out\n")
  273. }
  274. }
  275. // Concurrent calls interleaved with eviction cycles must not deadlock or race.
  276. func TestConcurrentLoadOrStoreWithEviction(t *testing.T) {
  277. bank := NewLruStringBank(5, 50*time.Millisecond).(*lruStringBank)
  278. defer bank.Stop()
  279. const goroutines = 10
  280. const duration = 300 * time.Millisecond
  281. var wg sync.WaitGroup
  282. for i := 0; i < goroutines; i++ {
  283. g := i
  284. stop := time.After(duration)
  285. wg.Go(func() {
  286. for {
  287. select {
  288. case <-stop:
  289. return
  290. default:
  291. key := fmt.Sprintf("g%d", g)
  292. bank.LoadOrStore(key, key)
  293. }
  294. }
  295. })
  296. }
  297. waiter := func() chan struct{} {
  298. st := make(chan struct{})
  299. go func() {
  300. wg.Wait()
  301. close(st)
  302. }()
  303. return st
  304. }
  305. select {
  306. case <-waiter():
  307. t.Logf("Completed Successfully\n")
  308. case <-time.After(10 * time.Second):
  309. t.Logf("Timed out\n")
  310. }
  311. }
  312. // Concurrent Clear calls alongside reads/writes must not panic.
  313. func TestConcurrentClear(t *testing.T) {
  314. bank := NewLruStringBank(10, time.Minute).(*lruStringBank)
  315. defer bank.Stop()
  316. var wg sync.WaitGroup
  317. for i := 0; i < 5; i++ {
  318. wg.Add(1)
  319. go func(i int) {
  320. defer wg.Done()
  321. bank.LoadOrStore(fmt.Sprintf("k%d", i), "v")
  322. }(i)
  323. }
  324. for i := 0; i < 3; i++ {
  325. wg.Add(1)
  326. go func() {
  327. defer wg.Done()
  328. bank.Clear()
  329. }()
  330. }
  331. wg.Wait()
  332. }