├── go.sum ├── go.mod ├── .gitignore ├── LICENSE ├── example_test.go ├── example_expire_map_test.go ├── README.md ├── expire_map_benchmark_test.go ├── expire_map_test.go └── expire_map.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nursik/go-expire-map 2 | 3 | go 1.20 4 | 5 | retract v1.1.0 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # IDEs 15 | .idea/ 16 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nursultan Zarlyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package expiremap_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | expiremap "github.com/nursik/go-expire-map" 8 | ) 9 | 10 | func ExampleExpireMap_Notify() { 11 | expireMap := expiremap.New() 12 | defer expireMap.Close() 13 | 14 | c := make(chan expiremap.Event, 10) 15 | expireMap.Notify(c, expiremap.AllEvents) 16 | var event expiremap.Event 17 | // Set 18 | expireMap.Set("key1", "value1", time.Second) 19 | event = <-c 20 | fmt.Println(event.Key, event.Value, event.Type == expiremap.Set) 21 | 22 | // Update 23 | expireMap.Set("key1", "value1", time.Second) 24 | event = <-c 25 | fmt.Println(event.Key, event.Value, event.Type == expiremap.Update) 26 | 27 | expireMap.Set("key2", "value2", time.Hour) 28 | <-c 29 | expireMap.Delete("key2") 30 | event = <-c 31 | fmt.Println(event.Key, event.Value, event.Type == expiremap.Delete) 32 | 33 | // Causes Expire event to be fired 34 | time.Sleep(time.Second) 35 | 36 | event = <-c 37 | fmt.Println(event.Key, event.Value, event.Type == expiremap.Expire) 38 | 39 | // Output: 40 | // key1 value1 true 41 | // key1 value1 true 42 | // key2 value2 true 43 | // key1 value1 true 44 | } 45 | -------------------------------------------------------------------------------- /example_expire_map_test.go: -------------------------------------------------------------------------------- 1 | package expiremap_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | expiremap "github.com/nursik/go-expire-map" 8 | ) 9 | 10 | func Example() { 11 | expireMap := expiremap.New() 12 | // You must call Close(), when you do not need the map anymore 13 | defer expireMap.Close() 14 | 15 | // GMT Wednesday, 1 January 2025, 0:00:00 16 | far := time.Unix(1735689600, 0) 17 | 18 | ttl := time.Until(far) 19 | 20 | // Insert 21 | expireMap.Set(1, 1, ttl) 22 | 23 | // Get value 24 | v, ok := expireMap.Get(1) 25 | fmt.Println(v, ok) 26 | // Output 1 true 27 | 28 | // Get TTL 29 | v = expireMap.GetTTL(1) 30 | // The output is equal to ~ ttl 31 | fmt.Println(v) 32 | 33 | // Update TTL 34 | v, ok = expireMap.SetTTL(1, time.Second) 35 | fmt.Println(v, ok) 36 | // Output 1 true 37 | 38 | time.Sleep(time.Second + time.Millisecond) 39 | 40 | // Because key is already expired, it returns nil, false 41 | v, ok = expireMap.SetTTL(1, ttl) 42 | fmt.Println(v, ok) 43 | // Output nil false 44 | 45 | // Because key is already expired, it returns nil, false 46 | v, ok = expireMap.Get(1) 47 | fmt.Println(v, ok) 48 | // Output nil false 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go expire map 2 | [![GoDoc](https://godoc.org/github.com/nursik/go-expire-map?status.svg)](https://godoc.org/github.com/nursik/go-expire-map) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/nursik/go-expire-map)](https://goreportcard.com/report/github.com/nursik/go-expire-map) 4 | 5 | # Disclaimer!! 6 | This package is considered deprecated as there are more API rich and faster solutions like [ccache](https://github.com/karlseguin/ccache) or [ttlcache](https://github.com/jellydator/ttlcache/blob/v3/cache.go). Also, library uses UnixNano instead of time.Time, which makes it to fail during system time (wall clock) adjustments. 7 | ## Quick start 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "github.com/nursik/go-expire-map" 15 | "time" 16 | ) 17 | func main() { 18 | expireMap := expiremap.New() 19 | // You must call Close(), when you do not need the map anymore 20 | defer expireMap.Close() 21 | 22 | // GMT Wednesday, 1 January 2025, 0:00:00 23 | far := time.Unix(1735689600, 0) 24 | 25 | ttl := far.Sub(time.Now()) 26 | 27 | // Insert 28 | expireMap.Set(1, 1, ttl) 29 | 30 | // Get value 31 | v, ok := expireMap.Get(1) 32 | fmt.Println(v, ok) 33 | // Output 1 true 34 | 35 | // Get TTL 36 | v = expireMap.GetTTL(1) 37 | // The output is equal to ~ ttl 38 | fmt.Println(v) 39 | 40 | // Update TTL 41 | v, ok = expireMap.SetTTL(1, time.Second) 42 | fmt.Println(v, ok) 43 | // Output 1 true 44 | 45 | time.Sleep(time.Second + time.Millisecond) 46 | 47 | // Because key is already expired, it returns nil, false 48 | v, ok = expireMap.SetTTL(1, ttl) 49 | fmt.Println(v, ok) 50 | // Output nil false 51 | 52 | // Because key is already expired, it returns nil, false 53 | v, ok = expireMap.Get(1) 54 | fmt.Println(v, ok) 55 | // Output nil false 56 | } 57 | 58 | ``` 59 | 60 | ## Why/when 61 | * You need thread safe map with `comparable` keys and `interface` values 62 | * You have a lot of inserts with the same TTL. If they all expire at the same time you don't want your app freeze 63 | * You need both active and passive expiration 64 | * You need notifications about inserts, update, deletes and expirations 65 | -------------------------------------------------------------------------------- /expire_map_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package expiremap 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "runtime" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func BenchmarkExpireMap_Get(b *testing.B) { 14 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 15 | 16 | ttl := time.Hour 17 | 18 | for _, pN := range presetN { 19 | pNN := pN 20 | b.Run(fmt.Sprintf("N_%d", pNN), func(b *testing.B) { 21 | b.StopTimer() 22 | expireMap := New() 23 | for j := 0; j < pNN; j++ { 24 | expireMap.Set(j, j, ttl) 25 | } 26 | b.StartTimer() 27 | for i := 0; i < b.N; i++ { 28 | _, _ = expireMap.Get(i % pNN) 29 | } 30 | b.StopTimer() 31 | expireMap.Close() 32 | expireMap = New() 33 | runtime.GC() 34 | b.StartTimer() 35 | expireMap.Close() 36 | }) 37 | } 38 | } 39 | 40 | func BenchmarkExpireMap_Set(b *testing.B) { 41 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 42 | 43 | ttl := time.Hour 44 | 45 | for _, pN := range presetN { 46 | pNN := pN 47 | b.Run(fmt.Sprintf("N_%d", pNN), func(b *testing.B) { 48 | expireMap := New() 49 | for i := 0; i < b.N; i++ { 50 | for j := 0; j < pNN; j++ { 51 | expireMap.Set(j, j, ttl) 52 | } 53 | b.StopTimer() 54 | expireMap.Close() 55 | expireMap = New() 56 | runtime.GC() 57 | b.StartTimer() 58 | } 59 | expireMap.Close() 60 | }) 61 | } 62 | } 63 | 64 | func BenchmarkExpireMap_Set2(b *testing.B) { 65 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 66 | 67 | ttl := time.Hour 68 | 69 | for _, pN := range presetN { 70 | pNN := pN 71 | b.Run(fmt.Sprintf("N_%d", pNN), func(b *testing.B) { 72 | expireMap := New() 73 | for j := 0; j < pNN; j++ { 74 | expireMap.Set(j, j, ttl) 75 | } 76 | b.ResetTimer() 77 | for i := 0; i < b.N; i++ { 78 | for j := 0; j < pNN; j++ { 79 | expireMap.Set(j, j, ttl) 80 | } 81 | } 82 | expireMap.Close() 83 | }) 84 | } 85 | } 86 | 87 | func BenchmarkExpireMap_Delete(b *testing.B) { 88 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 89 | 90 | ttl := time.Hour 91 | 92 | for _, pN := range presetN { 93 | pNN := pN 94 | b.Run(fmt.Sprintf("N_%d", pNN), func(b *testing.B) { 95 | for i := 0; i < b.N; i++ { 96 | b.StopTimer() 97 | runtime.GC() 98 | expireMap := New() 99 | for j := 0; j < pNN; j++ { 100 | expireMap.Set(j, j, ttl) 101 | } 102 | b.StartTimer() 103 | for j := 0; j < pNN; j++ { 104 | expireMap.Delete(j) 105 | } 106 | expireMap.Close() 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func BenchmarkExpireMap_SetTTL(b *testing.B) { 113 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 114 | 115 | ttl := time.Hour 116 | 117 | for _, pN := range presetN { 118 | pNN := pN 119 | b.Run(fmt.Sprintf("N_%d", pNN), func(b *testing.B) { 120 | expireMap := New() 121 | for j := 0; j < pNN; j++ { 122 | expireMap.Set(j, j, ttl) 123 | } 124 | b.ResetTimer() 125 | for i := 0; i < b.N; i++ { 126 | for j := 0; j < pNN; j++ { 127 | expireMap.SetTTL(j, ttl) 128 | } 129 | } 130 | expireMap.Close() 131 | }) 132 | } 133 | } 134 | 135 | func BenchmarkExpireMap_SetTTL2(b *testing.B) { 136 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 137 | 138 | ttl := time.Hour 139 | 140 | for _, pN := range presetN { 141 | pNN := pN 142 | b.Run(fmt.Sprintf("N_%d", pNN), func(b *testing.B) { 143 | for i := 0; i < b.N; i++ { 144 | b.StopTimer() 145 | runtime.GC() 146 | expireMap := New() 147 | for j := 0; j < pNN; j++ { 148 | expireMap.Set(j, j, ttl) 149 | } 150 | b.StartTimer() 151 | for j := 0; j < pNN; j++ { 152 | expireMap.SetTTL(j, time.Nanosecond) 153 | } 154 | expireMap.Close() 155 | } 156 | }) 157 | } 158 | } 159 | 160 | func BenchmarkRWParallel(b *testing.B) { 161 | const steps = 100000 162 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 163 | rcnt := []int{1, 2, 4} 164 | wcnt := []int{1, 2} 165 | 166 | for _, pN := range presetN { 167 | for _, rN := range rcnt { 168 | for _, wN := range wcnt { 169 | pNN := pN 170 | rNN := rN 171 | wNN := wN 172 | b.Run(fmt.Sprintf("N_%d_Rn_%d_Wn_%d_Steps_%d", pNN, rNN, wNN, steps), func(b *testing.B) { 173 | expireMap := New() 174 | for i := 0; i < b.N; i++ { 175 | var wg sync.WaitGroup 176 | wg.Add(rNN + wNN) 177 | for j := 0; j < rNN; j++ { 178 | go reader(expireMap, &wg, steps, pNN) 179 | } 180 | for j := 0; j < wNN; j++ { 181 | go writer(expireMap, &wg, steps, pNN, time.Second) 182 | } 183 | wg.Wait() 184 | } 185 | expireMap.Close() 186 | }) 187 | } 188 | } 189 | } 190 | } 191 | 192 | func BenchmarkRWParallel2(b *testing.B) { 193 | const steps = 100000 194 | presetN := []int{1000, 10000, 100000, 1000000, 10000000} 195 | rcnt := []int{1, 2, 4} 196 | wcnt := []int{1, 2} 197 | 198 | for _, pN := range presetN { 199 | for _, rN := range rcnt { 200 | for _, wN := range wcnt { 201 | pNN := pN 202 | rNN := rN 203 | wNN := wN 204 | b.Run(fmt.Sprintf("N_%d_Rn_%d_Wn_%d_Steps_%d", pNN, rNN, wNN, steps), func(b *testing.B) { 205 | expireMap := New() 206 | for i := 0; i < b.N; i++ { 207 | var wg sync.WaitGroup 208 | wg.Add(rNN + wNN) 209 | for j := 0; j < rNN; j++ { 210 | go reader(expireMap, &wg, steps, pNN) 211 | } 212 | for j := 0; j < wNN; j++ { 213 | go writer(expireMap, &wg, steps, pNN, time.Millisecond*10) 214 | } 215 | wg.Wait() 216 | } 217 | expireMap.Close() 218 | }) 219 | } 220 | } 221 | } 222 | } 223 | 224 | func reader(expireMap *ExpireMap, wg *sync.WaitGroup, steps, N int) { 225 | for steps >= 0 { 226 | steps-- 227 | r := rand.Intn(N) 228 | v, ok := expireMap.Get(r) 229 | if ok && v != r { 230 | log.Panicf("got wrong value %v, %v, %v", ok, v, r) 231 | } 232 | } 233 | wg.Done() 234 | } 235 | 236 | func writer(expireMap *ExpireMap, wg *sync.WaitGroup, steps, N int, ttl time.Duration) { 237 | for steps >= 0 { 238 | steps-- 239 | r := rand.Int() 240 | switch r % 4 { 241 | case 3: 242 | expireMap.Set(r%N, r%N, ttl) 243 | case 0: 244 | expireMap.Set(r%N, r%N, ttl) 245 | case 1: 246 | expireMap.Delete(r % N) 247 | case 2: 248 | expireMap.SetTTL(r%N, ttl) 249 | } 250 | } 251 | wg.Done() 252 | } 253 | -------------------------------------------------------------------------------- /expire_map_test.go: -------------------------------------------------------------------------------- 1 | package expiremap 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestExpireMap_SetAndGet(t *testing.T) { 10 | em := New() 11 | defer em.Close() 12 | 13 | for i := 0; i < 1<<12; i++ { 14 | v, ok := em.Get(i) 15 | if ok || v != nil { 16 | t.Fatal("got value or ok") 17 | } 18 | em.Set(i, i, time.Hour) 19 | 20 | v, ok = em.Get(i) 21 | if !ok || v != i { 22 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, i, true) 23 | } 24 | } 25 | } 26 | 27 | func TestExpireMap_SetAndGetExpired(t *testing.T) { 28 | em := New() 29 | defer em.Close() 30 | 31 | em.now = func() int64 { return 0 } 32 | for i := 0; i < 1<<12; i++ { 33 | em.Set(i, i, time.Duration(2-i%2)) 34 | } 35 | 36 | for i := 0; i < 1<<12; i++ { 37 | v, ok := em.Get(i) 38 | if !ok || v != i { 39 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, i, true) 40 | } 41 | } 42 | 43 | em.now = func() int64 { return 1 } 44 | 45 | for i := 0; i < 1<<12; i++ { 46 | v, ok := em.Get(i) 47 | if i%2 == 0 && (!ok || v != i) { 48 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, i, true) 49 | } 50 | 51 | if i%2 == 1 && (ok || v != nil) { 52 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, nil, false) 53 | } 54 | } 55 | 56 | em.now = func() int64 { return 2 } 57 | 58 | for i := 0; i < 1<<12; i++ { 59 | v, ok := em.Get(i) 60 | if ok || v != nil { 61 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, nil, false) 62 | } 63 | } 64 | } 65 | 66 | func TestExpireMap_GetCheckDifferentTypes(t *testing.T) { 67 | em := New() 68 | defer em.Close() 69 | 70 | ttl := time.Hour 71 | ar := make([]int, 3) 72 | ar[0] = 3 73 | 74 | em.Set(1, ar, ttl) 75 | v, _ := em.Get(1) 76 | 77 | v.([]int)[0] = 4 78 | 79 | if ar[0] != 4 { 80 | t.Fatal("got invalid slice") 81 | } 82 | 83 | x := 10 84 | em.Set(1, &x, ttl) 85 | v, _ = em.Get(1) 86 | 87 | *v.(*int) = 11 88 | 89 | if x != 11 { 90 | t.Fatal("got invalid pointer") 91 | } 92 | } 93 | 94 | func TestExpireMap_GetTTL(t *testing.T) { 95 | em := New() 96 | defer em.Close() 97 | 98 | if v := em.GetTTL(1); v != 0 { 99 | t.Fatal("got non zero ttl for non existing key") 100 | } 101 | 102 | em.now = func() int64 { return 0 } 103 | 104 | ttl := time.Duration(1) 105 | em.Set(1, 1, ttl) 106 | 107 | em.now = func() int64 { return 2 } 108 | 109 | if v := em.GetTTL(1); v != 0 { 110 | t.Fatal("got non zero ttl for expired key") 111 | } 112 | } 113 | 114 | func TestExpireMap_Size(t *testing.T) { 115 | em := New() 116 | defer em.Close() 117 | if sz := em.Size(); sz != 0 { 118 | t.Fatalf("got %v, want %v", sz, 0) 119 | } 120 | 121 | em.now = func() int64 { return 0 } 122 | ttl := time.Duration(10) 123 | 124 | for i := 0; i < 100000; i++ { 125 | em.Set(i, i, ttl) 126 | } 127 | 128 | if sz := em.Size(); sz != 100000 { 129 | t.Fatalf("got %v, want %v", sz, 100000) 130 | } 131 | 132 | for i := 0; i < 100000; i++ { 133 | em.Set(i, i, ttl) 134 | } 135 | 136 | if sz := em.Size(); sz != 100000 { 137 | t.Fatalf("got %v, want %v", sz, 100000) 138 | } 139 | 140 | // Check that after some time size still returns 141 | // the same value, as no methods altering map are called 142 | em.now = func() int64 { return 2 } 143 | 144 | if sz := em.Size(); sz != 100000 { 145 | t.Fatalf("got %v, want %v", sz, 100000) 146 | } 147 | 148 | // Check that after deleting all keys in the map, Size() returns 0 149 | for i := 0; i < 100000; i++ { 150 | em.Delete(i) 151 | } 152 | 153 | if sz := em.Size(); sz != 0 { 154 | t.Fatalf("got %v, want %v", sz, 0) 155 | } 156 | 157 | // Test 4 - Check that after calling SetTTL() for all keys in the map, Size() returns 0 158 | ttl = time.Second 159 | for i := 0; i < 100000; i++ { 160 | em.Set(i, i, ttl) 161 | } 162 | 163 | if sz := em.Size(); sz != 100000 { 164 | t.Fatalf("got %v, want %v", sz, 100000) 165 | } 166 | 167 | ttl = 0 168 | 169 | for i := 0; i < 100000; i++ { 170 | em.SetTTL(i, ttl) 171 | } 172 | 173 | if sz := em.Size(); sz != 0 { 174 | t.Fatalf("got %v, want %v", sz, 0) 175 | } 176 | } 177 | 178 | func TestExpireMap_SetTTL(t *testing.T) { 179 | em := New() 180 | defer em.Close() 181 | ttl := time.Second 182 | 183 | v, ok := em.SetTTL("key", ttl) 184 | 185 | if v != nil || ok { 186 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, nil, false) 187 | } 188 | 189 | em.Set("key", "value", ttl) 190 | 191 | if v, ok = em.SetTTL("key", ttl); !ok || v != "value" { 192 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, "value", true) 193 | } 194 | 195 | if v, ok = em.Get("key"); !ok || v != "value" { 196 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, "value", true) 197 | } 198 | 199 | em.SetTTL("key", 0) 200 | 201 | if v, ok := em.Get("key"); ok || v != nil { 202 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, nil, false) 203 | } 204 | } 205 | 206 | func TestExpireMap_Delete(t *testing.T) { 207 | em := New() 208 | defer em.Close() 209 | ttl := 10 * time.Second 210 | 211 | em.Set("key", "value", ttl) 212 | em.Delete("key") 213 | 214 | if v, ok := em.Get("key"); ok || v != nil { 215 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, nil, false) 216 | } 217 | 218 | for i := 0; i < 100000; i++ { 219 | em.Set(i, i, ttl) 220 | } 221 | for i := 0; i < 100000; i += 2 { 222 | em.Delete(i) 223 | em.Delete(i) 224 | em.Delete(i) 225 | } 226 | for i := 0; i < 100000; i += 2 { 227 | if v, ok := em.Get(i); ok { 228 | t.Fatalf("got %v, %v - expected %v, %v", v, ok, nil, false) 229 | } 230 | } 231 | } 232 | 233 | func TestExpireMap_StoppedAndClose(t *testing.T) { 234 | em := New() 235 | defer em.Close() 236 | if stopped := em.Stopped(); stopped { 237 | t.Fatal("did not call Close(), but it is stopped") 238 | } 239 | em.Close() 240 | if stopped := em.Stopped(); !stopped { 241 | t.Fatal("called Close(), but it is not stopped") 242 | } 243 | em.Close() 244 | em.Close() 245 | em.Close() 246 | } 247 | 248 | func TestExpireMap_GetAll(t *testing.T) { 249 | em := New() 250 | defer em.Close() 251 | // Test 1 - Test basic 252 | var ttl1, ttl2 time.Duration = 1, 5 253 | 254 | em.now = func() int64 { return 0 } 255 | 256 | for i := 0; i < 1000; i++ { 257 | if i%2 == 1 { 258 | em.Set(i, i, ttl1) 259 | } else { 260 | em.Set(i, i, ttl2) 261 | } 262 | } 263 | 264 | kvs := em.GetAll() 265 | 266 | if len(kvs) != 1000 { 267 | t.Fatalf("got %v, expected %v", len(kvs), 1000) 268 | } 269 | 270 | slices.SortFunc(kvs, func(a, b KeyValue) int { 271 | return a.Key.(int) - b.Key.(int) 272 | }) 273 | 274 | for i, kv := range kvs { 275 | if kv.Key != i || kv.Value != i { 276 | t.Fatal("slice does not contain expected keys and values") 277 | } 278 | } 279 | em.now = func() int64 { return 2 } 280 | 281 | kvs = em.GetAll() 282 | 283 | if len(kvs) != 500 { 284 | t.Fatalf("got %v, want %v", len(kvs), 500) 285 | return 286 | } 287 | 288 | slices.SortFunc(kvs, func(a, b KeyValue) int { 289 | return a.Key.(int) - b.Key.(int) 290 | }) 291 | 292 | for i, kv := range kvs { 293 | if kv.Key != i*2 || kv.Value != i*2 { 294 | t.Fatal("slice does not contain expected values") 295 | return 296 | } 297 | } 298 | } 299 | 300 | func TestExpireMap_Notify(t *testing.T) { 301 | em := New() 302 | defer em.Close() 303 | 304 | em.now = func() int64 { return 0 } 305 | 306 | c := make(chan Event, 10) 307 | 308 | em.Notify(c, AllEvents) 309 | em.Set(1, 1, 1) 310 | 311 | if e := <-c; e.Key != 1 || e.Value != 1 || e.Type != Set { 312 | t.Fatalf("got %v, %v, %v - expected %v, %v, %v", e.Key, e.Value, e.Type, 1, 1, Set) 313 | } 314 | 315 | em.Set(1, 2, 2) 316 | 317 | if e := <-c; e.Key != 1 || e.Value != 2 || e.Type != Update { 318 | t.Fatalf("got %v, %v, %v - expected %v, %v, %v", e.Key, e.Value, e.Type, 1, 2, Update) 319 | } 320 | 321 | em.Set(2, 2, 1) 322 | 323 | if e := <-c; e.Key != 2 || e.Value != 2 || e.Type != Set { 324 | t.Fatalf("got %v, %v, %v - expected %v, %v, %v", e.Key, e.Value, e.Type, 2, 2, Set) 325 | } 326 | 327 | em.Delete(2) 328 | 329 | if e := <-c; e.Key != 2 || e.Value != 2 || e.Type != Delete { 330 | t.Fatalf("got %v, %v, %v - expected %v, %v, %v", e.Key, e.Value, e.Type, 2, 2, Delete) 331 | } 332 | 333 | em.now = func() int64 { return 2 } 334 | 335 | if e := <-c; e.Key != 1 || e.Value != 2 || e.Type != Expire { 336 | t.Fatalf("got %v, %v, %v - expected %v, %v, %v", e.Key, e.Value, e.Type, 1, 2, Expire) 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /expire_map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nursultan Zarlyk. All rights reserved. 2 | // Use of this source code is governed by the MIT License that can be found in the LICENSE file. 3 | 4 | // Package expiremap provides a thread-safe map with expiring keys. 5 | // You must pay attention for these facts: 6 | // 1. Current implementation may hold up to 1 billion keys 7 | // 2. After creating a new map (calling New()), goroutine is created for deletion 8 | // expired keys. It exists until Close() method is called 9 | // 3. There are active and passive expirations. Active expiration is done during Get(), 10 | // and SetTTL() calls. Passive expiration happens in background and is done by goroutine 11 | // 4. Passive expiration occurs every 100ms. It is done in two steps - first step is 12 | // inspired by algorithm used in Redis and second step is sequential expiration 13 | // 5. It is guaranteed by sequential expiration, that no expired key will live more than 14 | // map.Size() / 200 seconds 15 | // 16 | // First step's (or random expire) algorithm is following: 17 | // 1. Check the size of the map. If it is less than 100, just iterate over all keys 18 | // and stop algorithm 19 | // 2. Check 20 random keys. Remove all expired keys. If there were at least 5 deletions, 20 | // do the step 2 again (step 2 is done maximum 10 times) 21 | // 22 | // Second step's (or rotate expire) algorithm is following: 23 | // 1. Load to X a key on which we stopped on the previous call. If on previous call 24 | // we hit the bottom of the map, load top key of the map 25 | // 2. Start from the key X and from that key expire 20 consecutive keys or stop if 26 | // we hit a bottom of the map 27 | // 28 | // It means that at maximum 2200 expires per second may occur (not counting active expiration). 29 | // If you have a lot of insertions with unique keys, but you rarely call methods Get and SetTTL 30 | // on these keys, your map will grow faster than expiration rate and you may hit 1 billion keys 31 | // limit. 32 | package expiremap 33 | 34 | import ( 35 | "math/rand" 36 | "sync" 37 | "time" 38 | ) 39 | 40 | // time interval for calling randomExpire and rotateExpire methods 41 | const expireInterval = 100 * time.Millisecond 42 | 43 | // EventType is used for notification channel 44 | type EventType uint8 45 | 46 | const ( 47 | // Expire event is fired, when key is deleted due to expiration 48 | Expire EventType = 1 << iota 49 | // Delete event is fired, when key explicitly deleted using Delete 50 | // or SetTTL with non positive TTL 51 | Delete 52 | // Update event is fired, when TTL or value is updated 53 | Update 54 | // Set event is fired, when new key is inserted 55 | Set 56 | // AllEvents is a helper constant, which is the same as 57 | // Expire | Delete | Update | Set 58 | AllEvents = Expire | Delete | Update | Set 59 | // NoEvents is a helper constant, which is the same as 0 (no events) 60 | NoEvents = 0 61 | ) 62 | 63 | // Event is used for notification channel 64 | type Event struct { 65 | Key interface{} 66 | Value interface{} 67 | // Time is when this event occurred (in Unix nanoseconds) 68 | Time int64 69 | // Due is when this key will expire (in Unix nanoseconds, 0 for expired) 70 | Due int64 71 | // Type of the Event. Equal to Expire, Delete, Update or Set. 72 | Type EventType 73 | } 74 | 75 | // KeyValue is used only for GetAll method 76 | type KeyValue struct { 77 | Key interface{} 78 | Value interface{} 79 | } 80 | 81 | type item struct { 82 | // due is unix nanoseconds. Instance should live up to this time 83 | due int64 84 | key interface{} 85 | value interface{} 86 | } 87 | 88 | // ExpireMap stores keys and corresponding values and TTLs. 89 | type ExpireMap struct { 90 | keys map[interface{}]int 91 | values []item 92 | mutex sync.RWMutex 93 | stopped bool 94 | now func() int64 95 | c chan<- Event 96 | events EventType 97 | } 98 | 99 | // SetTTL updates ttl for the given key. If ttl was successfully updated, 100 | // it returns value and "true". It happens, if and only if key presents 101 | // in the map and "ttl" variable is greater than timeResolution. In any other 102 | // case it returns nil and "false". Also, if "ttl" variable is non positive, 103 | // it just removes a key. 104 | func (m *ExpireMap) SetTTL(key interface{}, ttl time.Duration) (interface{}, bool) { 105 | if ttl <= 0 { 106 | m.Delete(key) 107 | return nil, false 108 | } 109 | curtime := m.now() 110 | due := int64(ttl/time.Nanosecond) + curtime 111 | 112 | m.mutex.Lock() 113 | if m.stopped { 114 | m.mutex.Unlock() 115 | return nil, false 116 | } 117 | id, ok := m.keys[key] 118 | if !ok { 119 | m.mutex.Unlock() 120 | return nil, false 121 | } 122 | v := m.values[id] 123 | if v.due <= m.now() { 124 | m.del(key, id, Expire) 125 | m.mutex.Unlock() 126 | return nil, false 127 | } 128 | 129 | if (m.events&Update) > 0 && m.c != nil { 130 | m.c <- Event{ 131 | Key: key, 132 | Value: v.value, 133 | Type: Update, 134 | Time: curtime, 135 | Due: due, 136 | } 137 | } 138 | 139 | v.due = due 140 | m.values[id] = v 141 | m.mutex.Unlock() 142 | return v.value, true 143 | } 144 | 145 | // Get returns value for the given key. If map does not contain 146 | // such key or key is expired, it returns nil and "false". If key is expired, 147 | // then it waits for write lock, checks a ttl again (as during wait of 148 | // write lock, value and ttl could be updated) and if it is still expired, 149 | // removes the given key (otherwise it returns a value and "true"). 150 | func (m *ExpireMap) Get(key interface{}) (interface{}, bool) { 151 | m.mutex.RLock() 152 | if m.stopped { 153 | m.mutex.RUnlock() 154 | return nil, false 155 | } 156 | id, ok := m.keys[key] 157 | if !ok { 158 | m.mutex.RUnlock() 159 | return nil, false 160 | } 161 | v := m.values[id] 162 | if v.due > m.now() { 163 | m.mutex.RUnlock() 164 | return v.value, true 165 | } 166 | m.mutex.RUnlock() 167 | m.mutex.Lock() 168 | if m.stopped { 169 | m.mutex.Unlock() 170 | return nil, false 171 | } 172 | 173 | id, ok = m.keys[key] 174 | if !ok { 175 | m.mutex.Unlock() 176 | return nil, false 177 | } 178 | 179 | v = m.values[id] 180 | if v.due > m.now() { 181 | m.mutex.Unlock() 182 | return v.value, true 183 | } 184 | 185 | m.del(key, id, Expire) 186 | m.mutex.Unlock() 187 | return nil, false 188 | } 189 | 190 | // GetTTL returns time in nanoseconds, when key will die. If key is expired 191 | // or does not exist in the map, it returns 0. 192 | func (m *ExpireMap) GetTTL(key interface{}) int64 { 193 | m.mutex.RLock() 194 | if m.stopped { 195 | m.mutex.RUnlock() 196 | return 0 197 | } 198 | id, ok := m.keys[key] 199 | if !ok { 200 | m.mutex.RUnlock() 201 | return 0 202 | } 203 | v := m.values[id] 204 | if cur := m.now(); v.due > cur { 205 | ttl := v.due - cur 206 | m.mutex.RUnlock() 207 | return ttl 208 | } 209 | m.mutex.RUnlock() 210 | return 0 211 | } 212 | 213 | // Delete removes key from the map. 214 | func (m *ExpireMap) Delete(key interface{}) { 215 | m.mutex.Lock() 216 | if m.stopped { 217 | m.mutex.Unlock() 218 | return 219 | } 220 | if id, ok := m.keys[key]; ok { 221 | m.del(key, id, Delete) 222 | } 223 | m.mutex.Unlock() 224 | } 225 | 226 | // Close stops goroutine and channel. 227 | func (m *ExpireMap) Close() { 228 | m.mutex.Lock() 229 | if !m.stopped { 230 | m.stopped = true 231 | m.keys = nil 232 | m.values = nil 233 | if m.c != nil { 234 | close(m.c) 235 | } 236 | m.c = nil 237 | } 238 | m.mutex.Unlock() 239 | } 240 | 241 | // Set sets or updates value and ttl for the given key 242 | func (m *ExpireMap) Set(key interface{}, value interface{}, ttl time.Duration) { 243 | curtime := m.now() 244 | due := int64(ttl/time.Nanosecond) + curtime 245 | m.mutex.Lock() 246 | if m.stopped { 247 | m.mutex.Unlock() 248 | return 249 | } 250 | 251 | t := Update 252 | id, ok := m.keys[key] 253 | if !ok { 254 | id = len(m.keys) 255 | m.keys[key] = id 256 | m.values = append(m.values, item{}) 257 | t = Set 258 | } 259 | m.values[id] = item{ 260 | key: key, 261 | value: value, 262 | due: due, 263 | } 264 | if (m.events&t) > 0 && m.c != nil { 265 | m.c <- Event{ 266 | Key: key, 267 | Value: value, 268 | Due: due, 269 | Time: curtime, 270 | Type: t, 271 | } 272 | } 273 | m.mutex.Unlock() 274 | } 275 | 276 | // GetAll returns a slice of KeyValue. 277 | func (m *ExpireMap) GetAll() []KeyValue { 278 | m.mutex.RLock() 279 | if m.stopped { 280 | m.mutex.RUnlock() 281 | return nil 282 | } 283 | var ans []KeyValue 284 | curtime := m.now() 285 | for _, v := range m.values { 286 | if v.due > curtime { 287 | ans = append(ans, KeyValue{Key: v.key, Value: v.value}) 288 | } 289 | } 290 | m.mutex.RUnlock() 291 | return ans 292 | } 293 | 294 | // Size returns a number of keys in the map, both expired and unexpired. 295 | func (m *ExpireMap) Size() int { 296 | m.mutex.RLock() 297 | sz := len(m.keys) 298 | m.mutex.RUnlock() 299 | return sz 300 | } 301 | 302 | // Stopped indicates that map is stopped. 303 | func (m *ExpireMap) Stopped() bool { 304 | m.mutex.RLock() 305 | defer m.mutex.RUnlock() 306 | return m.stopped 307 | } 308 | 309 | // Notify sets a channel and causes a map to send Event to a channel based on the given 310 | // EventType. To get events X1, X2... pass X1|X2|...(for example, to get Update and Set 311 | // events pass Update|Set). To receive all events use AllEvents constant (the same as 312 | // Update|Set|Delete|Expire). To receive no events use NoEvents constant or set nil chan. 313 | // When the map stops, no events are guaranteed to be sent to the channel. 314 | // It is up to the user to close the channel. 315 | func (m *ExpireMap) Notify(c chan<- Event, events EventType) { 316 | m.mutex.Lock() 317 | if m.stopped { 318 | m.mutex.Unlock() 319 | return 320 | } 321 | m.c = c 322 | m.events = events 323 | m.mutex.Unlock() 324 | } 325 | 326 | // del is helper method to delete key and associated id from the map 327 | func (m *ExpireMap) del(key interface{}, id int, t EventType) { 328 | itemToDelete := m.values[id] 329 | 330 | if last := len(m.keys) - 1; id != last { 331 | lastItem := m.values[last] 332 | m.values[id] = lastItem 333 | m.keys[lastItem.key] = id 334 | } 335 | 336 | delete(m.keys, key) 337 | m.values[len(m.values)-1] = item{} 338 | m.values = m.values[:len(m.values)-1] 339 | 340 | if (m.events&t) > 0 && m.c != nil { 341 | m.c <- Event{ 342 | Key: key, 343 | Value: itemToDelete.value, 344 | Time: m.now(), 345 | Type: t, 346 | } 347 | } 348 | 349 | } 350 | 351 | // randomExpire randomly gets keys and checks for expiration. 352 | // The common logic was inspired by Redis. 353 | func (m *ExpireMap) randomExpire() bool { 354 | const totalChecks = 20 355 | const bruteForceThreshold = 100 356 | if m.stopped { 357 | return false 358 | } 359 | // Because the number of keys is small, just iterate over all keys 360 | if sz := len(m.keys); sz <= bruteForceThreshold { 361 | for _, id := range m.keys { 362 | v := m.values[id] 363 | if v.due <= m.now() { 364 | m.del(v.key, id, Expire) 365 | } 366 | } 367 | return false 368 | } 369 | 370 | expiredFound := 0 371 | 372 | for i := 0; i < totalChecks; i++ { 373 | sz := len(m.keys) 374 | id := rand.Intn(sz) 375 | v := m.values[id] 376 | if v.due <= m.now() { 377 | m.del(v.key, id, Expire) 378 | expiredFound++ 379 | } 380 | } 381 | 382 | return expiredFound*4 >= totalChecks 383 | } 384 | 385 | // rotateExpire checks keys sequentially for expiration. 386 | // Some keys may live too long, because randomExpire cannot hit them, and 387 | // that's why this method was written. Basically, it iterates over 20 keys 388 | // and checks them. The passed variable is k-th key, which previously was 389 | // checked. 390 | func (m *ExpireMap) rotateExpire(kth int) int { 391 | const totalChecks = 20 392 | if m.stopped { 393 | return 0 394 | } 395 | sz := len(m.keys) 396 | if sz == 0 { 397 | return 0 398 | } 399 | if kth >= sz || kth <= 0 { 400 | kth = sz - 1 401 | } 402 | for i := 0; i < totalChecks; i++ { 403 | v := m.values[kth] 404 | if v.due <= m.now() { 405 | m.del(v.key, kth, Expire) 406 | } 407 | kth-- 408 | if kth < 0 { 409 | break 410 | } 411 | } 412 | return kth 413 | } 414 | 415 | // Curtime returns current time in Unix nanoseconds 416 | func (m *ExpireMap) Curtime() int64 { 417 | return m.now() 418 | } 419 | 420 | // start starts two goroutines - first for updating curtime variable and 421 | // second for expiration of keys. for loops with time.Sleep are used instead 422 | // of time tickers. 423 | func (m *ExpireMap) start() { 424 | go func() { 425 | kth := 0 426 | for !m.Stopped() { 427 | start := time.Now() 428 | for i := 0; i < 10; i++ { 429 | m.mutex.Lock() 430 | if !m.randomExpire() { 431 | m.mutex.Unlock() 432 | break 433 | } 434 | m.mutex.Unlock() 435 | } 436 | m.mutex.Lock() 437 | kth = m.rotateExpire(kth) 438 | m.mutex.Unlock() 439 | diff := time.Since(start) 440 | time.Sleep(expireInterval - diff) 441 | } 442 | }() 443 | } 444 | 445 | // New returns a new map. 446 | func New() *ExpireMap { 447 | rl := &ExpireMap{ 448 | keys: make(map[interface{}]int), 449 | values: nil, 450 | now: func() int64 { return time.Now().UnixNano() }, 451 | } 452 | rl.start() 453 | return rl 454 | } 455 | --------------------------------------------------------------------------------