lrubank_test.go 11 KB

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