├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── adapter.go ├── assets └── logo.png ├── cache.go ├── cache_test.go ├── doc └── img │ └── caching.png ├── empty.go ├── empty_test.go ├── event.go ├── event_enum.go ├── event_enum_test.go ├── event_test.go ├── example_advanced_test.go ├── example_readthrough_test.go ├── example_setandget_test.go ├── factory.go ├── factory_test.go ├── go.mod ├── go.sum ├── interface.go ├── key.go ├── key_test.go ├── marshaler.go ├── marshaler_test.go ├── options.go ├── pubsub.go ├── redis.go ├── redis_test.go ├── tinylfu.go └── tinylfu_test.go /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '28 8 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac only 2 | .DS_Store 3 | 4 | # for IDE 5 | .idea 6 | .vscode 7 | 8 | 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # ========================================================================== 3 | # Golang Pre-Commit Hooks | https://github.com/tekwizely/pre-commit-golang 4 | # 5 | # Visit the project home page to learn more about the available Hooks, 6 | # including useful arguments you might want to pass into them. 7 | # 8 | # File-Based Hooks: 9 | # Run against matching staged files individually. 10 | # 11 | # Module-Based Hooks: 12 | # Run against module root folders containing matching staged files. 13 | # 14 | # Package-Based Hooks: 15 | # Run against folders containing one or more staged files. 16 | # 17 | # Repo-Based Hooks: 18 | # Run against the entire repo. 19 | # The hooks only run once (if any matching files are staged), 20 | # and are NOT provided the list of staged files, 21 | # 22 | # My-Cmd-* Hooks 23 | # Allow you to invoke custom tools in varous contexts. 24 | # Can be useful if your favorite tool(s) are not built-in (yet) 25 | # 26 | # Hook Suffixes 27 | # Hooks have suffixes in their name that indicate their targets: 28 | # 29 | # +-----------+--------------+ 30 | # | Suffix | Target | 31 | # |-----------+--------------+ 32 | # | | Files | 33 | # | -mod | Module | 34 | # | -pkg | Package | 35 | # | -repo | Repo Root | 36 | # | -repo-mod | All Modules | 37 | # | -repo-pkg | All Packages | 38 | # +-----------+--------------+ 39 | # 40 | # ! Multiple Hook Invocations 41 | # ! Due to OS command-line-length limits, Pre-Commit can invoke a hook 42 | # ! multiple times if a large number of files are staged. 43 | # ! For file and repo-based hooks, this isn't an issue, but for module 44 | # ! and package-based hooks, there is a potential for the hook to run 45 | # ! against the same module or package multiple times, duplicating any 46 | # ! errors or warnings. 47 | # 48 | # Useful Hook Parameters: 49 | # - id: hook-id 50 | # args: [arg1, arg2, ..., '--'] # Pass options ('--' is optional) 51 | # always_run: true # Run even if no matching files staged 52 | # alias: hook-alias # Create an alias 53 | # 54 | # Passing Options To Hooks: 55 | # If your options contain a reference to an existing file, then you will 56 | # need to use a trailing '--' argument to separate the hook options from 57 | # the modified-file list that Pre-Commit passes into the hook. 58 | # NOTE: For repo-based hooks, '--' is not needed. 59 | # 60 | # Always Run: 61 | # By default, hooks ONLY run when matching file types are staged. 62 | # When configured to "always_run", a hook is executed as if EVERY matching 63 | # file were staged. 64 | # 65 | # Aliases: 66 | # Consider adding aliases to longer-named hooks for easier CLI usage. 67 | # ========================================================================== 68 | - repo: https://github.com/tekwizely/pre-commit-golang 69 | rev: v1.0.0-rc.1 70 | hooks: 71 | # 72 | # Go Build 73 | # 74 | - id: go-build-mod 75 | # - id: go-build-pkg 76 | - id: go-build-repo-mod 77 | # - id: go-build-repo-pkg 78 | # 79 | # Go Mod Tidy 80 | # 81 | - id: go-mod-tidy 82 | - id: go-mod-tidy-repo 83 | # 84 | # Go Test 85 | # 86 | - id: go-test-mod 87 | args: [-v, -race] 88 | # - id: go-test-pkg 89 | - id: go-test-repo-mod 90 | args: [-v, -race] 91 | # - id: go-test-repo-pkg 92 | # 93 | # Go Vet 94 | # 95 | # - id: go-vet 96 | - id: go-vet-mod 97 | # - id: go-vet-pkg 98 | - id: go-vet-repo-mod 99 | # - id: go-vet-repo-pkg 100 | # 101 | # Revive 102 | # 103 | - id: go-revive 104 | - id: go-revive-mod 105 | - id: go-revive-repo-mod 106 | # 107 | # StaticCheck 108 | # 109 | - id: go-staticcheck-mod 110 | # - id: go-staticcheck-pkg 111 | - id: go-staticcheck-repo-mod 112 | # - id: go-staticcheck-repo-pkg 113 | # 114 | # Formatters 115 | # 116 | - id: go-fmt 117 | args: [-w] 118 | - id: go-fmt-repo 119 | args: [-w] 120 | - id: go-imports # replaces go-fmt 121 | args: [--local, github.com/viney-shih, -w] 122 | - id: go-imports-repo # replaces go-fmt-repo 123 | args: [--local, github.com/viney-shih, -w] 124 | # 125 | # Style Checkers 126 | # 127 | - id: go-lint 128 | # -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.17" 5 | - "1.18" 6 | - "1.19" 7 | 8 | services: 9 | - docker 10 | 11 | before_install: 12 | - go get github.com/mattn/goveralls 13 | - docker pull redis:5.0.14-alpine 14 | - docker run --name redis -p 6379:6379 -d redis:5.0.14-alpine 15 | - docker ps -a 16 | 17 | script: 18 | - go test -v -race -covermode=atomic -coverprofile=coverage.out ./... 19 | 20 | after_success: 21 | - $GOPATH/bin/goveralls -coverprofile=coverage.out -service=travis-ci # upload to `coveralls` 22 | - bash <(curl -s https://codecov.io/bash) # upload to `codecov` 23 | - docker rm -f redis 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-cache 2 | 3 | [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/viney-shih/go-cache?tab=doc) 4 | [![Build Status](https://app.travis-ci.com/viney-shih/go-cache.svg?branch=master)](https://app.travis-ci.com/viney-shih/go-cache) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/viney-shih/go-cache)](https://goreportcard.com/report/github.com/viney-shih/go-cache) 6 | [![codecov](https://codecov.io/gh/viney-shih/go-cache/branch/master/graph/badge.svg?token=QKRiNSU5Gn)](https://codecov.io/gh/viney-shih/go-cache) 7 | [![Coverage Status](https://coveralls.io/repos/github/viney-shih/go-cache/badge.svg?branch=master)](https://coveralls.io/github/viney-shih/go-cache?branch=master) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/5b70576957b8af88de6e/maintainability)](https://codeclimate.com/github/viney-shih/go-cache/maintainability) 9 | [![Sourcegraph](https://sourcegraph.com/github.com/viney-shih/go-cache/-/badge.svg)](https://sourcegraph.com/github.com/viney-shih/go-cache?badge) 10 | [![License](http://img.shields.io/badge/License-Apache_2-red.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) 11 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fviney-shih%2Fgo-cache.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fviney-shih%2Fgo-cache?ref=badge_shield) 12 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#caches) 13 | 14 |

15 | 16 | Photo by Ashley McNamara, via ashleymcnamara/gophers (CC BY-NC-SA 4.0) 17 |

