From 7da0cd57ce907e42b3c8d2778f5247e3809b6d1d Mon Sep 17 00:00:00 2001 From: andrey1s Date: Mon, 26 Apr 2021 17:13:36 +0300 Subject: [PATCH] first commit --- .drone.yml | 23 ++ .gitignore | 17 ++ .golangci.yml | 43 ++++ LICENSE | 19 ++ README.md | 378 ++++++++++++++++++++++++++++ bench_test.go | 67 +++++ cache.go | 121 +++++++++ cache_example_test.go | 226 +++++++++++++++++ cache_test.go | 208 +++++++++++++++ docker-compose.yml | 13 + error.go | 33 +++ go.mod | 15 ++ go.sum | 185 ++++++++++++++ item.go | 147 +++++++++++ item/expired.go | 56 +++++ item/expired_easyjson.go | 99 ++++++++ mw/fallback.go | 134 ++++++++++ mw/fallback_test.go | 221 ++++++++++++++++ mw/gc.go | 78 ++++++ mw/gc_test.go | 68 +++++ mw/metrics.go | 60 +++++ mw/prometheus/metrics.go | 93 +++++++ operation.go | 21 ++ provider.go | 50 ++++ provider/bench_provider_test.go | 194 ++++++++++++++ provider/lru/provider.go | 66 +++++ provider/lru/provider_test.go | 18 ++ provider/memcache/provider.go | 52 ++++ provider/memcache/provider_test.go | 14 ++ provider/memory/encoding.go | 93 +++++++ provider/memory/encoding_test.go | 13 + provider/memory/map.go | 155 ++++++++++++ provider/memory/map_test.go | 18 ++ provider/ns/provider.go | 22 ++ provider/pebble/provider.go | 54 ++++ provider/pebble/provider_test.go | 17 ++ provider/redis/pool.go | 110 ++++++++ provider/redis/pool_test.go | 14 ++ provider/redis/redigo.go | 20 ++ provider/ristretto/provider.go | 69 +++++ provider/ristretto/provider_test.go | 26 ++ test/helpers.go | 84 +++++++ test/provider.go | 57 +++++ test/sute.go | 133 ++++++++++ type_assert.go | 99 ++++++++ 45 files changed, 3703 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bench_test.go create mode 100644 cache.go create mode 100644 cache_example_test.go create mode 100644 cache_test.go create mode 100644 docker-compose.yml create mode 100644 error.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 item.go create mode 100644 item/expired.go create mode 100644 item/expired_easyjson.go create mode 100644 mw/fallback.go create mode 100644 mw/fallback_test.go create mode 100644 mw/gc.go create mode 100644 mw/gc_test.go create mode 100644 mw/metrics.go create mode 100644 mw/prometheus/metrics.go create mode 100644 operation.go create mode 100644 provider.go create mode 100644 provider/bench_provider_test.go create mode 100644 provider/lru/provider.go create mode 100644 provider/lru/provider_test.go create mode 100644 provider/memcache/provider.go create mode 100644 provider/memcache/provider_test.go create mode 100644 provider/memory/encoding.go create mode 100644 provider/memory/encoding_test.go create mode 100644 provider/memory/map.go create mode 100644 provider/memory/map_test.go create mode 100644 provider/ns/provider.go create mode 100644 provider/pebble/provider.go create mode 100644 provider/pebble/provider_test.go create mode 100644 provider/redis/pool.go create mode 100644 provider/redis/pool_test.go create mode 100644 provider/redis/redigo.go create mode 100644 provider/ristretto/provider.go create mode 100644 provider/ristretto/provider_test.go create mode 100644 test/helpers.go create mode 100644 test/provider.go create mode 100644 test/sute.go create mode 100644 type_assert.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..b6b9f00 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,23 @@ +kind: pipeline +name: default + +services: + - name: redis + image: redis + - name: memcache + image: memcached + +environment: + FDEVS_CACHE_REDIS_HOST: redis:6379 + FDEVS_CACHE_MEMCACHE_HOST: memcache:11211 + +steps: +- name: test + image: golang + commands: + - go test ./... + +- name: golangci-lint + image: golangci/golangci-lint:v1.39 + commands: + - golangci-lint run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4d432a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# ---> Go +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..46c35b5 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,43 @@ +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 2 + min-occurrences: 2 + gocyclo: + min-complexity: 15 + golint: + min-confidence: 0 + govet: + check-shadowing: true + lll: + line-length: 140 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + enable-all: true + disable: + - exhaustivestruct + - maligned + - interfacer + - scopelint + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - gomnd + - exhaustivestruct + - wrapcheck + - path: test/* + linters: + - gomnd + - exhaustivestruct + - wrapcheck diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0dcfb43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MIT License Copyright (c) 2020 go-4devs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9e94e0 --- /dev/null +++ b/README.md @@ -0,0 +1,378 @@ +# cache + +[![Build Status](https://drone.gitoa.ru/api/badges/go-4devs/cache/status.svg)](https://drone.gitoa.ru/go-4devs/cache) +[![Go Report Card](https://goreportcard.com/badge/gitoa.ru/go-4devs/cache)](https://goreportcard.com/report/gitoa.ru/go-4devs/cache) +[![GoDoc](https://godoc.org/gitoa.ru/go-4devs/cache?status.svg)](http://godoc.org/gitoa.ru/go-4devs/cache) + +## Benchmark cache + +`go test -v -timeout 25m -cpu 1,2,4,8,16 -benchmem -run=^$ -bench . bench_test.go` + +```bash +goos: darwin +goarch: amd64 +BenchmarkCacheGetStruct +BenchmarkCacheGetStruct/encoding_json +BenchmarkCacheGetStruct/encoding_json 1519932 783 ns/op 320 B/op 6 allocs/op +BenchmarkCacheGetStruct/encoding_json-2 1478414 780 ns/op 320 B/op 6 allocs/op +BenchmarkCacheGetStruct/encoding_json-4 1353025 916 ns/op 320 B/op 6 allocs/op +BenchmarkCacheGetStruct/encoding_json-8 1284042 839 ns/op 320 B/op 6 allocs/op +BenchmarkCacheGetStruct/encoding_json-16 1422788 848 ns/op 320 B/op 6 allocs/op +BenchmarkCacheGetStruct/encoding_gob +BenchmarkCacheGetStruct/encoding_gob 83661 15323 ns/op 6944 B/op 180 allocs/op +BenchmarkCacheGetStruct/encoding_gob-2 81745 14407 ns/op 6944 B/op 180 allocs/op +BenchmarkCacheGetStruct/encoding_gob-4 73537 15142 ns/op 6944 B/op 180 allocs/op +BenchmarkCacheGetStruct/encoding_gob-8 85412 14494 ns/op 6944 B/op 180 allocs/op +BenchmarkCacheGetStruct/encoding_gob-16 75748 15219 ns/op 6944 B/op 180 allocs/op +BenchmarkCacheGetStruct/map +BenchmarkCacheGetStruct/map 6162325 199 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map-2 5740689 195 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map-4 6018531 200 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map-8 5452492 210 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map-16 5933622 202 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map_shards +BenchmarkCacheGetStruct/map_shards 5299807 230 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map_shards-2 5087726 238 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map_shards-4 4990490 243 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map_shards-8 4899127 225 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/map_shards-16 5229320 233 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/ristretto +BenchmarkCacheGetStruct/ristretto 5511872 227 ns/op 96 B/op 1 allocs/op +BenchmarkCacheGetStruct/ristretto-2 4664298 257 ns/op 103 B/op 1 allocs/op +BenchmarkCacheGetStruct/ristretto-4 4524751 265 ns/op 103 B/op 1 allocs/op +BenchmarkCacheGetStruct/ristretto-8 4425381 260 ns/op 103 B/op 1 allocs/op +BenchmarkCacheGetStruct/ristretto-16 4649698 258 ns/op 103 B/op 1 allocs/op +BenchmarkCacheGetStruct/lru +BenchmarkCacheGetStruct/lru 4730811 250 ns/op 144 B/op 2 allocs/op +BenchmarkCacheGetStruct/lru-2 4627194 252 ns/op 144 B/op 2 allocs/op +BenchmarkCacheGetStruct/lru-4 4627082 257 ns/op 144 B/op 2 allocs/op +BenchmarkCacheGetStruct/lru-8 4755622 252 ns/op 144 B/op 2 allocs/op +BenchmarkCacheGetStruct/lru-16 4717584 250 ns/op 144 B/op 2 allocs/op +BenchmarkCacheGetStruct/redis_json +BenchmarkCacheGetStruct/redis_json 572 2132479 ns/op 9848 B/op 34 allocs/op +BenchmarkCacheGetStruct/redis_json-2 565 2161113 ns/op 9848 B/op 34 allocs/op +BenchmarkCacheGetStruct/redis_json-4 543 2183219 ns/op 9848 B/op 34 allocs/op +BenchmarkCacheGetStruct/redis_json-8 531 2148630 ns/op 9848 B/op 34 allocs/op +BenchmarkCacheGetStruct/redis_json-16 544 2212659 ns/op 9848 B/op 34 allocs/op +BenchmarkCacheGetStruct/redis_gob +BenchmarkCacheGetStruct/redis_gob 553 2206583 ns/op 16504 B/op 208 allocs/op +BenchmarkCacheGetStruct/redis_gob-2 549 2256638 ns/op 16505 B/op 208 allocs/op +BenchmarkCacheGetStruct/redis_gob-4 540 2230342 ns/op 16504 B/op 208 allocs/op +BenchmarkCacheGetStruct/redis_gob-8 537 2178895 ns/op 16504 B/op 208 allocs/op +BenchmarkCacheGetStruct/redis_gob-16 541 2206298 ns/op 16504 B/op 208 allocs/op +BenchmarkCacheGetStruct/memcache_json +BenchmarkCacheGetStruct/memcache_json 1352 882575 ns/op 560 B/op 16 allocs/op +BenchmarkCacheGetStruct/memcache_json-2 1332 869724 ns/op 560 B/op 16 allocs/op +BenchmarkCacheGetStruct/memcache_json-4 1326 824555 ns/op 561 B/op 16 allocs/op +BenchmarkCacheGetStruct/memcache_json-8 1375 880741 ns/op 562 B/op 16 allocs/op +BenchmarkCacheGetStruct/memcache_json-16 1346 872861 ns/op 563 B/op 16 allocs/op +BenchmarkCacheGetStruct/memcache_gob +BenchmarkCacheGetStruct/memcache_gob 1431 828348 ns/op 7216 B/op 190 allocs/op +BenchmarkCacheGetStruct/memcache_gob-2 1266 875339 ns/op 7216 B/op 190 allocs/op +BenchmarkCacheGetStruct/memcache_gob-4 1327 908142 ns/op 7218 B/op 190 allocs/op +BenchmarkCacheGetStruct/memcache_gob-8 1286 840878 ns/op 7219 B/op 190 allocs/op +BenchmarkCacheGetStruct/memcache_gob-16 1540 797765 ns/op 7220 B/op 190 allocs/op +``` + +## Benchmark providers + +`go test -v -timeout 25m -cpu 1,2,4,8,16 -benchmem -run=^$ -bench . ./provider/bench_provider_test.go` + +```bash +goos: darwin +goarch: amd64 +BenchmarkCacheGetRandomKeyString +BenchmarkCacheGetRandomKeyString/encoding +BenchmarkCacheGetRandomKeyString/encoding 3100226 389 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyString/encoding-2 3142849 379 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyString/encoding-4 3118212 379 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyString/encoding-8 3064170 387 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyString/encoding-16 3128031 384 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyString/map +BenchmarkCacheGetRandomKeyString/map 7342993 157 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/map-2 7268864 158 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/map-4 7233045 162 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/map-8 7393652 159 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/map-16 7463053 159 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/shard +BenchmarkCacheGetRandomKeyString/shard 3330136 351 ns/op 64 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/shard-2 3518775 335 ns/op 64 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/shard-4 3477537 336 ns/op 64 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/shard-8 3514064 335 ns/op 64 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/shard-16 3412119 341 ns/op 64 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/lru +BenchmarkCacheGetRandomKeyString/lru 5013633 249 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/lru-2 4871456 247 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/lru-4 4786940 238 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/lru-8 4721556 238 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/lru-16 4870622 241 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyString/ristretto +BenchmarkCacheGetRandomKeyString/ristretto 5569208 205 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/ristretto-2 3892068 295 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/ristretto-4 4490196 266 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/ristretto-8 4381441 266 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/ristretto-16 4185096 273 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyString/memcache +BenchmarkCacheGetRandomKeyString/memcache 1492 811587 ns/op 528 B/op 12 allocs/op +BenchmarkCacheGetRandomKeyString/memcache-2 1400 840429 ns/op 528 B/op 12 allocs/op +BenchmarkCacheGetRandomKeyString/memcache-4 1381 793654 ns/op 528 B/op 12 allocs/op +BenchmarkCacheGetRandomKeyString/memcache-8 1455 826461 ns/op 530 B/op 12 allocs/op +BenchmarkCacheGetRandomKeyString/memcache-16 1380 803712 ns/op 532 B/op 12 allocs/op +BenchmarkCacheGetRandomKeyString/redis +BenchmarkCacheGetRandomKeyString/redis 540 2908289 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetRandomKeyString/redis-2 514 2287030 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetRandomKeyString/redis-4 542 2195917 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetRandomKeyString/redis-8 536 2209508 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetRandomKeyString/redis-16 544 2275867 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetRandomKeyString/pebble +BenchmarkCacheGetRandomKeyString/pebble 672912 1801 ns/op 1408 B/op 6 allocs/op +BenchmarkCacheGetRandomKeyString/pebble-2 773318 1691 ns/op 1408 B/op 6 allocs/op +BenchmarkCacheGetRandomKeyString/pebble-4 729020 1556 ns/op 1408 B/op 6 allocs/op +BenchmarkCacheGetRandomKeyString/pebble-8 778066 1491 ns/op 1408 B/op 6 allocs/op +BenchmarkCacheGetRandomKeyString/pebble-16 838596 1441 ns/op 1408 B/op 6 allocs/op +BenchmarkCacheGetRandomKeyInt +BenchmarkCacheGetRandomKeyInt/encoding +BenchmarkCacheGetRandomKeyInt/encoding 2825020 410 ns/op 207 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyInt/encoding-2 2932910 409 ns/op 207 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyInt/encoding-4 2837827 408 ns/op 207 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyInt/encoding-8 2842040 418 ns/op 207 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyInt/encoding-16 2866555 409 ns/op 207 B/op 2 allocs/op +BenchmarkCacheGetRandomKeyInt/map +BenchmarkCacheGetRandomKeyInt/map 7312549 150 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/map-2 7884612 150 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/map-4 7450554 158 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/map-8 7471407 156 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/map-16 7469587 158 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/shard +BenchmarkCacheGetRandomKeyInt/shard 6709964 187 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/shard-2 6430581 183 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/shard-4 6375858 187 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/shard-8 6399346 180 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/shard-16 6580282 175 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/lru +BenchmarkCacheGetRandomKeyInt/lru 5183596 225 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyInt/lru-2 5217847 220 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyInt/lru-4 5078146 223 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyInt/lru-8 4722044 225 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyInt/lru-16 4989286 224 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetRandomKeyInt/ristretto +BenchmarkCacheGetRandomKeyInt/ristretto 6920838 169 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/ristretto-2 4763511 216 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/ristretto-4 5163074 220 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/ristretto-8 5133212 220 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/ristretto-16 5089780 219 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetRandomKeyInt/memcache +BenchmarkCacheGetRandomKeyInt/memcache 1332 820272 ns/op 544 B/op 14 allocs/op +BenchmarkCacheGetRandomKeyInt/memcache-2 1408 840124 ns/op 544 B/op 14 allocs/op +BenchmarkCacheGetRandomKeyInt/memcache-4 1443 809845 ns/op 544 B/op 14 allocs/op +BenchmarkCacheGetRandomKeyInt/memcache-8 1449 832162 ns/op 545 B/op 14 allocs/op +BenchmarkCacheGetRandomKeyInt/memcache-16 1333 855560 ns/op 547 B/op 14 allocs/op +BenchmarkCacheGetRandomKeyInt/redis +BenchmarkCacheGetRandomKeyInt/redis 525 2211523 ns/op 9767 B/op 31 allocs/op +BenchmarkCacheGetRandomKeyInt/redis-2 542 2146253 ns/op 9767 B/op 31 allocs/op +BenchmarkCacheGetRandomKeyInt/redis-4 531 2271602 ns/op 9767 B/op 31 allocs/op +BenchmarkCacheGetRandomKeyInt/redis-8 522 2273678 ns/op 9767 B/op 31 allocs/op +BenchmarkCacheGetRandomKeyInt/redis-16 552 2180911 ns/op 9767 B/op 31 allocs/op +BenchmarkCacheGetRandomKeyInt/pebble +BenchmarkCacheGetRandomKeyInt/pebble 752023 1575 ns/op 1359 B/op 7 allocs/op +BenchmarkCacheGetRandomKeyInt/pebble-2 699300 1557 ns/op 1359 B/op 7 allocs/op +BenchmarkCacheGetRandomKeyInt/pebble-4 730688 1534 ns/op 1359 B/op 7 allocs/op +BenchmarkCacheGetRandomKeyInt/pebble-8 768183 1508 ns/op 1359 B/op 7 allocs/op +BenchmarkCacheGetRandomKeyInt/pebble-16 735848 1506 ns/op 1359 B/op 7 allocs/op +BenchmarkCacheGetStruct +BenchmarkCacheGetStruct/encoding +BenchmarkCacheGetStruct/encoding 2252955 524 ns/op 208 B/op 4 allocs/op +BenchmarkCacheGetStruct/encoding-2 2332430 515 ns/op 208 B/op 4 allocs/op +BenchmarkCacheGetStruct/encoding-4 2251696 525 ns/op 208 B/op 4 allocs/op +BenchmarkCacheGetStruct/encoding-8 2235301 520 ns/op 208 B/op 4 allocs/op +BenchmarkCacheGetStruct/encoding-16 2224682 527 ns/op 208 B/op 4 allocs/op +BenchmarkCacheGetStruct/map +BenchmarkCacheGetStruct/map 8009500 141 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/map-2 8406175 143 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/map-4 8249924 145 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/map-8 8324671 145 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/map-16 8102042 145 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/shard +BenchmarkCacheGetStruct/shard 7179788 164 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/shard-2 7332114 164 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/shard-4 6999268 174 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/shard-8 7028054 170 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/shard-16 6986014 170 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/lru +BenchmarkCacheGetStruct/lru 5818656 207 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetStruct/lru-2 5859214 204 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetStruct/lru-4 5518066 210 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetStruct/lru-8 5618907 209 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetStruct/lru-16 5617592 214 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetStruct/ristretto +BenchmarkCacheGetStruct/ristretto 7409641 158 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetStruct/ristretto-2 6809439 175 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetStruct/ristretto-4 6004058 194 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetStruct/ristretto-8 6170220 192 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetStruct/ristretto-16 6219170 190 ns/op 7 B/op 0 allocs/op +BenchmarkCacheGetStruct/memcache +BenchmarkCacheGetStruct/memcache 1412 801366 ns/op 424 B/op 14 allocs/op +BenchmarkCacheGetStruct/memcache-2 1395 845730 ns/op 424 B/op 14 allocs/op +BenchmarkCacheGetStruct/memcache-4 1454 754811 ns/op 424 B/op 14 allocs/op +BenchmarkCacheGetStruct/memcache-8 1509 754192 ns/op 425 B/op 14 allocs/op +BenchmarkCacheGetStruct/memcache-16 1354 800273 ns/op 428 B/op 14 allocs/op +BenchmarkCacheGetStruct/redis +BenchmarkCacheGetStruct/redis 553 2131603 ns/op 9720 B/op 32 allocs/op +BenchmarkCacheGetStruct/redis-2 548 2139096 ns/op 9720 B/op 32 allocs/op +BenchmarkCacheGetStruct/redis-4 537 2211997 ns/op 9720 B/op 32 allocs/op +BenchmarkCacheGetStruct/redis-8 524 2189316 ns/op 9720 B/op 32 allocs/op +BenchmarkCacheGetStruct/redis-16 548 2185637 ns/op 9720 B/op 32 allocs/op +BenchmarkCacheGetStruct/pebble +BenchmarkCacheGetStruct/pebble 1427671 796 ns/op 1248 B/op 7 allocs/op +BenchmarkCacheGetStruct/pebble-2 1448547 830 ns/op 1248 B/op 7 allocs/op +BenchmarkCacheGetStruct/pebble-4 1405844 835 ns/op 1248 B/op 7 allocs/op +BenchmarkCacheGetStruct/pebble-8 1441484 831 ns/op 1248 B/op 7 allocs/op +BenchmarkCacheGetStruct/pebble-16 1387006 827 ns/op 1248 B/op 7 allocs/op +BenchmarkCacheSetStruct +BenchmarkCacheSetStruct/encoding +BenchmarkCacheSetStruct/encoding 1000000 1625 ns/op 651 B/op 5 allocs/op +BenchmarkCacheSetStruct/encoding-2 1720123 945 ns/op 457 B/op 5 allocs/op +BenchmarkCacheSetStruct/encoding-4 1669809 705 ns/op 183 B/op 4 allocs/op +BenchmarkCacheSetStruct/encoding-8 1657442 706 ns/op 183 B/op 4 allocs/op +BenchmarkCacheSetStruct/encoding-16 1648228 709 ns/op 184 B/op 4 allocs/op +BenchmarkCacheSetStruct/map +BenchmarkCacheSetStruct/map 1000000 1280 ns/op 410 B/op 9 allocs/op +BenchmarkCacheSetStruct/map-2 1878842 1517 ns/op 341 B/op 7 allocs/op +BenchmarkCacheSetStruct/map-4 1790534 692 ns/op 263 B/op 6 allocs/op +BenchmarkCacheSetStruct/map-8 1792663 665 ns/op 263 B/op 6 allocs/op +BenchmarkCacheSetStruct/map-16 1762833 677 ns/op 263 B/op 6 allocs/op +BenchmarkCacheSetStruct/shard +BenchmarkCacheSetStruct/shard 1000000 1437 ns/op 411 B/op 9 allocs/op +BenchmarkCacheSetStruct/shard-2 1716608 830 ns/op 346 B/op 7 allocs/op +BenchmarkCacheSetStruct/shard-4 1647408 736 ns/op 263 B/op 6 allocs/op +BenchmarkCacheSetStruct/shard-8 1657657 710 ns/op 263 B/op 6 allocs/op +BenchmarkCacheSetStruct/shard-16 1651122 711 ns/op 263 B/op 6 allocs/op +BenchmarkCacheSetStruct/lru +BenchmarkCacheSetStruct/lru 1669929 717 ns/op 330 B/op 8 allocs/op +BenchmarkCacheSetStruct/lru-2 1666970 686 ns/op 330 B/op 8 allocs/op +BenchmarkCacheSetStruct/lru-4 1569268 707 ns/op 330 B/op 8 allocs/op +BenchmarkCacheSetStruct/lru-8 1569517 701 ns/op 330 B/op 8 allocs/op +BenchmarkCacheSetStruct/lru-16 1569993 720 ns/op 330 B/op 8 allocs/op +BenchmarkCacheSetStruct/ristretto +BenchmarkCacheSetStruct/ristretto 1665415 1203 ns/op 406 B/op 5 allocs/op +BenchmarkCacheSetStruct/ristretto-2 1000000 1111 ns/op 325 B/op 5 allocs/op +BenchmarkCacheSetStruct/ristretto-4 1000000 1204 ns/op 319 B/op 5 allocs/op +BenchmarkCacheSetStruct/ristretto-8 1000000 1193 ns/op 319 B/op 5 allocs/op +BenchmarkCacheSetStruct/ristretto-16 946750 1171 ns/op 324 B/op 5 allocs/op +BenchmarkCacheSetStruct/memcache +BenchmarkCacheSetStruct/memcache 1572 733672 ns/op 286 B/op 9 allocs/op +BenchmarkCacheSetStruct/memcache-2 1341 799704 ns/op 286 B/op 9 allocs/op +BenchmarkCacheSetStruct/memcache-4 1492 810459 ns/op 287 B/op 9 allocs/op +BenchmarkCacheSetStruct/memcache-8 1500 807919 ns/op 289 B/op 9 allocs/op +BenchmarkCacheSetStruct/memcache-16 1598 773923 ns/op 290 B/op 9 allocs/op +BenchmarkCacheSetStruct/redis +BenchmarkCacheSetStruct/redis 848 1312946 ns/op 9788 B/op 35 allocs/op +BenchmarkCacheSetStruct/redis-2 834 1370112 ns/op 9789 B/op 35 allocs/op +BenchmarkCacheSetStruct/redis-4 858 1367748 ns/op 9789 B/op 35 allocs/op +BenchmarkCacheSetStruct/redis-8 906 1348890 ns/op 9790 B/op 35 allocs/op +BenchmarkCacheSetStruct/redis-16 856 1377737 ns/op 9791 B/op 35 allocs/op +BenchmarkCacheSetStruct/pebble +BenchmarkCacheSetStruct/pebble 172 6891869 ns/op 179 B/op 4 allocs/op +BenchmarkCacheSetStruct/pebble-2 176 7100201 ns/op 189 B/op 4 allocs/op +BenchmarkCacheSetStruct/pebble-4 176 6765299 ns/op 417 B/op 4 allocs/op +BenchmarkCacheSetStruct/pebble-8 174 6709812 ns/op 196 B/op 4 allocs/op +BenchmarkCacheSetStruct/pebble-16 176 6872531 ns/op 207 B/op 4 allocs/op +BenchmarkCacheGetParallel +BenchmarkCacheGetParallel/encoding +BenchmarkCacheGetParallel/encoding 3755816 393 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetParallel/encoding-2 6620756 200 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetParallel/encoding-4 10706964 126 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetParallel/encoding-8 15889144 83.4 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetParallel/encoding-16 18838454 67.2 ns/op 192 B/op 2 allocs/op +BenchmarkCacheGetParallel/map +BenchmarkCacheGetParallel/map 8287477 137 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/map-2 11197053 101 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/map-4 19310756 58.6 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/map-8 28979271 37.3 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/map-16 40122621 25.3 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/shard +BenchmarkCacheGetParallel/shard 6388084 175 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/shard-2 9824578 119 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/shard-4 16162353 70.2 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/shard-8 23337940 45.7 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/shard-16 34489749 31.5 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/lru +BenchmarkCacheGetParallel/lru 5497556 219 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetParallel/lru-2 5108966 239 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetParallel/lru-4 4236541 277 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetParallel/lru-8 3867518 313 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetParallel/lru-16 3719572 323 ns/op 48 B/op 1 allocs/op +BenchmarkCacheGetParallel/ristretto +BenchmarkCacheGetParallel/ristretto 6272048 170 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/ristretto-2 10652374 103 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/ristretto-4 15653863 73.7 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/ristretto-8 17346794 64.7 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/ristretto-16 18895278 57.4 ns/op 0 B/op 0 allocs/op +BenchmarkCacheGetParallel/memcache +BenchmarkCacheGetParallel/memcache 1398 868052 ns/op 432 B/op 12 allocs/op +BenchmarkCacheGetParallel/memcache-2 2768 446176 ns/op 432 B/op 12 allocs/op +BenchmarkCacheGetParallel/memcache-4 4094 244463 ns/op 439 B/op 12 allocs/op +BenchmarkCacheGetParallel/memcache-8 8526 141027 ns/op 642 B/op 12 allocs/op +BenchmarkCacheGetParallel/memcache-16 10852 108739 ns/op 871 B/op 13 allocs/op +BenchmarkCacheGetParallel/redis +BenchmarkCacheGetParallel/redis 524 2255655 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetParallel/redis-2 933 1244186 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetParallel/redis-4 1560 790918 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetParallel/redis-8 1885 613956 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetParallel/redis-16 2102 558509 ns/op 9704 B/op 30 allocs/op +BenchmarkCacheGetParallel/pebble +BenchmarkCacheGetParallel/pebble 1377561 970 ns/op 1248 B/op 5 allocs/op +BenchmarkCacheGetParallel/pebble-2 2307592 630 ns/op 1248 B/op 5 allocs/op +BenchmarkCacheGetParallel/pebble-4 3651379 352 ns/op 1248 B/op 5 allocs/op +BenchmarkCacheGetParallel/pebble-8 5771799 222 ns/op 1248 B/op 5 allocs/op +BenchmarkCacheGetParallel/pebble-16 7370930 187 ns/op 1248 B/op 5 allocs/op +BenchmarkCacheSetParallel +BenchmarkCacheSetParallel/encoding +BenchmarkCacheSetParallel/encoding 2877936 417 ns/op 176 B/op 5 allocs/op +BenchmarkCacheSetParallel/encoding-2 3406563 377 ns/op 176 B/op 5 allocs/op +BenchmarkCacheSetParallel/encoding-4 3595508 334 ns/op 176 B/op 5 allocs/op +BenchmarkCacheSetParallel/encoding-8 3169240 378 ns/op 176 B/op 5 allocs/op +BenchmarkCacheSetParallel/encoding-16 3012076 400 ns/op 176 B/op 5 allocs/op +BenchmarkCacheSetParallel/map +BenchmarkCacheSetParallel/map 2573331 523 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/map-2 3006007 453 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/map-4 3168489 392 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/map-8 2839058 440 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/map-16 2834168 431 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/shard +BenchmarkCacheSetParallel/shard 2250218 516 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/shard-2 3581533 370 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/shard-4 3066703 415 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/shard-8 2774422 428 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/shard-16 2749574 432 ns/op 256 B/op 7 allocs/op +BenchmarkCacheSetParallel/lru +BenchmarkCacheSetParallel/lru 3272673 430 ns/op 240 B/op 6 allocs/op +BenchmarkCacheSetParallel/lru-2 4692276 278 ns/op 240 B/op 6 allocs/op +BenchmarkCacheSetParallel/lru-4 3994620 312 ns/op 240 B/op 6 allocs/op +BenchmarkCacheSetParallel/lru-8 3531354 341 ns/op 240 B/op 6 allocs/op +BenchmarkCacheSetParallel/lru-16 3414451 353 ns/op 240 B/op 6 allocs/op +BenchmarkCacheSetParallel/ristretto +BenchmarkCacheSetParallel/ristretto 2669528 456 ns/op 224 B/op 5 allocs/op +BenchmarkCacheSetParallel/ristretto-2 2214732 547 ns/op 224 B/op 5 allocs/op +BenchmarkCacheSetParallel/ristretto-4 2122172 564 ns/op 224 B/op 5 allocs/op +BenchmarkCacheSetParallel/ristretto-8 1858959 639 ns/op 224 B/op 5 allocs/op +BenchmarkCacheSetParallel/ristretto-16 1821427 656 ns/op 224 B/op 5 allocs/op +BenchmarkCacheSetParallel/memcache +BenchmarkCacheSetParallel/memcache 1395469 863 ns/op 352 B/op 8 allocs/op +BenchmarkCacheSetParallel/memcache-2 1672177 705 ns/op 352 B/op 8 allocs/op +BenchmarkCacheSetParallel/memcache-4 848569 1414 ns/op 406 B/op 8 allocs/op +BenchmarkCacheSetParallel/memcache-8 742070 1361 ns/op 402 B/op 8 allocs/op +BenchmarkCacheSetParallel/memcache-16 1346508 950 ns/op 353 B/op 8 allocs/op +BenchmarkCacheSetParallel/redis +BenchmarkCacheSetParallel/redis 100 21280373 ns/op 1526 B/op 31 allocs/op +BenchmarkCacheSetParallel/redis-2 908 1203995 ns/op 1520 B/op 31 allocs/op +BenchmarkCacheSetParallel/redis-4 901 1171409 ns/op 1520 B/op 31 allocs/op +BenchmarkCacheSetParallel/redis-8 948 1185400 ns/op 1521 B/op 31 allocs/op +BenchmarkCacheSetParallel/redis-16 852 1247485 ns/op 1523 B/op 31 allocs/op +BenchmarkCacheSetParallel/pebble +BenchmarkCacheSetParallel/pebble 174 6865631 ns/op 178 B/op 5 allocs/op +BenchmarkCacheSetParallel/pebble-2 189 6658668 ns/op 397 B/op 5 allocs/op +BenchmarkCacheSetParallel/pebble-4 360 3477886 ns/op 184 B/op 5 allocs/op +BenchmarkCacheSetParallel/pebble-8 717 1716858 ns/op 184 B/op 5 allocs/op +BenchmarkCacheSetParallel/pebble-16 1417 956456 ns/op 182 B/op 5 allocs/op +``` diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..7917369 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,67 @@ +package cache_test + +import ( + "context" + "testing" + "time" + + glru "github.com/hashicorp/golang-lru" + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/provider/lru" + "gitoa.ru/go-4devs/cache/provider/memcache" + "gitoa.ru/go-4devs/cache/provider/memory" + "gitoa.ru/go-4devs/cache/provider/pebble" + "gitoa.ru/go-4devs/cache/provider/redis" + "gitoa.ru/go-4devs/cache/provider/ristretto" + "gitoa.ru/go-4devs/cache/test" + "gitoa.ru/go-4devs/encoding/gob" +) + +type cacheBench struct { + name string + cache *cache.Cache +} + +func cacheBenchList() []cacheBench { + client, _ := glru.New(10000) + db, cl := test.PebbleDB() + + defer cl() + + return []cacheBench{ + {"encoding json", cache.New(memory.NewEncoding())}, + {"encoding gob", cache.New(memory.NewEncoding(), cache.WithDataOption(cache.WithMarshal(gob.Unmarshal, gob.Marshal)))}, + {"map", cache.New(memory.NewMap())}, + {"map shards", cache.New(memory.NewMapShard())}, + {"ristretto", cache.New(ristretto.New(test.RistrettoClient()))}, + {"lru", cache.New(lru.New(client))}, + {"redis json", cache.New(redis.New(test.RedisClient()))}, + {"redis gob", cache.New(redis.New(test.RedisClient()), cache.WithDataOption(cache.WithMarshal(gob.Unmarshal, gob.Marshal)))}, + {"memcache json", cache.New(memcache.New(test.MemcacheClient()))}, + {"memcache gob", cache.New(memcache.New(test.MemcacheClient()), cache.WithDataOption(cache.WithMarshal(gob.Unmarshal, gob.Marshal)))}, + {"pebble json", cache.New(pebble.New(db))}, + } +} + +type testStruct struct { + Key string + Val string +} + +func BenchmarkCacheGetStruct(b *testing.B) { + ctx := context.Background() + + var val testStruct + + for _, c := range cacheBenchList() { + current := c.cache + require.Nil(b, current.Set(ctx, "key", testStruct{"key", c.name}, cache.WithTTL(time.Minute))) + + b.Run(c.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = current.Get(ctx, "key", &val) + } + }) + } +} diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..683e469 --- /dev/null +++ b/cache.go @@ -0,0 +1,121 @@ +package cache + +import ( + "context" + "time" +) + +// Configure configure cache. +type Configure func(*Cache) + +// WithDataOption sets cache default data options. +func WithDataOption(do ...Option) Configure { + return func(c *Cache) { + factory := c.dataFactory + c.dataFactory = func(key, value interface{}, opts ...Option) *Item { + return factory(key, value, append(do, opts...)...) + } + } +} + +// WithDefaultNamespace sets cache default namespace. +func WithDefaultNamespace(ns, separator string) Configure { + return WithDataOption(WithNamespace(ns, separator)) +} + +// WithDefaultTTL sets cache default ttl. +func WithDefaultTTL(ttl time.Duration) Configure { + return WithDataOption(WithTTL(ttl)) +} + +// WithHandleSet add a handler for the set operation. +func WithHandleSet(m ...Handle) Configure { + return WithHandleOperation(OperationSet, m...) +} + +// WithHandleGet add a handler for the get operation. +func WithHandleGet(m ...Handle) Configure { + return WithHandleOperation(OperationGet, m...) +} + +// WithHandleDelete add a handler for the delete operation. +func WithHandleDelete(m ...Handle) Configure { + return WithHandleOperation(OperationDelete, m...) +} + +// WithHandleOperation add a handler for the operation. +func WithHandleOperation(op string, m ...Handle) Configure { + handle := ChainHandle(m...) + + return WithMiddleware(func(ctx context.Context, operation string, item *Item, next Provider) error { + if operation == op { + return handle(ctx, op, item, next) + } + + return next(ctx, operation, item) + }) +} + +// WithMiddleware sets middleware to provider. +func WithMiddleware(mw ...Handle) Configure { + return func(c *Cache) { + prov := c.provider + c.provider = chain(prov, mw...) + } +} + +// New creates new cache by provider. +func New(prov Provider, opts ...Configure) *Cache { + c := &Cache{ + provider: prov, + dataFactory: NewItem, + } + + for _, o := range opts { + o(c) + } + + return c +} + +// Cache base cache. +type Cache struct { + dataFactory func(key, value interface{}, opts ...Option) *Item + provider Provider +} + +func (c *Cache) With(opts ...Configure) *Cache { + cache := &Cache{ + provider: c.provider, + dataFactory: c.dataFactory, + } + + for _, o := range opts { + o(cache) + } + + return cache +} + +func (c *Cache) Item(key, value interface{}, opts ...Option) *Item { + return c.dataFactory(key, value, opts...) +} + +func (c *Cache) Execute(ctx context.Context, operation string, key, value interface{}, opts ...Option) error { + return c.provider(ctx, operation, c.Item(key, value, opts...)) +} + +// Set handles middlewares and sets value by key and options. +func (c *Cache) Set(ctx context.Context, key, value interface{}, opts ...Option) error { + return c.Execute(ctx, OperationSet, key, value, opts...) +} + +// Get handles middlewares and gets value by key and options. +func (c *Cache) Get(ctx context.Context, key, value interface{}, opts ...Option) error { + return c.Execute(ctx, OperationGet, key, value, opts...) +} + +// Delete handles middlewares and delete value by key and options. +func (c *Cache) Delete(ctx context.Context, key interface{}, opts ...Option) error { + return c.Execute(ctx, OperationDelete, key, nil, opts...) +} diff --git a/cache_example_test.go b/cache_example_test.go new file mode 100644 index 0000000..c8cd460 --- /dev/null +++ b/cache_example_test.go @@ -0,0 +1,226 @@ +package cache_test + +import ( + "context" + "fmt" + "time" + + glru "github.com/hashicorp/golang-lru" + prom "github.com/prometheus/client_golang/prometheus" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/mw" + "gitoa.ru/go-4devs/cache/mw/prometheus" + "gitoa.ru/go-4devs/cache/provider/lru" + "gitoa.ru/go-4devs/cache/provider/memcache" + "gitoa.ru/go-4devs/cache/provider/memory" + "gitoa.ru/go-4devs/cache/provider/redis" + "gitoa.ru/go-4devs/cache/test" + "gitoa.ru/go-4devs/encoding/gob" +) + +func ExampleCache_map() { + ctx := context.Background() + c := cache.New(memory.NewMap()) + + var cached string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "not found key", &cached), cached) + fmt.Printf("err: %v\n", c.Set(ctx, "key", "some value")) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "key", &cached), cached) + // Output: + // err: cache miss: map, value: '' + // err: + // err: , value: 'some value' +} + +func ExampleCache_encoding() { + ctx := context.Background() + c := cache.New(memory.NewEncoding(), cache.WithDataOption(cache.WithMarshal(gob.Unmarshal, gob.Marshal))) + + var cached string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "not found key", &cached), cached) + fmt.Printf("err: %v\n", c.Set(ctx, "key", "some value")) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "key", &cached), cached) + // Output: + // err: cache miss: encoding, value: '' + // err: + // err: , value: 'some value' +} + +func ExampleCache_redis() { + ctx := context.Background() + c := cache.New(redis.New(test.RedisClient())) + + var cached string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "not found redis key", &cached), cached) + fmt.Printf("err: %v\n", c.Set(ctx, "key", "some redis value", cache.WithNamespace("redis", ":"))) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "redis:key", &cached), cached) + // Output: + // err: cache miss: redis pool, value: '' + // err: + // err: , value: 'some redis value' +} + +func ExampleCache_memacache() { + ctx := context.Background() + c := cache.New(memcache.New(test.MemcacheClient()), cache.WithDataOption(cache.WithNamespace("memcache", ":"))) + + var cached string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "not found memcached key", &cached), cached) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "not:found:memcached:key", &cached), cached) + fmt.Printf("err: %v\n", c.Set(ctx, "key", "some mamcache value")) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "key", &cached), cached) + // Output: + // err: key is not valid: memcache, value: '' + // err: cache miss: memcache, value: '' + // err: + // err: , value: 'some mamcache value' +} + +func ExampleCache_lru() { + ctx := context.Background() + client, _ := glru.New(10) + + c := cache.New(lru.New(client), cache.WithDataOption(cache.WithTTL(time.Hour))) + + var cached string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "not found lru key", &cached), cached) + fmt.Printf("err: %v\n", c.Set(ctx, "key", "some lru value")) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, "key", &cached), cached) + fmt.Printf("deleted err: %v\n", c.Delete(ctx, "key")) + // Output: + // err: cache miss: lru, value: '' + // err: + // err: , value: 'some lru value' + // deleted err: +} + +func ExampleCache_withNamespace() { + ctx := context.Background() + c := cache.New(provider(), cache.WithDataOption( + cache.WithNamespace("prefix", ":"), + cache.WithTTL(time.Hour), + )) + + var cached, cached2 string + + fmt.Printf("prefix err: %v, value: '%v'\n", c.Get(ctx, "key", &cached), cached) + fmt.Printf("prefix err: %v\n", c.Set(ctx, "key", "some value", cache.WithTTL(time.Minute))) + fmt.Printf("prefix2 err: %v\n", c.Set(ctx, "key", "some value2", cache.WithNamespace("prefix2", ":"))) + fmt.Printf("prefix err: %v, value: '%v'\n", c.Get(ctx, "key", &cached), cached) + fmt.Printf("prefix2 err: %v, value: '%v'\n", c.Get(ctx, "key", &cached2, cache.WithNamespace("prefix2", ":")), cached2) + // Output: + // prefix err: cache miss: map, value: '' + // prefix err: + // prefix2 err: + // prefix err: , value: 'some value' + // prefix2 err: , value: 'some value2' +} + +func ExampleCache_withFallback() { + ctx := context.Background() + c := cache.New(provider(), mw.WithFallback( + func(ctx context.Context, key, value interface{}) error { + fmt.Printf("loaded key: %#v\n", key) + + return cache.TypeAssert("some loaded data", value) + }, + func(i *cache.Item, e error) bool { + return e != nil + }, + )) + + var cached, cached2 string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached), cached) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached2), cached2) + // Output: + // loaded key: 1 + // err: , value: 'some loaded data' + // err: , value: 'some loaded data' +} + +func ExampleCache_clearByContext() { + type ctxKey int + + var ( + requestID ctxKey = 1 + cached, cached2 string + ) + + ctx, cancel := context.WithCancel(context.WithValue(context.Background(), requestID, "unique ctx key")) + ctx2 := context.WithValue(context.Background(), requestID, "unique ctx key2") + c := cache.New(provider(), + mw.WithClearByContext(requestID), + cache.WithDataOption(cache.WithNamespace("clear_by_ctx", "")), + ) + + fmt.Printf("err: %v\n", c.Set(ctx, 1, "some ctx loaded data", cache.WithTTL(time.Hour))) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached), cached) + cancel() + time.Sleep(time.Millisecond) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx2, 1, &cached2), cached2) + // Output: + // err: + // err: , value: 'some ctx loaded data' + // err: cache miss: map, value: '' +} + +func ExampleCache_clearByTTL() { + ctx := context.Background() + c := cache.New(provider(), + mw.WithClearByTTL(), + cache.WithDataOption(cache.WithNamespace("clear_by_ttl", "")), + ) + + var cached, cached2 string + + fmt.Printf("err: %v\n", c.Set(ctx, 1, "some ttl loaded data", cache.WithTTL(time.Microsecond*200))) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached), cached) + time.Sleep(time.Second) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached2), cached2) + // Output: + // err: + // err: , value: 'some ttl loaded data' + // err: cache miss: map, value: '' +} + +func ExampleCache_withMetrics() { + ctx := context.Background() + cacheLabel := "cache_label" + c := cache.New(provider(), + mw.WithMetrics(prometheus.Metrics{}, mw.LabelName(cacheLabel)), + cache.WithDataOption(cache.WithNamespace("metrics", ":")), + ) + + var cached, cached2 string + + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached), cached) + fmt.Printf("err: %v\n", c.Set(ctx, 1, "cached")) + fmt.Printf("err: %v, value: '%v'\n", c.Get(ctx, 1, &cached2), cached2) + + mfs, _ := prom.DefaultGatherer.Gather() + for _, mf := range mfs { + for _, metric := range mf.GetMetric() { + label := metric.GetLabel() + if len(label) > 0 && metric.Counter != nil { + fmt.Printf("name:%s, label:%s, value: %.0f\n", *mf.Name, *label[0].Value, mf.GetMetric()[0].Counter.GetValue()) + } + } + } + + // Output: + // err: cache miss: map, value: '' + // err: + // err: , value: 'cached' + // name:cache_hit_total, label:cache_label, value: 1 + // name:cache_miss_total, label:cache_label, value: 1 +} + +func provider() cache.Provider { + return memory.NewMap() +} diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 0000000..5f487dd --- /dev/null +++ b/cache_test.go @@ -0,0 +1,208 @@ +package cache_test + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/test" +) + +func TestCache_Get(t *testing.T) { + t.Parallel() + + ctx := context.Background() + pro := test.NewProviderMock(t, test.WithGet(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + u := test.NewUser(1) + + return cache.TypeAssert(u, d.Value) + } + })) + cache := cache.New(pro) + + var user test.User + + require.Nil(t, cache.Get(ctx, 1, &user)) + require.Equal(t, test.NewUser(1), user) +} + +func TestCache_Set(t *testing.T) { + t.Parallel() + + ctx := context.Background() + pro := test.NewProviderMock(t, test.WithSet(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + require.Equal(t, 1, d.Key.Key) + require.Equal(t, test.NewUser(1), d.Value) + require.Equal(t, "1", d.Key.String()) + + return nil + } + })) + cache := cache.New(pro) + + require.Nil(t, cache.Set(ctx, 1, test.NewUser(1))) +} + +func TestCache_Delete(t *testing.T) { + t.Parallel() + + ctx := context.Background() + pro := test.NewProviderMock(t, test.WithDelete(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + require.Equal(t, 1, d.Key.Key) + require.Empty(t, d.Value) + require.Equal(t, "1", d.Key.String()) + + return nil + } + })) + cache := cache.New(pro) + + require.Nil(t, cache.Delete(ctx, 1)) +} + +func TestCache_Get_withMiddleware(t *testing.T) { + t.Parallel() + + ctx := context.Background() + pro := test.NewProviderMock(t, + test.WithSet(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + require.Equal(t, 2, d.Key.Key) + require.Equal(t, test.NewUser(2), d.Value) + require.Equal(t, "mw_prefix::_2", d.Key.String()) + require.Equal(t, time.Minute, d.TTL) + + return nil + } + }), + test.WithGet(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + require.Equal(t, 2, d.Key.Key) + require.Equal(t, "mw_prefix----2", d.Key.String()) + + return nil + } + }), + ) + c := cache.New(pro, + cache.WithDataOption( + func(i *cache.Item) { + i.Key.Prefix = "mw_prefix" + }, + cache.WithTTL(time.Hour), + ), + cache.WithHandleSet( + func(ctx context.Context, op string, d *cache.Item, n cache.Provider) error { + d.Key.Separator = "::" + + return n(ctx, op, d) + }, func(ctx context.Context, op string, d *cache.Item, n cache.Provider) error { + d.Key.Separator += "_" + + return n(ctx, op, d) + }), + cache.WithHandleGet(func(ctx context.Context, op string, d *cache.Item, n cache.Provider) error { + d.Key.Separator = "----" + + return n(ctx, op, d) + }), + ) + + var user test.User + + require.Nil(t, c.Set(ctx, 2, test.NewUser(2), cache.WithTTL(time.Minute))) + require.Nil(t, c.Get(ctx, 2, &user)) +} + +func TestCacheWith(t *testing.T) { + t.Parallel() + + ctx := context.Background() + pro := test.NewProviderMock(t, + test.WithGet(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + switch d.Key.Key.(int) { + case 1: + require.Equal(t, "ns:1", d.Key.String()) + case 2: + require.Equal(t, "new_ns_2", d.Key.String()) + default: + t.Errorf("key %v no allowed", d.Key.Key) + } + + return nil + } + }), + test.WithSet(func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, d *cache.Item) error { + switch d.Key.Key.(int) { + case 1: + require.Equal(t, time.Hour, d.TTL) + case 2: + require.Equal(t, time.Minute, d.TTL) + default: + t.Errorf("key %v no allowed", d.Key.Key) + } + + return nil + } + }), + ) + + var ( + cntSetCache2, cntGetCache1 int32 + val1, val2 string + ) + + cache1 := cache.New(pro, + cache.WithHandleGet(func(ctx context.Context, operation string, item *cache.Item, next cache.Provider) error { + atomic.AddInt32(&cntGetCache1, 1) + + return next(ctx, operation, item) + }), + cache.WithDataOption( + cache.WithNamespace("ns", ":"), + cache.WithTTL(time.Hour), + ), + ) + cache2 := cache1.With( + cache.WithHandleSet(func(ctx context.Context, operation string, item *cache.Item, next cache.Provider) error { + atomic.AddInt32(&cntSetCache2, 1) + + return next(ctx, operation, item) + }), + cache.WithDataOption( + cache.WithNamespace("new_ns", "_"), + cache.WithTTL(time.Minute), + ), + ) + + require.NoError(t, cache1.Get(ctx, 1, &val1)) + require.NoError(t, cache2.Get(ctx, 2, &val2)) + + require.NoError(t, cache1.Set(ctx, 1, val1)) + require.NoError(t, cache2.Set(ctx, 2, val2)) + + require.Equal(t, int32(1), cntSetCache2) + require.Equal(t, int32(2), cntGetCache1) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dc7ea68 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + redis: + container_name: 4devs_cache_redis + image: redis:latest + ports: + - "127.0.0.1:6379:6379" + memcached: + container_name: 4devs_cache_memcached + image: memcached:latest + ports: + - "11211:11211" diff --git a/error.go b/error.go new file mode 100644 index 0000000..222cc41 --- /dev/null +++ b/error.go @@ -0,0 +1,33 @@ +package cache + +import ( + "errors" + "fmt" +) + +// Cached errors. +var ( + ErrCacheMiss = errors.New("cache miss") + ErrCacheExpired = errors.New("cache expired") + ErrSourceNotValid = errors.New("source is not valid") + ErrKeyNotValid = errors.New("key is not valid") + ErrTargetNil = errors.New("target is nil") + ErrOperationNotAllwed = errors.New("operation not allowed") +) + +var _ error = NewErrorTarget(nil) + +// NewErrorTarget creates new target error. +func NewErrorTarget(target interface{}) ErrorTarget { + return ErrorTarget{target: target} +} + +// ErrorTarget errs target is not a settable. +type ErrorTarget struct { + target interface{} +} + +// ErrorTarget errors. +func (e ErrorTarget) Error() string { + return fmt.Sprintf("target is not a settable %T", e.target) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3ba411e --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module gitoa.ru/go-4devs/cache + +go 1.15 + +require ( + github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b + github.com/cockroachdb/pebble v0.0.0-20200916123908-284ba0668391 + github.com/dgraph-io/ristretto v0.0.3 + github.com/gomodule/redigo v1.8.2 + github.com/hashicorp/golang-lru v0.5.4 + github.com/mailru/easyjson v0.7.6 + github.com/prometheus/client_golang v1.7.1 + github.com/stretchr/testify v1.6.1 + gitoa.ru/go-4devs/encoding v0.0.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d9f64a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,185 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA= +github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/pebble v0.0.0-20200916123908-284ba0668391 h1:iCTxMsf8E/rUVDw+tqgOHnYBJiXg4dU9psCv4jjI9n0= +github.com/cockroachdb/pebble v0.0.0-20200916123908-284ba0668391/go.mod h1:hU7vhtrqonEphNF+xt8/lHdaBprxmV1h8BOGrd9XwmQ= +github.com/cockroachdb/redact v0.0.0-20200622112456-cd282804bbd3 h1:2+dpIJzYMSbLi0587YXpi8tOJT52qCOI/1I0UNThc/I= +github.com/cockroachdb/redact v0.0.0-20200622112456-cd282804bbd3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI= +github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.2-0.20190904063534-ff6b7dc882cf h1:gFVkHXmVAhEbxZVDln5V9GKrLaluNoFHDbrZwAWZgws= +github.com/golang/snappy v0.0.2-0.20190904063534-ff6b7dc882cf/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gitoa.ru/go-4devs/encoding v0.0.3 h1:Rqjs0lsnco5PfMZ4iP3+TFYd/dHbG8FqMXxmbgTdfq4= +gitoa.ru/go-4devs/encoding v0.0.3/go.mod h1:TPHAyATVNvRFt4Z+4beRhg9+E/VpXxwpniZfzkH9258= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200513190911-00229845015e h1:rMqLP+9XLy+LdbCXHjJHAmTfXCr93W7oruWA6Hq1Alc= +golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0= +gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/item.go b/item.go new file mode 100644 index 0000000..7e90e96 --- /dev/null +++ b/item.go @@ -0,0 +1,147 @@ +package cache + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "gitoa.ru/go-4devs/encoding" +) + +var _ fmt.Stringer = (*Key)(nil) + +// Option ffor the configuration item. +type Option func(*Item) + +// WithNamespace sets prefix and separator. +func WithNamespace(prefix, sep string) Option { + return func(d *Item) { + d.Key.Prefix = prefix + d.Key.Separator = sep + } +} + +// WithTTL sets ttl. +func WithTTL(ttl time.Duration) Option { + return func(d *Item) { + d.TTL = ttl + } +} + +// WithMarshal sets marshal and unmarshal. +func WithMarshal(unmarshal encoding.Unmarshal, marshal encoding.Marshal) Option { + return func(d *Item) { + d.unmarshal = unmarshal + d.marshal = marshal + } +} + +// NewItem creates and configure new item. +func NewItem(key, value interface{}, opts ...Option) *Item { + item := &Item{ + Key: Key{ + Key: key, + Prefix: "", + Separator: "", + }, + Value: value, + TTL: 0, + unmarshal: json.Unmarshal, + marshal: json.Marshal, + } + + for _, opt := range opts { + opt(item) + } + + return item +} + +// Item to pass to the provider. +type Item struct { + Key Key + Value interface{} + TTL time.Duration + unmarshal encoding.Unmarshal + marshal encoding.Marshal +} + +func (i *Item) With(key, val interface{}, opts ...Option) *Item { + return NewItem(key, val, append(i.Options(), opts...)...) +} + +// IsExpired checks expired item. +func (i *Item) IsExpired() bool { + return i.TTL < 0 +} + +func (i *Item) Marshal() ([]byte, error) { + return i.marshal(i.Value) +} + +func (i *Item) Unmarshal(data []byte) error { + return i.unmarshal(data, i.Value) +} + +// Options gets item options. +func (i *Item) Options() []Option { + opts := []Option{WithTTL(i.TTL), WithMarshal(i.unmarshal, i.marshal)} + + if i.Key.Prefix != "" { + opts = append(opts, WithNamespace(i.Key.Prefix, i.Key.Separator)) + } + + return opts +} + +// TTLInSecond to set the ttl in seconds. +func (i *Item) TTLInSecond(in int64) { + i.TTL = time.Second * time.Duration(in) +} + +// Expired get the time when the ttl is outdated. +func (i *Item) Expired() time.Time { + return time.Now().Add(i.TTL) +} + +// Key with prefix and separator. +type Key struct { + Key interface{} + Prefix string + Separator string +} + +func (k Key) Value() interface{} { + if v, ok := k.Key.(interface{ Value() interface{} }); ok { + return v.Value() + } + + return k.Key +} + +// String returns a formatted key. +func (k Key) String() string { + if k.Prefix != "" { + return fmt.Sprint(k.Prefix, k.Separator, k.Key) + } + + switch v := k.Key.(type) { + case int: + return strconv.Itoa(v) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + case string: + return v + default: + return fmt.Sprint(v) + } +} diff --git a/item/expired.go b/item/expired.go new file mode 100644 index 0000000..35cf2ab --- /dev/null +++ b/item/expired.go @@ -0,0 +1,56 @@ +package item + +import ( + "fmt" + "time" + + "gitoa.ru/go-4devs/cache" +) + +//go:generate easyjson + +//easyjson:json +type expiredByte struct { + Data []byte `json:"d"` + Expired time.Time `json:"e"` +} + +func MarshalExpired(item *cache.Item) ([]byte, error) { + var ( + e expiredByte + err error + ) + + e.Data, err = item.Marshal() + if err != nil { + return nil, fmt.Errorf("failed marshal expired: %w", err) + } + + if item.TTL > 0 { + e.Expired = item.Expired() + } + + return e.MarshalJSON() +} + +func UnmarshalExpired(item *cache.Item, d []byte) error { + var e expiredByte + + if err := e.UnmarshalJSON(d); err != nil { + return err + } + + if !e.Expired.IsZero() { + item.TTL = time.Until(e.Expired) + } + + if item.IsExpired() { + return cache.ErrCacheExpired + } + + if err := item.Unmarshal(e.Data); err != nil { + return fmt.Errorf("failed unmarshal expired: %w", err) + } + + return nil +} diff --git a/item/expired_easyjson.go b/item/expired_easyjson.go new file mode 100644 index 0000000..caea1d7 --- /dev/null +++ b/item/expired_easyjson.go @@ -0,0 +1,99 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package item + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonB8950805DecodeGitoaRuGo4devsCacheItem(in *jlexer.Lexer, out *expiredByte) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "d": + if in.IsNull() { + in.Skip() + out.Data = nil + } else { + out.Data = in.Bytes() + } + case "e": + if data := in.Raw(); in.Ok() { + in.AddError((out.Expired).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonB8950805EncodeGitoaRuGo4devsCacheItem(out *jwriter.Writer, in expiredByte) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"d\":" + out.RawString(prefix[1:]) + out.Base64Bytes(in.Data) + } + { + const prefix string = ",\"e\":" + out.RawString(prefix) + out.Raw((in.Expired).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v expiredByte) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonB8950805EncodeGitoaRuGo4devsCacheItem(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v expiredByte) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonB8950805EncodeGitoaRuGo4devsCacheItem(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *expiredByte) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonB8950805DecodeGitoaRuGo4devsCacheItem(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *expiredByte) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonB8950805DecodeGitoaRuGo4devsCacheItem(l, v) +} diff --git a/mw/fallback.go b/mw/fallback.go new file mode 100644 index 0000000..151b152 --- /dev/null +++ b/mw/fallback.go @@ -0,0 +1,134 @@ +package mw + +import ( + "context" + "fmt" + "sync" + + "gitoa.ru/go-4devs/cache" +) + +type Fallback func(ctx context.Context, key, value interface{}) error + +type Getter func(ctx context.Context, key interface{}) (interface{}, error) + +// HandleByErr checks if cache return err. +func HandleByErr(_ *cache.Item, err error) bool { + return err != nil +} + +// LockFallback locks run fallback by item key. +func LockFallback(fallback Fallback) Fallback { + var mu sync.Mutex + + type entry struct { + item interface{} + err error + } + + keys := make(map[interface{}]chan entry) + + return func(ctx context.Context, key, value interface{}) error { + mu.Lock() + if _, ok := keys[key]; !ok { + keys[key] = make(chan entry, 1) + mu.Unlock() + + err := fallback(ctx, key, value) + keys[key] <- entry{ + item: value, + err: err, + } + + defer func() { + close(keys[key]) + delete(keys, key) + }() + + return err + } + mu.Unlock() + + entry := <-keys[key] + if entry.err != nil { + return entry.err + } + + if err := cache.TypeAssert(entry.item, value); err != nil { + return fmt.Errorf("%w: assert value", err) + } + + return nil + } +} + +// WithFallback sets fallback when cache handle success and set result in cache. +func WithFallback(fallback Fallback, isHandleFallback func(*cache.Item, error) bool) cache.Configure { + return cache.WithHandleGet(func(ctx context.Context, op string, item *cache.Item, next cache.Provider) error { + err := next(ctx, op, item) + if isHandleFallback(item, err) { + if ferr := fallback(ctx, item.Key.Key, item.Value); ferr != nil { + return ferr + } + + return next(ctx, cache.OperationSet, item) + } + + return err + }) +} + +// WithLockGetter sets values from getter when cache handle success and set result in cache. +func WithLockGetter(getter Getter, isHandle func(*cache.Item, error) bool) cache.Configure { + var mu sync.Mutex + + type entry struct { + value interface{} + err error + } + + keys := make(map[cache.Key]chan entry) + + return cache.WithHandleGet(func(ctx context.Context, op string, item *cache.Item, next cache.Provider) error { + if err := next(ctx, op, item); !isHandle(item, err) { + return err + } + + mu.Lock() + if _, ok := keys[item.Key]; !ok { + keys[item.Key] = make(chan entry, 1) + mu.Unlock() + value, gerr := getter(ctx, item.Key.Value()) + keys[item.Key] <- entry{ + value: value, + err: gerr, + } + + defer func() { + close(keys[item.Key]) + delete(keys, item.Key) + }() + if gerr != nil { + return gerr + } + + if err := cache.TypeAssert(value, item.Value); err != nil { + return fmt.Errorf("lock failed assert type: %w", err) + } + + return nil + } + mu.Unlock() + + entry := <-keys[item.Key] + if entry.err != nil { + return entry.err + } + + if err := cache.TypeAssert(entry.value, item.Value); err != nil { + return fmt.Errorf("lock failed assert type: %w", err) + } + + return next(ctx, cache.OperationSet, item) + }) +} diff --git a/mw/fallback_test.go b/mw/fallback_test.go new file mode 100644 index 0000000..a1982ff --- /dev/null +++ b/mw/fallback_test.go @@ -0,0 +1,221 @@ +package mw_test + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/mw" + "gitoa.ru/go-4devs/cache/test" +) + +var ( + errFallback = errors.New("fallback error") + errKey = errors.New("unexpected key") +) + +func TestWithFallback(t *testing.T) { + t.Parallel() + + ctx := context.Background() + key1 := "fallback:key1" + key2 := "fb:key2" + + prov := test.NewProviderMock(t, + test.WithGet(cacheGetMiss), + test.WithSet(cacheSetMiss(map[interface{}]test.User{ + key1: test.NewUser(1), + key2: test.NewUser(2), + })), + ) + c := cache.New(prov, mw.WithFallback( + func(ctx context.Context, key, value interface{}) error { + switch key.(string) { + case key1: + *value.(*test.User) = test.NewUser(1) + case key2: + *value.(*test.User) = test.NewUser(2) + default: + t.Errorf("unexpected key: %s", key) + } + + return nil + }, + mw.HandleByErr, + )) + + var user test.User + + require.Nil(t, c.Get(ctx, key1, &user)) + require.Equal(t, test.NewUser(1), user) + + require.Nil(t, c.Get(ctx, key2, &user, cache.WithNamespace("namespace", ":"))) + require.Equal(t, test.NewUser(2), user) +} + +func TestLockFallback(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + var ( + val1, val2, val3 string + wg sync.WaitGroup + cnt int32 + ) + + fallback := mw.LockFallback(func(ctx context.Context, key, value interface{}) error { + time.Sleep(time.Second) + atomic.AddInt32(&cnt, 1) + *value.(*string) = fmt.Sprintf("value:%v", cnt) + + return nil + }) + + wg.Add(2) + + go func() { + assert.Nil(t, fallback(ctx, 1, &val1)) + wg.Done() + }() + go func() { + assert.Nil(t, fallback(ctx, 1, &val2)) + wg.Done() + }() + wg.Wait() + + require.Equal(t, "value:1", val1) + require.Equal(t, "value:1", val2) + + assert.Nil(t, fallback(ctx, 1, &val3)) + require.Equal(t, "value:2", val3) +} + +func TestLockFallback_Error(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + var ( + val1, val2, val3 string + wg sync.WaitGroup + cnt int32 + ) + + fallback := mw.LockFallback(func(ctx context.Context, key, value interface{}) error { + time.Sleep(time.Second) + atomic.AddInt32(&cnt, 1) + + return fmt.Errorf("%w:%v", errFallback, cnt) + }) + + wg.Add(2) + + go func() { + assert.EqualError(t, fallback(ctx, 1, &val1), "fallback error:1") + wg.Done() + }() + go func() { + assert.EqualError(t, fallback(ctx, 1, &val2), "fallback error:1") + wg.Done() + }() + wg.Wait() + + require.Empty(t, val1) + require.Empty(t, val2) + + assert.EqualError(t, fallback(ctx, 1, val3), "fallback error:2") + require.Empty(t, val3) +} + +func TestWithLockGetter(t *testing.T) { + t.Parallel() + + ctx := context.Background() + key1 := "getter:key1" + + var cnt int32 + + prov := test.NewProviderMock(t, + test.WithGet(cacheGetMiss), + test.WithSet(cacheSetMiss( + map[interface{}]test.User{ + key1: test.NewUser(1), + }, + )), + ) + c := cache.New(prov, + cache.WithDataOption( + cache.WithNamespace("gn", ":"), + ), + mw.WithLockGetter( + func(ctx context.Context, key interface{}) (interface{}, error) { + atomic.AddInt32(&cnt, 1) + time.Sleep(time.Second / 2) + switch key.(string) { + case key1: + return test.NewUser(1), nil + case "key2": + return test.NewUser(2), nil + } + + return nil, fmt.Errorf("%w: key '%v'", errKey, key) + }, + mw.HandleByErr, + )) + + var ( + user1, user2 test.User + wg sync.WaitGroup + ) + + wg.Add(2) + + go func() { + require.Nil(t, c.Get(ctx, key1, &user1)) + wg.Done() + }() + go func() { + require.Nil(t, c.Get(ctx, key1, &user2)) + wg.Done() + }() + + wg.Wait() + + require.Equal(t, test.NewUser(1), user1) + require.Equal(t, test.NewUser(1), user2) + require.Equal(t, int32(1), cnt) +} + +func cacheGetMiss(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, item *cache.Item) error { + return cache.ErrCacheMiss + } +} + +func cacheSetMiss(items map[interface{}]test.User) func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + return func(t *testing.T) func(ctx context.Context, item *cache.Item) error { + t.Helper() + + return func(ctx context.Context, item *cache.Item) error { + if value, ok := items[item.Key.Key]; ok { + require.Equal(t, &value, item.Value) + + return nil + } + + t.Errorf("unexpected key %v", item.Key.String()) + + return nil + } + } +} diff --git a/mw/gc.go b/mw/gc.go new file mode 100644 index 0000000..1fb5480 --- /dev/null +++ b/mw/gc.go @@ -0,0 +1,78 @@ +package mw + +import ( + "context" + "fmt" + "sync" + "time" + + "gitoa.ru/go-4devs/cache" +) + +type key struct { + key interface{} + ctxPrefix string +} + +func (k key) Value() interface{} { + return k.key +} + +func (k key) String() string { + return fmt.Sprint(k.ctxPrefix, k.key) +} + +// WithClearByContext clear cache if context done. +func WithClearByContext(ctxKey interface{}) cache.Configure { + operation := func(ctx context.Context, op string, item *cache.Item, next cache.Provider) error { + ctxPrefix, ok := ctx.Value(ctxKey).(string) + if !ok { + return fmt.Errorf("%w: must be unique ctx key", cache.ErrKeyNotValid) + } + + k := item.Key.Key + item.Key.Key = key{ + key: k, + ctxPrefix: ctxPrefix, + } + + return next(ctx, op, item) + } + + return cache.WithMiddleware( + func(ctx context.Context, op string, item *cache.Item, next cache.Provider) error { + if op == cache.OperationSet { + go func(ctx context.Context, item *cache.Item) { + <-ctx.Done() + _ = next(ctx, cache.OperationDelete, item) + }(ctx, item) + } + + return operation(ctx, op, item, next) + }) +} + +// WithClearByTTL clear cache by key after ttl. +func WithClearByTTL() cache.Configure { + keys := make(map[cache.Key]*time.Timer) + mu := sync.Mutex{} + + return cache.WithHandleSet(func(ctx context.Context, op string, item *cache.Item, next cache.Provider) error { + if item.TTL > 0 { + go func() { + mu.Lock() + defer mu.Unlock() + if t, ok := keys[item.Key]; ok { + t.Reset(item.TTL) + } else { + keys[item.Key] = time.AfterFunc(item.TTL, func() { + _ = next(ctx, cache.OperationDelete, item) + delete(keys, item.Key) + }) + } + }() + } + + return next(ctx, op, item) + }) +} diff --git a/mw/gc_test.go b/mw/gc_test.go new file mode 100644 index 0000000..528928b --- /dev/null +++ b/mw/gc_test.go @@ -0,0 +1,68 @@ +package mw_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/mw" + "gitoa.ru/go-4devs/cache/provider/memory" +) + +func TestWithClearByTTL(t *testing.T) { + t.Parallel() + + ctx := context.Background() + gcMap := cache.New(memory.NewMap(), mw.WithClearByTTL()) + cacheMap := cache.New(memory.NewMap()) + + var ( + value string + err error + ) + + require.NoError(t, gcMap.Set(ctx, "keys", "value", cache.WithTTL(time.Second/3))) + require.NoError(t, cacheMap.Set(ctx, "keys", "value", cache.WithTTL(time.Second/3))) + time.Sleep(time.Second) + + err = gcMap.Get(ctx, "keys", &value) + require.EqualError(t, err, cache.ErrCacheMiss.Error()+": map") + + err = cacheMap.Get(ctx, "keys", &value) + require.EqualError(t, err, cache.ErrCacheExpired.Error()+": map") + + require.NoError(t, gcMap.Set(ctx, "keys", "value", cache.WithTTL(time.Second/2))) + time.AfterFunc(time.Second/3, func() { + require.NoError(t, gcMap.Set(ctx, "keys", "value", cache.WithTTL(time.Second))) + }) + time.Sleep(time.Second / 2) + require.NoError(t, gcMap.Get(ctx, "keys", &value)) + require.Equal(t, value, "value") +} + +func TestWithClearByContext(t *testing.T) { + t.Parallel() + + type ctxKey int + + var ( + requestID ctxKey = 1 + data string + ) + + ctx1, cancel1 := context.WithCancel(context.WithValue(context.Background(), requestID, "request1")) + ctx2, cancel2 := context.WithCancel(context.WithValue(context.Background(), requestID, "request2")) + + cacheMap := cache.New(memory.NewMap(), mw.WithClearByContext(requestID)) + + require.NoError(t, cacheMap.Set(ctx1, "key", "value")) + require.EqualError(t, cacheMap.Get(ctx2, "key", &data), "cache miss: map") + require.NoError(t, cacheMap.Get(ctx1, "key", &data)) + cancel1() + + time.Sleep(time.Millisecond) + require.EqualError(t, cacheMap.Get(ctx1, "key", &data), "cache miss: map") + cancel2() +} diff --git a/mw/metrics.go b/mw/metrics.go new file mode 100644 index 0000000..1bc4b4b --- /dev/null +++ b/mw/metrics.go @@ -0,0 +1,60 @@ +package mw + +import ( + "context" + "errors" + "time" + + "gitoa.ru/go-4devs/cache" +) + +// Metrics interface for middleware. +type Metrics interface { + Hit(label string) + Miss(label string) + Expired(label string) + Err(label string, operation string) + Observe(label string, operation string, start time.Time) +} + +// LabelName sets static name. +func LabelName(name string) func(ctx context.Context, item *cache.Item) string { + return func(_ context.Context, _ *cache.Item) string { + return name + } +} + +// LabelPreficKey gets lebale by item prefix. +func LabelPreficKey(ctx context.Context, item *cache.Item) string { + return item.Key.Prefix +} + +// WithMetrics cache middleware metrics. +func WithMetrics(m Metrics, labelCallback func(ctx context.Context, item *cache.Item) string) cache.Configure { + return cache.WithMiddleware( + func(ctx context.Context, op string, item *cache.Item, next cache.Provider) error { + label := labelCallback(ctx, item) + start := time.Now() + err := next(ctx, op, item) + m.Observe(label, op, start) + if err != nil { + switch { + case errors.Is(err, cache.ErrCacheMiss): + m.Miss(label) + case errors.Is(err, cache.ErrCacheExpired): + m.Expired(label) + default: + m.Err(label, op) + } + + return err + } + + if op == cache.OperationGet { + m.Hit(label) + } + + return nil + }, + ) +} diff --git a/mw/prometheus/metrics.go b/mw/prometheus/metrics.go new file mode 100644 index 0000000..29faf37 --- /dev/null +++ b/mw/prometheus/metrics.go @@ -0,0 +1,93 @@ +package prometheus + +import ( + "time" + + metrics "github.com/prometheus/client_golang/prometheus" + "gitoa.ru/go-4devs/cache/mw" +) + +const ( + labelSet = "label" + labelOperation = "operation" +) + +//nolint: gochecknoglobals +var ( + hitCount = metrics.NewCounterVec( + metrics.CounterOpts{ + Name: "cache_hit_total", + Help: "Counter of hits cache.", + }, + []string{labelSet}, + ) + missCount = metrics.NewCounterVec( + metrics.CounterOpts{ + Name: "cache_miss_total", + Help: "Counter of misses cache.", + }, + []string{labelSet}, + ) + expiredCount = metrics.NewCounterVec( + metrics.CounterOpts{ + Name: "cache_expired_total", + Help: "Counter of expired items in cache.", + }, + []string{labelSet}, + ) + errorsCount = metrics.NewCounterVec( + metrics.CounterOpts{ + Name: "cache_errors_total", + Help: "Counter of errors in cache.", + }, + []string{labelSet, labelOperation}, + ) + responseTime = metrics.NewHistogramVec( + metrics.HistogramOpts{ + Name: "cache_request_duration_seconds", + Help: "Histogram of RT for the request cache (seconds).", + }, + []string{labelSet, labelOperation}, + ) +) + +//nolint: gochecknoinits +func init() { + metrics.MustRegister( + hitCount, + missCount, + expiredCount, + errorsCount, + responseTime, + ) +} + +var _ mw.Metrics = Metrics{} + +// Metrics prometeus. +type Metrics struct{} + +// Miss inc miss error cache. +func (m Metrics) Miss(label string) { + missCount.WithLabelValues(label).Inc() +} + +// Hit increment hit cache. +func (m Metrics) Hit(label string) { + hitCount.WithLabelValues(label).Inc() +} + +// Expired increment error expired. +func (m Metrics) Expired(label string) { + expiredCount.WithLabelValues(label).Inc() +} + +// Err increment base undefined error. +func (m Metrics) Err(label string, operation string) { + errorsCount.WithLabelValues(label, operation).Inc() +} + +// Observe time from start. +func (m Metrics) Observe(label string, operation string, start time.Time) { + responseTime.WithLabelValues(label, operation).Observe(float64(time.Since(start)) / float64(time.Second)) +} diff --git a/operation.go b/operation.go new file mode 100644 index 0000000..cab36ea --- /dev/null +++ b/operation.go @@ -0,0 +1,21 @@ +package cache + +import "context" + +// available operation. +const ( + OperationGet = "get" + OperationSet = "set" + OperationDelete = "delete" +) + +// OperationProvider creating a provider based on available operations. +func OperationProvider(prov map[string]func(ctx context.Context, item *Item) error) Provider { + return func(ctx context.Context, operation string, item *Item) error { + if method, ok := prov[operation]; ok { + return method(ctx, item) + } + + return ErrOperationNotAllwed + } +} diff --git a/provider.go b/provider.go new file mode 100644 index 0000000..bdf5c09 --- /dev/null +++ b/provider.go @@ -0,0 +1,50 @@ +package cache + +import ( + "context" +) + +// Provider for different types of cache, in memory, lru, redis. +type Provider func(ctx context.Context, operation string, item *Item) error + +// Handle middleware before/after provider. +type Handle func(ctx context.Context, operation string, item *Item, next Provider) error + +// ChainHandle chain handle middleware. +func ChainHandle(handle ...Handle) Handle { + if n := len(handle); n > 1 { + lastI := n - 1 + + return func(ctx context.Context, operation string, item *Item, next Provider) error { + var ( + chainHandler func(context.Context, string, *Item) error + curI int + ) + + chainHandler = func(currentCtx context.Context, currentOperation string, currentData *Item) error { + if curI == lastI { + return next(currentCtx, currentOperation, currentData) + } + curI++ + err := handle[curI](currentCtx, currentOperation, currentData, chainHandler) + curI-- + + return err + } + + return handle[0](ctx, operation, item, chainHandler) + } + } + + return handle[0] +} + +func chain(init Provider, handleFunc ...Handle) Provider { + if len(handleFunc) > 0 { + return func(ctx context.Context, operation string, item *Item) error { + return ChainHandle(handleFunc...)(ctx, operation, item, init) + } + } + + return init +} diff --git a/provider/bench_provider_test.go b/provider/bench_provider_test.go new file mode 100644 index 0000000..730cd0b --- /dev/null +++ b/provider/bench_provider_test.go @@ -0,0 +1,194 @@ +package provider_test + +import ( + "context" + "crypto/rand" + "fmt" + "math" + "math/big" + "testing" + "time" + + glru "github.com/hashicorp/golang-lru" + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/provider/lru" + "gitoa.ru/go-4devs/cache/provider/memcache" + "gitoa.ru/go-4devs/cache/provider/memory" + "gitoa.ru/go-4devs/cache/provider/pebble" + "gitoa.ru/go-4devs/cache/provider/redis" + "gitoa.ru/go-4devs/cache/provider/ristretto" + "gitoa.ru/go-4devs/cache/test" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +type provider struct { + name string + prov cache.Provider +} + +func providers() []provider { + client, _ := glru.New(10000) + db, cl := test.PebbleDB() + + defer cl() + + return []provider{ + {"encoding", memory.NewEncoding()}, + {"map", memory.NewMap()}, + {"shard", memory.NewMapShard()}, + {"lru", lru.New(client)}, + {"ristretto", ristretto.New(test.RistrettoClient())}, + {"memcache", memcache.New(test.MemcacheClient())}, + {"redis", redis.New(test.RedisClient())}, + {"pebble", pebble.New(db)}, + } +} + +func randStringBytes(n int64) string { + b := make([]byte, n) + + for i := range b { + b[i] = letterBytes[randInt64(int64(len(letterBytes)))] + } + + return string(b) +} + +func randInt64(max int64) int64 { + m := big.NewInt(max) + nBig, _ := rand.Int(rand.Reader, m) + + return nBig.Int64() +} + +func BenchmarkCacheGetRandomKeyString(b *testing.B) { + ctx := context.Background() + keysLen := 10000 + + for _, p := range providers() { + prov := p.prov + items := make([]*cache.Item, keysLen) + + for i := 0; i < keysLen; i++ { + var val string + + key := randStringBytes(55) + items[i] = cache.NewItem(key, &val) + require.Nil(b, prov(ctx, cache.OperationSet, cache.NewItem(key, "value: "+p.name, cache.WithTTL(time.Minute)))) + } + + b.Run(p.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = prov(ctx, cache.OperationGet, items[i%keysLen]) + } + }) + } +} + +func BenchmarkCacheGetRandomKeyInt(b *testing.B) { + ctx := context.Background() + keysLen := 10000 + + for _, p := range providers() { + prov := p.prov + items := make([]*cache.Item, keysLen) + + for i := 0; i < keysLen; i++ { + var val int64 + + key := randInt64(math.MaxInt64) + + items[i] = cache.NewItem(key, &val) + require.Nil(b, prov(ctx, cache.OperationSet, cache.NewItem(key, randInt64(math.MaxInt64), cache.WithTTL(time.Minute)))) + } + + b.Run(p.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = prov(ctx, cache.OperationGet, items[i%keysLen]) + } + }) + } +} + +func BenchmarkCacheGetStruct(b *testing.B) { + ctx := context.Background() + + type testStruct struct { + Key string + val string + } + + var val testStruct + item := cache.NewItem("key", &val) + + for _, p := range providers() { + prov := p.prov + require.Nil(b, prov(ctx, cache.OperationSet, cache.NewItem("key", testStruct{Key: "key", val: ""}, cache.WithTTL(time.Minute)))) + + b.Run(p.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = prov(ctx, cache.OperationGet, item) + } + }) + } +} + +func BenchmarkCacheSetStruct(b *testing.B) { + ctx := context.Background() + + type testStruct struct { + Key string + Val int + } + + for _, p := range providers() { + prov := p.prov + b.Run(p.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + item := cache.NewItem(i, testStruct{"k", i}, cache.WithTTL(time.Hour)) + _ = prov(ctx, cache.OperationSet, item) + } + }) + } +} + +func BenchmarkCacheGetParallel(b *testing.B) { + ctx := context.Background() + + for _, p := range providers() { + prov := p.prov + key := fmt.Sprintf("key_%s", p.name) + val := fmt.Sprintf("value_%s", p.name) + item := cache.NewItem(key, &val, cache.WithTTL(time.Minute)) + require.Nil(b, prov(ctx, cache.OperationSet, item)) + + b.Run(p.name, func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = prov(ctx, cache.OperationGet, item) + } + }) + }) + } +} + +func BenchmarkCacheSetParallel(b *testing.B) { + ctx := context.Background() + + for _, p := range providers() { + prov := p.prov + key := fmt.Sprintf("key: %v", prov) + val := fmt.Sprintf("value: %v", prov) + + b.Run(p.name, func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + item := cache.NewItem(key, val, cache.WithTTL(time.Hour)) + _ = prov(ctx, cache.OperationSet, item) + } + }) + }) + } +} diff --git a/provider/lru/provider.go b/provider/lru/provider.go new file mode 100644 index 0000000..bf6b411 --- /dev/null +++ b/provider/lru/provider.go @@ -0,0 +1,66 @@ +package lru + +import ( + "context" + "fmt" + "time" + + lru "github.com/hashicorp/golang-lru" + "gitoa.ru/go-4devs/cache" +) + +// New create new lru cache provider. +func New(client *lru.Cache) cache.Provider { + return func(ctx context.Context, operation string, item *cache.Item) error { + switch operation { + case cache.OperationGet: + val, ok := client.Get(item.Key) + if !ok { + return wrapErr(cache.ErrCacheMiss) + } + + it, _ := val.(expired) + + if !it.ex.IsZero() { + item.TTL = time.Until(it.ex) + } + + if item.IsExpired() { + return wrapErr(cache.ErrCacheExpired) + } + + return wrapErr(cache.TypeAssert(it.value, item.Value)) + case cache.OperationSet: + it := expired{ + value: item.Value, + ex: time.Time{}, + } + if item.TTL > 0 { + it.ex = item.Expired() + } + + _ = client.Add(item.Key, it) + + return nil + case cache.OperationDelete: + _ = client.Remove(item.Key) + + return nil + } + + return wrapErr(cache.ErrOperationNotAllwed) + } +} + +type expired struct { + ex time.Time + value interface{} +} + +func wrapErr(err error) error { + if err != nil { + return fmt.Errorf("%w: lru", err) + } + + return nil +} diff --git a/provider/lru/provider_test.go b/provider/lru/provider_test.go new file mode 100644 index 0000000..ca7d2a8 --- /dev/null +++ b/provider/lru/provider_test.go @@ -0,0 +1,18 @@ +package lru_test + +import ( + "testing" + + glru "github.com/hashicorp/golang-lru" + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache/provider/lru" + "gitoa.ru/go-4devs/cache/test" +) + +func TestEncoding(t *testing.T) { + t.Parallel() + + client, err := glru.New(10) + require.Nil(t, err) + test.RunSute(t, lru.New(client)) +} diff --git a/provider/memcache/provider.go b/provider/memcache/provider.go new file mode 100644 index 0000000..caa7b3b --- /dev/null +++ b/provider/memcache/provider.go @@ -0,0 +1,52 @@ +package memcache + +import ( + "context" + "errors" + "fmt" + + "github.com/bradfitz/gomemcache/memcache" + "gitoa.ru/go-4devs/cache" +) + +// New memcache provider. +func New(client *memcache.Client) cache.Provider { + return func(ctx context.Context, operation string, item *cache.Item) error { + key := item.Key.String() + + switch operation { + case cache.OperationGet: + ci, err := client.Get(item.Key.String()) + + switch { + case errors.Is(err, memcache.ErrCacheMiss): + return wrapErr(cache.ErrCacheMiss) + case errors.Is(err, memcache.ErrMalformedKey): + return wrapErr(cache.ErrKeyNotValid) + case err != nil: + return wrapErr(err) + } + + return wrapErr(item.Unmarshal(ci.Value)) + case cache.OperationSet: + data, err := item.Marshal() + if err != nil { + return wrapErr(err) + } + + return wrapErr(client.Set(&memcache.Item{Key: key, Flags: 0, Value: data, Expiration: int32(item.TTL.Seconds())})) + case cache.OperationDelete: + return wrapErr(client.Delete(key)) + } + + return wrapErr(cache.ErrOperationNotAllwed) + } +} + +func wrapErr(err error) error { + if err != nil { + return fmt.Errorf("%w: memcache", err) + } + + return nil +} diff --git a/provider/memcache/provider_test.go b/provider/memcache/provider_test.go new file mode 100644 index 0000000..19512d2 --- /dev/null +++ b/provider/memcache/provider_test.go @@ -0,0 +1,14 @@ +package memcache_test + +import ( + "testing" + + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/provider/memcache" + "gitoa.ru/go-4devs/cache/test" +) + +func TestProvider(t *testing.T) { + t.Parallel() + test.RunSute(t, memcache.New(test.MemcacheClient()), test.WithExpire(cache.ErrCacheMiss)) +} diff --git a/provider/memory/encoding.go b/provider/memory/encoding.go new file mode 100644 index 0000000..ba11763 --- /dev/null +++ b/provider/memory/encoding.go @@ -0,0 +1,93 @@ +package memory + +import ( + "context" + "fmt" + "sync" + "time" + + "gitoa.ru/go-4devs/cache" +) + +// NewEncoding create new provider. +func NewEncoding() cache.Provider { + items := make(map[cache.Key]encodedEntry) + mu := sync.RWMutex{} + + return func(ctx context.Context, operation string, item *cache.Item) error { + switch operation { + case cache.OperationSet: + i, err := newEncodedEntry(item) + if err != nil { + return err + } + + mu.Lock() + items[item.Key] = i + mu.Unlock() + + return nil + case cache.OperationDelete: + mu.Lock() + delete(items, item.Key) + mu.Unlock() + + return nil + case cache.OperationGet: + mu.RLock() + i, ok := items[item.Key] + mu.RUnlock() + + if !ok { + return wrapErr(cache.ErrCacheMiss) + } + + return resolveEncodedEntry(i, item) + } + + return wrapErr(cache.ErrOperationNotAllwed) + } +} + +type encodedEntry struct { + data []byte + expired time.Time +} + +func wrapErr(err error) error { + if err != nil { + return fmt.Errorf("%w: encoding", err) + } + + return nil +} + +func newEncodedEntry(item *cache.Item) (encodedEntry, error) { + var ( + e encodedEntry + err error + ) + + e.data, err = item.Marshal() + if err != nil { + return e, wrapErr(err) + } + + if item.TTL > 0 { + e.expired = item.Expired() + } + + return e, nil +} + +func resolveEncodedEntry(e encodedEntry, item *cache.Item) error { + if !e.expired.IsZero() { + item.TTL = time.Until(e.expired) + } + + if item.IsExpired() { + return wrapErr(cache.ErrCacheExpired) + } + + return wrapErr(item.Unmarshal(e.data)) +} diff --git a/provider/memory/encoding_test.go b/provider/memory/encoding_test.go new file mode 100644 index 0000000..edcc543 --- /dev/null +++ b/provider/memory/encoding_test.go @@ -0,0 +1,13 @@ +package memory_test + +import ( + "testing" + + "gitoa.ru/go-4devs/cache/provider/memory" + "gitoa.ru/go-4devs/cache/test" +) + +func TestEncoding(t *testing.T) { + t.Parallel() + test.RunSute(t, memory.NewEncoding()) +} diff --git a/provider/memory/map.go b/provider/memory/map.go new file mode 100644 index 0000000..d79f906 --- /dev/null +++ b/provider/memory/map.go @@ -0,0 +1,155 @@ +package memory + +import ( + "context" + "fmt" + "hash/crc64" + "sync" + "time" + + "gitoa.ru/go-4devs/cache" +) + +const defaultShards = 255 + +// NewMap creates new map cache. +func NewMap() cache.Provider { + m := sync.Map{} + + return func(ctx context.Context, op string, item *cache.Item) error { + switch op { + case cache.OperationDelete: + m.Delete(item.Key) + + return nil + case cache.OperationSet: + m.Store(item.Key, newEntry(item)) + + return nil + case cache.OperationGet: + e, ok := m.Load(item.Key) + if !ok { + return fmt.Errorf("%w: map", cache.ErrCacheMiss) + } + + return resolveEntry(e.(entry), item) + } + + return fmt.Errorf("%w: map", cache.ErrOperationNotAllwed) + } +} + +func resolveEntry(e entry, item *cache.Item) error { + if !e.expired.IsZero() { + item.TTL = time.Until(e.expired) + } + + if item.IsExpired() { + return fmt.Errorf("%w: map", cache.ErrCacheExpired) + } + + if err := cache.TypeAssert(e.data, item.Value); err != nil { + return fmt.Errorf("%w: map", err) + } + + return nil +} + +func newEntry(item *cache.Item) entry { + e := entry{data: item.Value} + + if item.TTL > 0 { + e.expired = item.Expired() + } + + return e +} + +type entry struct { + data interface{} + expired time.Time +} + +type settings struct { + numShards uint64 + hashString func(in cache.Key) uint64 +} +type Option func(*settings) + +func WithNumShards(num uint64) Option { + return func(s *settings) { + s.numShards = num + } +} + +func WithHashKey(f func(in cache.Key) uint64) Option { + return func(s *settings) { + s.hashString = f + } +} + +//nolint: gochecknoglobals +var table = crc64.MakeTable(crc64.ISO) + +func hashString(in cache.Key) uint64 { + switch k := in.Key.(type) { + case int64: + return uint64(k) + case int32: + return uint64(k) + case int: + return uint64(k) + case uint64: + return k + case uint32: + return uint64(k) + case uint: + return uint64(k) + default: + return crc64.Checksum([]byte(in.String()), table) + } +} + +func NewMapShard(opts ...Option) cache.Provider { + s := settings{ + numShards: defaultShards, + hashString: hashString, + } + + for _, opt := range opts { + opt(&s) + } + + items := make([]*sync.Map, s.numShards) + for i := range items { + items[i] = &sync.Map{} + } + + return func(ctx context.Context, operation string, item *cache.Item) error { + idx := s.hashString(item.Key) + + switch operation { + case cache.OperationDelete: + items[idx%s.numShards].Delete(item.Key) + + return nil + case cache.OperationSet: + items[idx%s.numShards].Store(item.Key, newEntry(item)) + + return nil + case cache.OperationGet: + e, ok := items[idx%s.numShards].Load(item.Key) + if !ok { + return wrapShardErr(cache.ErrCacheMiss) + } + + return resolveEntry(e.(entry), item) + } + + return wrapShardErr(cache.ErrOperationNotAllwed) + } +} + +func wrapShardErr(err error) error { + return fmt.Errorf("%w: memory shards", err) +} diff --git a/provider/memory/map_test.go b/provider/memory/map_test.go new file mode 100644 index 0000000..955028c --- /dev/null +++ b/provider/memory/map_test.go @@ -0,0 +1,18 @@ +package memory_test + +import ( + "testing" + + "gitoa.ru/go-4devs/cache/provider/memory" + "gitoa.ru/go-4devs/cache/test" +) + +func TestMap(t *testing.T) { + t.Parallel() + test.RunSute(t, memory.NewMap()) +} + +func TestMapShard(t *testing.T) { + t.Parallel() + test.RunSute(t, memory.NewMapShard()) +} diff --git a/provider/ns/provider.go b/provider/ns/provider.go new file mode 100644 index 0000000..a3e59dd --- /dev/null +++ b/provider/ns/provider.go @@ -0,0 +1,22 @@ +package ns + +import ( + "context" + "errors" + + "gitoa.ru/go-4devs/cache" +) + +var ErrProviderNotFound = errors.New("provider not found") + +func New(providers map[string]cache.Provider) cache.Provider { + return func(ctx context.Context, operation string, item *cache.Item) error { + if prov, ok := providers[item.Key.Prefix]; ok { + item.Key.Prefix = "" + + return prov(ctx, operation, item) + } + + return ErrProviderNotFound + } +} diff --git a/provider/pebble/provider.go b/provider/pebble/provider.go new file mode 100644 index 0000000..3ca0ae6 --- /dev/null +++ b/provider/pebble/provider.go @@ -0,0 +1,54 @@ +package pebble + +import ( + "context" + "errors" + "fmt" + + "github.com/cockroachdb/pebble" + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/item" +) + +func New(db *pebble.DB) cache.Provider { + return func(ctx context.Context, operation string, i *cache.Item) error { + key := []byte(i.Key.String()) + + switch operation { + case cache.OperationGet: + val, cl, err := db.Get([]byte(i.Key.String())) + if err != nil { + if errors.Is(err, pebble.ErrNotFound) { + return wrapErr(cache.ErrCacheMiss) + } + + return wrapErr(err) + } + + defer func() { + _ = cl.Close() + }() + + return wrapErr(item.UnmarshalExpired(i, val)) + case cache.OperationSet: + b, err := item.MarshalExpired(i) + if err != nil { + return wrapErr(err) + } + + return wrapErr(db.Set(key, b, pebble.Sync)) + case cache.OperationDelete: + return wrapErr(db.Delete(key, pebble.Sync)) + } + + return wrapErr(cache.ErrOperationNotAllwed) + } +} + +func wrapErr(err error) error { + if err != nil { + return fmt.Errorf("%w: pebble", err) + } + + return nil +} diff --git a/provider/pebble/provider_test.go b/provider/pebble/provider_test.go new file mode 100644 index 0000000..41b0e32 --- /dev/null +++ b/provider/pebble/provider_test.go @@ -0,0 +1,17 @@ +package pebble_test + +import ( + "testing" + + "gitoa.ru/go-4devs/cache/provider/pebble" + "gitoa.ru/go-4devs/cache/test" +) + +func TestPebble(t *testing.T) { + t.Parallel() + + db, cl := test.PebbleDB() + defer cl() + + test.RunSute(t, pebble.New(db)) +} diff --git a/provider/redis/pool.go b/provider/redis/pool.go new file mode 100644 index 0000000..95267d7 --- /dev/null +++ b/provider/redis/pool.go @@ -0,0 +1,110 @@ +package redis + +import ( + "context" + "fmt" + + "gitoa.ru/go-4devs/cache" +) + +type Conn interface { + Do(commandName string, args ...interface{}) (reply interface{}, err error) + Send(commandName string, args ...interface{}) error + Flush() error + Close() error +} + +// New creates new provider. +func New(pool func(context.Context) (Conn, error)) cache.Provider { + return func(ctx context.Context, operation string, item *cache.Item) error { + conn, err := pool(ctx) + if err != nil { + return wrapErr(err) + } + defer conn.Close() + + key := item.Key.String() + + switch operation { + case cache.OperationGet: + data, ttl, err := get(conn, key) + if err != nil { + return err + } + + item.TTLInSecond(ttl) + + return wrapErr(item.Unmarshal(data)) + case cache.OperationSet: + data, err := item.Marshal() + if err != nil { + return wrapErr(err) + } + + return set(conn, key, data, int(item.TTL.Seconds())) + case cache.OperationDelete: + return del(conn, key) + } + + return wrapErr(cache.ErrOperationNotAllwed) + } +} + +func get(conn Conn, key string) ([]byte, int64, error) { + data, err := conn.Do("GET", key) + if err != nil { + return nil, 0, wrapErr(err) + } + + if data == nil { + return nil, 0, wrapErr(cache.ErrCacheMiss) + } + + v, ok := data.([]byte) + if !ok { + return nil, 0, wrapErr(cache.ErrSourceNotValid) + } + + expire, err := conn.Do("TTL", key) + if err != nil { + return v, 0, wrapErr(err) + } + + ex, _ := expire.(int64) + + return v, ex, nil +} + +func set(conn Conn, key string, data []byte, ttl int) error { + if err := conn.Send("SET", key, data); err != nil { + return wrapErr(err) + } + + if ttl > 0 { + if err := conn.Send("EXPIRE", key, ttl); err != nil { + return wrapErr(err) + } + } + + if err := conn.Flush(); err != nil { + return fmt.Errorf("failed flush then set %s by %w", key, conn.Flush()) + } + + return nil +} + +func del(conn Conn, key string) error { + if _, err := conn.Do("DEL", key); err != nil { + return wrapErr(err) + } + + return nil +} + +func wrapErr(err error) error { + if err != nil { + return fmt.Errorf("%w: redis pool", err) + } + + return nil +} diff --git a/provider/redis/pool_test.go b/provider/redis/pool_test.go new file mode 100644 index 0000000..8a32fbe --- /dev/null +++ b/provider/redis/pool_test.go @@ -0,0 +1,14 @@ +package redis_test + +import ( + "testing" + + "gitoa.ru/go-4devs/cache" + "gitoa.ru/go-4devs/cache/provider/redis" + "gitoa.ru/go-4devs/cache/test" +) + +func TestRedisPool(t *testing.T) { + t.Parallel() + test.RunSute(t, redis.New(test.RedisClient()), test.WithExpire(cache.ErrCacheMiss)) +} diff --git a/provider/redis/redigo.go b/provider/redis/redigo.go new file mode 100644 index 0000000..5e20097 --- /dev/null +++ b/provider/redis/redigo.go @@ -0,0 +1,20 @@ +package redis + +import ( + "context" + "fmt" + + "github.com/gomodule/redigo/redis" +) + +// NewPool creates redigo pool. +func NewPool(pool *redis.Pool) func(context.Context) (Conn, error) { + return func(ctx context.Context) (Conn, error) { + conn, err := pool.GetContext(ctx) + if err != nil { + return nil, fmt.Errorf("failed get connect: %w", err) + } + + return conn, nil + } +} diff --git a/provider/ristretto/provider.go b/provider/ristretto/provider.go new file mode 100644 index 0000000..0d9bc37 --- /dev/null +++ b/provider/ristretto/provider.go @@ -0,0 +1,69 @@ +package ristretto + +import ( + "context" + "errors" + "fmt" + + "github.com/dgraph-io/ristretto" + "gitoa.ru/go-4devs/cache" +) + +var ErrSetValue = errors.New("failed set value") + +type Option func(*setting) + +func WithCost(cost int64) Option { + return func(s *setting) { + s.cost = cost + } +} + +type setting struct { + cost int64 +} + +func New(retto *ristretto.Cache, opts ...Option) cache.Provider { + s := setting{ + cost: 1, + } + + for _, opt := range opts { + opt(&s) + } + + return func(ctx context.Context, operation string, item *cache.Item) error { + var key interface{} + if item.Key.Prefix != "" { + key = item.Key.String() + } else { + key = item.Key.Key + } + + switch operation { + case cache.OperationGet: + res, ok := retto.Get(key) + if !ok { + return fmt.Errorf("%w: ristretto", cache.ErrCacheMiss) + } + + if err := cache.TypeAssert(res, item.Value); err != nil { + return fmt.Errorf("failed assert type: %w", err) + } + + return nil + case cache.OperationDelete: + retto.Del(key) + + return nil + case cache.OperationSet: + if ok := retto.SetWithTTL(key, item.Value, s.cost, item.TTL); !ok { + return ErrSetValue + } + + return nil + } + + return cache.ErrOperationNotAllwed + } +} diff --git a/provider/ristretto/provider_test.go b/provider/ristretto/provider_test.go new file mode 100644 index 0000000..1ea705b --- /dev/null +++ b/provider/ristretto/provider_test.go @@ -0,0 +1,26 @@ +package ristretto_test + +import ( + "testing" + "time" + + "github.com/dgraph-io/ristretto" + "github.com/stretchr/testify/require" + "gitoa.ru/go-4devs/cache" + provider "gitoa.ru/go-4devs/cache/provider/ristretto" + "gitoa.ru/go-4devs/cache/test" +) + +func TestRistretto(t *testing.T) { + t.Parallel() + + retto, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + require.Nil(t, err) + test.RunSute(t, provider.New(retto), test.WithWaitGet(func() { + time.Sleep(10 * time.Millisecond) + }), test.WithExpire(cache.ErrCacheMiss)) +} diff --git a/test/helpers.go b/test/helpers.go new file mode 100644 index 0000000..cb9c34f --- /dev/null +++ b/test/helpers.go @@ -0,0 +1,84 @@ +package test + +import ( + "context" + "os" + "path/filepath" + "runtime" + "time" + + gom "github.com/bradfitz/gomemcache/memcache" + "github.com/cockroachdb/pebble" + "github.com/dgraph-io/ristretto" + redigo "github.com/gomodule/redigo/redis" + "gitoa.ru/go-4devs/cache/provider/redis" +) + +// RedisClient created redis client. +func RedisClient() func(ctx context.Context) (redis.Conn, error) { + host, ok := os.LookupEnv("FDEVS_CACHE_REDIS_HOST") + if !ok { + host = ":6379" + } + + client := &redigo.Pool{ + DialContext: func(ctx context.Context) (redigo.Conn, error) { + return redigo.DialContext(ctx, "tcp", host) + }, + } + + return redis.NewPool(client) +} + +// MemcacheClient created memcached client. +func MemcacheClient() *gom.Client { + host, ok := os.LookupEnv("FDEVS_CACHE_MEMCACHE_HOST") + if !ok { + host = "localhost:11211" + } + + return gom.New(host) +} + +// RistrettoClient creates ristretto client. +func RistrettoClient() *ristretto.Cache { + cache, _ := ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e7, // number of keys to track frequency of (10M). + MaxCost: 1 << 30, // maximum cost of cache (1GB). + BufferItems: 64, // number of keys per Get buffer. + }) + + return cache +} + +// PebbleDB creates pebble DB. +func PebbleDB() (*pebble.DB, func()) { + path := "demo.test" + if _, filename, _, ok := runtime.Caller(0); ok { + path = filepath.Dir(filename) + "/" + path + } + + db, _ := pebble.Open(path, &pebble.Options{}) + + return db, func() { + os.RemoveAll(path) + } +} + +// User tested user data. +type User struct { + ID int + Name string + UpdateAt time.Time + CreatedAt time.Time +} + +// NewUser create mocks data user. +func NewUser(id int) User { + return User{ + ID: id, + Name: "andrey", + UpdateAt: time.Date(2020, 2, 1, 1, 2, 3, 4, time.UTC), + CreatedAt: time.Date(1999, 2, 1, 1, 2, 3, 4, time.UTC), + } +} diff --git a/test/provider.go b/test/provider.go new file mode 100644 index 0000000..5ef05cf --- /dev/null +++ b/test/provider.go @@ -0,0 +1,57 @@ +package test + +import ( + "context" + "fmt" + "testing" + + "gitoa.ru/go-4devs/cache" +) + +var _ cache.Provider = NewProviderMock(&testing.T{}) + +// OptionMock configure mock. +type OptionMock func(*ProviderMock) + +// WithDelete sets delete method. +func WithDelete(f func(t *testing.T) func(ctx context.Context, item *cache.Item) error) OptionMock { + return func(pm *ProviderMock) { pm.operations[cache.OperationDelete] = f } +} + +// WithGet sets get method. +func WithGet(f func(t *testing.T) func(ctx context.Context, item *cache.Item) error) OptionMock { + return func(pm *ProviderMock) { pm.operations[cache.OperationGet] = f } +} + +// WithSet sets set method. +func WithSet(f func(t *testing.T) func(ctx context.Context, item *cache.Item) error) OptionMock { + return func(pm *ProviderMock) { pm.operations[cache.OperationSet] = f } +} + +// NewProviderMock create new mock provider. +func NewProviderMock(t *testing.T, opts ...OptionMock) cache.Provider { + t.Helper() + + pm := &ProviderMock{ + t: t, + operations: make(map[string]func(t *testing.T) func(ctx context.Context, item *cache.Item) error, 3), + } + + for _, o := range opts { + o(pm) + } + + return func(ctx context.Context, operation string, item *cache.Item) error { + if m, ok := pm.operations[operation]; ok { + return m(pm.t)(ctx, item) + } + + return fmt.Errorf("%w: %s", cache.ErrOperationNotAllwed, operation) + } +} + +// ProviderMock mock. +type ProviderMock struct { + t *testing.T + operations map[string]func(t *testing.T) func(ctx context.Context, item *cache.Item) error +} diff --git a/test/sute.go b/test/sute.go new file mode 100644 index 0000000..790975e --- /dev/null +++ b/test/sute.go @@ -0,0 +1,133 @@ +package test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gitoa.ru/go-4devs/cache" +) + +const ( + expire = time.Second + waitExpire = expire * 2 +) + +// Option configure sute. +type Option func(*ProviderSuite) + +// WithExpire sets expired errors. +func WithExpire(err error) Option { + return func(ps *ProviderSuite) { ps.expire = err } +} + +func WithWaitGet(f func()) Option { + return func(ps *ProviderSuite) { ps.waitGet = f } +} + +// RunSute run test by provider. +func RunSute(t *testing.T, provider cache.Provider, opts ...Option) { + t.Helper() + + cs := &ProviderSuite{ + provider: provider, + expire: cache.ErrCacheExpired, + waitGet: func() {}, + } + + for _, o := range opts { + o(cs) + } + + suite.Run(t, cs) +} + +// ProviderSuite for testing providers. +type ProviderSuite struct { + provider cache.Provider + expire error + waitGet func() + suite.Suite +} + +// TestGet tested get. +func (s *ProviderSuite) TestGet() { + s.T().Parallel() + + ctx := context.Background() + + var val string + + require.Nil(s.T(), s.provider(ctx, cache.OperationSet, cache.NewItem("get", "some value"))) + s.waitGet() + require.Nil(s.T(), s.provider(ctx, cache.OperationGet, cache.NewItem("get", &val))) + require.Equal(s.T(), "some value", val) + + var user User + + cachedUser := NewUser(1) + + require.Nil(s.T(), s.provider(ctx, cache.OperationSet, cache.NewItem("get_user", cachedUser))) + s.waitGet() + require.Nil(s.T(), s.provider(ctx, cache.OperationGet, cache.NewItem("get_user", &user))) + require.Equal(s.T(), cachedUser, user) +} + +// TestCacheMiss tested cache miss error. +func (s *ProviderSuite) TestCacheMiss() { + s.T().Parallel() + + ctx := context.Background() + + require.True(s.T(), + errors.Is(s.provider(ctx, cache.OperationGet, cache.NewItem("cache_miss", nil)), cache.ErrCacheMiss), + "failed expect errors", + ) +} + +// TestExpired tested error expired. +func (s *ProviderSuite) TestExpired() { + s.T().Parallel() + + ctx := context.Background() + + var val string + + require.Nil(s.T(), s.provider(ctx, cache.OperationSet, cache.NewItem("expired", "some value", cache.WithTTL(expire)))) + time.Sleep(waitExpire) + + err := s.provider(ctx, cache.OperationGet, cache.NewItem("expired", nil)) + require.Truef(s.T(), errors.Is(err, s.expire), "failed expired error got:%s", err) + require.Equal(s.T(), "", val) +} + +// TestTTL tested set ttl. +func (s *ProviderSuite) TestTTL() { + s.T().Parallel() + + ctx := context.Background() + + var val string + + require.Nil(s.T(), s.provider(ctx, cache.OperationSet, cache.NewItem("ttl", "some ttl value", cache.WithTTL(time.Hour)))) + s.waitGet() + require.Nil(s.T(), s.provider(ctx, cache.OperationGet, cache.NewItem("ttl", &val))) + require.Equal(s.T(), "some ttl value", val) +} + +// TestDelete tested delete method. +func (s *ProviderSuite) TestDelete() { + s.T().Parallel() + + ctx := context.Background() + + require.Nil(s.T(), s.provider(ctx, cache.OperationSet, cache.NewItem("delete:key", "some delete value"))) + require.Nil(s.T(), s.provider(ctx, cache.OperationDelete, cache.NewItem("delete:key", nil))) + require.True(s.T(), + errors.Is(s.provider(ctx, cache.OperationGet, cache.NewItem("cache_miss", nil)), cache.ErrCacheMiss), + "failed delete errors", + ) +} diff --git a/type_assert.go b/type_assert.go new file mode 100644 index 0000000..ad4da26 --- /dev/null +++ b/type_assert.go @@ -0,0 +1,99 @@ +package cache + +import ( + "reflect" + "time" +) + +// TypeAssert assert source to target. +func TypeAssert(source, target interface{}) (err error) { + if source == nil { + return nil + } + + if directTypeAssert(source, target) { + return nil + } + + v := reflect.ValueOf(target) + if !v.IsValid() { + return ErrTargetNil + } + + if v.Kind() != reflect.Ptr { + return NewErrorTarget(target) + } + + v = v.Elem() + + if !v.IsValid() { + return NewErrorTarget(target) + } + + s := reflect.ValueOf(source) + if !s.IsValid() { + return ErrSourceNotValid + } + + s = deReference(s) + v.Set(s) + + return nil +} + +// nolint: cyclop,gocyclo +func directTypeAssert(source, target interface{}) bool { + var ok bool + switch v := target.(type) { + case *string: + *v, ok = source.(string) + case *[]byte: + *v, ok = source.([]byte) + case *int: + *v, ok = source.(int) + case *int8: + *v, ok = source.(int8) + case *int16: + *v, ok = source.(int16) + case *int32: + *v, ok = source.(int32) + case *int64: + *v, ok = source.(int64) + case *uint: + *v, ok = source.(uint) + case *uint8: + *v, ok = source.(uint8) + case *uint16: + *v, ok = source.(uint16) + case *uint32: + *v, ok = source.(uint32) + case *uint64: + *v, ok = source.(uint64) + case *bool: + *v, ok = source.(bool) + case *float32: + *v, ok = source.(float32) + case *float64: + *v, ok = source.(float64) + case *time.Duration: + *v, ok = source.(time.Duration) + case *time.Time: + *v, ok = source.(time.Time) + case *[]string: + *v, ok = source.([]string) + case *map[string]string: + *v, ok = source.(map[string]string) + case *map[string]interface{}: + *v, ok = source.(map[string]interface{}) + } + + return ok +} + +func deReference(v reflect.Value) reflect.Value { + if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && !v.IsNil() { + return v.Elem() + } + + return v +}