18 | 19 | A flexible multi-layered caching library interacts with **private (in-memory) cache** and **shared cache** (i.e. Redis) in Go. It provides `Cache-Aside` strategy when dealing with both, and maintains the consistency of private cache between distributed systems by `Pub-Sub` pattern. 20 | 21 | Caching is a common technique that aims to improve the performance and scalability of a system. It does this by temporarily copying frequently accessed data to fast storage close to the application. Distributed applications typically implement either or both of the following strategies when caching data: 22 | - Using a **private cache**, where data is held locally on the computer that's running an instance of an application or service. 23 | - Using a **shared cache**, serving as a common source that can be accessed by multiple processes and machines. 24 | 25 | ![Using a local private cache with a shared cache](./doc/img/caching.png) 26 | Ref: [https://docs.microsoft.com/en-us/azure/architecture/best-practices/images/caching/caching3.png](https://docs.microsoft.com/en-us/azure/architecture/best-practices/images/caching/caching3.png "Using a local private cache with a shared cache") 27 | 28 | Considering the flexibility, efficiency and consistency, we starts to build up our own framework. 29 | 30 | ## Features 31 | - **Easy to use** : provide a friendly interface to deal with both caching mechnaism by simple configuration. Limit the size of memory on single instance (pod) as well. 32 | - **Maintain consistency** : evict keys between distributed systems by `Pub-Sub` pattern. 33 | - **Data compression** : provide customized marshal and unmarshal functions. 34 | - **Fix concurrency issue** : prevent data racing happened on single instance (pod). 35 | - **Metric** : provide callback functions to measure the performance. (i.e. hit rate, private cache usage, ...) 36 | 37 | ## Data flow 38 | ### Load the cache with `Cache-Aside` strategy 39 | ```mermaid 40 | sequenceDiagram 41 | participant APP as Application 42 | participant M as go-cache 43 | participant L as Local Cache 44 | participant S as Shared Cache 45 | participant R as Resource (Microservice / DB) 46 | 47 | APP ->> M: Cache.Get() / Cache.MGet() 48 | alt Local Cache hit 49 | M ->> L: Adapter.MGet() 50 | L -->> M: {[]Value, error} 51 | M -->> APP: return 52 | else Local Cache miss but Shared Cache hit 53 | M ->> L: Adapter.MGet() 54 | L -->> M: cache miss 55 | M ->> S: Adapter.MGet() 56 | S -->> M: {[]Value, error} 57 | M ->> L: Adapter.MSet() 58 | M -->> APP: return 59 | else All miss 60 | M ->> L: Adapter.MGet() 61 | L -->> M: cache miss 62 | M ->> S: Adapter.MGet() 63 | S -->> M: cache miss 64 | M ->> R: OneTimeGetterFunc() / MGetterFunc() 65 | R -->> M: return from getter 66 | M ->> S: Adapter.MSet() 67 | M ->> L: Adapter.MSet() 68 | M -->> APP: return 69 | end 70 | 71 | ``` 72 | 73 | ### Evict the cache 74 | ```mermaid 75 | sequenceDiagram 76 | participant APP as Application 77 | participant M as go-cache 78 | participant L as Local Cache 79 | participant S as Shared Cache 80 | participant PS as PubSub 81 | 82 | APP ->> M: Cache.Del() 83 | M ->> S: Adapter.Del() 84 | S -->> M: return error if necessary 85 | M ->> L: Adapter.Del() 86 | L -->> M: return error if necessary 87 | M ->> PS: Pubsub.Pub() (broadcast key eviction) 88 | M -->> APP: return nil or error 89 | ``` 90 | 91 | ## Installation 92 | ```sh 93 | go get github.com/viney-shih/go-cache 94 | ``` 95 | 96 | ## Get Started 97 | ### Basic usage: Set-And-Get 98 | 99 | By adopting `Singleton` pattern, initialize the *Factory* in main.go at the beginning, and deliver it to each package or business logic. 100 | 101 | ```go 102 | // Initialize the Factory in main.go 103 | tinyLfu := cache.NewTinyLFU(10000) 104 | rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ 105 | Addrs: map[string]string{ 106 | "server1": ":6379", 107 | }, 108 | })) 109 | 110 | cacheFactory := cache.NewFactory(rds, tinyLfu) 111 | ``` 112 | 113 | Treat it as a common **key:value** store like Redis. But more advanced, it coordinated the usage between multi-layered caching mechanism inside. 114 | 115 | ```go 116 | type Object struct { 117 | Str string 118 | Num int 119 | } 120 | 121 | func Example_setAndGetPattern() { 122 | // We create a group of cache named "set-and-get". 123 | // It uses the shared cache only. Each key will be expired within ten seconds. 124 | c := cacheFactory.NewCache([]cache.Setting{ 125 | { 126 | Prefix: "set-and-get", 127 | CacheAttributes: map[cache.Type]cache.Attribute{ 128 | cache.SharedCacheType: {TTL: 10 * time.Second}, 129 | }, 130 | }, 131 | }) 132 | 133 | ctx := context.TODO() 134 | 135 | // set the cache 136 | obj := &Object{ 137 | Str: "value1", 138 | Num: 1, 139 | } 140 | if err := c.Set(ctx, "set-and-get", "key", obj); err != nil { 141 | panic("not expected") 142 | } 143 | 144 | // read the cache 145 | container := &Object{} 146 | if err := c.Get(ctx, "set-and-get", "key", container); err != nil { 147 | panic("not expected") 148 | } 149 | fmt.Println(container) // Output: Object{ Str: "value1", Num: 1} 150 | 151 | // read the cache but failed 152 | if err := c.Get(ctx, "set-and-get", "no-such-key", container); err != nil { 153 | fmt.Println(err) // Output: errors.New("cache key is missing") 154 | } 155 | 156 | // Output: 157 | // &{value1 1} 158 | // cache key is missing 159 | } 160 | 161 | ``` 162 | 163 | ### Advanced usage: `Cache-Aside` strategy 164 | 165 | `GetByFunc()` is the easier way to deal with the cache by implementing the **getter function** in the parameter. When the cache is missing, it is going to refill the cache automatically. 166 | 167 | ```go 168 | func ExampleCache_GetByFunc() { 169 | // We create a group of cache named "get-by-func". 170 | // It uses the local cache only with TTL of ten minutes. 171 | c := cacheFactory.NewCache([]cache.Setting{ 172 | { 173 | Prefix: "get-by-func", 174 | CacheAttributes: map[cache.Type]cache.Attribute{ 175 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 176 | }, 177 | MarshalFunc: msgpack.Marshal, // msgpack is from "github.com/vmihailenco/msgpack/v5" 178 | UnmarshalFunc: msgpack.Unmarshal, 179 | }, 180 | }) 181 | 182 | ctx := context.TODO() 183 | container2 := &Object{} 184 | if err := c.GetByFunc(ctx, "get-by-func", "key2", container2, func() (interface{}, error) { 185 | // The getter is used to generate data when cache missed, and refill the cache automatically.. 186 | // You can read from DB or other microservices. 187 | // Assume we read from MySQL according to the key "key2" and get the value of Object{Str: "value2", Num: 2} 188 | return Object{Str: "value2", Num: 2}, nil 189 | }); err != nil { 190 | panic("not expected") 191 | } 192 | 193 | fmt.Println(container2) // Object{ Str: "value2", Num: 2} 194 | 195 | // Output: 196 | // &{value2 2} 197 | } 198 | ``` 199 | 200 | `MGetter` is another approaching way to do this. Set this function durning registering the *Setting*. 201 | 202 | ```go 203 | func ExampleService_Create_mGetter() { 204 | // We create a group of cache named "mgetter". 205 | // It uses both shared and local caches with separated TTL of one hour and ten minutes. 206 | c := cacheFactory.NewCache([]cache.Setting{ 207 | { 208 | Prefix: "mgetter", 209 | CacheAttributes: map[cache.Type]cache.Attribute{ 210 | cache.SharedCacheType: {TTL: time.Hour}, 211 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 212 | }, 213 | MGetter: func(keys ...string) (interface{}, error) { 214 | // The MGetter is used to generate data when cache missed, and refill the cache automatically.. 215 | // You can read from DB or other microservices. 216 | // Assume we read from MySQL according to the key "key3" and get the value of Object{Str: "value3", Num: 3} 217 | // HINT: remember to return as a slice, and the item order needs to consist with the keys in the parameters. 218 | return []Object{{Str: "value3", Num: 3}}, nil 219 | }, 220 | MarshalFunc: cache.Marshal, 221 | UnmarshalFunc: cache.Unmarshal, 222 | }, 223 | }) 224 | 225 | ctx := context.TODO() 226 | container3 := &Object{} 227 | if err := c.Get(ctx, "mgetter", "key3", container3); err != nil { 228 | panic("not expected") 229 | } 230 | 231 | fmt.Println(container3) // Object{ Str: "value3", Num: 3} 232 | 233 | // Output: 234 | // &{value3 3} 235 | } 236 | ``` 237 | 238 | [More examples](./example_advanced_test.go) 239 | 240 | ## References 241 | - https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching 242 | - https://github.com/vmihailenco/go-cache-benchmark 243 | - https://github.com/go-redis/cache 244 | 245 | ## License 246 | [Apache-2.0](https://opensource.org/licenses/Apache-2.0) 247 | 248 | 249 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fviney-shih%2Fgo-cache.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fviney-shih%2Fgo-cache?ref=badge_large) -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /adapter.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Adapter is the interface communicating with shared/local caches. 9 | type Adapter interface { 10 | MGet(context context.Context, keys []string) ([]Value, error) 11 | MSet(context context.Context, keyVals map[string][]byte, ttl time.Duration, options ...MSetOptions) error 12 | Del(context context.Context, keys ...string) error 13 | } 14 | 15 | // MSetOptions is an alias for functional argument. 16 | type MSetOptions func(opts *msetOptions) 17 | 18 | type msetOptions struct { 19 | onCostAdd func(key string, cost int) 20 | onCostEvict func(key string, cost int) 21 | } 22 | 23 | // WithOnCostAddFunc sets up the callback when adding the cache with key and cost. 24 | func WithOnCostAddFunc(f func(key string, cost int)) MSetOptions { 25 | return func(opts *msetOptions) { 26 | opts.onCostAdd = f 27 | } 28 | } 29 | 30 | // WithOnCostEvictFunc sets up the callback when evicting the cache with key and cost. 31 | func WithOnCostEvictFunc(f func(key string, cost int)) MSetOptions { 32 | return func(opts *msetOptions) { 33 | opts.onCostEvict = f 34 | } 35 | } 36 | 37 | func loadMSetOptions(options ...MSetOptions) *msetOptions { 38 | opts := &msetOptions{} 39 | for _, option := range options { 40 | option(opts) 41 | } 42 | 43 | return opts 44 | } 45 | 46 | // Value is returned by MGet() 47 | type Value struct { 48 | // Valid stands for existing in cache or not. 49 | Valid bool 50 | // Bytes stands for the return value in byte format. 51 | Bytes []byte 52 | } 53 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viney-shih/go-cache/49f768117824728aa53f0ac9485c450ede7e335c/assets/logo.png -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "time" 7 | 8 | "golang.org/x/sync/singleflight" 9 | ) 10 | 11 | type cache struct { 12 | configs map[string]*config 13 | onCacheHit func(prefix string, key string, count int) 14 | onCacheMiss func(prefix string, key string, count int) 15 | onLCCostAdd func(key string, cost int) 16 | onLCCostEvict func(key string, cost int) 17 | mb *messageBroker 18 | 19 | singleflight singleflight.Group 20 | } 21 | 22 | type config struct { 23 | shared Adapter 24 | local Adapter 25 | sharedTTL time.Duration 26 | localTTL time.Duration 27 | mGetter MGetterFunc 28 | marshal MarshalFunc 29 | unmarshal UnmarshalFunc 30 | } 31 | 32 | func (c *cache) GetByFunc(ctx context.Context, prefix, key string, container interface{}, getter OneTimeGetterFunc) error { 33 | cfg, ok := c.configs[prefix] 34 | if !ok { 35 | return ErrPfxNotRegistered 36 | } 37 | 38 | intf, err, _ := c.singleflight.Do(getCacheKey(prefix, key), func() (interface{}, error) { 39 | cacheKey := getCacheKey(prefix, key) 40 | cacheVals, err := c.load(ctx, cfg, cacheKey) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // cache hit 46 | if cacheVals[0].Valid { 47 | c.onCacheHit(prefix, key, 1) 48 | return cacheVals[0].Bytes, nil 49 | } 50 | 51 | // cache missed once 52 | c.onCacheMiss(prefix, key, 1) 53 | 54 | // using oneTimeGetter to implement Cache-Aside pattern 55 | intf, err := getter() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | b, err := cfg.marshal(intf) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | // refill cache 66 | if err := c.refill(ctx, cfg, map[string][]byte{cacheKey: b}); err != nil { 67 | return nil, err 68 | } 69 | 70 | return b, nil 71 | }) 72 | 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return cfg.unmarshal(intf.([]byte), container) 78 | } 79 | 80 | func (c *cache) Get(ctx context.Context, prefix, key string, container interface{}) error { 81 | intf, err, _ := c.singleflight.Do(getCacheKey(prefix, key), func() (interface{}, error) { 82 | return c.MGet(ctx, prefix, key) 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return intf.(Result).Get(ctx, 0, container) 89 | } 90 | 91 | func (c *cache) MGet(ctx context.Context, prefix string, keys ...string) (Result, error) { 92 | cfg, ok := c.configs[prefix] 93 | if !ok { 94 | return nil, ErrPfxNotRegistered 95 | } 96 | 97 | if len(keys) == 0 { 98 | return &result{unmarshal: cfg.unmarshal}, nil 99 | } 100 | 101 | // TODO: support singleflight in the future 102 | 103 | // IdxM means internal index map 104 | // dKeys means deduped keys 105 | IdxM, dKeys := dedup(keys) 106 | 107 | res := &result{ 108 | internalIdx: IdxM, 109 | vals: make([][]byte, len(dKeys)), 110 | errs: make([]error, len(dKeys)), 111 | unmarshal: cfg.unmarshal, 112 | } 113 | 114 | // 1. get from cache 115 | keyIdx := getKeyIndex(dKeys) 116 | cacheKeys := getCacheKeys(prefix, dKeys) 117 | 118 | cacheVals, err := c.load(ctx, cfg, cacheKeys...) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | missKeys := []string{} 124 | for i, k := range dKeys { 125 | if !cacheVals[i].Valid { 126 | missKeys = append(missKeys, k) 127 | res.errs[i] = ErrCacheMiss 128 | c.onCacheMiss(prefix, k, 1) 129 | continue 130 | } 131 | 132 | res.vals[i] = cacheVals[i].Bytes 133 | c.onCacheHit(prefix, k, 1) 134 | } 135 | 136 | // no cache missing 137 | if len(missKeys) == 0 { 138 | return res, nil 139 | } 140 | 141 | // no mGetter, simple Get & Set pattern, return it directly 142 | if cfg.mGetter == nil { 143 | return res, nil 144 | } 145 | 146 | // 2. using mGetter to implement Cache-Aside pattern 147 | intfs, err := cfg.mGetter(missKeys...) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | vs := reflect.ValueOf(intfs) 153 | if vs.Kind() != reflect.Slice { 154 | return nil, ErrMGetterResponseNotSlice 155 | } 156 | if vs.Len() != len(missKeys) { 157 | return nil, ErrMGetterResponseLengthInvalid 158 | } 159 | 160 | m := map[string][]byte{} 161 | for i, mk := range missKeys { 162 | v := vs.Index(i).Interface() 163 | b, err := cfg.marshal(v) 164 | if err != nil { 165 | res.errs[keyIdx[mk]] = err 166 | continue 167 | } 168 | 169 | m[getCacheKey(prefix, mk)] = b 170 | res.vals[keyIdx[mk]] = b 171 | res.errs[keyIdx[mk]] = nil 172 | } 173 | 174 | // 3. load the cache 175 | c.refill(ctx, cfg, m) 176 | 177 | return res, nil 178 | } 179 | 180 | func (c *cache) Del(ctx context.Context, prefix string, keys ...string) error { 181 | cfg, ok := c.configs[prefix] 182 | if !ok { 183 | return ErrPfxNotRegistered 184 | } 185 | 186 | if len(keys) == 0 { 187 | return nil 188 | } 189 | 190 | return c.del(ctx, cfg, getCacheKeys(prefix, keys)...) 191 | } 192 | 193 | func (c *cache) Set(ctx context.Context, prefix string, key string, value interface{}) error { 194 | return c.MSet(ctx, prefix, map[string]interface{}{key: value}) 195 | } 196 | 197 | func (c *cache) MSet(ctx context.Context, prefix string, keyValues map[string]interface{}) error { 198 | cfg, ok := c.configs[prefix] 199 | if !ok { 200 | return ErrPfxNotRegistered 201 | } 202 | 203 | m := map[string][]byte{} 204 | for k, value := range keyValues { 205 | b, err := cfg.marshal(value) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | m[getCacheKey(prefix, k)] = b 211 | } 212 | 213 | return c.refill(ctx, cfg, m) 214 | } 215 | 216 | func getKeyIndex(keys []string) map[string]int { 217 | keyIdx := map[string]int{} 218 | for i, k := range keys { 219 | keyIdx[k] = i 220 | } 221 | 222 | return keyIdx 223 | } 224 | 225 | func dedup(params []string) (map[int]int, []string) { 226 | if len(params) == 1 { 227 | return map[int]int{0: 0}, params 228 | } 229 | 230 | dedupedKeys := []string{} 231 | // dedupedIdx is an indirect index that maps un-dedup idx to dedup idx 232 | dedupedIdx := map[int]int{} 233 | // m maps param to dedup idx 234 | m := map[string]int{} 235 | for i, param := range params { 236 | if _, ok := m[param]; ok { 237 | dedupedIdx[i] = m[param] 238 | continue 239 | } 240 | 241 | dedupedIdx[i] = len(dedupedKeys) 242 | m[param] = len(dedupedKeys) 243 | dedupedKeys = append(dedupedKeys, param) 244 | } 245 | 246 | return dedupedIdx, dedupedKeys 247 | } 248 | 249 | // load loads data from cache, and refill it if necessary 250 | func (c *cache) load(ctx context.Context, cfg *config, keys ...string) ([]Value, error) { 251 | vals := make([]Value, len(keys)) 252 | missKeys := make([]string, len(keys)) 253 | copy(missKeys, keys) 254 | 255 | keyIdx := getKeyIndex(keys) 256 | 257 | // 1. load from local cache 258 | if cfg.local != nil { 259 | // allow the failure when getting local cache 260 | vals, _ = cfg.local.MGet(ctx, keys) 261 | 262 | missKeys = []string{} 263 | for i, val := range vals { 264 | if !val.Valid { 265 | missKeys = append(missKeys, keys[i]) 266 | } 267 | } 268 | } 269 | 270 | // no cache missing 271 | if len(missKeys) == 0 { 272 | return vals, nil 273 | } 274 | 275 | // 2. load from shared cache 276 | if cfg.shared != nil { 277 | missVals, err := cfg.shared.MGet(ctx, missKeys) 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | // refill missing values into vals 283 | for i, mVal := range missVals { 284 | vals[keyIdx[missKeys[i]]] = mVal 285 | } 286 | } 287 | 288 | // 3. refill the local cache if possible 289 | if cfg.local != nil { 290 | m := map[string][]byte{} 291 | for _, k := range keys { 292 | val := vals[keyIdx[k]] 293 | if val.Valid { 294 | m[k] = val.Bytes 295 | } 296 | } 297 | 298 | if len(m) != 0 { 299 | cfg.local.MSet(ctx, m, cfg.localTTL, 300 | WithOnCostAddFunc(c.onLCCostAdd), 301 | WithOnCostEvictFunc(c.onLCCostEvict), 302 | ) 303 | 304 | c.evictRemoteKeyMap(ctx, m) 305 | } 306 | } 307 | 308 | return vals, nil 309 | } 310 | 311 | // refill refills the cache with given keyBytes 312 | func (c *cache) refill(ctx context.Context, cfg *config, keyBytes map[string][]byte) error { 313 | // set shared cache first if necessary 314 | if cfg.shared != nil { 315 | if err := cfg.shared.MSet(ctx, keyBytes, cfg.sharedTTL); err != nil { 316 | return err 317 | } 318 | } 319 | 320 | // then, set local cache if necessary 321 | if cfg.local != nil { 322 | if err := cfg.local.MSet(ctx, keyBytes, cfg.localTTL, 323 | WithOnCostAddFunc(c.onLCCostAdd), 324 | WithOnCostEvictFunc(c.onLCCostEvict), 325 | ); err != nil { 326 | return nil 327 | } 328 | 329 | c.evictRemoteKeyMap(ctx, keyBytes) 330 | } 331 | 332 | return nil 333 | } 334 | 335 | func (c *cache) del(ctx context.Context, cfg *config, keys ...string) error { 336 | if cfg.shared != nil { 337 | if err := cfg.shared.Del(ctx, keys...); err != nil { 338 | return err 339 | } 340 | } 341 | 342 | if cfg.local != nil { 343 | if err := cfg.local.Del(ctx, keys...); err != nil { 344 | return err 345 | } 346 | 347 | c.evictRemoteKeys(ctx, keys...) 348 | } 349 | 350 | return nil 351 | } 352 | 353 | func (c *cache) evictRemoteKeyMap(ctx context.Context, keyM map[string][]byte) error { 354 | if !c.mb.registered() { 355 | // no pubsub, do nothing 356 | return nil 357 | } 358 | 359 | keys := make([]string, len(keyM)) 360 | i := 0 361 | for k := range keyM { 362 | keys[i] = k 363 | i++ 364 | } 365 | 366 | return c.evictRemoteKeys(ctx, keys...) 367 | } 368 | 369 | func (c *cache) evictRemoteKeys(ctx context.Context, keys ...string) error { 370 | if !c.mb.registered() { 371 | // no pubsub, do nothing 372 | return nil 373 | } 374 | 375 | return c.mb.send(ctx, event{ 376 | Type: EventTypeEvict, 377 | Body: eventBody{Keys: keys}, 378 | }) 379 | } 380 | 381 | type result struct { 382 | internalIdx map[int]int 383 | vals [][]byte 384 | errs []error 385 | unmarshal UnmarshalFunc 386 | } 387 | 388 | func (r *result) Len() int { 389 | return len(r.internalIdx) 390 | } 391 | 392 | func (r *result) Get(ctx context.Context, idx int, container interface{}) error { 393 | if idx < 0 || idx >= r.Len() { 394 | return ErrResultIndexInvalid 395 | } 396 | 397 | if r.errs[r.internalIdx[idx]] != nil { 398 | return r.errs[r.internalIdx[idx]] 399 | } 400 | 401 | return r.unmarshal(r.vals[r.internalIdx[idx]], container) 402 | } 403 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/stretchr/testify/suite" 12 | "github.com/vmihailenco/go-tinylfu" 13 | ) 14 | 15 | const ( 16 | mockString = "mock-string" 17 | ) 18 | 19 | var ( 20 | mockCacheCTX = context.Background() 21 | ) 22 | 23 | type cacheSuite struct { 24 | suite.Suite 25 | 26 | factory *factory 27 | rds *rds 28 | lfu *tinyLFU 29 | ring *redis.Ring 30 | } 31 | 32 | func (s *cacheSuite) SetupSuite() { 33 | s.ring = redis.NewRing(&redis.RingOptions{ 34 | Addrs: map[string]string{ 35 | "server1": ":6379", 36 | }, 37 | }) 38 | } 39 | 40 | func (s *cacheSuite) TearDownSuite() {} 41 | 42 | func (s *cacheSuite) SetupTest() { 43 | s.rds = NewRedis(s.ring).(*rds) 44 | s.lfu = NewTinyLFU(10000).(*tinyLFU) 45 | s.factory = NewFactory(s.rds, s.lfu).(*factory) 46 | } 47 | 48 | func (s *cacheSuite) TearDownTest() { 49 | // prevent registering twice 50 | ClearPrefix() 51 | // flush redis 52 | _ = s.ring.ForEachShard(mockCacheCTX, func(ctx context.Context, client *redis.Client) error { 53 | return client.FlushDB(ctx).Err() 54 | }) 55 | 56 | s.factory.Close() 57 | } 58 | 59 | func TestCacheSuite(t *testing.T) { 60 | suite.Run(t, new(cacheSuite)) 61 | } 62 | 63 | func (s *cacheSuite) TestMSet() { 64 | tests := []struct { 65 | Desc string 66 | Settings []Setting 67 | Prefix string 68 | KeyValues map[string]interface{} 69 | ExpError map[string]error 70 | CheckFunc map[string]func(string) 71 | }{ 72 | { 73 | Desc: "prefix not registered", 74 | Settings: []Setting{{ 75 | Prefix: "registered", CacheAttributes: map[Type]Attribute{SharedCacheType: {TTL: time.Hour}}, 76 | }}, 77 | Prefix: "not-registered", 78 | ExpError: map[string]error{ 79 | "not-registered": ErrPfxNotRegistered, 80 | }, 81 | }, 82 | { 83 | Desc: "normal MSet", 84 | Settings: []Setting{ 85 | { 86 | Prefix: "mixed", 87 | CacheAttributes: map[Type]Attribute{ 88 | SharedCacheType: {TTL: time.Hour}, 89 | LocalCacheType: {TTL: time.Hour}, 90 | }, 91 | }, 92 | { 93 | Prefix: "redis", 94 | CacheAttributes: map[Type]Attribute{ 95 | SharedCacheType: {TTL: time.Hour}, 96 | }, 97 | }, 98 | { 99 | Prefix: "local", 100 | CacheAttributes: map[Type]Attribute{ 101 | LocalCacheType: {TTL: time.Hour}, 102 | }, 103 | }, 104 | }, 105 | KeyValues: map[string]interface{}{ 106 | "keyS": mockString, 107 | "keyI": 80, 108 | }, 109 | ExpError: map[string]error{ 110 | "mixed": nil, 111 | "redis": nil, 112 | "local": nil, 113 | }, 114 | CheckFunc: map[string]func(desc string){ 115 | "mixed": func(desc string) { 116 | cacheKeyS := getCacheKey("mixed", "keyS") 117 | cacheKeyI := getCacheKey("mixed", "keyI") 118 | expSB, _ := json.Marshal(mockString) 119 | expIB, _ := json.Marshal(80) 120 | 121 | b, exist := s.lfu.lfu.Get(cacheKeyS) 122 | s.Require().True(exist, desc, "mixed") 123 | s.Require().Equal(expSB, b, desc, "mixed") 124 | b, exist = s.lfu.lfu.Get(cacheKeyI) 125 | s.Require().True(exist, desc, "mixed") 126 | s.Require().Equal(expIB, b, desc, "mixed") 127 | 128 | b, err := s.ring.Get(mockCacheCTX, cacheKeyS).Bytes() 129 | s.Require().NoError(err, desc, "mixed") 130 | s.Require().Equal(expSB, b, desc, "mixed") 131 | b, err = s.ring.Get(mockCacheCTX, cacheKeyI).Bytes() 132 | s.Require().NoError(err, desc, "mixed") 133 | s.Require().Equal(expIB, b, desc, "mixed") 134 | }, 135 | "redis": func(desc string) { 136 | cacheKeyS := getCacheKey("redis", "keyS") 137 | cacheKeyI := getCacheKey("redis", "keyI") 138 | expSB, _ := json.Marshal(mockString) 139 | expIB, _ := json.Marshal(80) 140 | 141 | _, exist := s.lfu.lfu.Get(cacheKeyS) 142 | s.Require().False(exist, desc, "redis") 143 | _, exist = s.lfu.lfu.Get(cacheKeyI) 144 | s.Require().False(exist, desc, "redis") 145 | 146 | b, err := s.ring.Get(mockCacheCTX, cacheKeyS).Bytes() 147 | s.Require().NoError(err, desc, "redis") 148 | s.Require().Equal(expSB, b, desc, "redis") 149 | b, err = s.ring.Get(mockCacheCTX, cacheKeyI).Bytes() 150 | s.Require().NoError(err, desc, "redis") 151 | s.Require().Equal(expIB, b, desc, "redis") 152 | }, 153 | "local": func(desc string) { 154 | cacheKeyS := getCacheKey("local", "keyS") 155 | cacheKeyI := getCacheKey("local", "keyI") 156 | expSB, _ := json.Marshal(mockString) 157 | expIB, _ := json.Marshal(80) 158 | 159 | b, exist := s.lfu.lfu.Get(cacheKeyS) 160 | s.Require().True(exist, desc, "local") 161 | s.Require().Equal(expSB, b, desc, "local") 162 | b, exist = s.lfu.lfu.Get(cacheKeyI) 163 | s.Require().True(exist, desc, "local") 164 | s.Require().Equal(expIB, b, desc, "local") 165 | 166 | _, err := s.ring.Get(mockCacheCTX, cacheKeyS).Bytes() 167 | s.Require().Equal(redis.Nil, err, desc, "local") 168 | _, err = s.ring.Get(mockCacheCTX, cacheKeyI).Bytes() 169 | s.Require().Equal(redis.Nil, err, desc, "local") 170 | }, 171 | }, 172 | }, 173 | } 174 | 175 | for _, t := range tests { 176 | c := s.factory.NewCache(t.Settings) 177 | 178 | for _, sett := range t.Settings { 179 | pfx := sett.Prefix 180 | if t.Prefix != "" { 181 | pfx = t.Prefix 182 | } 183 | err := c.MSet(mockCacheCTX, pfx, t.KeyValues) 184 | s.Require().Equal(t.ExpError[pfx], err, t.Desc) 185 | 186 | if t.CheckFunc[pfx] != nil { 187 | t.CheckFunc[pfx](t.Desc) 188 | } 189 | 190 | s.TearDownTest() 191 | } 192 | } 193 | } 194 | 195 | func (s *cacheSuite) TestSet() { 196 | tests := []struct { 197 | Desc string 198 | Settings []Setting 199 | Prefix string 200 | Key string 201 | Value interface{} 202 | ExpError map[string]error 203 | CheckFunc map[string]func(string) 204 | }{ 205 | { 206 | Desc: "prefix not registered", 207 | Settings: []Setting{{ 208 | Prefix: "registered", CacheAttributes: map[Type]Attribute{SharedCacheType: {TTL: time.Hour}}, 209 | }}, 210 | Prefix: "not-registered", 211 | ExpError: map[string]error{ 212 | "not-registered": ErrPfxNotRegistered, 213 | }, 214 | }, 215 | { 216 | Desc: "normal Set", 217 | Settings: []Setting{ 218 | { 219 | Prefix: "mixed", 220 | CacheAttributes: map[Type]Attribute{ 221 | SharedCacheType: {TTL: time.Hour}, 222 | LocalCacheType: {TTL: time.Hour}, 223 | }, 224 | }, 225 | { 226 | Prefix: "redis", 227 | CacheAttributes: map[Type]Attribute{ 228 | SharedCacheType: {TTL: time.Hour}, 229 | }, 230 | }, 231 | { 232 | Prefix: "local", 233 | CacheAttributes: map[Type]Attribute{ 234 | LocalCacheType: {TTL: time.Hour}, 235 | }, 236 | }, 237 | }, 238 | Key: "key", 239 | Value: float32(13.38), 240 | ExpError: map[string]error{ 241 | "mixed": nil, 242 | "redis": nil, 243 | "local": nil, 244 | }, 245 | CheckFunc: map[string]func(desc string){ 246 | "mixed": func(desc string) { 247 | cacheKey := getCacheKey("mixed", "key") 248 | expB, _ := json.Marshal(float32(13.38)) 249 | 250 | b, exist := s.lfu.lfu.Get(cacheKey) 251 | s.Require().True(exist, desc, "mixed") 252 | s.Require().Equal(expB, b, desc, "mixed") 253 | 254 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 255 | s.Require().NoError(err, desc, "mixed") 256 | s.Require().Equal(expB, b, desc, "mixed") 257 | }, 258 | "redis": func(desc string) { 259 | cacheKey := getCacheKey("redis", "key") 260 | expB, _ := json.Marshal(float32(13.38)) 261 | 262 | _, exist := s.lfu.lfu.Get(cacheKey) 263 | s.Require().False(exist, desc, "redis") 264 | 265 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 266 | s.Require().NoError(err, desc, "redis") 267 | s.Require().Equal(expB, b, desc, "redis") 268 | }, 269 | "local": func(desc string) { 270 | cacheKey := getCacheKey("local", "key") 271 | expB, _ := json.Marshal(float32(13.38)) 272 | 273 | b, exist := s.lfu.lfu.Get(cacheKey) 274 | s.Require().True(exist, desc, "local") 275 | s.Require().Equal(expB, b, desc, "local") 276 | 277 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 278 | s.Require().Equal(redis.Nil, err, desc, "local") 279 | }, 280 | }, 281 | }, 282 | } 283 | 284 | for _, t := range tests { 285 | c := s.factory.NewCache(t.Settings) 286 | 287 | for _, sett := range t.Settings { 288 | pfx := sett.Prefix 289 | if t.Prefix != "" { 290 | pfx = t.Prefix 291 | } 292 | err := c.Set(mockCacheCTX, pfx, t.Key, t.Value) 293 | s.Require().Equal(t.ExpError[pfx], err, t.Desc) 294 | 295 | if t.CheckFunc[pfx] != nil { 296 | t.CheckFunc[pfx](t.Desc) 297 | } 298 | 299 | s.TearDownTest() 300 | } 301 | } 302 | } 303 | 304 | func (s *cacheSuite) TestDel() { 305 | tests := []struct { 306 | Desc string 307 | Settings []Setting 308 | Prefix string 309 | SetupTest map[string]func(string) 310 | Keys []string 311 | ExpError map[string]error 312 | CheckFunc map[string]func(string) 313 | }{ 314 | { 315 | Desc: "prefix not registered", 316 | Settings: []Setting{{ 317 | Prefix: "registered", CacheAttributes: map[Type]Attribute{SharedCacheType: {TTL: time.Hour}}, 318 | }}, 319 | Prefix: "not-registered", 320 | ExpError: map[string]error{ 321 | "not-registered": ErrPfxNotRegistered, 322 | }, 323 | }, 324 | { 325 | Desc: "normal Del", 326 | Settings: []Setting{ 327 | { 328 | Prefix: "mixed", 329 | CacheAttributes: map[Type]Attribute{ 330 | SharedCacheType: {TTL: time.Hour}, 331 | LocalCacheType: {TTL: time.Hour}, 332 | }, 333 | }, 334 | { 335 | Prefix: "redis", 336 | CacheAttributes: map[Type]Attribute{ 337 | SharedCacheType: {TTL: time.Hour}, 338 | }, 339 | }, 340 | { 341 | Prefix: "local", 342 | CacheAttributes: map[Type]Attribute{ 343 | LocalCacheType: {TTL: time.Hour}, 344 | }, 345 | }, 346 | }, 347 | SetupTest: map[string]func(desc string){ 348 | "mixed": func(desc string) { 349 | cacheKey := getCacheKey("mixed", "key") 350 | expB, _ := json.Marshal(mockString) 351 | 352 | s.lfu.lfu.Set(&tinylfu.Item{ 353 | Key: cacheKey, 354 | Value: expB, 355 | ExpireAt: time.Now().Add(time.Hour), 356 | }) 357 | b, exist := s.lfu.lfu.Get(cacheKey) 358 | s.Require().True(exist, desc, "mixed") 359 | s.Require().Equal(expB, b, desc, "mixed") 360 | 361 | s.Require().NoError(s.ring.Set(mockCacheCTX, cacheKey, expB, time.Hour).Err(), desc) 362 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 363 | s.Require().NoError(err, desc, "mixed") 364 | s.Require().Equal(expB, b, desc, "mixed") 365 | }, 366 | "redis": func(desc string) { 367 | cacheKey := getCacheKey("redis", "key") 368 | expB, _ := json.Marshal(mockString) 369 | 370 | s.lfu.lfu.Set(&tinylfu.Item{ 371 | Key: cacheKey, 372 | Value: expB, 373 | ExpireAt: time.Now().Add(time.Hour), 374 | }) 375 | b, exist := s.lfu.lfu.Get(cacheKey) 376 | s.Require().True(exist, desc, "redis") 377 | s.Require().Equal(expB, b, desc, "redis") 378 | 379 | s.Require().NoError(s.ring.Set(mockCacheCTX, cacheKey, expB, time.Hour).Err(), desc) 380 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 381 | s.Require().NoError(err, desc, "redis") 382 | s.Require().Equal(expB, b, desc, "redis") 383 | }, 384 | "local": func(desc string) { 385 | cacheKey := getCacheKey("local", "key") 386 | expB, _ := json.Marshal(mockString) 387 | 388 | s.lfu.lfu.Set(&tinylfu.Item{ 389 | Key: cacheKey, 390 | Value: expB, 391 | ExpireAt: time.Now().Add(time.Hour), 392 | }) 393 | b, exist := s.lfu.lfu.Get(cacheKey) 394 | s.Require().True(exist, desc, "local") 395 | s.Require().Equal(expB, b, desc, "local") 396 | 397 | s.Require().NoError(s.ring.Set(mockCacheCTX, cacheKey, expB, time.Hour).Err(), desc) 398 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 399 | s.Require().NoError(err, desc, "local") 400 | s.Require().Equal(expB, b, desc, "local") 401 | }, 402 | }, 403 | Keys: []string{"key", "not-existed"}, 404 | ExpError: map[string]error{ 405 | "mixed": nil, 406 | "redis": nil, 407 | "local": nil, 408 | }, 409 | CheckFunc: map[string]func(desc string){ 410 | "mixed": func(desc string) { 411 | cacheKey := getCacheKey("mixed", "key") 412 | 413 | _, exist := s.lfu.lfu.Get(cacheKey) 414 | s.Require().False(exist, desc, "mixed") 415 | 416 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 417 | s.Require().Equal(redis.Nil, err, desc, "mixed") 418 | }, 419 | "redis": func(desc string) { 420 | cacheKey := getCacheKey("redis", "key") 421 | expB, _ := json.Marshal(mockString) 422 | 423 | b, exist := s.lfu.lfu.Get(cacheKey) 424 | s.Require().True(exist, desc, "redis") 425 | s.Require().Equal(expB, b, desc, "redis") 426 | 427 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 428 | s.Require().Equal(redis.Nil, err, desc, "redis") 429 | }, 430 | "local": func(desc string) { 431 | cacheKey := getCacheKey("local", "key") 432 | expB, _ := json.Marshal(mockString) 433 | 434 | _, exist := s.lfu.lfu.Get(cacheKey) 435 | s.Require().False(exist, desc, "local") 436 | 437 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 438 | s.Require().NoError(err, desc, "local") 439 | s.Require().Equal(expB, b, desc, "local") 440 | }, 441 | }, 442 | }, 443 | } 444 | 445 | for _, t := range tests { 446 | c := s.factory.NewCache(t.Settings) 447 | 448 | for _, sett := range t.Settings { 449 | pfx := sett.Prefix 450 | if t.Prefix != "" { 451 | pfx = t.Prefix 452 | } 453 | 454 | if t.SetupTest[pfx] != nil { 455 | t.SetupTest[pfx](t.Desc) 456 | } 457 | 458 | err := c.Del(mockCacheCTX, pfx, t.Keys...) 459 | s.Require().Equal(t.ExpError[pfx], err, t.Desc) 460 | 461 | if t.CheckFunc[pfx] != nil { 462 | t.CheckFunc[pfx](t.Desc) 463 | } 464 | 465 | s.TearDownTest() 466 | } 467 | } 468 | } 469 | 470 | type resultPair struct { 471 | err error 472 | value interface{} 473 | } 474 | 475 | func (s *cacheSuite) TestMGet() { 476 | tests := []struct { 477 | Desc string 478 | Settings []Setting 479 | Prefix string 480 | SetupTest map[string]func(string) 481 | Keys []string 482 | ExpError map[string]error 483 | ExpResultLen map[string]int 484 | ExpResultValue map[string][]resultPair 485 | CheckFunc map[string]func(string) 486 | }{ 487 | { 488 | Desc: "prefix not registered", 489 | Settings: []Setting{{ 490 | Prefix: "registered", CacheAttributes: map[Type]Attribute{SharedCacheType: {TTL: time.Hour}}, 491 | }}, 492 | Prefix: "not-registered", 493 | ExpError: map[string]error{ 494 | "not-registered": ErrPfxNotRegistered, 495 | }, 496 | }, 497 | { 498 | Desc: "MGet in local", 499 | Settings: []Setting{ 500 | { 501 | Prefix: "mixed", 502 | CacheAttributes: map[Type]Attribute{ 503 | SharedCacheType: {TTL: time.Hour}, 504 | LocalCacheType: {TTL: time.Hour}, 505 | }, 506 | }, 507 | { 508 | Prefix: "redis", 509 | CacheAttributes: map[Type]Attribute{ 510 | SharedCacheType: {TTL: time.Hour}, 511 | }, 512 | }, 513 | { 514 | Prefix: "local", 515 | CacheAttributes: map[Type]Attribute{ 516 | LocalCacheType: {TTL: time.Hour}, 517 | }, 518 | }, 519 | }, 520 | SetupTest: map[string]func(desc string){ 521 | "mixed": func(desc string) { 522 | cacheKey := getCacheKey("mixed", "key") 523 | expB, _ := json.Marshal(mockString) 524 | 525 | s.lfu.lfu.Set(&tinylfu.Item{ 526 | Key: cacheKey, 527 | Value: expB, 528 | ExpireAt: time.Now().Add(time.Hour), 529 | }) 530 | b, exist := s.lfu.lfu.Get(cacheKey) 531 | s.Require().True(exist, desc, "mixed") 532 | s.Require().Equal(expB, b, desc, "mixed") 533 | 534 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 535 | s.Require().Equal(redis.Nil, err, desc, "mixed") 536 | }, 537 | "redis": func(desc string) { 538 | cacheKey := getCacheKey("redis", "key") 539 | expB, _ := json.Marshal(mockString) 540 | 541 | s.lfu.lfu.Set(&tinylfu.Item{ 542 | Key: cacheKey, 543 | Value: expB, 544 | ExpireAt: time.Now().Add(time.Hour), 545 | }) 546 | b, exist := s.lfu.lfu.Get(cacheKey) 547 | s.Require().True(exist, desc, "redis") 548 | s.Require().Equal(expB, b, desc, "redis") 549 | 550 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 551 | s.Require().Equal(redis.Nil, err, desc, "redis") 552 | }, 553 | "local": func(desc string) { 554 | cacheKey := getCacheKey("local", "key") 555 | expB, _ := json.Marshal(mockString) 556 | 557 | s.lfu.lfu.Set(&tinylfu.Item{ 558 | Key: cacheKey, 559 | Value: expB, 560 | ExpireAt: time.Now().Add(time.Hour), 561 | }) 562 | b, exist := s.lfu.lfu.Get(cacheKey) 563 | s.Require().True(exist, desc, "local") 564 | s.Require().Equal(expB, b, desc, "local") 565 | 566 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 567 | s.Require().Equal(redis.Nil, err, desc, "local") 568 | }, 569 | }, 570 | Keys: []string{"key", "not-existed"}, 571 | ExpError: map[string]error{ 572 | "mixed": nil, 573 | "redis": nil, 574 | "local": nil, 575 | }, 576 | ExpResultLen: map[string]int{ 577 | "mixed": 2, 578 | "redis": 2, 579 | "local": 2, 580 | }, 581 | ExpResultValue: map[string][]resultPair{ 582 | "mixed": {{value: mockString, err: nil}, {value: "", err: ErrCacheMiss}}, 583 | "redis": {{value: "", err: ErrCacheMiss}, {value: "", err: ErrCacheMiss}}, 584 | "local": {{value: mockString, err: nil}, {value: "", err: ErrCacheMiss}}, 585 | }, 586 | CheckFunc: map[string]func(desc string){ 587 | "mixed": func(desc string) { 588 | cacheKey := getCacheKey("mixed", "key") 589 | expB, _ := json.Marshal(mockString) 590 | 591 | b, exist := s.lfu.lfu.Get(cacheKey) 592 | s.Require().True(exist, desc, "mixed") 593 | s.Require().Equal(expB, b, desc, "mixed") 594 | 595 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 596 | s.Require().Equal(redis.Nil, err, desc, "mixed") 597 | }, 598 | "redis": func(desc string) { 599 | cacheKey := getCacheKey("redis", "key") 600 | expB, _ := json.Marshal(mockString) 601 | 602 | b, exist := s.lfu.lfu.Get(cacheKey) 603 | s.Require().True(exist, desc, "redis") 604 | s.Require().Equal(expB, b, desc, "redis") 605 | 606 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 607 | s.Require().Equal(redis.Nil, err, desc, "redis") 608 | }, 609 | "local": func(desc string) { 610 | cacheKey := getCacheKey("local", "key") 611 | expB, _ := json.Marshal(mockString) 612 | 613 | b, exist := s.lfu.lfu.Get(cacheKey) 614 | s.Require().True(exist, desc, "local") 615 | s.Require().Equal(expB, b, desc, "local") 616 | 617 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 618 | s.Require().Equal(redis.Nil, err, desc, "local") 619 | }, 620 | }, 621 | }, 622 | { 623 | Desc: "MGet in redis", 624 | Settings: []Setting{ 625 | { 626 | Prefix: "mixed", 627 | CacheAttributes: map[Type]Attribute{ 628 | SharedCacheType: {TTL: time.Hour}, 629 | LocalCacheType: {TTL: time.Hour}, 630 | }, 631 | }, 632 | { 633 | Prefix: "redis", 634 | CacheAttributes: map[Type]Attribute{ 635 | SharedCacheType: {TTL: time.Hour}, 636 | }, 637 | }, 638 | { 639 | Prefix: "local", 640 | CacheAttributes: map[Type]Attribute{ 641 | LocalCacheType: {TTL: time.Hour}, 642 | }, 643 | }, 644 | }, 645 | SetupTest: map[string]func(desc string){ 646 | "mixed": func(desc string) { 647 | cacheKey := getCacheKey("mixed", "key") 648 | expB, _ := json.Marshal(mockString) 649 | 650 | _, exist := s.lfu.lfu.Get(cacheKey) 651 | s.Require().False(exist, desc, "mixed") 652 | 653 | s.Require().NoError(s.ring.Set(mockCacheCTX, cacheKey, expB, time.Hour).Err(), desc) 654 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 655 | s.Require().NoError(err, desc, "mixed") 656 | s.Require().Equal(expB, b, desc, "mixed") 657 | }, 658 | "redis": func(desc string) { 659 | cacheKey := getCacheKey("redis", "key") 660 | expB, _ := json.Marshal(mockString) 661 | 662 | _, exist := s.lfu.lfu.Get(cacheKey) 663 | s.Require().False(exist, desc, "redis") 664 | 665 | s.Require().NoError(s.ring.Set(mockCacheCTX, cacheKey, expB, time.Hour).Err(), desc) 666 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 667 | s.Require().NoError(err, desc, "redis") 668 | s.Require().Equal(expB, b, desc, "redis") 669 | }, 670 | "local": func(desc string) { 671 | cacheKey := getCacheKey("local", "key") 672 | expB, _ := json.Marshal(mockString) 673 | 674 | _, exist := s.lfu.lfu.Get(cacheKey) 675 | s.Require().False(exist, desc, "local") 676 | 677 | s.Require().NoError(s.ring.Set(mockCacheCTX, cacheKey, expB, time.Hour).Err(), desc) 678 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 679 | s.Require().NoError(err, desc, "local") 680 | s.Require().Equal(expB, b, desc, "local") 681 | }, 682 | }, 683 | Keys: []string{"key", "not-existed"}, 684 | ExpError: map[string]error{ 685 | "mixed": nil, 686 | "redis": nil, 687 | "local": nil, 688 | }, 689 | ExpResultLen: map[string]int{ 690 | "mixed": 2, 691 | "redis": 2, 692 | "local": 2, 693 | }, 694 | ExpResultValue: map[string][]resultPair{ 695 | "mixed": {{value: mockString, err: nil}, {value: "", err: ErrCacheMiss}}, 696 | "redis": {{value: mockString, err: nil}, {value: "", err: ErrCacheMiss}}, 697 | "local": {{value: "", err: ErrCacheMiss}, {value: "", err: ErrCacheMiss}}, 698 | }, 699 | CheckFunc: map[string]func(desc string){ 700 | "mixed": func(desc string) { 701 | cacheKey := getCacheKey("mixed", "key") 702 | expB, _ := json.Marshal(mockString) 703 | 704 | // refill the cache in local 705 | b, exist := s.lfu.lfu.Get(cacheKey) 706 | s.Require().True(exist, desc, "mixed") 707 | s.Require().Equal(expB, b, desc, "mixed") 708 | 709 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 710 | s.Require().NoError(err, desc, "mixed") 711 | s.Require().Equal(expB, b, desc, "mixed") 712 | }, 713 | "redis": func(desc string) { 714 | cacheKey := getCacheKey("redis", "key") 715 | expB, _ := json.Marshal(mockString) 716 | 717 | _, exist := s.lfu.lfu.Get(cacheKey) 718 | s.Require().False(exist, desc, "redis") 719 | 720 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 721 | s.Require().NoError(err, desc, "redis") 722 | s.Require().Equal(expB, b, desc, "redis") 723 | }, 724 | "local": func(desc string) { 725 | cacheKey := getCacheKey("local", "key") 726 | expB, _ := json.Marshal(mockString) 727 | 728 | _, exist := s.lfu.lfu.Get(cacheKey) 729 | s.Require().False(exist, desc, "local") 730 | 731 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 732 | s.Require().NoError(err, desc, "local") 733 | s.Require().Equal(expB, b, desc, "local") 734 | }, 735 | }, 736 | }, 737 | { 738 | Desc: "MGet in local with MGetter", 739 | Settings: []Setting{ 740 | { 741 | Prefix: "mixed", 742 | CacheAttributes: map[Type]Attribute{ 743 | SharedCacheType: {TTL: time.Hour}, 744 | LocalCacheType: {TTL: time.Hour}, 745 | }, 746 | MGetter: func(keys ...string) (interface{}, error) { 747 | s.Require().Equal([]string{"not-existed"}, keys) 748 | return []string{"mgetter-existed"}, nil 749 | }, 750 | }, 751 | { 752 | Prefix: "redis", 753 | CacheAttributes: map[Type]Attribute{ 754 | SharedCacheType: {TTL: time.Hour}, 755 | }, 756 | MGetter: func(keys ...string) (interface{}, error) { 757 | s.Require().Equal([]string{"key", "not-existed"}, keys) 758 | return []string{"mgetter-key", "mgetter-existed"}, nil 759 | }, 760 | }, 761 | { 762 | Prefix: "local", 763 | CacheAttributes: map[Type]Attribute{ 764 | LocalCacheType: {TTL: time.Hour}, 765 | }, 766 | MGetter: func(keys ...string) (interface{}, error) { 767 | s.Require().Equal([]string{"not-existed"}, keys) 768 | return []string{"mgetter-existed"}, nil 769 | }, 770 | }, 771 | }, 772 | SetupTest: map[string]func(desc string){ 773 | "mixed": func(desc string) { 774 | cacheKey := getCacheKey("mixed", "key") 775 | expB, _ := json.Marshal(mockString) 776 | 777 | s.lfu.lfu.Set(&tinylfu.Item{ 778 | Key: cacheKey, 779 | Value: expB, 780 | ExpireAt: time.Now().Add(time.Hour), 781 | }) 782 | b, exist := s.lfu.lfu.Get(cacheKey) 783 | s.Require().True(exist, desc, "mixed") 784 | s.Require().Equal(expB, b, desc, "mixed") 785 | 786 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 787 | s.Require().Equal(redis.Nil, err, desc, "mixed") 788 | }, 789 | "redis": func(desc string) { 790 | cacheKey := getCacheKey("redis", "key") 791 | expB, _ := json.Marshal(mockString) 792 | 793 | s.lfu.lfu.Set(&tinylfu.Item{ 794 | Key: cacheKey, 795 | Value: expB, 796 | ExpireAt: time.Now().Add(time.Hour), 797 | }) 798 | b, exist := s.lfu.lfu.Get(cacheKey) 799 | s.Require().True(exist, desc, "redis") 800 | s.Require().Equal(expB, b, desc, "redis") 801 | 802 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 803 | s.Require().Equal(redis.Nil, err, desc, "redis") 804 | }, 805 | "local": func(desc string) { 806 | cacheKey := getCacheKey("local", "key") 807 | expB, _ := json.Marshal(mockString) 808 | 809 | s.lfu.lfu.Set(&tinylfu.Item{ 810 | Key: cacheKey, 811 | Value: expB, 812 | ExpireAt: time.Now().Add(time.Hour), 813 | }) 814 | b, exist := s.lfu.lfu.Get(cacheKey) 815 | s.Require().True(exist, desc, "local") 816 | s.Require().Equal(expB, b, desc, "local") 817 | 818 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 819 | s.Require().Equal(redis.Nil, err, desc, "local") 820 | }, 821 | }, 822 | Keys: []string{"key", "not-existed", "key"}, // duplicated key 823 | ExpError: map[string]error{ 824 | "mixed": nil, 825 | "redis": nil, 826 | "local": nil, 827 | }, 828 | ExpResultLen: map[string]int{ 829 | "mixed": 3, 830 | "redis": 3, 831 | "local": 3, 832 | }, 833 | ExpResultValue: map[string][]resultPair{ 834 | "mixed": {{value: mockString, err: nil}, {value: "mgetter-existed", err: nil}, {value: mockString, err: nil}}, 835 | "redis": {{value: "mgetter-key", err: nil}, {value: "mgetter-existed", err: nil}, {value: "mgetter-key", err: nil}}, 836 | "local": {{value: mockString, err: nil}, {value: "mgetter-existed", err: nil}, {value: mockString, err: nil}}, 837 | }, 838 | CheckFunc: map[string]func(desc string){ 839 | "mixed": func(desc string) { 840 | cacheKey := getCacheKey("mixed", "key") 841 | expB, _ := json.Marshal(mockString) 842 | 843 | b, exist := s.lfu.lfu.Get(cacheKey) 844 | s.Require().True(exist, desc, "mixed") 845 | s.Require().Equal(expB, b, desc, "mixed") 846 | 847 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 848 | s.Require().Equal(redis.Nil, err, desc, "mixed") 849 | 850 | // check refilled in cache 851 | notExistKey := getCacheKey("mixed", "not-existed") 852 | notExistB, _ := json.Marshal("mgetter-existed") 853 | 854 | b, exist = s.lfu.lfu.Get(notExistKey) 855 | s.Require().True(exist, desc, "mixed") 856 | s.Require().Equal(notExistB, b, desc, "mixed") 857 | 858 | b, err = s.ring.Get(mockCacheCTX, notExistKey).Bytes() 859 | s.Require().NoError(err, desc, "mixed") 860 | s.Require().Equal(notExistB, b, desc, "mixed") 861 | }, 862 | "redis": func(desc string) { 863 | cacheKey := getCacheKey("redis", "key") 864 | expB, _ := json.Marshal(mockString) 865 | mGetterB, _ := json.Marshal("mgetter-key") 866 | 867 | b, exist := s.lfu.lfu.Get(cacheKey) 868 | s.Require().True(exist, desc, "redis") 869 | s.Require().Equal(expB, b, desc, "redis") 870 | 871 | b, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 872 | s.Require().NoError(err, desc, "redis") 873 | s.Require().Equal(mGetterB, b, desc, "redis") 874 | 875 | // check refilled in cache 876 | notExistKey := getCacheKey("redis", "not-existed") 877 | notExistB, _ := json.Marshal("mgetter-existed") 878 | 879 | _, exist = s.lfu.lfu.Get(notExistKey) 880 | s.Require().False(exist, desc, "redis") 881 | 882 | b, err = s.ring.Get(mockCacheCTX, notExistKey).Bytes() 883 | s.Require().NoError(err, desc, "redis") 884 | s.Require().Equal(notExistB, b, desc, "redis") 885 | }, 886 | "local": func(desc string) { 887 | cacheKey := getCacheKey("local", "key") 888 | expB, _ := json.Marshal(mockString) 889 | 890 | b, exist := s.lfu.lfu.Get(cacheKey) 891 | s.Require().True(exist, desc, "local") 892 | s.Require().Equal(expB, b, desc, "local") 893 | 894 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 895 | s.Require().Equal(redis.Nil, err, desc, "local") 896 | 897 | // check refilled in cache 898 | notExistKey := getCacheKey("local", "not-existed") 899 | notExistB, _ := json.Marshal("mgetter-existed") 900 | 901 | b, exist = s.lfu.lfu.Get(notExistKey) 902 | s.Require().True(exist, desc, "local") 903 | s.Require().Equal(notExistB, b, desc, "local") 904 | 905 | _, err = s.ring.Get(mockCacheCTX, notExistKey).Bytes() 906 | s.Require().Equal(redis.Nil, err, desc, "local") 907 | }, 908 | }, 909 | }, 910 | } 911 | 912 | for _, t := range tests { 913 | c := s.factory.NewCache(t.Settings).(*cache) 914 | 915 | for _, sett := range t.Settings { 916 | pfx := sett.Prefix 917 | if t.Prefix != "" { 918 | pfx = t.Prefix 919 | } 920 | 921 | if t.SetupTest[pfx] != nil { 922 | t.SetupTest[pfx](t.Desc) 923 | } 924 | 925 | r, err := c.MGet(mockCacheCTX, pfx, t.Keys...) 926 | s.Require().Equal(t.ExpError[pfx], err, t.Desc) 927 | if err == nil { 928 | s.Require().Equal(t.ExpResultLen[pfx], r.Len()) 929 | 930 | if r.Len() != 0 { 931 | vs := make([]string, r.Len()) 932 | rs := make([]resultPair, r.Len()) 933 | for i := 0; i < r.Len(); i++ { 934 | err := r.Get(mockCacheCTX, i, &vs[i]) 935 | rs[i].err = err 936 | rs[i].value = vs[i] 937 | } 938 | s.Require().Equal(t.ExpResultValue[pfx], rs, t.Desc) 939 | } 940 | } 941 | 942 | if t.CheckFunc[pfx] != nil { 943 | t.CheckFunc[pfx](t.Desc) 944 | } 945 | 946 | // clean up the cache 947 | s.TearDownTest() 948 | s.factory.localCache.Del(mockCacheCTX, getCacheKeys(sett.Prefix, t.Keys)...) 949 | } 950 | } 951 | } 952 | 953 | func (s *cacheSuite) TestGet() { 954 | tests := []struct { 955 | Desc string 956 | Settings []Setting 957 | Prefix string 958 | SetupTest map[string]func(string) 959 | Key string 960 | ExpError map[string]error 961 | ExpResult map[string]interface{} 962 | }{ 963 | { 964 | Desc: "prefix not registered", 965 | Settings: []Setting{{ 966 | Prefix: "registered", CacheAttributes: map[Type]Attribute{SharedCacheType: {TTL: time.Hour}}, 967 | }}, 968 | Prefix: "not-registered", 969 | ExpError: map[string]error{ 970 | "not-registered": ErrPfxNotRegistered, 971 | }, 972 | }, 973 | { 974 | Desc: "Get hit", 975 | Settings: []Setting{ 976 | { 977 | Prefix: "mixed", 978 | CacheAttributes: map[Type]Attribute{ 979 | SharedCacheType: {TTL: time.Hour}, 980 | LocalCacheType: {TTL: time.Hour}, 981 | }, 982 | }, 983 | }, 984 | SetupTest: map[string]func(desc string){ 985 | "mixed": func(desc string) { 986 | cacheKey := getCacheKey("mixed", "key") 987 | expB, _ := json.Marshal(mockString) 988 | 989 | s.lfu.lfu.Set(&tinylfu.Item{ 990 | Key: cacheKey, 991 | Value: expB, 992 | ExpireAt: time.Now().Add(time.Hour), 993 | }) 994 | b, exist := s.lfu.lfu.Get(cacheKey) 995 | s.Require().True(exist, desc, "mixed") 996 | s.Require().Equal(expB, b, desc, "mixed") 997 | 998 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 999 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1000 | }, 1001 | }, 1002 | Key: "key", 1003 | ExpError: map[string]error{ 1004 | "mixed": nil, 1005 | }, 1006 | ExpResult: map[string]interface{}{ 1007 | "mixed": mockString, 1008 | }, 1009 | }, 1010 | { 1011 | Desc: "Get miss", 1012 | Settings: []Setting{ 1013 | { 1014 | Prefix: "mixed", 1015 | CacheAttributes: map[Type]Attribute{ 1016 | SharedCacheType: {TTL: time.Hour}, 1017 | LocalCacheType: {TTL: time.Hour}, 1018 | }, 1019 | }, 1020 | }, 1021 | SetupTest: map[string]func(desc string){ 1022 | "mixed": func(desc string) { 1023 | cacheKey := getCacheKey("mixed", "key") 1024 | expB, _ := json.Marshal(mockString) 1025 | 1026 | s.lfu.lfu.Set(&tinylfu.Item{ 1027 | Key: cacheKey, 1028 | Value: expB, 1029 | ExpireAt: time.Now().Add(time.Hour), 1030 | }) 1031 | b, exist := s.lfu.lfu.Get(cacheKey) 1032 | s.Require().True(exist, desc, "mixed") 1033 | s.Require().Equal(expB, b, desc, "mixed") 1034 | 1035 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 1036 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1037 | }, 1038 | }, 1039 | Key: "not-existed", 1040 | ExpError: map[string]error{ 1041 | "mixed": ErrCacheMiss, 1042 | }, 1043 | }, 1044 | { 1045 | Desc: "Get miss but refill again", 1046 | Settings: []Setting{ 1047 | { 1048 | Prefix: "mixed", 1049 | CacheAttributes: map[Type]Attribute{ 1050 | SharedCacheType: {TTL: time.Hour}, 1051 | LocalCacheType: {TTL: time.Hour}, 1052 | }, 1053 | MGetter: func(keys ...string) (interface{}, error) { 1054 | s.Require().Equal([]string{"not-existed"}, keys) 1055 | return []string{"mgetter-existed"}, nil 1056 | }, 1057 | }, 1058 | }, 1059 | SetupTest: map[string]func(desc string){ 1060 | "mixed": func(desc string) { 1061 | cacheKey := getCacheKey("mixed", "key") 1062 | expB, _ := json.Marshal(mockString) 1063 | 1064 | s.lfu.lfu.Set(&tinylfu.Item{ 1065 | Key: cacheKey, 1066 | Value: expB, 1067 | ExpireAt: time.Now().Add(time.Hour), 1068 | }) 1069 | b, exist := s.lfu.lfu.Get(cacheKey) 1070 | s.Require().True(exist, desc, "mixed") 1071 | s.Require().Equal(expB, b, desc, "mixed") 1072 | 1073 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 1074 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1075 | }, 1076 | }, 1077 | Key: "not-existed", 1078 | ExpError: map[string]error{ 1079 | "mixed": nil, 1080 | }, 1081 | ExpResult: map[string]interface{}{ 1082 | "mixed": "mgetter-existed", 1083 | }, 1084 | }, 1085 | { 1086 | Desc: "Get miss but refill failed", 1087 | Settings: []Setting{ 1088 | { 1089 | Prefix: "mixed", 1090 | CacheAttributes: map[Type]Attribute{ 1091 | SharedCacheType: {TTL: time.Hour}, 1092 | LocalCacheType: {TTL: time.Hour}, 1093 | }, 1094 | MGetter: func(keys ...string) (interface{}, error) { 1095 | s.Require().Equal([]string{"XD"}, keys) 1096 | return nil, errors.New("XD") 1097 | }, 1098 | }, 1099 | }, 1100 | SetupTest: map[string]func(desc string){ 1101 | "mixed": func(desc string) { 1102 | cacheKey := getCacheKey("mixed", "key") 1103 | expB, _ := json.Marshal(mockString) 1104 | 1105 | s.lfu.lfu.Set(&tinylfu.Item{ 1106 | Key: cacheKey, 1107 | Value: expB, 1108 | ExpireAt: time.Now().Add(time.Hour), 1109 | }) 1110 | b, exist := s.lfu.lfu.Get(cacheKey) 1111 | s.Require().True(exist, desc, "mixed") 1112 | s.Require().Equal(expB, b, desc, "mixed") 1113 | 1114 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 1115 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1116 | }, 1117 | }, 1118 | Key: "XD", 1119 | ExpError: map[string]error{ 1120 | "mixed": errors.New("XD"), 1121 | }, 1122 | }, 1123 | } 1124 | 1125 | for _, t := range tests { 1126 | c := s.factory.NewCache(t.Settings).(*cache) 1127 | 1128 | for _, sett := range t.Settings { 1129 | pfx := sett.Prefix 1130 | if t.Prefix != "" { 1131 | pfx = t.Prefix 1132 | } 1133 | 1134 | if t.SetupTest[pfx] != nil { 1135 | t.SetupTest[pfx](t.Desc) 1136 | } 1137 | 1138 | result := "" 1139 | err := c.Get(mockCacheCTX, pfx, t.Key, &result) 1140 | s.Require().Equal(t.ExpError[pfx], err, t.Desc) 1141 | if err == nil { 1142 | s.Require().Equal(t.ExpResult[pfx], result, t.Desc) 1143 | } 1144 | 1145 | s.TearDownTest() 1146 | } 1147 | } 1148 | } 1149 | 1150 | func (s *cacheSuite) TestGetByFunc() { 1151 | tests := []struct { 1152 | Desc string 1153 | Settings []Setting 1154 | Prefix string 1155 | SetupTest map[string]func(string) 1156 | Key string 1157 | Getter map[string]func() (interface{}, error) 1158 | ExpError map[string]error 1159 | ExpResult map[string]interface{} 1160 | CheckFunc map[string]func(string) 1161 | }{ 1162 | { 1163 | Desc: "prefix not registered", 1164 | Settings: []Setting{{ 1165 | Prefix: "registered", CacheAttributes: map[Type]Attribute{SharedCacheType: {TTL: time.Hour}}, 1166 | }}, 1167 | Prefix: "not-registered", 1168 | ExpError: map[string]error{ 1169 | "not-registered": ErrPfxNotRegistered, 1170 | }, 1171 | }, 1172 | { 1173 | Desc: "Get hit", 1174 | Settings: []Setting{ 1175 | { 1176 | Prefix: "mixed", 1177 | CacheAttributes: map[Type]Attribute{ 1178 | SharedCacheType: {TTL: time.Hour}, 1179 | LocalCacheType: {TTL: time.Hour}, 1180 | }, 1181 | }, 1182 | }, 1183 | SetupTest: map[string]func(desc string){ 1184 | "mixed": func(desc string) { 1185 | cacheKey := getCacheKey("mixed", "key") 1186 | expB, _ := json.Marshal(mockString) 1187 | 1188 | s.lfu.lfu.Set(&tinylfu.Item{ 1189 | Key: cacheKey, 1190 | Value: expB, 1191 | ExpireAt: time.Now().Add(time.Hour), 1192 | }) 1193 | b, exist := s.lfu.lfu.Get(cacheKey) 1194 | s.Require().True(exist, desc, "mixed") 1195 | s.Require().Equal(expB, b, desc, "mixed") 1196 | 1197 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 1198 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1199 | }, 1200 | }, 1201 | Key: "key", 1202 | ExpError: map[string]error{ 1203 | "mixed": nil, 1204 | }, 1205 | ExpResult: map[string]interface{}{ 1206 | "mixed": mockString, 1207 | }, 1208 | }, 1209 | { 1210 | Desc: "Get miss but refill again", 1211 | Settings: []Setting{ 1212 | { 1213 | Prefix: "mixed", 1214 | CacheAttributes: map[Type]Attribute{ 1215 | SharedCacheType: {TTL: time.Hour}, 1216 | LocalCacheType: {TTL: time.Hour}, 1217 | }, 1218 | }, 1219 | }, 1220 | SetupTest: map[string]func(desc string){ 1221 | "mixed": func(desc string) { 1222 | cacheKey := getCacheKey("mixed", "key") 1223 | expB, _ := json.Marshal(mockString) 1224 | 1225 | s.lfu.lfu.Set(&tinylfu.Item{ 1226 | Key: cacheKey, 1227 | Value: expB, 1228 | ExpireAt: time.Now().Add(time.Hour), 1229 | }) 1230 | b, exist := s.lfu.lfu.Get(cacheKey) 1231 | s.Require().True(exist, desc, "mixed") 1232 | s.Require().Equal(expB, b, desc, "mixed") 1233 | 1234 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 1235 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1236 | }, 1237 | }, 1238 | Key: "not-existed", 1239 | Getter: map[string]func() (interface{}, error){ 1240 | "mixed": func() (interface{}, error) { 1241 | return "one-time-getter-existed", nil 1242 | }, 1243 | }, 1244 | ExpError: map[string]error{ 1245 | "mixed": nil, 1246 | }, 1247 | ExpResult: map[string]interface{}{ 1248 | "mixed": "one-time-getter-existed", 1249 | }, 1250 | CheckFunc: map[string]func(desc string){ 1251 | "mixed": func(desc string) { 1252 | // check refilled in cache 1253 | notExistKey := getCacheKey("mixed", "not-existed") 1254 | notExistB, _ := json.Marshal("one-time-getter-existed") 1255 | 1256 | b, exist := s.lfu.lfu.Get(notExistKey) 1257 | s.Require().True(exist, desc, "mixed") 1258 | s.Require().Equal(notExistB, b, desc, "mixed") 1259 | 1260 | b, err := s.ring.Get(mockCacheCTX, notExistKey).Bytes() 1261 | s.Require().NoError(err, desc, "mixed") 1262 | s.Require().Equal(notExistB, b, desc, "mixed") 1263 | }, 1264 | }, 1265 | }, 1266 | { 1267 | Desc: "Get miss but refill failed", 1268 | Settings: []Setting{ 1269 | { 1270 | Prefix: "mixed", 1271 | CacheAttributes: map[Type]Attribute{ 1272 | SharedCacheType: {TTL: time.Hour}, 1273 | LocalCacheType: {TTL: time.Hour}, 1274 | }, 1275 | }, 1276 | }, 1277 | SetupTest: map[string]func(desc string){ 1278 | "mixed": func(desc string) { 1279 | cacheKey := getCacheKey("mixed", "key") 1280 | expB, _ := json.Marshal(mockString) 1281 | 1282 | s.lfu.lfu.Set(&tinylfu.Item{ 1283 | Key: cacheKey, 1284 | Value: expB, 1285 | ExpireAt: time.Now().Add(time.Hour), 1286 | }) 1287 | b, exist := s.lfu.lfu.Get(cacheKey) 1288 | s.Require().True(exist, desc, "mixed") 1289 | s.Require().Equal(expB, b, desc, "mixed") 1290 | 1291 | _, err := s.ring.Get(mockCacheCTX, cacheKey).Bytes() 1292 | s.Require().Equal(redis.Nil, err, desc, "mixed") 1293 | }, 1294 | }, 1295 | Key: "XD", 1296 | Getter: map[string]func() (interface{}, error){ 1297 | "mixed": func() (interface{}, error) { 1298 | return nil, errors.New("XD") 1299 | }, 1300 | }, 1301 | ExpError: map[string]error{ 1302 | "mixed": errors.New("XD"), 1303 | }, 1304 | }, 1305 | } 1306 | 1307 | for _, t := range tests { 1308 | c := s.factory.NewCache(t.Settings).(*cache) 1309 | 1310 | for _, sett := range t.Settings { 1311 | pfx := sett.Prefix 1312 | if t.Prefix != "" { 1313 | pfx = t.Prefix 1314 | } 1315 | 1316 | if t.SetupTest[pfx] != nil { 1317 | t.SetupTest[pfx](t.Desc) 1318 | } 1319 | 1320 | result := "" 1321 | err := c.GetByFunc(mockCacheCTX, pfx, t.Key, &result, t.Getter[pfx]) 1322 | s.Require().Equal(t.ExpError[pfx], err, t.Desc) 1323 | if err == nil { 1324 | s.Require().Equal(t.ExpResult[pfx], result, t.Desc) 1325 | } 1326 | 1327 | if t.CheckFunc[pfx] != nil { 1328 | t.CheckFunc[pfx](t.Desc) 1329 | } 1330 | 1331 | s.TearDownTest() 1332 | } 1333 | } 1334 | } 1335 | -------------------------------------------------------------------------------- /doc/img/caching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viney-shih/go-cache/49f768117824728aa53f0ac9485c450ede7e335c/doc/img/caching.png -------------------------------------------------------------------------------- /empty.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // NewEmpty generates Adapter without implementation 9 | func NewEmpty() Adapter { 10 | return &empty{} 11 | } 12 | 13 | type empty struct{} 14 | 15 | func (adp *empty) MSet(ctx context.Context, keyItems map[string][]byte, ttl time.Duration, options ...MSetOptions) error { 16 | // do nothing 17 | return nil 18 | } 19 | 20 | func (adp *empty) MGet(ctx context.Context, keys []string) ([]Value, error) { 21 | // do nothing 22 | return make([]Value, len(keys)), nil 23 | } 24 | 25 | func (adp *empty) Del(ctx context.Context, keys ...string) error { 26 | // do nothing 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /empty_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | const ( 12 | mockEmptyPfx = "empty-pfx" 13 | mockEmptyKey = "empty-key" 14 | ) 15 | 16 | var ( 17 | mockEmptyCTX = context.Background() 18 | ) 19 | 20 | type emptySuite struct { 21 | suite.Suite 22 | } 23 | 24 | func (s *emptySuite) SetupSuite() { 25 | } 26 | 27 | func (s *emptySuite) TearDownSuite() {} 28 | 29 | func (s *emptySuite) SetupTest() {} 30 | 31 | func (s *emptySuite) TearDownTest() { 32 | // prevent registering twice 33 | ClearPrefix() 34 | } 35 | 36 | func TestEmptySuite(t *testing.T) { 37 | suite.Run(t, new(emptySuite)) 38 | } 39 | 40 | func (s *emptySuite) TestEmptyAdapter() { 41 | f := NewFactory(NewEmpty(), NewEmpty()) 42 | c := f.NewCache([]Setting{ 43 | { 44 | Prefix: mockEmptyPfx, 45 | CacheAttributes: map[Type]Attribute{ 46 | SharedCacheType: {time.Hour}, 47 | LocalCacheType: {10 * time.Second}, 48 | }, 49 | }, 50 | }) 51 | 52 | var intf interface{} 53 | s.Require().Equal(ErrCacheMiss, c.Get(mockEmptyCTX, mockEmptyPfx, mockEmptyKey, &intf)) 54 | s.Require().NoError(c.Set(mockEmptyCTX, mockEmptyPfx, mockEmptyKey, 123)) 55 | s.Require().NoError(c.Del(mockEmptyCTX, mockEmptyPfx, mockEmptyKey)) 56 | } 57 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | //go:generate go-enum -f=$GOFILE --nocase 2 | 3 | package cache 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | // errSelfEvent indicates event triggered by itself. 14 | errSelfEvent = errors.New("event triggered by itself") 15 | // errNoEventType indicates no event types 16 | errNoEventType = errors.New("no event types") 17 | ) 18 | 19 | // eventType is an enumeration of events used to communicate with each other via Pubsub. 20 | /* 21 | ENUM( 22 | None // Not registered Event by default. 23 | Evict // Evict presents eviction event. 24 | ) 25 | */ 26 | type eventType int32 27 | 28 | var regTopicEventMap map[string]eventType 29 | 30 | func init() { 31 | regTopicEventMap = map[string]eventType{} 32 | 33 | for typ := range _eventTypeMap { 34 | if typ == EventTypeNone { 35 | continue 36 | } 37 | 38 | regTopicEventMap[typ.Topic()] = typ 39 | } 40 | } 41 | 42 | // Topic generates the topic for specified event. 43 | func (x eventType) Topic() string { 44 | return getTopic(x.String()) 45 | } 46 | 47 | type event struct { 48 | Type eventType 49 | Body eventBody 50 | } 51 | 52 | type eventBody struct { 53 | FID string 54 | Keys []string 55 | } 56 | 57 | type messageBroker struct { 58 | pubsub Pubsub 59 | fid string 60 | wg sync.WaitGroup 61 | } 62 | 63 | func newMessageBroker(fid string, pb Pubsub) *messageBroker { 64 | return &messageBroker{ 65 | fid: fid, 66 | pubsub: pb, 67 | } 68 | } 69 | 70 | func (mb *messageBroker) registered() bool { 71 | return mb.pubsub != nil 72 | } 73 | 74 | func (mb *messageBroker) close() { 75 | if !mb.registered() { 76 | return 77 | } 78 | 79 | // close s 80 | mb.pubsub.Close() 81 | mb.wg.Wait() 82 | } 83 | 84 | func (mb *messageBroker) send(ctx context.Context, e event) error { 85 | if !mb.registered() { 86 | return nil 87 | } 88 | 89 | e.Body.FID = mb.fid 90 | bs, err := json.Marshal(e.Body) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return mb.pubsub.Pub(ctx, e.Type.Topic(), bs) 96 | } 97 | 98 | func (mb *messageBroker) listen( 99 | ctx context.Context, types []eventType, cb func(context.Context, *event, error), 100 | ) error { 101 | if !mb.registered() { 102 | return nil 103 | } 104 | 105 | if len(types) == 0 { 106 | return errNoEventType 107 | } 108 | 109 | topics := make([]string, len(types)) 110 | for i := 0; i < len(types); i++ { 111 | topics[i] = types[i].Topic() 112 | } 113 | 114 | mb.wg.Add(1) 115 | go func() { 116 | defer mb.wg.Done() 117 | 118 | for mess := range mb.pubsub.Sub(ctx, topics...) { 119 | typ, ok := regTopicEventMap[mess.Topic()] 120 | if !ok { 121 | cb(ctx, nil, errors.New("no such topic registered")) 122 | continue 123 | } 124 | 125 | e := event{Type: typ} 126 | if err := json.Unmarshal(mess.Content(), &e.Body); err != nil { 127 | cb(ctx, nil, err) 128 | continue 129 | } 130 | 131 | if e.Body.FID == mb.fid { 132 | cb(ctx, &e, errSelfEvent) 133 | continue 134 | } 135 | 136 | cb(ctx, &e, nil) 137 | } 138 | }() 139 | 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /event_enum.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-enum DO NOT EDIT. 2 | // Version: 3 | // Revision: 4 | // Build Date: 5 | // Built By: 6 | 7 | package cache 8 | 9 | import ( 10 | "fmt" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | // EventTypeNone is a eventType of type None. 16 | // Not registered Event by default. 17 | EventTypeNone eventType = iota 18 | // EventTypeEvict is a eventType of type Evict. 19 | // Evict presents eviction event. 20 | EventTypeEvict 21 | ) 22 | 23 | const _eventTypeName = "NoneEvict" 24 | 25 | var _eventTypeMap = map[eventType]string{ 26 | EventTypeNone: _eventTypeName[0:4], 27 | EventTypeEvict: _eventTypeName[4:9], 28 | } 29 | 30 | // String implements the Stringer interface. 31 | func (x eventType) String() string { 32 | if str, ok := _eventTypeMap[x]; ok { 33 | return str 34 | } 35 | return fmt.Sprintf("eventType(%d)", x) 36 | } 37 | 38 | var _eventTypeValue = map[string]eventType{ 39 | _eventTypeName[0:4]: EventTypeNone, 40 | strings.ToLower(_eventTypeName[0:4]): EventTypeNone, 41 | _eventTypeName[4:9]: EventTypeEvict, 42 | strings.ToLower(_eventTypeName[4:9]): EventTypeEvict, 43 | } 44 | 45 | // ParseeventType attempts to convert a string to a eventType. 46 | func ParseeventType(name string) (eventType, error) { 47 | if x, ok := _eventTypeValue[name]; ok { 48 | return x, nil 49 | } 50 | // Case insensitive parse, do a separate lookup to prevent unnecessary cost of lowercasing a string if we don't need to. 51 | if x, ok := _eventTypeValue[strings.ToLower(name)]; ok { 52 | return x, nil 53 | } 54 | return eventType(0), fmt.Errorf("%s is not a valid eventType", name) 55 | } 56 | -------------------------------------------------------------------------------- /event_enum_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type enumSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (s *enumSuite) SetupSuite() {} 15 | 16 | func (s *enumSuite) TearDownSuite() {} 17 | 18 | func (s *enumSuite) SetupTest() {} 19 | 20 | func (s *enumSuite) TearDownTest() {} 21 | 22 | func TestEnumSuite(t *testing.T) { 23 | suite.Run(t, new(enumSuite)) 24 | } 25 | 26 | func (s *enumSuite) TestString() { 27 | s.Require().Equal("Evict", EventTypeEvict.String()) 28 | 29 | notExisted := eventType(1000) 30 | s.Require().Equal("eventType(1000)", notExisted.String()) 31 | } 32 | 33 | func (s *enumSuite) TestParseEventType() { 34 | var typ eventType 35 | var err error 36 | 37 | // normal case 38 | typ, err = ParseeventType("Evict") 39 | s.Require().NoError(err) 40 | s.Require().Equal(EventTypeEvict, typ) 41 | 42 | // lower case 43 | typ, err = ParseeventType("evict") 44 | s.Require().NoError(err) 45 | s.Require().Equal(EventTypeEvict, typ) 46 | 47 | // upper case 48 | typ, err = ParseeventType("NONE") 49 | s.Require().NoError(err) 50 | s.Require().Equal(EventTypeNone, typ) 51 | 52 | // err 53 | _, err = ParseeventType("not-existed") 54 | s.Require().Equal(fmt.Errorf("not-existed is not a valid eventType"), err) 55 | } 56 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | const ( 13 | mockEventPfx = "event-pfx" 14 | mockEventKey = "event-key" 15 | mockEventUUID = "event-from-others" 16 | ) 17 | 18 | var ( 19 | mockEventCTX = context.Background() 20 | ) 21 | 22 | type eventSuite struct { 23 | suite.Suite 24 | 25 | factory *factory 26 | rds *rds 27 | lfu *tinyLFU 28 | ring *redis.Ring 29 | mb *messageBroker 30 | } 31 | 32 | func (s *eventSuite) SetupSuite() { 33 | s.ring = redis.NewRing(&redis.RingOptions{ 34 | Addrs: map[string]string{ 35 | "server1": ":6379", 36 | }, 37 | }) 38 | } 39 | 40 | func (s *eventSuite) TearDownSuite() {} 41 | 42 | func (s *eventSuite) SetupTest() { 43 | s.rds = NewRedis(s.ring).(*rds) 44 | s.lfu = NewTinyLFU(10000).(*tinyLFU) 45 | s.mb = newMessageBroker(mockEventUUID, s.rds) 46 | s.factory = NewFactory(s.rds, s.lfu, WithPubSub(s.rds)).(*factory) 47 | } 48 | 49 | func (s *eventSuite) TearDownTest() { 50 | // prevent registering twice 51 | ClearPrefix() 52 | // flush redis 53 | _ = s.ring.ForEachShard(mockCacheCTX, func(ctx context.Context, client *redis.Client) error { 54 | return client.FlushDB(ctx).Err() 55 | }) 56 | 57 | s.mb.close() // this makes sure only one (mb or factory) can trigger Close() once without panic 58 | s.factory.Close() 59 | } 60 | 61 | func TestEventSuite(t *testing.T) { 62 | suite.Run(t, new(eventSuite)) 63 | } 64 | 65 | func (s *eventSuite) TestSubscribedEventsHandlerWithSet() { 66 | c := s.factory.NewCache([]Setting{ 67 | { 68 | Prefix: mockEventPfx, 69 | CacheAttributes: map[Type]Attribute{ 70 | SharedCacheType: {time.Hour}, 71 | LocalCacheType: {10 * time.Second}, 72 | }, 73 | }, 74 | }) 75 | 76 | // Set() will trigger eviction in other machines 77 | s.Require().NoError(c.Set(mockEventCTX, mockEventPfx, mockEventKey, 100)) 78 | time.Sleep(time.Millisecond * 100) 79 | val, err := s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 80 | s.Require().NoError(err) 81 | s.Require().Equal([]Value{{Valid: true, Bytes: []byte("100")}}, val) // make sure the local value existed without impacted 82 | 83 | // trigger invalid event type, ignore it directly 84 | // TODO: handling error messages forwarding in the future 85 | s.Require().NoError(s.mb.send(mockEventCTX, event{Type: EventTypeNone})) 86 | time.Sleep(time.Millisecond * 100) 87 | val, err = s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 88 | s.Require().NoError(err) 89 | s.Require().Equal([]Value{{Valid: true, Bytes: []byte("100")}}, val) 90 | 91 | // trigger evict event without keys, nothing happened 92 | s.Require().NoError(s.mb.send(mockEventCTX, event{ 93 | Type: EventTypeEvict, 94 | Body: eventBody{Keys: []string{}}, 95 | })) 96 | time.Sleep(time.Millisecond * 100) 97 | val, err = s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 98 | s.Require().NoError(err) 99 | s.Require().Equal([]Value{{Valid: true, Bytes: []byte("100")}}, val) 100 | 101 | // simulate eviction from other machines 102 | s.Require().NoError(s.mb.send(mockEventCTX, event{ 103 | Type: EventTypeEvict, 104 | Body: eventBody{Keys: []string{getCacheKey(mockEventPfx, mockEventKey)}}, 105 | })) 106 | time.Sleep(time.Millisecond * 100) 107 | val, err = s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 108 | s.Require().NoError(err) 109 | s.Require().Equal([]Value{{}}, val) // local value evicted 110 | } 111 | 112 | func (s *eventSuite) TestSubscribedEventsHandlerWithDel() { 113 | c := s.factory.NewCache([]Setting{ 114 | { 115 | Prefix: mockEventPfx, 116 | CacheAttributes: map[Type]Attribute{ 117 | SharedCacheType: {time.Hour}, 118 | LocalCacheType: {10 * time.Second}, 119 | }, 120 | }, 121 | }) 122 | 123 | // Set() will trigger eviction in other machines 124 | s.Require().NoError(c.Set(mockEventCTX, mockEventPfx, mockEventKey, 100)) 125 | time.Sleep(time.Millisecond * 100) 126 | val, err := s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 127 | s.Require().NoError(err) 128 | s.Require().Equal([]Value{{Valid: true, Bytes: []byte("100")}}, val) // make sure the local value existed without impacted 129 | 130 | // Del is the same behavior as Set(), but the value is killed by itself. 131 | s.Require().NoError(c.Del(mockEventCTX, mockEventPfx, mockEventKey)) 132 | time.Sleep(time.Millisecond * 100) 133 | val, err = s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 134 | s.Require().NoError(err) 135 | s.Require().Equal([]Value{{}}, val) 136 | } 137 | 138 | func (s *eventSuite) TestUnnormalEvent() { 139 | c := s.factory.NewCache([]Setting{ 140 | { 141 | Prefix: mockEventPfx, 142 | CacheAttributes: map[Type]Attribute{ 143 | SharedCacheType: {time.Hour}, 144 | LocalCacheType: {10 * time.Second}, 145 | }, 146 | }, 147 | }) 148 | 149 | // Set() will trigger eviction in other machines 150 | s.Require().NoError(c.Set(mockEventCTX, mockEventPfx, mockEventKey, 100)) 151 | time.Sleep(time.Millisecond * 100) 152 | val, err := s.lfu.MGet(mockEventCTX, []string{getCacheKey(mockEventPfx, mockEventKey)}) 153 | s.Require().NoError(err) 154 | s.Require().Equal([]Value{{Valid: true, Bytes: []byte("100")}}, val) // make sure the local value existed without impacted 155 | 156 | // nothing happened due to no handling on such event 157 | s.Require().NoError(s.rds.Pub(mockEventCTX, "not-existed", nil)) 158 | 159 | // TODO: handle this error in the future 160 | // invalid json format. 161 | s.Require().NoError(s.rds.Pub(mockEventCTX, EventTypeEvict.Topic(), []byte(""))) 162 | } 163 | 164 | // not stable sometimes, skip it now 165 | // func (s *eventSuite) TestListenNoEvents() { 166 | // //s.T().Skip("not stable sometimes, skip it now") 167 | // rds := NewRedis(s.ring).(*rds) 168 | // mb := newMessageBroker(mockEventUUID, rds) 169 | // s.Require().Equal(errNoEventType, mb.listen(mockEventCTX, []eventType{}, func(ctx context.Context, e *event, err error) {})) 170 | // mb.close() 171 | // } 172 | -------------------------------------------------------------------------------- /example_advanced_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | "github.com/vmihailenco/msgpack/v5" 10 | 11 | "github.com/viney-shih/go-cache" 12 | ) 13 | 14 | type Person struct { 15 | FirstName string 16 | LastName string 17 | Age int 18 | } 19 | 20 | // Example_cacheAsidePattern demonstrates multi-layered caching and 21 | // multiple prefix keys at the same time. 22 | func Example_cacheAsidePattern() { 23 | tinyLfu := cache.NewTinyLFU(10000) 24 | rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ 25 | Addrs: map[string]string{ 26 | "server1": ":6379", 27 | }, 28 | })) 29 | 30 | cacheF := cache.NewFactory(rds, tinyLfu) 31 | 32 | c := cacheF.NewCache([]cache.Setting{ 33 | { 34 | Prefix: "teacher", 35 | CacheAttributes: map[cache.Type]cache.Attribute{ 36 | cache.SharedCacheType: {TTL: time.Hour}, 37 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 38 | }, 39 | MarshalFunc: msgpack.Marshal, 40 | UnmarshalFunc: msgpack.Unmarshal, 41 | }, 42 | { 43 | Prefix: "student", 44 | CacheAttributes: map[cache.Type]cache.Attribute{ 45 | cache.SharedCacheType: {TTL: time.Hour}, 46 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 47 | }, 48 | MGetter: func(keys ...string) (interface{}, error) { 49 | // The MGetter is used to generate data when cache missed, and refill the cache automatically.. 50 | // You can read from DB or other microservices. 51 | // Assume we read from MySQL according to the key "jacky" and get the value of 52 | // Person{FirstName: "jacky", LastName: "Lin", Age: 38} 53 | // HINT: remember to return as a slice, and the item order needs to consist with the keys in the parameters. 54 | if len(keys) == 1 && keys[0] == "jacky" { 55 | return []Person{{FirstName: "Jacky", LastName: "Lin", Age: 38}}, nil 56 | } 57 | 58 | return nil, fmt.Errorf("XD") 59 | }, 60 | MarshalFunc: cache.Marshal, 61 | UnmarshalFunc: cache.Unmarshal, 62 | }, 63 | }) 64 | 65 | ctx := context.TODO() 66 | teacher := &Person{} 67 | if err := c.GetByFunc(ctx, "teacher", "jacky", teacher, func() (interface{}, error) { 68 | // The getter is used to generate data when cache missed, and refill the cache automatically.. 69 | // You can read from DB or other microservices. 70 | // Assume we read from MySQL according to the key "jacky" and get the value of 71 | // Person{FirstName: "jacky", LastName: "Wang", Age: 83} . 72 | return Person{FirstName: "Jacky", LastName: "Wang", Age: 83}, nil 73 | }); err != nil { 74 | panic("not expected") 75 | } 76 | 77 | fmt.Println(teacher) // {FirstName: "Jacky", LastName: "Wang", Age: 83} 78 | 79 | student := &Person{} 80 | if err := c.Get(ctx, "student", "jacky", student); err != nil { 81 | panic("not expected") 82 | } 83 | 84 | fmt.Println(student) // {FirstName: "Jacky", LastName: "Lin", Age: 38} 85 | 86 | // Output: 87 | // &{Jacky Wang 83} 88 | // &{Jacky Lin 38} 89 | 90 | } 91 | 92 | // Example_pubsubPattern demonstrates how to leverage Pubsub pattern 93 | // to broadcast evictions between distributed systems, and 94 | // make in-memory cache consistency eventually ASAP. 95 | func Example_pubsubPattern() { 96 | tinyLfu := cache.NewTinyLFU(10000) 97 | rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ 98 | Addrs: map[string]string{ 99 | "server1": ":6379", 100 | }, 101 | })) 102 | 103 | cacheF := cache.NewFactory(rds, tinyLfu, cache.WithPubSub(rds)) 104 | c := cacheF.NewCache([]cache.Setting{ 105 | { 106 | Prefix: "user", 107 | CacheAttributes: map[cache.Type]cache.Attribute{ 108 | cache.SharedCacheType: {TTL: time.Hour}, 109 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 110 | }, 111 | }, 112 | }) 113 | 114 | ctx := context.TODO() 115 | user := &Person{} 116 | if err := c.GetByFunc(ctx, "user", "tony", user, func() (interface{}, error) { 117 | // The getter is used to generate data when cache missed, and refill the cache automatically. 118 | // Assume we read from MySQL according to the key "tony" and get the value of 119 | // Person{FirstName: "Tony", LastName: "Stock", Age: 87} . 120 | // At the same time, it will broadcast the eviction about the prefix "user" and the key "tony" to others. 121 | return Person{FirstName: "Tony", LastName: "Stock", Age: 87}, nil 122 | }); err != nil { 123 | panic("not expected") 124 | } 125 | 126 | fmt.Println(user) // {FirstName: "Tony", LastName: "Stock", Age: 87} 127 | // Output: 128 | // &{Tony Stock 87} 129 | } 130 | -------------------------------------------------------------------------------- /example_readthrough_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | 10 | "github.com/viney-shih/go-cache" 11 | ) 12 | 13 | func ExampleCache_GetByFunc() { 14 | tinyLfu := cache.NewTinyLFU(10000) 15 | rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ 16 | Addrs: map[string]string{ 17 | "server1": ":6379", 18 | }, 19 | })) 20 | 21 | cacheF := cache.NewFactory(rds, tinyLfu) 22 | 23 | // We create a group of cache named "get-by-func". 24 | // It uses the local cache only with TTL of ten minutes. 25 | c := cacheF.NewCache([]cache.Setting{ 26 | { 27 | Prefix: "get-by-func", 28 | CacheAttributes: map[cache.Type]cache.Attribute{ 29 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 30 | }, 31 | }, 32 | }) 33 | 34 | ctx := context.TODO() 35 | container2 := &Object{} 36 | if err := c.GetByFunc(ctx, "get-by-func", "key2", container2, func() (interface{}, error) { 37 | // The getter is used to generate data when cache missed, and refill the cache automatically.. 38 | // You can read from DB or other microservices. 39 | // Assume we read from MySQL according to the key "key2" and get the value of Object{Str: "value2", Num: 2} 40 | return Object{Str: "value2", Num: 2}, nil 41 | }); err != nil { 42 | panic("not expected") 43 | } 44 | 45 | fmt.Println(container2) // Object{ Str: "value2", Num: 2} 46 | 47 | // Output: 48 | // &{value2 2} 49 | } 50 | 51 | func ExampleFactory_NewCache_mGetter() { 52 | tinyLfu := cache.NewTinyLFU(10000) 53 | rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ 54 | Addrs: map[string]string{ 55 | "server1": ":6379", 56 | }, 57 | })) 58 | 59 | cacheF := cache.NewFactory(rds, tinyLfu) 60 | 61 | // We create a group of cache named "mgetter". 62 | // It uses both shared and local caches with separated TTL of one hour and ten minutes. 63 | c := cacheF.NewCache([]cache.Setting{ 64 | { 65 | Prefix: "mgetter", 66 | CacheAttributes: map[cache.Type]cache.Attribute{ 67 | cache.SharedCacheType: {TTL: time.Hour}, 68 | cache.LocalCacheType: {TTL: 10 * time.Minute}, 69 | }, 70 | MGetter: func(keys ...string) (interface{}, error) { 71 | // The MGetter is used to generate data when cache missed, and refill the cache automatically.. 72 | // You can read from DB or other microservices. 73 | // Assume we read from MySQL according to the key "key3" and get the value of Object{Str: "value3", Num: 3} 74 | // HINT: remember to return as a slice, and the item order needs to consist with the keys in the parameters. 75 | return []Object{{Str: "value3", Num: 3}}, nil 76 | }, 77 | }, 78 | }) 79 | 80 | ctx := context.TODO() 81 | container3 := &Object{} 82 | if err := c.Get(ctx, "mgetter", "key3", container3); err != nil { 83 | panic("not expected") 84 | } 85 | 86 | fmt.Println(container3) // Object{ Str: "value3", Num: 3} 87 | 88 | // Output: 89 | // &{value3 3} 90 | } 91 | -------------------------------------------------------------------------------- /example_setandget_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | 10 | "github.com/viney-shih/go-cache" 11 | ) 12 | 13 | type Object struct { 14 | Str string 15 | Num int 16 | } 17 | 18 | func Example_setAndGetPattern() { 19 | tinyLfu := cache.NewTinyLFU(10000) 20 | rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ 21 | Addrs: map[string]string{ 22 | "server1": ":6379", 23 | }, 24 | })) 25 | 26 | cacheF := cache.NewFactory(rds, tinyLfu) 27 | 28 | // We create a group of cache named "set-and-get". 29 | // It uses the shared cache only with TTL of ten seconds. 30 | c := cacheF.NewCache([]cache.Setting{ 31 | { 32 | Prefix: "set-and-get", 33 | CacheAttributes: map[cache.Type]cache.Attribute{ 34 | cache.SharedCacheType: {TTL: 10 * time.Second}, 35 | }, 36 | }, 37 | }) 38 | 39 | ctx := context.TODO() 40 | 41 | // set the cache 42 | obj := &Object{ 43 | Str: "value1", 44 | Num: 1, 45 | } 46 | if err := c.Set(ctx, "set-and-get", "key", obj); err != nil { 47 | panic("not expected") 48 | } 49 | 50 | // read the cache 51 | container := &Object{} 52 | if err := c.Get(ctx, "set-and-get", "key", container); err != nil { 53 | panic("not expected") 54 | } 55 | fmt.Println(container) // Object{ Str: "value1", Num: 1} 56 | 57 | // read the cache but failed 58 | if err := c.Get(ctx, "set-and-get", "no-such-key", container); err != nil { 59 | fmt.Println(err) // errors.New("cache key is missing") 60 | } 61 | 62 | // Output: 63 | // &{value1 1} 64 | // cache key is missing 65 | } 66 | -------------------------------------------------------------------------------- /factory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "sync" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | var ( 13 | // usedPrefixs records the prefixes registered before 14 | usedPrefixs = map[string]struct{}{} 15 | 16 | // decoupling 17 | uuidString = uuid.New().String 18 | ) 19 | 20 | func newFactory(sharedCache Adapter, localCache Adapter, options ...FactoryOptions) Factory { 21 | // load options 22 | o := loadFactoryOptions(options...) 23 | // need to specify marshalFunc and unmarshalFunc at the same time 24 | if o.marshalFunc == nil && o.unmarshalFunc != nil { 25 | panic(errors.New("both of Marshal and Unmarshal functions need to be specified")) 26 | } else if o.marshalFunc != nil && o.unmarshalFunc == nil { 27 | panic(errors.New("both of Marshal and Unmarshal functions need to be specified")) 28 | } 29 | 30 | var marshalFunc MarshalFunc 31 | var unmarshalFunc UnmarshalFunc 32 | marshalFunc = json.Marshal 33 | unmarshalFunc = json.Unmarshal 34 | 35 | if o.marshalFunc != nil { 36 | marshalFunc = o.marshalFunc 37 | } 38 | if o.unmarshalFunc != nil { 39 | unmarshalFunc = o.unmarshalFunc 40 | } 41 | 42 | id := uuidString() 43 | f := &factory{ 44 | id: id, 45 | sharedCache: sharedCache, 46 | localCache: localCache, 47 | mb: newMessageBroker(id, o.pubsub), 48 | marshal: marshalFunc, 49 | unmarshal: unmarshalFunc, 50 | onCacheHit: o.onCacheHit, 51 | onCacheMiss: o.onCacheMiss, 52 | onLCCostAdd: o.onLCCostAdd, 53 | onLCCostEvict: o.onLCCostEvict, 54 | } 55 | 56 | // subscribing events 57 | f.mb.listen(context.TODO(), []eventType{EventTypeEvict}, f.subscribedEventsHandler()) 58 | 59 | return f 60 | } 61 | 62 | type factory struct { 63 | sharedCache Adapter 64 | localCache Adapter 65 | mb *messageBroker 66 | 67 | marshal MarshalFunc 68 | unmarshal UnmarshalFunc 69 | onCacheHit func(prefix string, key string, count int) 70 | onCacheMiss func(prefix string, key string, count int) 71 | onLCCostAdd func(prefix string, key string, cost int) 72 | onLCCostEvict func(prefix string, key string, cost int) 73 | 74 | id string 75 | closeOnce sync.Once 76 | } 77 | 78 | func (f *factory) NewCache(settings []Setting) Cache { 79 | m := map[string]*config{} 80 | for _, setting := range settings { 81 | // check prefix 82 | if setting.Prefix == "" { 83 | panic(errors.New("not allowed empty prefix")) 84 | } 85 | if _, ok := usedPrefixs[setting.Prefix]; ok { 86 | panic(errors.New("duplicated prefix")) 87 | } 88 | usedPrefixs[setting.Prefix] = struct{}{} 89 | 90 | cfg := &config{ 91 | mGetter: setting.MGetter, 92 | marshal: f.marshal, 93 | unmarshal: f.unmarshal, 94 | } 95 | 96 | // need to specify marshalFunc and unmarshalFunc at the same time 97 | if setting.MarshalFunc == nil && setting.UnmarshalFunc != nil { 98 | panic(errors.New("both of Marshal and Unmarshal functions need to be specified")) 99 | } else if setting.MarshalFunc != nil && setting.UnmarshalFunc == nil { 100 | panic(errors.New("both of Marshal and Unmarshal functions need to be specified")) 101 | } 102 | 103 | if setting.MarshalFunc != nil { 104 | cfg.marshal = setting.MarshalFunc 105 | } 106 | if setting.UnmarshalFunc != nil { 107 | cfg.unmarshal = setting.UnmarshalFunc 108 | } 109 | 110 | for typ, attr := range setting.CacheAttributes { 111 | if typ == SharedCacheType { 112 | cfg.shared = f.sharedCache 113 | cfg.sharedTTL = attr.TTL 114 | } else if typ == LocalCacheType { 115 | cfg.local = f.localCache 116 | cfg.localTTL = attr.TTL 117 | } 118 | } 119 | 120 | // need to indicate at least one cache type 121 | if cfg.shared == nil && cfg.local == nil { 122 | panic(errors.New("no cache type indicated")) 123 | } 124 | 125 | m[setting.Prefix] = cfg 126 | } 127 | 128 | return &cache{ 129 | configs: m, 130 | mb: f.mb, 131 | onCacheHit: func(prefix string, key string, count int) { 132 | // trigger the callback on cache hitted if necessary 133 | if f.onCacheHit != nil { 134 | f.onCacheHit(prefix, key, count) 135 | } 136 | }, 137 | onCacheMiss: func(prefix string, key string, count int) { 138 | // trigger the callback on cache missed if necessary 139 | if f.onCacheMiss != nil { 140 | f.onCacheMiss(prefix, key, count) 141 | } 142 | }, 143 | onLCCostAdd: func(cKey string, cost int) { 144 | // trigger the callback on local cache added if necessary 145 | if f.onLCCostAdd != nil { 146 | pfx, key := getPrefixAndKey(cKey) 147 | f.onLCCostAdd(pfx, key, cost) 148 | } 149 | }, 150 | onLCCostEvict: func(cKey string, cost int) { 151 | // trigger the callback on local cache evicted if necessary 152 | if f.onLCCostEvict != nil { 153 | pfx, key := getPrefixAndKey(cKey) 154 | f.onLCCostEvict(pfx, key, cost) 155 | } 156 | }, 157 | } 158 | } 159 | 160 | func (f *factory) Close() { 161 | f.closeOnce.Do(func() { 162 | f.mb.close() 163 | }) 164 | } 165 | 166 | func (f *factory) subscribedEventsHandler() func(ctx context.Context, e *event, err error) { 167 | return func(ctx context.Context, e *event, err error) { 168 | if err == errSelfEvent { 169 | // do nothing 170 | return 171 | } else if err != nil { 172 | // TODO: forward error messages outside 173 | return 174 | } 175 | 176 | switch e.Type { 177 | case EventTypeEvict: 178 | keys := e.Body.Keys 179 | if f.localCache != nil && len(keys) > 0 { 180 | // evict local caches 181 | f.localCache.Del(ctx, keys...) 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /factory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "encoding/xml" 7 | "errors" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-redis/redis/v8" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | const ( 17 | mockFactPfx = "fact-pfx" 18 | mockFactKey = "fact-key" 19 | ) 20 | 21 | var ( 22 | mockFactoryCTX = context.Background() 23 | ) 24 | 25 | type factorySuite struct { 26 | suite.Suite 27 | 28 | factory *factory 29 | rds *rds 30 | lfu *tinyLFU 31 | ring *redis.Ring 32 | } 33 | 34 | func (s *factorySuite) SetupSuite() { 35 | s.ring = redis.NewRing(&redis.RingOptions{ 36 | Addrs: map[string]string{ 37 | "server1": ":6379", 38 | }, 39 | }) 40 | } 41 | 42 | func (s *factorySuite) TearDownSuite() {} 43 | 44 | func (s *factorySuite) SetupTest() { 45 | s.rds = NewRedis(s.ring).(*rds) 46 | s.lfu = NewTinyLFU(10000).(*tinyLFU) 47 | s.factory = NewFactory(s.rds, s.lfu).(*factory) 48 | } 49 | 50 | func (s *factorySuite) TearDownTest() { 51 | // prevent registering twice 52 | ClearPrefix() 53 | // flush redis 54 | _ = s.ring.ForEachShard(mockFactoryCTX, func(ctx context.Context, client *redis.Client) error { 55 | return client.FlushDB(ctx).Err() 56 | }) 57 | 58 | s.factory.Close() 59 | } 60 | 61 | func TestFactorySuite(t *testing.T) { 62 | suite.Run(t, new(factorySuite)) 63 | } 64 | 65 | func (s *factorySuite) TestNewFactoryWithOnlyMarshal() { 66 | defer func() { 67 | r := recover() 68 | s.Require().NotNil(r) 69 | s.Require().Equal(errors.New("both of Marshal and Unmarshal functions need to be specified"), r) 70 | }() 71 | NewFactory(s.rds, s.lfu, WithMarshalFunc(json.Marshal)) 72 | } 73 | 74 | func (s *factorySuite) TestNewFactoryWithOnlyUnmarshal() { 75 | defer func() { 76 | r := recover() 77 | s.Require().NotNil(r) 78 | s.Require().Equal(errors.New("both of Marshal and Unmarshal functions need to be specified"), r) 79 | }() 80 | NewFactory(s.rds, s.lfu, WithUnmarshalFunc(json.Unmarshal)) 81 | } 82 | 83 | func (s *factorySuite) TestNewFactoryWithBoth() { 84 | f := NewFactory(s.rds, s.lfu, WithMarshalFunc(xml.Marshal), WithUnmarshalFunc(xml.Unmarshal)).(*factory) 85 | s.Require().True(reflect.ValueOf(xml.Marshal).Pointer() == reflect.ValueOf(f.marshal).Pointer()) 86 | s.Require().True(reflect.ValueOf(xml.Unmarshal).Pointer() == reflect.ValueOf(f.unmarshal).Pointer()) 87 | } 88 | 89 | func (s *factorySuite) TestNewFactoryWithCacheHitAndMiss() { 90 | hitCount := 0 91 | missCount := 0 92 | 93 | // Due to use share cache only, init factory with NewEmpty() 94 | f := NewFactory(s.rds, NewEmpty(), 95 | OnCacheHitFunc(func(prefix, key string, count int) { 96 | s.Require().Equal(mockFactPfx, prefix) 97 | s.Require().Equal(mockFactKey, key) 98 | hitCount += count 99 | }), 100 | OnCacheMissFunc(func(prefix, key string, count int) { 101 | s.Require().Equal(mockFactPfx, prefix) 102 | s.Require().Equal(mockFactKey, key) 103 | missCount += count 104 | }), 105 | ) 106 | 107 | var ret int 108 | var stage string 109 | c := f.NewCache([]Setting{ 110 | { 111 | Prefix: mockFactPfx, 112 | CacheAttributes: map[Type]Attribute{SharedCacheType: {time.Hour}}, 113 | }, 114 | }) 115 | 116 | stage = "before" 117 | s.Require().Equal(0, hitCount, stage) 118 | s.Require().Equal(0, missCount, stage) 119 | 120 | stage = "get and miss" 121 | s.Require().Equal(ErrCacheMiss, c.Get(mockFactoryCTX, mockFactPfx, mockFactKey, &ret)) 122 | s.Require().Equal(0, ret, stage) 123 | s.Require().Equal(0, hitCount, stage) 124 | s.Require().Equal(1, missCount, stage) 125 | 126 | stage = "set and get" 127 | s.Require().NoError(c.Set(mockFactoryCTX, mockFactPfx, mockFactKey, 100)) 128 | s.Require().NoError(c.Get(mockFactoryCTX, mockFactPfx, mockFactKey, &ret)) 129 | s.Require().Equal(100, ret, stage) 130 | s.Require().Equal(1, hitCount, stage) 131 | s.Require().Equal(1, missCount, stage) 132 | } 133 | 134 | func (s *factorySuite) TestNewFactoryWithCostAddAndEvict() { 135 | costAdd := 0 136 | costEvict := 0 137 | 138 | f := NewFactory(s.rds, s.lfu, 139 | OnLocalCacheCostAddFunc(func(prefix, key string, cost int) { 140 | s.Require().Equal(mockFactPfx, prefix) 141 | s.Require().Equal(mockFactKey, key) 142 | costAdd += cost 143 | }), 144 | OnLocalCacheCostEvictFunc(func(prefix, key string, cost int) { 145 | s.Require().Equal(mockFactPfx, prefix) 146 | s.Require().Equal(mockFactKey, key) 147 | costEvict += cost 148 | }), 149 | ) 150 | 151 | //var ret int 152 | var stage string 153 | var bs []byte 154 | var err error 155 | c := f.NewCache([]Setting{ 156 | { 157 | Prefix: mockFactPfx, 158 | CacheAttributes: map[Type]Attribute{ 159 | SharedCacheType: {time.Hour}, 160 | LocalCacheType: {10 * time.Second}, 161 | }, 162 | MarshalFunc: json.Marshal, 163 | UnmarshalFunc: json.Unmarshal, 164 | }, 165 | }) 166 | 167 | stage = "before" 168 | s.Require().Equal(0, costAdd, stage) 169 | s.Require().Equal(0, costEvict, stage) 170 | 171 | stage = "set" 172 | s.Require().NoError(c.Set(mockFactoryCTX, mockFactPfx, mockFactKey, 100)) 173 | bs, err = json.Marshal(100) 174 | s.Require().NoError(err, stage) 175 | s.Require().Equal(len(bs), costAdd, stage) 176 | s.Require().Equal(0, costEvict, stage) 177 | 178 | stage = "del" 179 | s.Require().NoError(c.Del(mockFactoryCTX, mockFactPfx, mockFactKey)) 180 | s.Require().Equal(len(bs), costAdd, stage) 181 | s.Require().Equal(len(bs), costEvict, stage) 182 | } 183 | 184 | func (s *factorySuite) TestNewCacheWithoutCacheType() { 185 | defer func() { 186 | r := recover() 187 | s.Require().NotNil(r) 188 | s.Require().Equal(errors.New("no cache type indicated"), r) 189 | }() 190 | s.factory.NewCache([]Setting{{Prefix: "noCacheType"}}) 191 | } 192 | 193 | func (s *factorySuite) TestNewCacheWithEmptyPrefix() { 194 | defer func() { 195 | r := recover() 196 | s.Require().NotNil(r) 197 | s.Require().Equal(errors.New("not allowed empty prefix"), r) 198 | }() 199 | s.factory.NewCache([]Setting{{Prefix: ""}}) 200 | } 201 | 202 | func (s *factorySuite) TestNewCacheWithDuplicatedPrefix() { 203 | defer func() { 204 | r := recover() 205 | s.Require().NotNil(r) 206 | s.Require().Equal(errors.New("duplicated prefix"), r) 207 | }() 208 | s.factory.NewCache([]Setting{ 209 | { 210 | Prefix: "exist", 211 | CacheAttributes: map[Type]Attribute{SharedCacheType: {time.Hour}}, 212 | }, 213 | { 214 | Prefix: "exist", 215 | CacheAttributes: map[Type]Attribute{SharedCacheType: {time.Second}}, 216 | }, 217 | }) 218 | } 219 | 220 | func (s *factorySuite) TestNewCacheWithOnlyMarshal() { 221 | defer func() { 222 | r := recover() 223 | s.Require().NotNil(r) 224 | s.Require().Equal(errors.New("both of Marshal and Unmarshal functions need to be specified"), r) 225 | }() 226 | 227 | s.factory.NewCache([]Setting{ 228 | { 229 | Prefix: "OnlyMarshal", 230 | CacheAttributes: map[Type]Attribute{SharedCacheType: {time.Hour}}, 231 | MarshalFunc: json.Marshal, 232 | }, 233 | }) 234 | } 235 | 236 | func (s *factorySuite) TestNewCacheWithOnlyUnmarshal() { 237 | defer func() { 238 | r := recover() 239 | s.Require().NotNil(r) 240 | s.Require().Equal(errors.New("both of Marshal and Unmarshal functions need to be specified"), r) 241 | }() 242 | 243 | s.factory.NewCache([]Setting{ 244 | { 245 | Prefix: "OnlyMarshal", 246 | CacheAttributes: map[Type]Attribute{SharedCacheType: {time.Hour}}, 247 | UnmarshalFunc: json.Unmarshal, 248 | }, 249 | }) 250 | } 251 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/viney-shih/go-cache 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.4 7 | github.com/google/uuid v1.3.0 8 | github.com/klauspost/compress v1.15.14 9 | github.com/stretchr/testify v1.7.0 10 | github.com/vmihailenco/go-tinylfu v0.2.2 11 | github.com/vmihailenco/msgpack/v5 v5.3.5 12 | golang.org/x/exp v0.0.0-20210526181343-b47a03e3048a 13 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 14 | ) 15 | 16 | require ( 17 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 2 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 3 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 10 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 11 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 12 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 13 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 14 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 15 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 16 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 17 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 18 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 19 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 20 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 21 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 22 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 23 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 24 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 25 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 30 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 31 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 32 | github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc= 33 | github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= 34 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 35 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 36 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 37 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 38 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 39 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 40 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 41 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 42 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 43 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 44 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 49 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 51 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 52 | github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 53 | github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 54 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 55 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 56 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 57 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 58 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 61 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 62 | golang.org/x/exp v0.0.0-20210526181343-b47a03e3048a h1:15PzmCQfHRcBYKPW5s3hmJVO2H/SpTv5rsEh10maPMk= 63 | golang.org/x/exp v0.0.0-20210526181343-b47a03e3048a/go.mod h1:MSdmUWF4ZWBPSUbgUX/gaau5kvnbkSs9pgtY6B9JXDE= 64 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 65 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 66 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 67 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 68 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 69 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 70 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 71 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 72 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 74 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 75 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 76 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 78 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= 88 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 90 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 91 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 92 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 93 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 94 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 95 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 96 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 97 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 102 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 103 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 104 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 105 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 106 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 107 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 108 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 109 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 110 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 111 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 112 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 113 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 114 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 115 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 116 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 118 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 119 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 120 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // ErrCacheMiss indicates the key is missing 11 | ErrCacheMiss = errors.New("cache key is missing") 12 | // ErrPfxNotRegistered means the prefix is not registered 13 | ErrPfxNotRegistered = errors.New("prefix not registered") 14 | // ErrMGetterResponseLengthInvalid means mgetter return a slice with wrong length, 15 | // the response length should be equal to the getterParams length 16 | ErrMGetterResponseLengthInvalid = errors.New("wrong mgetter response length") 17 | // ErrMGetterResponseNotSlice means mgetter's response type is not slice 18 | ErrMGetterResponseNotSlice = errors.New("mgetter response not a slice") 19 | // ErrResultIndexInvalid means the index for Result.Get is out of range 20 | ErrResultIndexInvalid = errors.New("index out of range") 21 | ) 22 | 23 | // OneTimeGetterFunc should be provided as a parameter in GetByFunc() 24 | type OneTimeGetterFunc func() (interface{}, error) 25 | 26 | // MGetterFunc should response a slice of elements which has 1-1 mapping with the provided keys 27 | type MGetterFunc func(keys ...string) (interface{}, error) 28 | 29 | // Type decides which components are used in multi-layer cache structure 30 | type Type int32 31 | 32 | // All kinds of cache component type 33 | const ( 34 | // NoneType 35 | NoneType Type = iota 36 | // SharedCacheType means shared caching. It ensures that different application instances see the same view of cached data. 37 | // The famous frameworks are Redis, Memcached, ... (Ref: https://en.wikipedia.org/wiki/Distributed_cache) 38 | SharedCacheType 39 | // LocalCacheType means private caching in a single application instance, and the most basic type of cache is an in-memory store. 40 | // It's held in the address space of a single process and accessed directly by the code that runs in that process. 41 | // Due to the limited space of memory, we need to consider the efficient cache eviction policy to keep the most important 42 | // items in it. (Ref: https://en.wikipedia.org/wiki/Cache_replacement_policies) 43 | LocalCacheType 44 | ) 45 | 46 | // Factory is initialized in the main.go, and used to generate the Cache for each business logic 47 | type Factory interface { 48 | NewCache(settings []Setting) Cache 49 | Close() 50 | } 51 | 52 | // NewFactory returns the Factory initialized in the main.go. 53 | func NewFactory(sharedCache Adapter, localCache Adapter, options ...FactoryOptions) Factory { 54 | return newFactory(sharedCache, localCache, options...) 55 | } 56 | 57 | // Cache is generated by Factory based on the need specified in the Setting slice. 58 | // Use the following methods to create key/value store. 59 | type Cache interface { 60 | // GetByFunc returns a value in the cache. It also follows up the Cache-Aside pattern. 61 | // When cache-miss happened, it relaods the value by the getter, and fill in the cache again. 62 | GetByFunc(context context.Context, prefix, key string, container interface{}, getter OneTimeGetterFunc) error 63 | // Get returns a value in the cache. 64 | // When cache-miss happened, it relaods the value by MGetter specified in the setting if possible. 65 | // Or returns the error of ErrCacheMiss. 66 | Get(context context.Context, prefix, key string, container interface{}) error 67 | // MGet returns values in the cache with the interface Result. 68 | // When cache-miss happened, it relaods values by MGetter specified in the setting if possible. 69 | // Or returns the error of ErrCacheMiss. 70 | MGet(context context.Context, prefix string, keys ...string) (Result, error) 71 | // Del remove keys in the cache 72 | Del(context context.Context, prefix string, keys ...string) error 73 | // Set sets up a value into the cache. 74 | Set(context context.Context, prefix string, key string, value interface{}) error 75 | // MSet sets up values into the cache. 76 | MSet(context context.Context, prefix string, keyValues map[string]interface{}) error 77 | } 78 | 79 | // Setting provides a relation between Prefix and detailed Attributes. 80 | // One Setting stands for a one group of a cache, and it use Prefix stands for the unique id. 81 | // In other words, a group of a cache has it's own Attributes like TTL. 82 | type Setting struct { 83 | // Prefix is unique id for a group of the cache. 84 | Prefix string 85 | // CacheAttributes includes all detail attributes. 86 | CacheAttributes map[Type]Attribute 87 | // MGetter should be provided when using Cache-Aside pattern 88 | MGetter MGetterFunc 89 | // MarshalFunc specified the marshal function 90 | // Needs to consider with unmarshal function at the same time. 91 | MarshalFunc MarshalFunc 92 | // UnmarshalFunc specified the unmarshal function 93 | // Needs to consider with marshal function at the same time. 94 | UnmarshalFunc UnmarshalFunc 95 | } 96 | 97 | // Attribute specified details. For example, you need to indicate the TTL for each key to expire. 98 | type Attribute struct { 99 | TTL time.Duration 100 | } 101 | 102 | // Result is the return values from MGet(). You need a for loop to parse whole values. 103 | type Result interface { 104 | Len() int 105 | Get(ctx context.Context, index int, container interface{}) error 106 | } 107 | 108 | // ClearPrefix is only used by unit tests that clean up registered prefix, otherwise 109 | // duplicated prefix registration panic might occur due to multiple tests. 110 | func ClearPrefix() { 111 | usedPrefixs = map[string]struct{}{} 112 | } 113 | 114 | // Register registers customized parameters in the package. 115 | func Register(packageKey string) { 116 | registerKey(packageKey) 117 | } 118 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | const ( 9 | packageKey = "ca" 10 | topicKey = "tp" 11 | 12 | // delimiters 13 | cacheDelim = ":" 14 | topicDelim = "#" 15 | ) 16 | 17 | var ( 18 | regPkgKey = packageKey 19 | // regKeyOnce limits key registeration happening once 20 | regKeyOnce = sync.Once{} 21 | ) 22 | 23 | func registerKey(pkgKey string) { 24 | regKeyOnce.Do(func() { 25 | regPkgKey = pkgKey 26 | }) 27 | } 28 | 29 | func customKey(delimiter string, components ...string) string { 30 | return strings.Join(components, delimiter) 31 | } 32 | 33 | func getTopic(topic string) string { 34 | return customKey(topicDelim, regPkgKey, topicKey, topic) 35 | } 36 | 37 | func getCacheKey(pfx, key string) string { 38 | if regPkgKey == "" { 39 | return customKey(cacheDelim, pfx, key) 40 | } 41 | 42 | return customKey(cacheDelim, regPkgKey, pfx, key) 43 | } 44 | 45 | func getCacheKeys(pfx string, keys []string) []string { 46 | cacheKeys := make([]string, len(keys)) 47 | for i, k := range keys { 48 | cacheKeys[i] = getCacheKey(pfx, k) 49 | } 50 | 51 | return cacheKeys 52 | } 53 | 54 | func getPrefixAndKey(cacheKey string) (string, string) { 55 | // 1) cacheKey = regPkgKey + prefix + key (normal case) 56 | // 2) cacheKey = prefix + key (if customized package key is empty) 57 | idx := strings.Index(cacheKey, cacheDelim) 58 | if idx < 0 { 59 | return cacheKey, "" // should not happen 60 | } 61 | 62 | if regPkgKey == "" { 63 | return cacheKey[:idx], cacheKey[idx+len(cacheDelim):] 64 | } 65 | 66 | // mixedKey = prefix + key 67 | mixedKey := cacheKey[idx+len(cacheDelim):] 68 | idx = strings.Index(mixedKey, cacheDelim) 69 | if idx < 0 { 70 | return mixedKey, "" 71 | } 72 | 73 | return mixedKey[:idx], mixedKey[idx+len(cacheDelim):] 74 | } 75 | -------------------------------------------------------------------------------- /key_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type keySuite struct { 12 | suite.Suite 13 | } 14 | 15 | func (s *keySuite) SetupSuite() {} 16 | 17 | func (s *keySuite) TearDownSuite() {} 18 | 19 | func (s *keySuite) SetupTest() {} 20 | 21 | func (s *keySuite) TearDownTest() { 22 | clearRegisteredKey() 23 | } 24 | 25 | func TestKeySuite(t *testing.T) { 26 | suite.Run(t, new(keySuite)) 27 | } 28 | 29 | func clearRegisteredKey() { 30 | regPkgKey = packageKey 31 | regKeyOnce = sync.Once{} 32 | } 33 | 34 | func (s *keySuite) TestGetPrefixAndKey() { 35 | tests := []struct { 36 | Desc string 37 | CacheKey string 38 | ExpPfx string 39 | ExpKey string 40 | }{ 41 | { 42 | Desc: "invalid cache key without delimiter", 43 | CacheKey: "12345", 44 | ExpPfx: "12345", 45 | ExpKey: "", 46 | }, 47 | { 48 | Desc: "invalid cache key with only one delimiter", 49 | CacheKey: fmt.Sprintf("%s%s%s", "123", cacheDelim, "abc"), 50 | ExpPfx: "abc", 51 | ExpKey: "", 52 | }, 53 | { 54 | Desc: "normal case", 55 | CacheKey: getCacheKey("prefix", "key"), 56 | ExpPfx: "prefix", 57 | ExpKey: "key", 58 | }, 59 | } 60 | 61 | for _, t := range tests { 62 | pfx, key := getPrefixAndKey(t.CacheKey) 63 | s.Require().Equal(t.ExpPfx, pfx, t.Desc) 64 | s.Require().Equal(t.ExpKey, key, t.Desc) 65 | 66 | s.TearDownTest() 67 | } 68 | } 69 | 70 | func (s *keySuite) TestRegister() { 71 | s.Require().Equal(packageKey, regPkgKey) 72 | 73 | Register("specified") 74 | s.Require().Equal("specified", regPkgKey) 75 | 76 | Register("another") 77 | s.Require().Equal("specified", regPkgKey) // no change 78 | 79 | clearRegisteredKey() 80 | s.Require().Equal(packageKey, regPkgKey) // set to default 81 | 82 | Register("another") 83 | s.Require().Equal("another", regPkgKey) // set to another 84 | } 85 | 86 | func (s *keySuite) TestRegisterAndGetCacheKey() { 87 | var cKey, pfx, key string 88 | 89 | s.Require().Equal(fmt.Sprintf("%s:pfx:key", packageKey), getCacheKey("pfx", "key")) 90 | 91 | Register("my") 92 | cKey = getCacheKey("pfx", "key") 93 | s.Require().Equal("my:pfx:key", cKey) 94 | pfx, key = getPrefixAndKey(cKey) 95 | s.Require().Equal(pfx, "pfx") 96 | s.Require().Equal(key, "key") 97 | 98 | clearRegisteredKey() 99 | s.Require().Equal(fmt.Sprintf("%s:pfx:key", packageKey), getCacheKey("pfx", "key")) // set to default 100 | 101 | Register("") // empty package key 102 | cKey = getCacheKey("pfx", "key") 103 | s.Require().Equal("pfx:key", cKey) 104 | pfx, key = getPrefixAndKey(cKey) 105 | s.Require().Equal(pfx, "pfx") 106 | s.Require().Equal(key, "key") 107 | } 108 | -------------------------------------------------------------------------------- /marshaler.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/klauspost/compress/s2" 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // ref: https://github.com/go-redis/cache/blob/v8/cache.go 11 | 12 | const ( 13 | compressionThreshold = 64 14 | timeLen = 4 15 | ) 16 | 17 | const ( 18 | noCompression = 0x0 19 | s2Compression = 0x1 20 | ) 21 | 22 | // Marshal marshals value by msgpack + compress 23 | func Marshal(value interface{}) ([]byte, error) { 24 | switch value := value.(type) { 25 | case nil: 26 | return nil, nil 27 | case []byte: 28 | return value, nil 29 | case string: 30 | return []byte(value), nil 31 | } 32 | 33 | b, err := msgpack.Marshal(value) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return compress(b), nil 39 | } 40 | 41 | func compress(data []byte) []byte { 42 | if len(data) < compressionThreshold { 43 | n := len(data) + 1 44 | b := make([]byte, n, n+timeLen) 45 | copy(b, data) 46 | b[len(b)-1] = noCompression 47 | return b 48 | } 49 | 50 | n := s2.MaxEncodedLen(len(data)) + 1 51 | b := make([]byte, n, n+timeLen) 52 | b = s2.Encode(b, data) 53 | b = append(b, s2Compression) 54 | return b 55 | } 56 | 57 | // Unmarshal unmarshals binary with the compress + msgpack 58 | func Unmarshal(b []byte, value interface{}) error { 59 | if len(b) == 0 { 60 | return nil 61 | } 62 | 63 | switch value := value.(type) { 64 | case nil: 65 | return nil 66 | case *[]byte: 67 | clone := make([]byte, len(b)) 68 | copy(clone, b) 69 | *value = clone 70 | return nil 71 | case *string: 72 | *value = string(b) 73 | return nil 74 | } 75 | 76 | switch c := b[len(b)-1]; c { 77 | case noCompression: 78 | b = b[:len(b)-1] 79 | case s2Compression: 80 | b = b[:len(b)-1] 81 | 82 | var err error 83 | b, err = s2.Decode(nil, b) 84 | if err != nil { 85 | return err 86 | } 87 | default: 88 | return fmt.Errorf("unknown compression method: %x", c) 89 | } 90 | 91 | return msgpack.Unmarshal(b, value) 92 | } 93 | -------------------------------------------------------------------------------- /marshaler_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | var ( 11 | mockTimeNow = time.Date(2022, 11, 23, 0, 0, 0, 0, time.Local) 12 | ) 13 | 14 | type marshalerSuite struct { 15 | suite.Suite 16 | } 17 | 18 | func (s *marshalerSuite) SetupSuite() {} 19 | 20 | func (s *marshalerSuite) TearDownSuite() {} 21 | 22 | func (s *marshalerSuite) SetupTest() {} 23 | 24 | func (s *marshalerSuite) TearDownTest() {} 25 | 26 | func TestMarshalerSuite(t *testing.T) { 27 | suite.Run(t, new(marshalerSuite)) 28 | } 29 | 30 | type mockStruct struct { 31 | ID int64 32 | Key string 33 | CreatedAt time.Time 34 | child *mockStruct 35 | } 36 | 37 | func (s *marshalerSuite) TestMarshaler() { 38 | var bs []byte 39 | var err error 40 | marshal := Marshal 41 | unmarshal := Unmarshal 42 | 43 | // nil 44 | var null error 45 | bs, err = marshal(null) 46 | s.Require().NoError(err) 47 | 48 | var retNull error 49 | s.Require().NoError(unmarshal(bs, &retNull)) 50 | s.Require().Equal(null, retNull) 51 | 52 | // bytes 53 | bytes := []byte("strings to bytes") 54 | bs, err = marshal(bytes) 55 | s.Require().NoError(err) 56 | 57 | var retBytes []byte 58 | s.Require().NoError(unmarshal(bs, &retBytes)) 59 | s.Require().Equal(bytes, retBytes) 60 | 61 | // string 62 | str := "this is a string" 63 | bs, err = marshal(str) 64 | s.Require().NoError(err) 65 | 66 | var retStr string 67 | s.Require().NoError(unmarshal(bs, &retStr)) 68 | s.Require().Equal(str, retStr) 69 | 70 | // pointer 71 | num := 100 72 | intPtr := &num 73 | bs, err = marshal(intPtr) 74 | s.Require().NoError(err) 75 | 76 | var retIntPtr *int 77 | s.Require().NoError(unmarshal(bs, &retIntPtr)) 78 | s.Require().Equal(intPtr, retIntPtr) 79 | 80 | // struct 81 | st := mockStruct{ 82 | ID: 28825252, 83 | Key: "I am rich", 84 | CreatedAt: mockTimeNow, 85 | } 86 | bs, err = marshal(st) 87 | s.Require().NoError(err) 88 | 89 | retSt := mockStruct{} 90 | s.Require().NoError(unmarshal(bs, &retSt)) 91 | s.Require().Equal(st, retSt) 92 | 93 | // struct without nil pointer 94 | st2 := mockStruct{ 95 | ID: 28825252, 96 | Key: "I am rich", 97 | CreatedAt: mockTimeNow, 98 | child: &mockStruct{ 99 | ID: 2266, 100 | }, 101 | } 102 | bs, err = marshal(st2) 103 | s.Require().NoError(err) 104 | 105 | var retSt2 mockStruct 106 | s.Require().NoError(unmarshal(bs, &retSt2)) 107 | s.Require().Equal(st, retSt2) 108 | 109 | // compress 110 | st3 := mockStruct{ 111 | ID: 1234567890, 112 | Key: `1234567890123456789012345678901234567890123456789012345678901234567890`, // 70 chars 113 | CreatedAt: mockTimeNow, 114 | } 115 | bs, err = marshal(st3) 116 | s.Require().NoError(err) 117 | 118 | var retSt3 mockStruct 119 | s.Require().NoError(unmarshal(bs, &retSt3)) 120 | s.Require().Equal(st3, retSt3) 121 | } 122 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // MarshalFunc specifies the algorithm during marshaling the value to bytes. 4 | // The default is json.Marshal. 5 | type MarshalFunc func(interface{}) ([]byte, error) 6 | 7 | // UnmarshalFunc specifies the algorithm during unmarshaling the bytes to the value. 8 | // The default is json.Unmarshal 9 | type UnmarshalFunc func([]byte, interface{}) error 10 | 11 | // FactoryOptions is an alias for functional argument. 12 | type FactoryOptions func(opts *factoryOptions) 13 | 14 | // factoryOptions contains all options which will be applied when calling NewFactory(). 15 | type factoryOptions struct { 16 | marshalFunc MarshalFunc 17 | unmarshalFunc UnmarshalFunc 18 | onCacheHit func(prefix string, key string, count int) 19 | onCacheMiss func(prefix string, key string, count int) 20 | onLCCostAdd func(prefix string, key string, cost int) 21 | onLCCostEvict func(prefix string, key string, cost int) 22 | pubsub Pubsub 23 | } 24 | 25 | // WithMarshalFunc sets up the specified marshal function. 26 | // Needs to consider with unmarshal function at the same time. 27 | func WithMarshalFunc(f MarshalFunc) FactoryOptions { 28 | return func(opts *factoryOptions) { 29 | opts.marshalFunc = f 30 | } 31 | } 32 | 33 | // WithUnmarshalFunc sets up the specified unmarshal function. 34 | // Needs to consider with marshal function at the same time. 35 | func WithUnmarshalFunc(f UnmarshalFunc) FactoryOptions { 36 | return func(opts *factoryOptions) { 37 | opts.unmarshalFunc = f 38 | } 39 | } 40 | 41 | // WithPubSub is used to evict keys in local cache 42 | func WithPubSub(pb Pubsub) FactoryOptions { 43 | return func(opts *factoryOptions) { 44 | opts.pubsub = pb 45 | } 46 | } 47 | 48 | // OnCacheHitFunc sets up the callback function on cache hitted 49 | func OnCacheHitFunc(f func(prefix string, key string, count int)) FactoryOptions { 50 | return func(opts *factoryOptions) { 51 | opts.onCacheHit = f 52 | } 53 | } 54 | 55 | // OnCacheMissFunc sets up the callback function on cache missed 56 | func OnCacheMissFunc(f func(prefix string, key string, count int)) FactoryOptions { 57 | return func(opts *factoryOptions) { 58 | opts.onCacheMiss = f 59 | } 60 | } 61 | 62 | // OnLocalCacheCostAddFunc sets up the callback function on adding the cost of key in local cache 63 | func OnLocalCacheCostAddFunc(f func(prefix string, key string, cost int)) FactoryOptions { 64 | return func(opts *factoryOptions) { 65 | opts.onLCCostAdd = f 66 | } 67 | } 68 | 69 | // OnLocalCacheCostEvictFunc sets up the callback function on evicting the cost of key in local cache 70 | func OnLocalCacheCostEvictFunc(f func(prefix string, key string, cost int)) FactoryOptions { 71 | return func(opts *factoryOptions) { 72 | opts.onLCCostEvict = f 73 | } 74 | } 75 | 76 | func loadFactoryOptions(options ...FactoryOptions) *factoryOptions { 77 | opts := &factoryOptions{} 78 | for _, option := range options { 79 | option(opts) 80 | } 81 | 82 | return opts 83 | } 84 | -------------------------------------------------------------------------------- /pubsub.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "context" 4 | 5 | // Pubsub is the interface to deal with the message queue 6 | type Pubsub interface { 7 | // Pub publishes the message to the message queue with specified topic 8 | Pub(context context.Context, topic string, message []byte) error 9 | // Sub subscribes messages from the message queue with specified topics 10 | Sub(context context.Context, topic ...string) <-chan Message 11 | // Close closes the subscription only if Sub() is used. 12 | // In other word, should handle un-normal usage when Sub() didn't happen before. 13 | Close() 14 | } 15 | 16 | // Message is the interface to receive messages from message queue 17 | type Message interface { 18 | // Topic returns the topic 19 | Topic() string 20 | // Content returns the content of the message 21 | Content() []byte 22 | } 23 | -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | // Redis support two interface: Adapter and Pubsub 12 | type Redis interface { 13 | Adapter 14 | Pubsub 15 | } 16 | 17 | // NewRedis generates Adapter with go-redis 18 | func NewRedis(ring *redis.Ring) Redis { 19 | return &rds{ 20 | ring: ring, 21 | messChan: make(chan Message), 22 | } 23 | } 24 | 25 | type rds struct { 26 | ring *redis.Ring 27 | subscriber *redis.PubSub 28 | 29 | subOnce sync.Once 30 | closeOnce sync.Once 31 | messChan chan Message 32 | subMut sync.Mutex 33 | } 34 | 35 | func (r *rds) MSet( 36 | ctx context.Context, keyVals map[string][]byte, ttl time.Duration, options ...MSetOptions, 37 | ) error { 38 | if len(keyVals) == 0 { 39 | return nil 40 | } 41 | 42 | _, err := r.ring.WithContext(ctx).Pipelined(ctx, func(pipe redis.Pipeliner) error { 43 | // set multiple pairs 44 | pairSlice := make([]interface{}, len(keyVals)*2) 45 | i := 0 46 | for key, b := range keyVals { 47 | pairSlice[i] = key 48 | pairSlice[i+1] = b 49 | 50 | i += 2 51 | } 52 | 53 | pipe.MSet(ctx, pairSlice) 54 | 55 | // set expiration for each key 56 | for key := range keyVals { 57 | pipe.PExpire(ctx, key, ttl) 58 | } 59 | return nil 60 | }) 61 | 62 | return err 63 | } 64 | 65 | func (r *rds) MGet(ctx context.Context, keys []string) ([]Value, error) { 66 | vals, err := r.ring.WithContext(ctx).MGet(ctx, keys...).Result() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | values := make([]Value, len(vals)) 72 | for i, val := range vals { 73 | if val == nil { 74 | values[i] = Value{Valid: false, Bytes: nil} 75 | continue 76 | } 77 | 78 | s, ok := val.(string) 79 | if !ok { 80 | values[i] = Value{Valid: false, Bytes: nil} 81 | continue 82 | } 83 | 84 | values[i] = Value{Valid: ok, Bytes: []byte(s)} 85 | } 86 | 87 | return values, nil 88 | } 89 | 90 | func (r *rds) Del(ctx context.Context, keys ...string) error { 91 | _, err := r.ring.WithContext(ctx).Del(ctx, keys...).Result() 92 | 93 | return err 94 | } 95 | 96 | type rdsMessage struct { 97 | topic string 98 | content string 99 | } 100 | 101 | func (m *rdsMessage) Topic() string { 102 | return m.topic 103 | } 104 | 105 | func (m *rdsMessage) Content() []byte { 106 | return []byte(m.content) 107 | } 108 | 109 | func (r *rds) Pub(ctx context.Context, topic string, message []byte) error { 110 | return r.ring.Publish(ctx, topic, message).Err() 111 | } 112 | 113 | func (r *rds) Sub(ctx context.Context, topic ...string) <-chan Message { 114 | r.subOnce.Do(func() { 115 | subscriber := r.ring.Subscribe(ctx, topic...) 116 | r.subMut.Lock() 117 | r.subscriber = subscriber 118 | r.subMut.Unlock() 119 | 120 | go func() { 121 | for mess := range subscriber.Channel() { 122 | r.messChan <- &rdsMessage{ 123 | topic: mess.Channel, 124 | content: mess.Payload, 125 | } 126 | } 127 | 128 | close(r.messChan) 129 | }() 130 | }) 131 | 132 | return r.messChan 133 | } 134 | 135 | func (r *rds) Close() { 136 | r.closeOnce.Do(func() { 137 | r.subMut.Lock() 138 | subscriber := r.subscriber 139 | r.subMut.Unlock() 140 | 141 | if subscriber != nil { 142 | subscriber.Close() 143 | } 144 | }) 145 | } 146 | -------------------------------------------------------------------------------- /redis_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | const ( 15 | mockRdsPayload = "mock-rds-payload" 16 | mockRdsString = "mock-rds-string" 17 | ) 18 | 19 | var ( 20 | mockRdsCTX = context.Background() 21 | mockRdsBytes = []byte(mockRdsString) 22 | mockEvictTopic = EventTypeEvict.Topic() 23 | ) 24 | 25 | type redisSuite struct { 26 | suite.Suite 27 | 28 | ring *redis.Ring 29 | rds *rds 30 | } 31 | 32 | func (s *redisSuite) SetupSuite() { 33 | s.ring = redis.NewRing(&redis.RingOptions{ 34 | Addrs: map[string]string{ 35 | "server1": ":6379", 36 | }, 37 | }) 38 | } 39 | 40 | func (s *redisSuite) TearDownSuite() {} 41 | 42 | func (s *redisSuite) SetupTest() { 43 | s.rds = NewRedis(s.ring).(*rds) 44 | } 45 | 46 | func (s *redisSuite) TearDownTest() { 47 | _ = s.ring.ForEachShard(mockRdsCTX, func(ctx context.Context, client *redis.Client) error { 48 | return client.FlushDB(ctx).Err() 49 | }) 50 | 51 | s.rds.Close() 52 | } 53 | 54 | func TestRedisSuite(t *testing.T) { 55 | suite.Run(t, new(redisSuite)) 56 | } 57 | 58 | func (s *redisSuite) TestMGet() { 59 | tests := []struct { 60 | Desc string 61 | SetupTest func(string) 62 | Keys []string 63 | ExpError error 64 | ExpResult []Value 65 | }{ 66 | { 67 | Desc: "not existed", 68 | Keys: []string{"not-existed"}, 69 | ExpError: nil, 70 | ExpResult: []Value{{Valid: false, Bytes: nil}}, 71 | }, 72 | { 73 | // diff from tinyLFU, because any values will be converted into string format in redis 74 | Desc: "invalid format", 75 | SetupTest: func(desc string) { 76 | s.Require().NoError(s.ring.Set(mockRdsCTX, "invalid", 80, time.Hour).Err(), desc) 77 | }, 78 | Keys: []string{"invalid"}, 79 | ExpError: nil, 80 | ExpResult: []Value{{Valid: true, Bytes: []byte(strconv.Itoa(80))}}, 81 | }, 82 | { 83 | Desc: "empty bytes", 84 | SetupTest: func(desc string) { 85 | s.Require().NoError(s.ring.Set(mockRdsCTX, "empty-bytes", []byte{}, time.Hour).Err(), desc) 86 | }, 87 | Keys: []string{"empty-bytes"}, 88 | ExpError: nil, 89 | ExpResult: []Value{{Valid: true, Bytes: []byte{}}}, 90 | }, 91 | { 92 | Desc: "normal get", 93 | SetupTest: func(desc string) { 94 | s.Require().NoError(s.ring.Set(mockRdsCTX, "normal-get", mockRdsBytes, time.Hour).Err(), desc) 95 | }, 96 | Keys: []string{"normal-get"}, 97 | ExpError: nil, 98 | ExpResult: []Value{{Valid: true, Bytes: mockRdsBytes}}, 99 | }, 100 | } 101 | 102 | for _, t := range tests { 103 | if t.SetupTest != nil { 104 | t.SetupTest(t.Desc) 105 | } 106 | 107 | values, err := s.rds.MGet(mockRdsCTX, t.Keys) 108 | s.Require().Equal(t.ExpError, err, t.Desc) 109 | if err == nil { 110 | s.Require().Equal(t.ExpResult, values, t.Desc) 111 | } 112 | 113 | s.TearDownTest() 114 | } 115 | } 116 | 117 | func (s *redisSuite) TestMSet() { 118 | tests := []struct { 119 | Desc string 120 | KeyVals map[string][]byte 121 | TTL time.Duration 122 | ExpError error 123 | CheckFunc func(string) 124 | }{ 125 | { 126 | Desc: "set empty", 127 | KeyVals: map[string][]byte{ 128 | "set-empty": nil, 129 | }, 130 | TTL: time.Hour, 131 | ExpError: nil, 132 | CheckFunc: func(desc string) { 133 | b, err := s.ring.Get(mockRdsCTX, "set-empty").Bytes() 134 | s.Require().NoError(err, desc) 135 | s.Require().Equal([]byte{}, b, desc) 136 | }, 137 | }, 138 | { 139 | Desc: "set nothing", 140 | KeyVals: map[string][]byte{}, 141 | TTL: time.Hour, 142 | ExpError: nil, 143 | CheckFunc: func(desc string) { 144 | b, err := s.ring.Get(mockRdsCTX, "set-nothing").Bytes() 145 | var nilBytes []byte 146 | s.Require().Equal(redis.Nil, err, desc) 147 | s.Require().Equal(nilBytes, b, desc) 148 | }, 149 | }, 150 | { 151 | Desc: "normal set", 152 | KeyVals: map[string][]byte{ 153 | "normal-set": mockLfuBytes, 154 | }, 155 | TTL: time.Hour, 156 | ExpError: nil, 157 | CheckFunc: func(desc string) { 158 | b, err := s.ring.Get(mockRdsCTX, "normal-set").Bytes() 159 | s.Require().NoError(err, desc) 160 | s.Require().Equal(mockLfuBytes, b, desc) 161 | }, 162 | }, 163 | { 164 | Desc: "normal set but expired", 165 | KeyVals: map[string][]byte{ 166 | "normal-set-expired": mockLfuBytes, 167 | }, 168 | TTL: 50 * time.Millisecond, 169 | ExpError: nil, 170 | CheckFunc: func(desc string) { 171 | // wait until it expired 172 | time.Sleep(time.Millisecond * 300) 173 | 174 | b, err := s.ring.Get(mockRdsCTX, "normal-set-expired").Bytes() 175 | var nilBytes []byte 176 | s.Require().Equal(redis.Nil, err, desc) 177 | s.Require().Equal(nilBytes, b, desc) 178 | }, 179 | }, 180 | } 181 | 182 | for _, t := range tests { 183 | err := s.rds.MSet(mockLfuCTX, t.KeyVals, t.TTL) 184 | s.Require().Equal(t.ExpError, err, t.Desc) 185 | 186 | if t.CheckFunc != nil { 187 | t.CheckFunc(t.Desc) 188 | } 189 | 190 | s.TearDownTest() 191 | } 192 | } 193 | 194 | func (s *redisSuite) TestDel() { 195 | tests := []struct { 196 | Desc string 197 | SetupTest func(string) 198 | Keys []string 199 | ExpError error 200 | CheckFunc func(string) 201 | }{ 202 | { 203 | Desc: "del not existed", 204 | Keys: []string{"del-not-existed"}, 205 | ExpError: nil, 206 | }, 207 | { 208 | Desc: "normal del", 209 | SetupTest: func(desc string) { 210 | s.Require().NoError(s.ring.Set(mockRdsCTX, "normal-del", mockLfuBytes, time.Hour).Err(), desc) 211 | 212 | // make sure it's in cache 213 | b, err := s.ring.Get(mockRdsCTX, "normal-del").Bytes() 214 | s.Require().NoError(err, desc) 215 | s.Require().Equal(mockLfuBytes, b, desc) 216 | }, 217 | Keys: []string{"normal-del"}, 218 | ExpError: nil, 219 | CheckFunc: func(desc string) { 220 | b, err := s.ring.Get(mockRdsCTX, "normal-del").Bytes() 221 | var nilBytes []byte 222 | s.Require().Equal(redis.Nil, err, desc) 223 | s.Require().Equal(nilBytes, b, desc) 224 | }, 225 | }, 226 | } 227 | 228 | for _, t := range tests { 229 | if t.SetupTest != nil { 230 | t.SetupTest(t.Desc) 231 | } 232 | 233 | err := s.rds.Del(mockLfuCTX, t.Keys...) 234 | s.Require().Equal(t.ExpError, err, t.Desc) 235 | 236 | if t.CheckFunc != nil { 237 | t.CheckFunc(t.Desc) 238 | } 239 | 240 | s.TearDownTest() 241 | } 242 | } 243 | 244 | func (s *redisSuite) TestPub() { 245 | sub := s.rds.ring.Subscribe(mockRdsCTX, mockEvictTopic) 246 | pause := make(chan struct{}) 247 | wg := &sync.WaitGroup{} 248 | 249 | wg.Add(1) 250 | go func() { 251 | defer wg.Done() 252 | 253 | close(pause) 254 | msg, err := sub.ReceiveMessage(mockRdsCTX) 255 | 256 | s.Require().NoError(err) 257 | s.Require().Equal(mockRdsPayload, msg.Payload) 258 | }() 259 | 260 | time.Sleep(time.Millisecond * 50) 261 | <-pause 262 | s.Require().NoError(s.rds.Pub(mockRdsCTX, mockEvictTopic, []byte(mockRdsPayload))) 263 | 264 | wg.Wait() 265 | } 266 | 267 | func (s *redisSuite) TestSub() { 268 | wg := &sync.WaitGroup{} 269 | pause := make(chan struct{}) 270 | pause2 := make(chan struct{}) 271 | 272 | wg.Add(1) 273 | go func() { 274 | defer wg.Done() 275 | 276 | close(pause) 277 | for mess := range s.rds.Sub(mockRdsCTX, mockEvictTopic) { 278 | s.Require().Equal([]byte(mockRdsPayload), mess.Content()) 279 | close(pause2) 280 | } 281 | }() 282 | 283 | time.Sleep(time.Millisecond * 50) 284 | <-pause 285 | s.Require().NoError(s.ring.Publish(mockRdsCTX, mockEvictTopic, []byte(mockRdsPayload)).Err()) 286 | 287 | <-pause2 288 | s.rds.Close() 289 | wg.Wait() 290 | } 291 | -------------------------------------------------------------------------------- /tinylfu.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vmihailenco/go-tinylfu" 10 | "golang.org/x/exp/rand" 11 | ) 12 | 13 | const ( 14 | maxOffset = 10 * time.Second 15 | defaultOffset = -1 16 | ) 17 | 18 | type tinyLFU struct { 19 | lfu *tinylfu.T 20 | // tinyLFU is not thread-safe, it needs a lock 21 | mut sync.Mutex 22 | rand *rand.Rand 23 | offset time.Duration 24 | } 25 | 26 | // NewTinyLFU generates Adapter with tinylfu 27 | func NewTinyLFU(size int, options ...TinyLFUOptions) Adapter { 28 | // samples are the number of keys to track frequency 29 | // TinyLFU works best for small number of keys (~ 100k) 30 | // Ref: https://github.com/vmihailenco/go-cache-benchmark 31 | // consider the discussing in (https://github.com/ben-manes/caffeine/issues/106), 32 | // choose ~10x the cache size as the default value. 33 | samples := size * 10 34 | 35 | o := loadtinyLFUOptions(options...) 36 | if o.offset != defaultOffset && o.offset < 0 { 37 | panic(errors.New("invalid offset")) 38 | } 39 | 40 | return &tinyLFU{ 41 | lfu: tinylfu.New(size, samples), 42 | rand: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))), 43 | offset: o.offset, 44 | } 45 | } 46 | 47 | // TinyLFUOptions is an alias for functional argument. 48 | type TinyLFUOptions func(opts *tinyLFUOptions) 49 | 50 | // tinyLFUOptions contains all options which will be applied when calling New(). 51 | type tinyLFUOptions struct { 52 | offset time.Duration 53 | } 54 | 55 | // WithOffset sets up the offset which is used to randomize TTL preventing 56 | // expiring at the same time. 57 | func WithOffset(offset time.Duration) TinyLFUOptions { 58 | return func(opts *tinyLFUOptions) { 59 | opts.offset = offset 60 | } 61 | } 62 | 63 | func loadtinyLFUOptions(options ...TinyLFUOptions) *tinyLFUOptions { 64 | opts := &tinyLFUOptions{offset: defaultOffset} 65 | for _, option := range options { 66 | option(opts) 67 | } 68 | 69 | return opts 70 | } 71 | 72 | func (lfu *tinyLFU) MSet( 73 | ctx context.Context, keyVals map[string][]byte, ttl time.Duration, options ...MSetOptions, 74 | ) error { 75 | if len(keyVals) == 0 { 76 | return nil 77 | } 78 | 79 | // load options 80 | o := loadMSetOptions(options...) 81 | // offset is used to adjust the ttl preventing expiring at the same time 82 | offset := lfu.offset 83 | if offset == defaultOffset { 84 | offset = ttl / 10 85 | if offset > maxOffset { 86 | offset = maxOffset 87 | } 88 | } 89 | 90 | lfu.mut.Lock() 91 | defer lfu.mut.Unlock() 92 | 93 | for key, b := range keyVals { 94 | t := ttl 95 | if offset > 0 { 96 | t += time.Duration(lfu.rand.Int63n(int64(offset))) 97 | } 98 | 99 | cost := len(b) 100 | if o.onCostAdd != nil { 101 | o.onCostAdd(key, cost) 102 | } 103 | 104 | lfu.lfu.Set(&tinylfu.Item{ 105 | Key: key, 106 | Value: b, 107 | ExpireAt: time.Now().Add(t), 108 | OnEvict: func() { 109 | if o.onCostEvict != nil { 110 | o.onCostEvict(key, cost) 111 | } 112 | }, 113 | }) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (lfu *tinyLFU) MGet(ctx context.Context, keys []string) ([]Value, error) { 120 | lfu.mut.Lock() 121 | defer lfu.mut.Unlock() 122 | 123 | vals := make([]Value, len(keys)) 124 | for i, key := range keys { 125 | val, ok := lfu.lfu.Get(key) 126 | if !ok { 127 | vals[i] = Value{Valid: false, Bytes: nil} 128 | continue 129 | } 130 | 131 | b, ok := val.([]byte) 132 | vals[i] = Value{Valid: ok, Bytes: b} 133 | } 134 | 135 | return vals, nil 136 | } 137 | 138 | func (lfu *tinyLFU) Del(ctx context.Context, keys ...string) error { 139 | lfu.mut.Lock() 140 | defer lfu.mut.Unlock() 141 | 142 | for _, key := range keys { 143 | lfu.lfu.Del(key) 144 | } 145 | 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /tinylfu_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/suite" 9 | "github.com/vmihailenco/go-tinylfu" 10 | ) 11 | 12 | const ( 13 | mockLfuString = "mock-string" 14 | ) 15 | 16 | var ( 17 | mockLfuCTX = context.Background() 18 | mockLfuBytes = []byte(mockLfuString) 19 | ) 20 | 21 | type tinyLFUSuite struct { 22 | suite.Suite 23 | 24 | lfu *tinyLFU 25 | } 26 | 27 | func (s *tinyLFUSuite) SetupSuite() {} 28 | 29 | func (s *tinyLFUSuite) TearDownSuite() {} 30 | 31 | func (s *tinyLFUSuite) SetupTest() { 32 | s.lfu = NewTinyLFU(10000).(*tinyLFU) 33 | } 34 | 35 | func (s *tinyLFUSuite) TearDownTest() {} 36 | 37 | func TestTinyLFUSuite(t *testing.T) { 38 | suite.Run(t, new(tinyLFUSuite)) 39 | } 40 | 41 | func (s *tinyLFUSuite) TestMGet() { 42 | tests := []struct { 43 | Desc string 44 | SetupTest func() 45 | Keys []string 46 | ExpError error 47 | ExpResult []Value 48 | }{ 49 | { 50 | Desc: "not existed", 51 | Keys: []string{"not-existed"}, 52 | ExpError: nil, 53 | ExpResult: []Value{{Valid: false, Bytes: nil}}, 54 | }, 55 | { 56 | Desc: "invalid format", 57 | SetupTest: func() { 58 | s.lfu.lfu.Set(&tinylfu.Item{ 59 | Key: "invalid", 60 | Value: 80, 61 | ExpireAt: time.Now().Add(time.Hour), 62 | }) 63 | }, 64 | Keys: []string{"invalid"}, 65 | ExpError: nil, 66 | ExpResult: []Value{{Valid: false, Bytes: nil}}, 67 | }, 68 | { 69 | Desc: "empty bytes", 70 | SetupTest: func() { 71 | s.lfu.lfu.Set(&tinylfu.Item{ 72 | Key: "empty-bytes", 73 | Value: []byte{}, 74 | ExpireAt: time.Now().Add(time.Hour), 75 | }) 76 | }, 77 | Keys: []string{"empty-bytes"}, 78 | ExpError: nil, 79 | ExpResult: []Value{{Valid: true, Bytes: []byte{}}}, 80 | }, 81 | { 82 | Desc: "normal get", 83 | SetupTest: func() { 84 | s.lfu.lfu.Set(&tinylfu.Item{ 85 | Key: "normal-get", 86 | Value: mockLfuBytes, 87 | ExpireAt: time.Now().Add(time.Hour), 88 | }) 89 | }, 90 | Keys: []string{"normal-get"}, 91 | ExpError: nil, 92 | ExpResult: []Value{{Valid: true, Bytes: mockLfuBytes}}, 93 | }, 94 | } 95 | 96 | for _, t := range tests { 97 | if t.SetupTest != nil { 98 | t.SetupTest() 99 | } 100 | 101 | values, err := s.lfu.MGet(mockLfuCTX, t.Keys) 102 | s.Require().Equal(t.ExpError, err, t.Desc) 103 | if err == nil { 104 | s.Require().Equal(t.ExpResult, values, t.Desc) 105 | } 106 | 107 | s.TearDownTest() 108 | } 109 | } 110 | 111 | func (s *tinyLFUSuite) TestMSet() { 112 | tests := []struct { 113 | Desc string 114 | KeyVals map[string][]byte 115 | TTL time.Duration 116 | ExpError error 117 | CheckFunc func(string) 118 | }{ 119 | { 120 | Desc: "set empty", 121 | KeyVals: map[string][]byte{ 122 | "set-empty": {}, 123 | }, 124 | TTL: time.Hour, 125 | ExpError: nil, 126 | CheckFunc: func(desc string) { 127 | b, exist := s.lfu.lfu.Get("set-empty") 128 | s.Require().True(exist, desc) 129 | s.Require().Equal([]byte{}, b, desc) 130 | }, 131 | }, 132 | { 133 | Desc: "set nothing", 134 | KeyVals: map[string][]byte{}, 135 | TTL: time.Hour, 136 | ExpError: nil, 137 | CheckFunc: func(desc string) { 138 | b, exist := s.lfu.lfu.Get("set-nothing") 139 | s.Require().False(exist, desc) 140 | s.Require().Equal(nil, b, desc) 141 | }, 142 | }, 143 | { 144 | Desc: "normal set", 145 | KeyVals: map[string][]byte{ 146 | "normal-set": mockLfuBytes, 147 | }, 148 | TTL: time.Hour, 149 | ExpError: nil, 150 | CheckFunc: func(desc string) { 151 | b, exist := s.lfu.lfu.Get("normal-set") 152 | s.Require().True(exist, desc) 153 | s.Require().Equal(mockLfuBytes, b, desc) 154 | }, 155 | }, 156 | { 157 | Desc: "normal set but expired", 158 | KeyVals: map[string][]byte{ 159 | "normal-set-expired": mockLfuBytes, 160 | }, 161 | TTL: 50 * time.Millisecond, 162 | ExpError: nil, 163 | CheckFunc: func(desc string) { 164 | // wait until it expired 165 | time.Sleep(time.Millisecond * 300) 166 | 167 | b, exist := s.lfu.lfu.Get("normal-set-expired") 168 | s.Require().False(exist, desc) 169 | s.Require().Equal(nil, b, desc) 170 | }, 171 | }, 172 | } 173 | 174 | for _, t := range tests { 175 | err := s.lfu.MSet(mockLfuCTX, t.KeyVals, t.TTL) 176 | s.Require().Equal(t.ExpError, err, t.Desc) 177 | 178 | if t.CheckFunc != nil { 179 | t.CheckFunc(t.Desc) 180 | } 181 | 182 | s.TearDownTest() 183 | } 184 | } 185 | 186 | func (s *tinyLFUSuite) TestDel() { 187 | tests := []struct { 188 | Desc string 189 | SetupTest func(string) 190 | Keys []string 191 | ExpError error 192 | CheckFunc func(string) 193 | }{ 194 | { 195 | Desc: "del not existed", 196 | Keys: []string{"del-not-existed"}, 197 | ExpError: nil, 198 | }, 199 | { 200 | Desc: "normal del", 201 | SetupTest: func(desc string) { 202 | s.lfu.lfu.Set(&tinylfu.Item{ 203 | Key: "normal-del", 204 | Value: mockLfuBytes, 205 | ExpireAt: time.Now().Add(time.Hour), 206 | }) 207 | 208 | // make sure it's in cache 209 | b, exist := s.lfu.lfu.Get("normal-del") 210 | s.Require().True(exist, desc) 211 | s.Require().Equal(mockLfuBytes, b, desc) 212 | }, 213 | Keys: []string{"normal-del"}, 214 | ExpError: nil, 215 | CheckFunc: func(desc string) { 216 | b, exist := s.lfu.lfu.Get("normal-del") 217 | s.Require().False(exist, desc) 218 | s.Require().Equal(nil, b, desc) 219 | }, 220 | }, 221 | } 222 | 223 | for _, t := range tests { 224 | if t.SetupTest != nil { 225 | t.SetupTest(t.Desc) 226 | } 227 | 228 | err := s.lfu.Del(mockLfuCTX, t.Keys...) 229 | s.Require().Equal(t.ExpError, err, t.Desc) 230 | 231 | if t.CheckFunc != nil { 232 | t.CheckFunc(t.Desc) 233 | } 234 | 235 | s.TearDownTest() 236 | } 237 | } 238 | --------------------------------------------------------------------------------