├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql_analysis.yml │ ├── git.yml │ ├── lint.yml │ ├── pull_request.yml │ ├── test-32-bit.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── assets └── logo.png ├── builder.go ├── builder_test.go ├── cache.go ├── cache_test.go ├── cmd └── generator │ └── main.go ├── entry.go ├── entry_test.go ├── extension.go ├── go.mod ├── go.sum ├── internal ├── core │ ├── cache.go │ ├── cache_test.go │ ├── task.go │ └── task_test.go ├── expiry │ ├── disabled.go │ ├── fixed.go │ ├── queue.go │ ├── queue_test.go │ ├── variable.go │ └── variable_test.go ├── generated │ └── node │ │ ├── b.go │ │ ├── bc.go │ │ ├── be.go │ │ ├── bec.go │ │ └── manager.go ├── hashtable │ ├── bucket.go │ ├── map.go │ └── map_test.go ├── lossy │ └── buffer.go ├── queue │ ├── growable.go │ ├── growable_test.go │ └── queue_bench_test.go ├── s3fifo │ ├── ghost.go │ ├── main.go │ ├── policy.go │ ├── policy_test.go │ ├── queue.go │ ├── queue_test.go │ └── small.go ├── stats │ ├── counter.go │ ├── counter_bench_test.go │ ├── counter_test.go │ ├── stats.go │ └── stats_test.go ├── unixtime │ ├── unixtime.go │ ├── unixtime_bench_test.go │ └── unixtime_test.go ├── xmath │ └── power.go └── xruntime │ ├── runtime.go │ ├── runtime_1.22.go │ └── xruntime.go ├── scripts ├── deps.sh └── pre-push.sh ├── stats.go └── stats_test.go /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: If you think our software behaves not the way it should, report a bug 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | ## Description 15 | 16 | 19 | 20 | ## To Reproduce 21 | 22 | Steps to reproduce the behavior: 23 | 24 | ## Expected behavior 25 | 26 | 29 | 30 | ## Environment 31 | 32 | - OS: 33 | 34 | ## Additional context 35 | 36 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request 3 | about: Suggest an improvement for this product 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Clarification and motivation 11 | 12 | 15 | 16 | # Acceptance criteria 17 | 18 | 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 8 | 9 | ## Related issue(s) 10 | 11 | 18 | 19 | ## :white_check_mark: Checklist for your Pull Request 20 | 21 | 30 | 31 | #### Related changes (conditional) 32 | 33 | - Tests 34 | - [ ] If I added new functionality, I added tests covering it. 35 | - [ ] If I fixed a bug, I added a regression test to prevent the bug from 36 | silently reappearing again. 37 | 38 | - Documentation 39 | - [ ] I checked whether I should update the docs and did so if necessary: 40 | - [README](../README.md) 41 | 42 | - Public contracts 43 | - [ ] My changes doesn't break project license. 44 | 45 | #### Stylistic guide (mandatory) 46 | 47 | - [ ] My code complies with the [styles guide](https://github.com/uber-go/guide/blob/master/style.md). 48 | - [ ] My commit history is clean (only contains changes relating to my 49 | issue/pull request and no reverted-my-earlier-commit changes) and commit 50 | messages start with identifiers of related issues in square brackets. 51 | 52 | **Example:** `[#42] Short commit description` 53 | 54 | If necessary both of these can be achieved even after the commits have been 55 | made/pushed using [rebase and squash](https://git-scm.com/docs/git-rebase). 56 | 57 | #### Before merging (mandatory) 58 | - [ ] Check __target__ branch of PR is set correctly 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql_analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '17 11 * * 2' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'go' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v3 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v3 39 | -------------------------------------------------------------------------------- /.github/workflows/git.yml: -------------------------------------------------------------------------------- 1 | name: Git Checks 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | block-fixup: 7 | name: Block merge with fixup commits 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: 13rac1/block-fixup-merge-action@v2.0.0 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | strategy: 8 | matrix: 9 | go-version: [1.19.x] 10 | platform: [ubuntu-latest] 11 | 12 | runs-on: ${{ matrix.platform }} 13 | 14 | steps: 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v4 23 | 24 | - name: lint 25 | uses: golangci/golangci-lint-action@v6 26 | with: 27 | version: v1.62 28 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | assign_author: 9 | name: Assign author to PR 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Assign author to PR 13 | uses: technote-space/assign-author@v1 14 | with: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/test-32-bit.yml: -------------------------------------------------------------------------------- 1 | name: Test 32-bit arch 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: [ 1.19.x, 1.22.x ] 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v4 22 | 23 | - name: Test 32-bit arch 24 | run: make test.32-bit 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: [ 1.19.x, 1.22.x ] 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v4 22 | 23 | - name: Unit tests 24 | run: make test.unit 25 | 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v5 28 | with: 29 | fail_ci_if_error: true 30 | files: ./coverage.txt 31 | token: ${{ secrets.CODECOV_TOKEN }} 32 | verbose: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | /.idea/ 18 | *.tmp 19 | *coverage.txt 20 | *lint.txt 21 | **/bin/ 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 8 3 | timeout: 5m 4 | build-tags: 5 | - integration 6 | modules-download-mode: readonly 7 | go: '1.22' 8 | output: 9 | formats: 10 | - format: tab 11 | path: lint.txt 12 | print-issued-lines: false 13 | uniq-by-line: false 14 | sort-results: true 15 | linters: 16 | enable: 17 | - asasalint 18 | - asciicheck 19 | - bidichk 20 | - bodyclose 21 | - contextcheck 22 | - durationcheck 23 | - errcheck 24 | - errname 25 | - errorlint 26 | - gocheckcompilerdirectives 27 | - gocritic 28 | - godot 29 | - gofumpt 30 | - gci 31 | - gomoddirectives 32 | - gosec 33 | - gosimple 34 | - govet 35 | - ineffassign 36 | - misspell 37 | - nakedret 38 | - nilerr 39 | - nilnil 40 | - noctx 41 | - nolintlint 42 | - prealloc 43 | - predeclared 44 | - promlinter 45 | - reassign 46 | - revive 47 | - rowserrcheck 48 | - sqlclosecheck 49 | - staticcheck 50 | - stylecheck 51 | - tagliatelle 52 | - tenv 53 | - testableexamples 54 | - thelper 55 | - tparallel 56 | - unconvert 57 | - unparam 58 | - usestdlibvars 59 | - wastedassign 60 | disable: 61 | - unused 62 | issues: 63 | max-issues-per-linter: 0 64 | max-same-issues: 0 65 | exclude-rules: 66 | - path: _test\.go 67 | linters: 68 | - gosec 69 | linters-settings: 70 | gci: 71 | sections: 72 | - standard # Standard lib 73 | - default # External dependencies 74 | - prefix(github.com/maypok86/otter) # Internal packages 75 | gocritic: 76 | enabled-tags: 77 | - diagnostic 78 | - experimental 79 | - opinionated 80 | - performance 81 | - style 82 | disabled-checks: 83 | - hugeParam 84 | - rangeExprCopy 85 | - rangeValCopy 86 | errcheck: 87 | check-type-assertions: true 88 | check-blank: true 89 | exclude-functions: 90 | - io/ioutil.ReadFile 91 | - io.Copy(*bytes.Buffer) 92 | - io.Copy(os.Stdout) 93 | nakedret: 94 | max-func-lines: 1 95 | revive: 96 | rules: 97 | - name: empty-block 98 | disabled: true 99 | tagliatelle: 100 | case: 101 | rules: 102 | json: snake 103 | yaml: snake 104 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.4 - 2024-11-23 2 | 3 | ### 🐞 Bug Fixes 4 | 5 | - Fixed a bug due to changing [gammazero/deque](https://github.com/gammazero/deque/pull/33) contracts without v2 release. ([#112](https://github.com/maypok86/otter/issues/112)) 6 | 7 | ## 1.2.3 - 2024-09-30 8 | 9 | ### 🐞 Bug Fixes 10 | 11 | - Added collection of eviction statistics for expired entries. ([#108](https://github.com/maypok86/otter/issues/108)) 12 | 13 | ## 1.2.2 - 2024-08-14 14 | 15 | ### ✨️Features 16 | 17 | - Implemented `fmt.Stringer` interface for `DeletionReason` type ([#100](https://github.com/maypok86/otter/issues/100)) 18 | 19 | ### 🐞 Bug Fixes 20 | 21 | - Fixed processing of an expired entry in the `Get` method ([#98](https://github.com/maypok86/otter/issues/98)) 22 | - Fixed inconsistent deletion listener behavior ([#98](https://github.com/maypok86/otter/issues/98)) 23 | - Fixed the behavior of `checkedAdd` when over/underflow ([#91](https://github.com/maypok86/otter/issues/91)) 24 | 25 | ## 1.2.1 - 2024-04-15 26 | 27 | ### 🐞 Bug Fixes 28 | 29 | - Fixed uint32 capacity overflow. 30 | 31 | ## 1.2.0 - 2024-03-12 32 | 33 | The main innovation of this release is the addition of an `Extension`, which makes it easy to add a huge number of features to otter. 34 | 35 | Usage example: 36 | 37 | ```go 38 | key := 1 39 | ... 40 | entry, ok := cache.Extension().GetEntry(key) 41 | ... 42 | key := entry.Key() 43 | value := entry.Value() 44 | cost := entry.Cost() 45 | expiration := entry.Expiration() 46 | ttl := entry.TTL() 47 | hasExpired := entry.HasExpired() 48 | ``` 49 | 50 | ### ✨️Features 51 | 52 | - Added `DeletionListener` to the builder ([#63](https://github.com/maypok86/otter/issues/63)) 53 | - Added `Extension` ([#56](https://github.com/maypok86/otter/issues/56)) 54 | 55 | ### 🚀 Improvements 56 | 57 | - Added support for Go 1.22 58 | - Memory consumption with small cache sizes is reduced to the level of other libraries ([#66](https://github.com/maypok86/otter/issues/66)) 59 | 60 | ## 1.1.1 - 2024-03-06 61 | 62 | ### 🐞 Bug Fixes 63 | 64 | - Fixed alignment issues on 32-bit archs 65 | 66 | ## 1.1.0 - 2024-03-04 67 | 68 | The main innovation of this release is node code generation. Thanks to it, the cache will no longer consume more memory due to features that it does not use. For example, if you do not need an expiration policy, then otter will not store the expiration time of each entry. It also allows otter to use more effective expiration policies. 69 | 70 | Another expected improvement is the correction of minor synchronization problems due to the state machine. Now otter, unlike other contention-free caches in Go, should not have them at all. 71 | 72 | ### ✨️Features 73 | 74 | - Added `DeleteByFunc` function to cache ([#44](https://github.com/maypok86/otter/issues/44)) 75 | - Added `InitialCapacity` function to builder ([#47](https://github.com/maypok86/otter/issues/47)) 76 | - Added collection of additional statistics ([#57](https://github.com/maypok86/otter/issues/57)) 77 | 78 | ### 🚀 Improvements 79 | 80 | - Added proactive queue-based and timer wheel-based expiration policies with O(1) time complexity ([#55](https://github.com/maypok86/otter/issues/55)) 81 | - Added node code generation ([#55](https://github.com/maypok86/otter/issues/55)) 82 | - Fixed the race condition when changing the order of events ([#59](https://github.com/maypok86/otter/issues/59)) 83 | - Reduced memory consumption on small caches 84 | 85 | ## 1.0.0 - 2024-01-26 86 | 87 | ### ✨️Features 88 | 89 | - Builder pattern support 90 | - Cleaner API compared to other caches ([#40](https://github.com/maypok86/otter/issues/40)) 91 | - Added `SetIfAbsent` and `Range` functions ([#27](https://github.com/maypok86/otter/issues/27)) 92 | - Statistics collection ([#4](https://github.com/maypok86/otter/issues/4)) 93 | - Cost based eviction 94 | - Support for generics and any comparable types as keys 95 | - Support ttl ([#14](https://github.com/maypok86/otter/issues/14)) 96 | - Excellent speed ([benchmark results](https://github.com/maypok86/otter?tab=readme-ov-file#-performance-)) 97 | - O(1) worst case time complexity for S3-FIFO instead of O(n) 98 | - Improved hit ratio of S3-FIFO on many traces ([simulator results](https://github.com/maypok86/otter?tab=readme-ov-file#-hit-ratio-)) 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | maypok86@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ## Reporting Issues 4 | 5 | Please [open an issue](https://github.com/maypok86/otter/issues) if you find a bug or have a feature request. 6 | Note: you need to login (e. g. using your GitHub account) first. 7 | Before submitting a bug report or feature request, check to make sure it hasn't 8 | already been submitted 9 | 10 | The more detailed your report is, the faster it can be resolved. 11 | If you report a bug, please provide steps to reproduce this bug and revision of 12 | code in which this bug reproduces. 13 | 14 | 15 | ## Code 16 | 17 | If you would like to contribute code to fix a bug, add a new feature, or 18 | otherwise improve our product, pull requests are most welcome. 19 | 20 | Our pull request template contains a [checklist](https://github.com/maypok86/otter/blob/main/.github/pull_request_template.md) of acceptance 21 | criteria for your pull request. 22 | Please read it before you start contributing and make sure your contributions 23 | adhere to this checklist. 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup 2 | setup: deps ## Setup development environment 3 | cp ./scripts/pre-push.sh .git/hooks/pre-push 4 | chmod +x .git/hooks/pre-push 5 | 6 | .PHONY: deps 7 | deps: ## Install all the build and lint dependencies 8 | bash scripts/deps.sh 9 | 10 | .PHONY: fmt 11 | fmt: ## Run format tools on all go files 12 | gci write --skip-vendor --skip-generated \ 13 | -s standard -s default -s "prefix(github.com/maypok86/otter)" . 14 | gofumpt -l -w . 15 | 16 | .PHONY: lint 17 | lint: ## Run all the linters 18 | golangci-lint run -v ./... 19 | 20 | .PHONY: test 21 | test: test.unit ## Run all the tests 22 | 23 | .PHONY: test.unit 24 | test.unit: ## Run all unit tests 25 | @echo 'mode: atomic' > coverage.txt 26 | go test -covermode=atomic -coverprofile=coverage.txt.tmp -coverpkg=./... -v -race ./... 27 | cat coverage.txt.tmp | grep -v -E "/generated/|/cmd/" > coverage.txt 28 | rm coverage.txt.tmp 29 | 30 | .PHONY: test.32-bit 31 | test.32-bit: ## Run tests on 32-bit arch 32 | GOARCH=386 go test -v ./... 33 | 34 | .PHONY: cover 35 | cover: test.unit ## Run all the tests and opens the coverage report 36 | go tool cover -html=coverage.txt 37 | 38 | .PHONY: ci 39 | ci: lint test ## Run all the tests and code checks 40 | 41 | .PHONY: generate 42 | generate: ## Generate files for the project 43 | go run ./cmd/generator ./internal/generated/node 44 | 45 | .PHONY: clean 46 | clean: ## Remove temporary files 47 | @go clean 48 | @rm -rf bin/ 49 | @rm -rf coverage.txt lint.txt 50 | @echo "SUCCESS!" 51 | 52 | .PHONY: help 53 | help: 54 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 55 | 56 | .DEFAULT_GOAL:= help 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

High performance in-memory cache

4 |

5 | 6 |

7 | Go Reference 8 | 9 | 10 | 11 | 12 | 13 | Mentioned in Awesome Go 14 |

15 | 16 | Otter is one of the most powerful caching libraries for Go based on researches in caching and concurrent data structures. Otter also uses the experience of designing caching libraries in other languages (for example, [caffeine](https://github.com/ben-manes/caffeine)). 17 | 18 | ## 📖 Contents 19 | 20 | - [Features](#features) 21 | - [Related works](#related-works) 22 | - [Usage](#usage) 23 | - [Requirements](#requirements) 24 | - [Installation](#installation) 25 | - [Examples](#examples) 26 | - [Performance](#performance) 27 | - [Throughput](#throughput) 28 | - [Hit ratio](#hit-ratio) 29 | - [Memory consumption](#memory-consumption) 30 | - [Contribute](#contribute) 31 | - [License](#license) 32 | 33 | ## ✨ Features 34 | 35 | - **Simple API**: Just set the parameters you want in the builder and enjoy 36 | - **Autoconfiguration**: Otter is automatically configured based on the parallelism of your application 37 | - **Generics**: You can safely use any comparable types as keys and any types as values 38 | - **TTL**: Expired values will be automatically deleted from the cache 39 | - **Cost-based eviction**: Otter supports eviction based on the cost of each entry 40 | - **Deletion listener**: You can pass a callback function in the builder that will be called when an entry is deleted from the cache 41 | - **Stats**: You can collect various usage statistics 42 | - **Excellent throughput**: Otter can handle a [huge number of requests](#throughput) 43 | - **Great hit ratio**: New S3-FIFO algorithm is used, which shows excellent [results](#hit-ratio) 44 | 45 | ## 🗃 Related works 46 | 47 | Otter is based on the following papers: 48 | 49 | - [BP-Wrapper: A Framework Making Any Replacement Algorithms (Almost) Lock Contention Free](https://www.researchgate.net/publication/220966845_BP-Wrapper_A_System_Framework_Making_Any_Replacement_Algorithms_Almost_Lock_Contention_Free) 50 | - [FIFO queues are all you need for cache eviction](https://dl.acm.org/doi/10.1145/3600006.3613147) 51 | - [A large scale analysis of hundreds of in-memory cache clusters at Twitter](https://www.usenix.org/system/files/osdi20-yang.pdf) 52 | 53 | ## 📚 Usage 54 | 55 | ### 📋 Requirements 56 | 57 | - Go 1.19+ 58 | 59 | ### 🛠️ Installation 60 | 61 | ```shell 62 | go get -u github.com/maypok86/otter 63 | ``` 64 | 65 | ### ✏️ Examples 66 | 67 | Otter uses a builder pattern that allows you to conveniently create a cache instance with different parameters. 68 | 69 | **Cache with const TTL** 70 | ```go 71 | package main 72 | 73 | import ( 74 | "fmt" 75 | "time" 76 | 77 | "github.com/maypok86/otter" 78 | ) 79 | 80 | func main() { 81 | // create a cache with capacity equal to 10000 elements 82 | cache, err := otter.MustBuilder[string, string](10_000). 83 | CollectStats(). 84 | Cost(func(key string, value string) uint32 { 85 | return 1 86 | }). 87 | WithTTL(time.Hour). 88 | Build() 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | // set item with ttl (1 hour) 94 | cache.Set("key", "value") 95 | 96 | // get value from cache 97 | value, ok := cache.Get("key") 98 | if !ok { 99 | panic("not found key") 100 | } 101 | fmt.Println(value) 102 | 103 | // delete item from cache 104 | cache.Delete("key") 105 | 106 | // delete data and stop goroutines 107 | cache.Close() 108 | } 109 | ``` 110 | 111 | **Cache with variable TTL** 112 | ```go 113 | package main 114 | 115 | import ( 116 | "fmt" 117 | "time" 118 | 119 | "github.com/maypok86/otter" 120 | ) 121 | 122 | func main() { 123 | // create a cache with capacity equal to 10000 elements 124 | cache, err := otter.MustBuilder[string, string](10_000). 125 | CollectStats(). 126 | Cost(func(key string, value string) uint32 { 127 | return 1 128 | }). 129 | WithVariableTTL(). 130 | Build() 131 | if err != nil { 132 | panic(err) 133 | } 134 | 135 | // set item with ttl (1 hour) 136 | cache.Set("key1", "value1", time.Hour) 137 | // set item with ttl (1 minute) 138 | cache.Set("key2", "value2", time.Minute) 139 | 140 | // get value from cache 141 | value, ok := cache.Get("key1") 142 | if !ok { 143 | panic("not found key") 144 | } 145 | fmt.Println(value) 146 | 147 | // delete item from cache 148 | cache.Delete("key1") 149 | 150 | // delete data and stop goroutines 151 | cache.Close() 152 | } 153 | ``` 154 | 155 | ## 📊 Performance 156 | 157 | The benchmark code can be found [here](https://github.com/maypok86/benchmarks). 158 | 159 | ### 🚀 Throughput 160 | 161 | Throughput benchmarks are a Go port of the caffeine [benchmarks](https://github.com/ben-manes/caffeine/blob/master/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/GetPutBenchmark.java). This microbenchmark compares the throughput of caches on a zipf distribution, which allows to show various inefficient places in implementations. 162 | 163 | You can find results [here](https://maypok86.github.io/otter/performance/throughput/). 164 | 165 | ### 🎯 Hit ratio 166 | 167 | The hit ratio simulator tests caches on various traces: 168 | 1. Synthetic (zipf distribution) 169 | 2. Traditional (widely known and used in various projects and papers) 170 | 3. Modern (recently collected from the production of the largest companies in the world) 171 | 172 | You can find results [here](https://maypok86.github.io/otter/performance/hit-ratio/). 173 | 174 | ### 💾 Memory consumption 175 | 176 | The memory overhead benchmark shows how much additional memory the cache will require at different capacities. 177 | 178 | You can find results [here](https://maypok86.github.io/otter/performance/memory-consumption/). 179 | 180 | ## 👏 Contribute 181 | 182 | Contributions are welcome as always, before submitting a new PR please make sure to open a new issue so community members can discuss it. 183 | For more information please see [contribution guidelines](./CONTRIBUTING.md). 184 | 185 | Additionally, you might find existing open issues which can help with improvements. 186 | 187 | This project follows a standard [code of conduct](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 188 | 189 | ## 📄 License 190 | 191 | This project is Apache 2.0 licensed, as found in the [LICENSE](./LICENSE). 192 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maypok86/otter/91b9c3231f86b4f56b4e3c87c97281eccca44053/assets/logo.png -------------------------------------------------------------------------------- /builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | "github.com/maypok86/otter/internal/core" 22 | ) 23 | 24 | const ( 25 | unsetCapacity = -1 26 | ) 27 | 28 | var ( 29 | // ErrIllegalCapacity means that a non-positive capacity has been passed to the NewBuilder. 30 | ErrIllegalCapacity = errors.New("capacity should be positive") 31 | // ErrIllegalInitialCapacity means that a non-positive capacity has been passed to the Builder.InitialCapacity. 32 | ErrIllegalInitialCapacity = errors.New("initial capacity should be positive") 33 | // ErrNilCostFunc means that a nil cost func has been passed to the Builder.Cost. 34 | ErrNilCostFunc = errors.New("setCostFunc func should not be nil") 35 | // ErrIllegalTTL means that a non-positive ttl has been passed to the Builder.WithTTL. 36 | ErrIllegalTTL = errors.New("ttl should be positive") 37 | ) 38 | 39 | type baseOptions[K comparable, V any] struct { 40 | capacity int 41 | initialCapacity int 42 | statsEnabled bool 43 | withCost bool 44 | costFunc func(key K, value V) uint32 45 | deletionListener func(key K, value V, cause DeletionCause) 46 | } 47 | 48 | func (o *baseOptions[K, V]) collectStats() { 49 | o.statsEnabled = true 50 | } 51 | 52 | func (o *baseOptions[K, V]) setCostFunc(costFunc func(key K, value V) uint32) { 53 | o.costFunc = costFunc 54 | o.withCost = true 55 | } 56 | 57 | func (o *baseOptions[K, V]) setInitialCapacity(initialCapacity int) { 58 | o.initialCapacity = initialCapacity 59 | } 60 | 61 | func (o *baseOptions[K, V]) setDeletionListener(deletionListener func(key K, value V, cause DeletionCause)) { 62 | o.deletionListener = deletionListener 63 | } 64 | 65 | func (o *baseOptions[K, V]) validate() error { 66 | if o.initialCapacity <= 0 && o.initialCapacity != unsetCapacity { 67 | return ErrIllegalInitialCapacity 68 | } 69 | if o.costFunc == nil { 70 | return ErrNilCostFunc 71 | } 72 | return nil 73 | } 74 | 75 | func (o *baseOptions[K, V]) toConfig() core.Config[K, V] { 76 | var initialCapacity *int 77 | if o.initialCapacity != unsetCapacity { 78 | initialCapacity = &o.initialCapacity 79 | } 80 | return core.Config[K, V]{ 81 | Capacity: o.capacity, 82 | InitialCapacity: initialCapacity, 83 | StatsEnabled: o.statsEnabled, 84 | CostFunc: o.costFunc, 85 | WithCost: o.withCost, 86 | DeletionListener: o.deletionListener, 87 | } 88 | } 89 | 90 | type constTTLOptions[K comparable, V any] struct { 91 | baseOptions[K, V] 92 | ttl time.Duration 93 | } 94 | 95 | func (o *constTTLOptions[K, V]) validate() error { 96 | if o.ttl <= 0 { 97 | return ErrIllegalTTL 98 | } 99 | return o.baseOptions.validate() 100 | } 101 | 102 | func (o *constTTLOptions[K, V]) toConfig() core.Config[K, V] { 103 | c := o.baseOptions.toConfig() 104 | c.TTL = &o.ttl 105 | return c 106 | } 107 | 108 | type variableTTLOptions[K comparable, V any] struct { 109 | baseOptions[K, V] 110 | } 111 | 112 | func (o *variableTTLOptions[K, V]) toConfig() core.Config[K, V] { 113 | c := o.baseOptions.toConfig() 114 | c.WithVariableTTL = true 115 | return c 116 | } 117 | 118 | // Builder is a one-shot builder for creating a cache instance. 119 | type Builder[K comparable, V any] struct { 120 | baseOptions[K, V] 121 | } 122 | 123 | // MustBuilder creates a builder and sets the future cache capacity. 124 | // 125 | // Panics if capacity <= 0. 126 | func MustBuilder[K comparable, V any](capacity int) *Builder[K, V] { 127 | b, err := NewBuilder[K, V](capacity) 128 | if err != nil { 129 | panic(err) 130 | } 131 | return b 132 | } 133 | 134 | // NewBuilder creates a builder and sets the future cache capacity. 135 | // 136 | // Returns an error if capacity <= 0. 137 | func NewBuilder[K comparable, V any](capacity int) (*Builder[K, V], error) { 138 | if capacity <= 0 { 139 | return nil, ErrIllegalCapacity 140 | } 141 | 142 | return &Builder[K, V]{ 143 | baseOptions: baseOptions[K, V]{ 144 | capacity: capacity, 145 | initialCapacity: unsetCapacity, 146 | statsEnabled: false, 147 | costFunc: func(key K, value V) uint32 { 148 | return 1 149 | }, 150 | }, 151 | }, nil 152 | } 153 | 154 | // CollectStats determines whether statistics should be calculated when the cache is running. 155 | // 156 | // By default, statistics calculating is disabled. 157 | func (b *Builder[K, V]) CollectStats() *Builder[K, V] { 158 | b.collectStats() 159 | return b 160 | } 161 | 162 | // InitialCapacity sets the minimum total size for the internal data structures. Providing a large enough estimate 163 | // at construction time avoids the need for expensive resizing operations later, but setting this 164 | // value unnecessarily high wastes memory. 165 | func (b *Builder[K, V]) InitialCapacity(initialCapacity int) *Builder[K, V] { 166 | b.setInitialCapacity(initialCapacity) 167 | return b 168 | } 169 | 170 | // Cost sets a function to dynamically calculate the cost of an item. 171 | // 172 | // By default, this function always returns 1. 173 | func (b *Builder[K, V]) Cost(costFunc func(key K, value V) uint32) *Builder[K, V] { 174 | b.setCostFunc(costFunc) 175 | return b 176 | } 177 | 178 | // DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any 179 | // DeletionCause cause. The cache will invoke this listener in the background goroutine 180 | // after the entry's deletion operation has completed. 181 | func (b *Builder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *Builder[K, V] { 182 | b.setDeletionListener(deletionListener) 183 | return b 184 | } 185 | 186 | // WithTTL specifies that each item should be automatically removed from the cache once a fixed duration 187 | // has elapsed after the item's creation. 188 | func (b *Builder[K, V]) WithTTL(ttl time.Duration) *ConstTTLBuilder[K, V] { 189 | return &ConstTTLBuilder[K, V]{ 190 | constTTLOptions[K, V]{ 191 | baseOptions: b.baseOptions, 192 | ttl: ttl, 193 | }, 194 | } 195 | } 196 | 197 | // WithVariableTTL specifies that each item should be automatically removed from the cache once a duration has 198 | // elapsed after the item's creation. Items are expired based on the custom ttl specified for each item separately. 199 | // 200 | // You should prefer WithTTL to this option whenever possible. 201 | func (b *Builder[K, V]) WithVariableTTL() *VariableTTLBuilder[K, V] { 202 | return &VariableTTLBuilder[K, V]{ 203 | variableTTLOptions[K, V]{ 204 | baseOptions: b.baseOptions, 205 | }, 206 | } 207 | } 208 | 209 | // Build creates a configured cache or 210 | // returns an error if invalid parameters were passed to the builder. 211 | func (b *Builder[K, V]) Build() (Cache[K, V], error) { 212 | if err := b.validate(); err != nil { 213 | return Cache[K, V]{}, err 214 | } 215 | 216 | return newCache(b.toConfig()), nil 217 | } 218 | 219 | // ConstTTLBuilder is a one-shot builder for creating a cache instance. 220 | type ConstTTLBuilder[K comparable, V any] struct { 221 | constTTLOptions[K, V] 222 | } 223 | 224 | // CollectStats determines whether statistics should be calculated when the cache is running. 225 | // 226 | // By default, statistics calculating is disabled. 227 | func (b *ConstTTLBuilder[K, V]) CollectStats() *ConstTTLBuilder[K, V] { 228 | b.collectStats() 229 | return b 230 | } 231 | 232 | // InitialCapacity sets the minimum total size for the internal data structures. Providing a large enough estimate 233 | // at construction time avoids the need for expensive resizing operations later, but setting this 234 | // value unnecessarily high wastes memory. 235 | func (b *ConstTTLBuilder[K, V]) InitialCapacity(initialCapacity int) *ConstTTLBuilder[K, V] { 236 | b.setInitialCapacity(initialCapacity) 237 | return b 238 | } 239 | 240 | // Cost sets a function to dynamically calculate the cost of an item. 241 | // 242 | // By default, this function always returns 1. 243 | func (b *ConstTTLBuilder[K, V]) Cost(costFunc func(key K, value V) uint32) *ConstTTLBuilder[K, V] { 244 | b.setCostFunc(costFunc) 245 | return b 246 | } 247 | 248 | // DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any 249 | // DeletionCause cause. The cache will invoke this listener in the background goroutine 250 | // after the entry's deletion operation has completed. 251 | func (b *ConstTTLBuilder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *ConstTTLBuilder[K, V] { 252 | b.setDeletionListener(deletionListener) 253 | return b 254 | } 255 | 256 | // Build creates a configured cache or 257 | // returns an error if invalid parameters were passed to the builder. 258 | func (b *ConstTTLBuilder[K, V]) Build() (Cache[K, V], error) { 259 | if err := b.validate(); err != nil { 260 | return Cache[K, V]{}, err 261 | } 262 | 263 | return newCache(b.toConfig()), nil 264 | } 265 | 266 | // VariableTTLBuilder is a one-shot builder for creating a cache instance. 267 | type VariableTTLBuilder[K comparable, V any] struct { 268 | variableTTLOptions[K, V] 269 | } 270 | 271 | // CollectStats determines whether statistics should be calculated when the cache is running. 272 | // 273 | // By default, statistics calculating is disabled. 274 | func (b *VariableTTLBuilder[K, V]) CollectStats() *VariableTTLBuilder[K, V] { 275 | b.collectStats() 276 | return b 277 | } 278 | 279 | // InitialCapacity sets the minimum total size for the internal data structures. Providing a large enough estimate 280 | // at construction time avoids the need for expensive resizing operations later, but setting this 281 | // value unnecessarily high wastes memory. 282 | func (b *VariableTTLBuilder[K, V]) InitialCapacity(initialCapacity int) *VariableTTLBuilder[K, V] { 283 | b.setInitialCapacity(initialCapacity) 284 | return b 285 | } 286 | 287 | // Cost sets a function to dynamically calculate the cost of an item. 288 | // 289 | // By default, this function always returns 1. 290 | func (b *VariableTTLBuilder[K, V]) Cost(costFunc func(key K, value V) uint32) *VariableTTLBuilder[K, V] { 291 | b.setCostFunc(costFunc) 292 | return b 293 | } 294 | 295 | // DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any 296 | // DeletionCause cause. The cache will invoke this listener in the background goroutine 297 | // after the entry's deletion operation has completed. 298 | func (b *VariableTTLBuilder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *VariableTTLBuilder[K, V] { 299 | b.setDeletionListener(deletionListener) 300 | return b 301 | } 302 | 303 | // Build creates a configured cache or 304 | // returns an error if invalid parameters were passed to the builder. 305 | func (b *VariableTTLBuilder[K, V]) Build() (CacheWithVariableTTL[K, V], error) { 306 | if err := b.validate(); err != nil { 307 | return CacheWithVariableTTL[K, V]{}, err 308 | } 309 | 310 | return newCacheWithVariableTTL(b.toConfig()), nil 311 | } 312 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "reflect" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestBuilder_MustFailed(t *testing.T) { 26 | defer func() { 27 | if r := recover(); r != nil { 28 | fmt.Println("recover: ", r) 29 | } 30 | }() 31 | MustBuilder[int, int](-1) 32 | t.Fatal("no panic detected") 33 | } 34 | 35 | func TestBuilder_NewFailed(t *testing.T) { 36 | _, err := NewBuilder[int, int](-63) 37 | if err == nil || !errors.Is(err, ErrIllegalCapacity) { 38 | t.Fatalf("should fail with an error %v, but got %v", ErrIllegalCapacity, err) 39 | } 40 | 41 | capacity := 100 42 | // negative const ttl 43 | _, err = MustBuilder[int, int](capacity).WithTTL(-1).Build() 44 | if err == nil || !errors.Is(err, ErrIllegalTTL) { 45 | t.Fatalf("should fail with an error %v, but got %v", ErrIllegalTTL, err) 46 | } 47 | 48 | // negative initial capacity 49 | _, err = MustBuilder[int, int](capacity).InitialCapacity(-2).Build() 50 | if err == nil || !errors.Is(err, ErrIllegalInitialCapacity) { 51 | t.Fatalf("should fail with an error %v, but got %v", ErrIllegalInitialCapacity, err) 52 | } 53 | 54 | _, err = MustBuilder[int, int](capacity).WithTTL(time.Hour).InitialCapacity(0).Build() 55 | if err == nil || !errors.Is(err, ErrIllegalInitialCapacity) { 56 | t.Fatalf("should fail with an error %v, but got %v", ErrIllegalInitialCapacity, err) 57 | } 58 | 59 | _, err = MustBuilder[int, int](capacity).WithVariableTTL().InitialCapacity(-5).Build() 60 | if err == nil || !errors.Is(err, ErrIllegalInitialCapacity) { 61 | t.Fatalf("should fail with an error %v, but got %v", ErrIllegalInitialCapacity, err) 62 | } 63 | 64 | // nil cost func 65 | _, err = MustBuilder[int, int](capacity).Cost(nil).Build() 66 | if err == nil || !errors.Is(err, ErrNilCostFunc) { 67 | t.Fatalf("should fail with an error %v, but got %v", ErrNilCostFunc, err) 68 | } 69 | } 70 | 71 | func TestBuilder_BuildSuccess(t *testing.T) { 72 | b := MustBuilder[int, int](10) 73 | 74 | _, err := b.InitialCapacity(unsetCapacity).Build() 75 | if err != nil { 76 | t.Fatalf("builded cache with error: %v", err) 77 | } 78 | 79 | c, err := b. 80 | CollectStats(). 81 | InitialCapacity(10). 82 | Cost(func(key int, value int) uint32 { 83 | return 2 84 | }).Build() 85 | if err != nil { 86 | t.Fatalf("builded cache with error: %v", err) 87 | } 88 | 89 | if !reflect.DeepEqual(reflect.TypeOf(Cache[int, int]{}), reflect.TypeOf(c)) { 90 | t.Fatalf("builder returned a different type of cache: %v", err) 91 | } 92 | 93 | cc, err := b.WithTTL(time.Minute).CollectStats().Cost(func(key int, value int) uint32 { 94 | return 2 95 | }).DeletionListener(func(key int, value int, cause DeletionCause) { 96 | fmt.Println("const ttl") 97 | }).Build() 98 | if err != nil { 99 | t.Fatalf("builded cache with error: %v", err) 100 | } 101 | 102 | if !reflect.DeepEqual(reflect.TypeOf(Cache[int, int]{}), reflect.TypeOf(cc)) { 103 | t.Fatalf("builder returned a different type of cache: %v", err) 104 | } 105 | 106 | cv, err := b.WithVariableTTL().CollectStats().Cost(func(key int, value int) uint32 { 107 | return 2 108 | }).DeletionListener(func(key int, value int, cause DeletionCause) { 109 | fmt.Println("variable ttl") 110 | }).Build() 111 | if err != nil { 112 | t.Fatalf("builded cache with error: %v", err) 113 | } 114 | 115 | if !reflect.DeepEqual(reflect.TypeOf(CacheWithVariableTTL[int, int]{}), reflect.TypeOf(cv)) { 116 | t.Fatalf("builder returned a different type of cache: %v", err) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "time" 19 | 20 | "github.com/maypok86/otter/internal/core" 21 | ) 22 | 23 | // DeletionCause the cause why a cached entry was deleted. 24 | type DeletionCause = core.DeletionCause 25 | 26 | const ( 27 | // Explicit the entry was manually deleted by the user. 28 | Explicit = core.Explicit 29 | // Replaced the entry itself was not actually deleted, but its value was replaced by the user. 30 | Replaced = core.Replaced 31 | // Size the entry was evicted due to size constraints. 32 | Size = core.Size 33 | // Expired the entry's expiration timestamp has passed. 34 | Expired = core.Expired 35 | ) 36 | 37 | type baseCache[K comparable, V any] struct { 38 | cache *core.Cache[K, V] 39 | } 40 | 41 | func newBaseCache[K comparable, V any](c core.Config[K, V]) baseCache[K, V] { 42 | return baseCache[K, V]{ 43 | cache: core.NewCache(c), 44 | } 45 | } 46 | 47 | // Has checks if there is an entry with the given key in the cache. 48 | func (bs baseCache[K, V]) Has(key K) bool { 49 | return bs.cache.Has(key) 50 | } 51 | 52 | // Get returns the value associated with the key in this cache. 53 | func (bs baseCache[K, V]) Get(key K) (V, bool) { 54 | return bs.cache.Get(key) 55 | } 56 | 57 | // Delete removes the association for this key from the cache. 58 | func (bs baseCache[K, V]) Delete(key K) { 59 | bs.cache.Delete(key) 60 | } 61 | 62 | // DeleteByFunc removes the association for this key from the cache when the given function returns true. 63 | func (bs baseCache[K, V]) DeleteByFunc(f func(key K, value V) bool) { 64 | bs.cache.DeleteByFunc(f) 65 | } 66 | 67 | // Range iterates over all entries in the cache. 68 | // 69 | // Iteration stops early when the given function returns false. 70 | func (bs baseCache[K, V]) Range(f func(key K, value V) bool) { 71 | bs.cache.Range(f) 72 | } 73 | 74 | // Clear clears the hash table, all policies, buffers, etc. 75 | // 76 | // NOTE: this operation must be performed when no requests are made to the cache otherwise the behavior is undefined. 77 | func (bs baseCache[K, V]) Clear() { 78 | bs.cache.Clear() 79 | } 80 | 81 | // Close clears the hash table, all policies, buffers, etc and stop all goroutines. 82 | // 83 | // NOTE: this operation must be performed when no requests are made to the cache otherwise the behavior is undefined. 84 | func (bs baseCache[K, V]) Close() { 85 | bs.cache.Close() 86 | } 87 | 88 | // Size returns the current number of entries in the cache. 89 | func (bs baseCache[K, V]) Size() int { 90 | return bs.cache.Size() 91 | } 92 | 93 | // Capacity returns the cache capacity. 94 | func (bs baseCache[K, V]) Capacity() int { 95 | return bs.cache.Capacity() 96 | } 97 | 98 | // Stats returns a current snapshot of this cache's cumulative statistics. 99 | func (bs baseCache[K, V]) Stats() Stats { 100 | return newStats(bs.cache.Stats()) 101 | } 102 | 103 | // Extension returns access to inspect and perform low-level operations on this cache based on its runtime 104 | // characteristics. These operations are optional and dependent on how the cache was constructed 105 | // and what abilities the implementation exposes. 106 | func (bs baseCache[K, V]) Extension() Extension[K, V] { 107 | return newExtension(bs.cache) 108 | } 109 | 110 | // Cache is a structure performs a best-effort bounding of a hash table using eviction algorithm 111 | // to determine which entries to evict when the capacity is exceeded. 112 | type Cache[K comparable, V any] struct { 113 | baseCache[K, V] 114 | } 115 | 116 | func newCache[K comparable, V any](c core.Config[K, V]) Cache[K, V] { 117 | return Cache[K, V]{ 118 | baseCache: newBaseCache(c), 119 | } 120 | } 121 | 122 | // Set associates the value with the key in this cache. 123 | // 124 | // If it returns false, then the key-value pair had too much cost and the Set was dropped. 125 | func (c Cache[K, V]) Set(key K, value V) bool { 126 | return c.cache.Set(key, value) 127 | } 128 | 129 | // SetIfAbsent if the specified key is not already associated with a value associates it with the given value. 130 | // 131 | // If the specified key is not already associated with a value, then it returns false. 132 | // 133 | // Also, it returns false if the key-value pair had too much cost and the SetIfAbsent was dropped. 134 | func (c Cache[K, V]) SetIfAbsent(key K, value V) bool { 135 | return c.cache.SetIfAbsent(key, value) 136 | } 137 | 138 | // CacheWithVariableTTL is a structure performs a best-effort bounding of a hash table using eviction algorithm 139 | // to determine which entries to evict when the capacity is exceeded. 140 | type CacheWithVariableTTL[K comparable, V any] struct { 141 | baseCache[K, V] 142 | } 143 | 144 | func newCacheWithVariableTTL[K comparable, V any](c core.Config[K, V]) CacheWithVariableTTL[K, V] { 145 | return CacheWithVariableTTL[K, V]{ 146 | baseCache: newBaseCache(c), 147 | } 148 | } 149 | 150 | // Set associates the value with the key in this cache and sets the custom ttl for this key-value pair. 151 | // 152 | // If it returns false, then the key-value pair had too much cost and the Set was dropped. 153 | func (c CacheWithVariableTTL[K, V]) Set(key K, value V, ttl time.Duration) bool { 154 | return c.cache.SetWithTTL(key, value, ttl) 155 | } 156 | 157 | // SetIfAbsent if the specified key is not already associated with a value associates it with the given value 158 | // and sets the custom ttl for this key-value pair. 159 | // 160 | // If the specified key is not already associated with a value, then it returns false. 161 | // 162 | // Also, it returns false if the key-value pair had too much cost and the SetIfAbsent was dropped. 163 | func (c CacheWithVariableTTL[K, V]) SetIfAbsent(key K, value V, ttl time.Duration) bool { 164 | return c.cache.SetIfAbsentWithTTL(key, value, ttl) 165 | } 166 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "container/heap" 19 | "fmt" 20 | "math/rand" 21 | "sync" 22 | "testing" 23 | "time" 24 | 25 | "github.com/maypok86/otter/internal/xruntime" 26 | ) 27 | 28 | func getRandomSize(t *testing.T) int { 29 | t.Helper() 30 | 31 | const ( 32 | minSize = 10 33 | maxSize = 1000 34 | ) 35 | 36 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 37 | 38 | return r.Intn(maxSize-minSize) + minSize 39 | } 40 | 41 | func TestCache_Set(t *testing.T) { 42 | size := getRandomSize(t) 43 | var mutex sync.Mutex 44 | m := make(map[DeletionCause]int) 45 | c, err := MustBuilder[int, int](size). 46 | WithTTL(time.Minute). 47 | CollectStats(). 48 | DeletionListener(func(key int, value int, cause DeletionCause) { 49 | mutex.Lock() 50 | m[cause]++ 51 | mutex.Unlock() 52 | }). 53 | Build() 54 | if err != nil { 55 | t.Fatalf("can not create cache: %v", err) 56 | } 57 | 58 | for i := 0; i < size; i++ { 59 | c.Set(i, i) 60 | } 61 | 62 | // update 63 | for i := 0; i < size; i++ { 64 | c.Set(i, i) 65 | } 66 | 67 | parallelism := xruntime.Parallelism() 68 | var wg sync.WaitGroup 69 | for i := 0; i < int(parallelism); i++ { 70 | wg.Add(1) 71 | go func() { 72 | defer wg.Done() 73 | 74 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 75 | for a := 0; a < 10000; a++ { 76 | k := r.Int() % size 77 | val, ok := c.Get(k) 78 | if !ok { 79 | err = fmt.Errorf("expected %d but got nil", k) 80 | break 81 | } 82 | if val != k { 83 | err = fmt.Errorf("expected %d but got %d", k, val) 84 | break 85 | } 86 | } 87 | }() 88 | } 89 | wg.Wait() 90 | 91 | if err != nil { 92 | t.Fatalf("not found key: %v", err) 93 | } 94 | ratio := c.Stats().Ratio() 95 | if ratio != 1.0 { 96 | t.Fatalf("cache hit ratio should be 1.0, but got %v", ratio) 97 | } 98 | 99 | mutex.Lock() 100 | defer mutex.Unlock() 101 | if len(m) != 1 || m[Replaced] != size { 102 | t.Fatalf("cache was supposed to replace %d, but replaced %d entries", size, m[Replaced]) 103 | } 104 | } 105 | 106 | func TestCache_SetIfAbsent(t *testing.T) { 107 | size := getRandomSize(t) 108 | c, err := MustBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build() 109 | if err != nil { 110 | t.Fatalf("can not create cache: %v", err) 111 | } 112 | 113 | for i := 0; i < size; i++ { 114 | if !c.SetIfAbsent(i, i) { 115 | t.Fatalf("set was dropped. key: %d", i) 116 | } 117 | } 118 | 119 | for i := 0; i < size; i++ { 120 | if !c.Has(i) { 121 | t.Fatalf("key should exists: %d", i) 122 | } 123 | } 124 | 125 | for i := 0; i < size; i++ { 126 | if c.SetIfAbsent(i, i) { 127 | t.Fatalf("set wasn't dropped. key: %d", i) 128 | } 129 | } 130 | 131 | c.Clear() 132 | 133 | cc, err := MustBuilder[int, int](size).WithVariableTTL().CollectStats().Build() 134 | if err != nil { 135 | t.Fatalf("can not create cache: %v", err) 136 | } 137 | 138 | for i := 0; i < size; i++ { 139 | if !cc.SetIfAbsent(i, i, time.Hour) { 140 | t.Fatalf("set was dropped. key: %d", i) 141 | } 142 | } 143 | 144 | for i := 0; i < size; i++ { 145 | if !cc.Has(i) { 146 | t.Fatalf("key should exists: %d", i) 147 | } 148 | } 149 | 150 | for i := 0; i < size; i++ { 151 | if cc.SetIfAbsent(i, i, time.Second) { 152 | t.Fatalf("set wasn't dropped. key: %d", i) 153 | } 154 | } 155 | 156 | if hits := cc.Stats().Hits(); hits != int64(size) { 157 | t.Fatalf("hit ratio should be 100%%. Hits: %d", hits) 158 | } 159 | 160 | cc.Close() 161 | } 162 | 163 | func TestCache_SetWithTTL(t *testing.T) { 164 | size := getRandomSize(t) 165 | var mutex sync.Mutex 166 | m := make(map[DeletionCause]int) 167 | c, err := MustBuilder[int, int](size). 168 | CollectStats(). 169 | InitialCapacity(size). 170 | WithTTL(time.Second). 171 | DeletionListener(func(key int, value int, cause DeletionCause) { 172 | mutex.Lock() 173 | m[cause]++ 174 | mutex.Unlock() 175 | }). 176 | Build() 177 | if err != nil { 178 | t.Fatalf("can not create builder: %v", err) 179 | } 180 | 181 | for i := 0; i < size; i++ { 182 | c.Set(i, i) 183 | } 184 | 185 | time.Sleep(3 * time.Second) 186 | for i := 0; i < size; i++ { 187 | if c.Has(i) { 188 | t.Fatalf("key should be expired: %d", i) 189 | } 190 | } 191 | 192 | time.Sleep(10 * time.Millisecond) 193 | 194 | if cacheSize := c.Size(); cacheSize != 0 { 195 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 196 | } 197 | 198 | mutex.Lock() 199 | if e := m[Expired]; len(m) != 1 || e != size { 200 | mutex.Unlock() 201 | t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, e) 202 | } 203 | if c.Stats().EvictedCount() != int64(m[Expired]) { 204 | mutex.Unlock() 205 | t.Fatalf( 206 | "Eviction statistics are not collected for expiration. EvictedCount: %d, expired entries: %d", 207 | c.Stats().EvictedCount(), 208 | m[Expired], 209 | ) 210 | } 211 | mutex.Unlock() 212 | 213 | m = make(map[DeletionCause]int) 214 | cc, err := MustBuilder[int, int](size). 215 | WithVariableTTL(). 216 | CollectStats(). 217 | DeletionListener(func(key int, value int, cause DeletionCause) { 218 | mutex.Lock() 219 | m[cause]++ 220 | mutex.Unlock() 221 | }). 222 | Build() 223 | if err != nil { 224 | t.Fatalf("can not create builder: %v", err) 225 | } 226 | 227 | for i := 0; i < size; i++ { 228 | cc.Set(i, i, 5*time.Second) 229 | } 230 | 231 | time.Sleep(7 * time.Second) 232 | 233 | for i := 0; i < size; i++ { 234 | if cc.Has(i) { 235 | t.Fatalf("key should be expired: %d", i) 236 | } 237 | } 238 | 239 | time.Sleep(10 * time.Millisecond) 240 | 241 | if cacheSize := cc.Size(); cacheSize != 0 { 242 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 243 | } 244 | if misses := cc.Stats().Misses(); misses != int64(size) { 245 | t.Fatalf("c.Stats().Misses() = %d, want = %d", misses, size) 246 | } 247 | mutex.Lock() 248 | defer mutex.Unlock() 249 | if len(m) != 1 || m[Expired] != size { 250 | t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, m[Expired]) 251 | } 252 | if c.Stats().EvictedCount() != int64(m[Expired]) { 253 | mutex.Unlock() 254 | t.Fatalf( 255 | "Eviction statistics are not collected for expiration. EvictedCount: %d, expired entries: %d", 256 | c.Stats().EvictedCount(), 257 | m[Expired], 258 | ) 259 | } 260 | } 261 | 262 | func TestCache_Delete(t *testing.T) { 263 | size := getRandomSize(t) 264 | var mutex sync.Mutex 265 | m := make(map[DeletionCause]int) 266 | c, err := MustBuilder[int, int](size). 267 | InitialCapacity(size). 268 | WithTTL(time.Hour). 269 | DeletionListener(func(key int, value int, cause DeletionCause) { 270 | mutex.Lock() 271 | m[cause]++ 272 | mutex.Unlock() 273 | }). 274 | Build() 275 | if err != nil { 276 | t.Fatalf("can not create builder: %v", err) 277 | } 278 | 279 | for i := 0; i < size; i++ { 280 | c.Set(i, i) 281 | } 282 | 283 | for i := 0; i < size; i++ { 284 | if !c.Has(i) { 285 | t.Fatalf("key should exists: %d", i) 286 | } 287 | } 288 | 289 | for i := 0; i < size; i++ { 290 | c.Delete(i) 291 | } 292 | 293 | for i := 0; i < size; i++ { 294 | if c.Has(i) { 295 | t.Fatalf("key should not exists: %d", i) 296 | } 297 | } 298 | 299 | time.Sleep(time.Second) 300 | 301 | mutex.Lock() 302 | defer mutex.Unlock() 303 | if len(m) != 1 || m[Explicit] != size { 304 | t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size, m[Explicit]) 305 | } 306 | } 307 | 308 | func TestCache_DeleteByFunc(t *testing.T) { 309 | size := getRandomSize(t) 310 | var mutex sync.Mutex 311 | m := make(map[DeletionCause]int) 312 | c, err := MustBuilder[int, int](size). 313 | InitialCapacity(size). 314 | WithTTL(time.Hour). 315 | DeletionListener(func(key int, value int, cause DeletionCause) { 316 | mutex.Lock() 317 | m[cause]++ 318 | mutex.Unlock() 319 | }). 320 | Build() 321 | if err != nil { 322 | t.Fatalf("can not create builder: %v", err) 323 | } 324 | 325 | for i := 0; i < size; i++ { 326 | c.Set(i, i) 327 | } 328 | 329 | c.DeleteByFunc(func(key int, value int) bool { 330 | return key%2 == 1 331 | }) 332 | 333 | c.Range(func(key int, value int) bool { 334 | if key%2 == 1 { 335 | t.Fatalf("key should be odd, but got: %d", key) 336 | } 337 | return true 338 | }) 339 | 340 | time.Sleep(time.Second) 341 | 342 | expected := size / 2 343 | mutex.Lock() 344 | defer mutex.Unlock() 345 | if len(m) != 1 || m[Explicit] != expected { 346 | t.Fatalf("cache was supposed to delete %d, but deleted %d entries", expected, m[Explicit]) 347 | } 348 | } 349 | 350 | func TestCache_Advanced(t *testing.T) { 351 | size := getRandomSize(t) 352 | defaultTTL := time.Hour 353 | c, err := MustBuilder[int, int](size). 354 | WithTTL(defaultTTL). 355 | Build() 356 | if err != nil { 357 | t.Fatalf("can not create builder: %v", err) 358 | } 359 | 360 | for i := 0; i < size; i++ { 361 | c.Set(i, i) 362 | } 363 | 364 | k1 := 4 365 | v1, ok := c.Extension().GetQuietly(k1) 366 | if !ok { 367 | t.Fatalf("not found key %d", k1) 368 | } 369 | 370 | e1, ok := c.Extension().GetEntryQuietly(k1) 371 | if !ok { 372 | t.Fatalf("not found key %d", k1) 373 | } 374 | 375 | e2, ok := c.Extension().GetEntry(k1) 376 | if !ok { 377 | t.Fatalf("not found key %d", k1) 378 | } 379 | 380 | time.Sleep(time.Second) 381 | 382 | isValidEntries := e1.Key() == k1 && 383 | e1.Value() == v1 && 384 | e1.Cost() == 1 && 385 | e1 == e2 && 386 | e1.TTL() < defaultTTL && 387 | !e1.HasExpired() 388 | 389 | if !isValidEntries { 390 | t.Fatalf("found not valid entries. e1: %+v, e2: %+v, v1:%d", e1, e2, v1) 391 | } 392 | 393 | if _, ok := c.Extension().GetQuietly(size); ok { 394 | t.Fatalf("found not valid key: %d", size) 395 | } 396 | if _, ok := c.Extension().GetEntryQuietly(size); ok { 397 | t.Fatalf("found not valid key: %d", size) 398 | } 399 | if _, ok := c.Extension().GetEntry(size); ok { 400 | t.Fatalf("found not valid key: %d", size) 401 | } 402 | } 403 | 404 | func TestCache_Ratio(t *testing.T) { 405 | var mutex sync.Mutex 406 | m := make(map[DeletionCause]int) 407 | c, err := MustBuilder[uint64, uint64](100). 408 | CollectStats(). 409 | DeletionListener(func(key uint64, value uint64, cause DeletionCause) { 410 | mutex.Lock() 411 | m[cause]++ 412 | mutex.Unlock() 413 | }). 414 | Build() 415 | if err != nil { 416 | t.Fatalf("can not create cache: %v", err) 417 | } 418 | 419 | z := rand.NewZipf(rand.New(rand.NewSource(time.Now().UnixNano())), 1.0001, 1, 1000) 420 | 421 | o := newOptimal(100) 422 | for i := 0; i < 10000; i++ { 423 | k := z.Uint64() 424 | 425 | o.Get(k) 426 | if !c.Has(k) { 427 | c.Set(k, k) 428 | } 429 | } 430 | 431 | t.Logf("actual size: %d, capacity: %d", c.Size(), c.Capacity()) 432 | t.Logf("actual: %.2f, optimal: %.2f", c.Stats().Ratio(), o.Ratio()) 433 | 434 | mutex.Lock() 435 | defer mutex.Unlock() 436 | t.Logf("evicted: %d", m[Size]) 437 | if len(m) != 1 || m[Size] <= 0 || m[Size] > 5000 { 438 | t.Fatalf("cache was supposed to evict positive number of entries, but evicted %d entries", m[Size]) 439 | } 440 | } 441 | 442 | type optimal struct { 443 | capacity uint64 444 | hits map[uint64]uint64 445 | access []uint64 446 | } 447 | 448 | func newOptimal(capacity uint64) *optimal { 449 | return &optimal{ 450 | capacity: capacity, 451 | hits: make(map[uint64]uint64), 452 | access: make([]uint64, 0), 453 | } 454 | } 455 | 456 | func (o *optimal) Get(key uint64) { 457 | o.hits[key]++ 458 | o.access = append(o.access, key) 459 | } 460 | 461 | func (o *optimal) Ratio() float64 { 462 | look := make(map[uint64]struct{}, o.capacity) 463 | data := &optimalHeap{} 464 | heap.Init(data) 465 | hits := 0 466 | misses := 0 467 | for _, key := range o.access { 468 | if _, has := look[key]; has { 469 | hits++ 470 | continue 471 | } 472 | if uint64(data.Len()) >= o.capacity { 473 | victim := heap.Pop(data) 474 | oi, ok := victim.(*optimalItem) 475 | if !ok { 476 | panic("can't cast victim") 477 | } 478 | delete(look, oi.key) 479 | } 480 | misses++ 481 | look[key] = struct{}{} 482 | heap.Push(data, &optimalItem{key, o.hits[key]}) 483 | } 484 | if hits == 0 && misses == 0 { 485 | return 0.0 486 | } 487 | return float64(hits) / float64(hits+misses) 488 | } 489 | 490 | type optimalItem struct { 491 | key uint64 492 | hits uint64 493 | } 494 | 495 | type optimalHeap []*optimalItem 496 | 497 | func (h optimalHeap) Len() int { return len(h) } 498 | func (h optimalHeap) Less(i, j int) bool { return h[i].hits < h[j].hits } 499 | func (h optimalHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 500 | 501 | func (h *optimalHeap) Push(x any) { 502 | oi, ok := x.(*optimalItem) 503 | if !ok { 504 | panic("can't cast x") 505 | } 506 | *h = append(*h, oi) 507 | } 508 | 509 | func (h *optimalHeap) Pop() any { 510 | old := *h 511 | n := len(old) 512 | x := old[n-1] 513 | *h = old[0 : n-1] 514 | return x 515 | } 516 | 517 | func Test_GetExpired(t *testing.T) { 518 | c, err := MustBuilder[string, string](1000000). 519 | CollectStats(). 520 | DeletionListener(func(key string, value string, cause DeletionCause) { 521 | fmt.Println(cause) 522 | if cause != Expired { 523 | t.Fatalf("err not expired: %v", cause) 524 | } 525 | }). 526 | WithVariableTTL(). 527 | Build() 528 | if err != nil { 529 | t.Fatal(err) 530 | } 531 | c.Set("test1", "123456", time.Duration(12)*time.Second) 532 | for i := 0; i < 5; i++ { 533 | c.Get("test1") 534 | time.Sleep(3 * time.Second) 535 | } 536 | time.Sleep(1 * time.Second) 537 | } 538 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import "time" 18 | 19 | // Entry is a key-value pair that may include policy metadata for the cached entry. 20 | // 21 | // It is an immutable snapshot of the cached data at the time of this entry's creation, and it will not 22 | // reflect changes afterward. 23 | type Entry[K comparable, V any] struct { 24 | key K 25 | value V 26 | expiration int64 27 | cost uint32 28 | } 29 | 30 | // Key returns the entry's key. 31 | func (e Entry[K, V]) Key() K { 32 | return e.key 33 | } 34 | 35 | // Value returns the entry's value. 36 | func (e Entry[K, V]) Value() V { 37 | return e.value 38 | } 39 | 40 | // Expiration returns the entry's expiration time as a unix time, 41 | // the number of seconds elapsed since January 1, 1970 UTC. 42 | // 43 | // If the cache was not configured with an expiration policy then this value is always 0. 44 | func (e Entry[K, V]) Expiration() int64 { 45 | return e.expiration 46 | } 47 | 48 | // TTL returns the entry's ttl. 49 | // 50 | // If the cache was not configured with an expiration policy then this value is always -1. 51 | // 52 | // If the entry is expired then this value is always 0. 53 | func (e Entry[K, V]) TTL() time.Duration { 54 | expiration := e.Expiration() 55 | if expiration == 0 { 56 | return -1 57 | } 58 | 59 | now := time.Now().Unix() 60 | if expiration <= now { 61 | return 0 62 | } 63 | 64 | return time.Duration(expiration-now) * time.Second 65 | } 66 | 67 | // HasExpired returns true if the entry has expired. 68 | func (e Entry[K, V]) HasExpired() bool { 69 | expiration := e.Expiration() 70 | if expiration == 0 { 71 | return false 72 | } 73 | 74 | return expiration <= time.Now().Unix() 75 | } 76 | 77 | // Cost returns the entry's cost. 78 | // 79 | // If the cache was not configured with a cost then this value is always 1. 80 | func (e Entry[K, V]) Cost() uint32 { 81 | return e.cost 82 | } 83 | -------------------------------------------------------------------------------- /entry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestEntry(t *testing.T) { 23 | k := 2 24 | v := 3 25 | exp := int64(0) 26 | c := uint32(5) 27 | e := Entry[int, int]{ 28 | key: k, 29 | value: v, 30 | expiration: exp, 31 | cost: c, 32 | } 33 | 34 | if e.Key() != k { 35 | t.Fatalf("not valid key. want %d, got %d", k, e.Key()) 36 | } 37 | if e.Value() != v { 38 | t.Fatalf("not valid value. want %d, got %d", v, e.Value()) 39 | } 40 | if e.Cost() != c { 41 | t.Fatalf("not valid cost. want %d, got %d", c, e.Cost()) 42 | } 43 | if e.Expiration() != exp { 44 | t.Fatalf("not valid expiration. want %d, got %d", exp, e.Expiration()) 45 | } 46 | if ttl := e.TTL(); ttl != -1 { 47 | t.Fatalf("not valid ttl. want -1, got %d", ttl) 48 | } 49 | if e.HasExpired() { 50 | t.Fatal("entry should not be expire") 51 | } 52 | 53 | newTTL := int64(10) 54 | e.expiration = time.Now().Unix() + newTTL 55 | if ttl := e.TTL(); ttl <= 0 || ttl > time.Duration(newTTL)*time.Second { 56 | t.Fatalf("ttl should be in the range (0, %d] seconds, but got %d seconds", newTTL, ttl/time.Second) 57 | } 58 | if e.HasExpired() { 59 | t.Fatal("entry should not be expire") 60 | } 61 | 62 | e.expiration -= 2 * newTTL 63 | if ttl := e.TTL(); ttl != 0 { 64 | t.Fatalf("ttl should be 0 seconds, but got %d seconds", ttl/time.Second) 65 | } 66 | if !e.HasExpired() { 67 | t.Fatalf("entry should have expired") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /extension.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "github.com/maypok86/otter/internal/core" 19 | "github.com/maypok86/otter/internal/generated/node" 20 | "github.com/maypok86/otter/internal/unixtime" 21 | ) 22 | 23 | func zeroValue[V any]() V { 24 | var zero V 25 | return zero 26 | } 27 | 28 | // Extension is an access point for inspecting and performing low-level operations based on the cache's runtime 29 | // characteristics. These operations are optional and dependent on how the cache was constructed 30 | // and what abilities the implementation exposes. 31 | type Extension[K comparable, V any] struct { 32 | cache *core.Cache[K, V] 33 | } 34 | 35 | func newExtension[K comparable, V any](cache *core.Cache[K, V]) Extension[K, V] { 36 | return Extension[K, V]{ 37 | cache: cache, 38 | } 39 | } 40 | 41 | func (e Extension[K, V]) createEntry(n node.Node[K, V]) Entry[K, V] { 42 | var expiration int64 43 | if e.cache.WithExpiration() { 44 | expiration = unixtime.StartTime() + int64(n.Expiration()) 45 | } 46 | 47 | return Entry[K, V]{ 48 | key: n.Key(), 49 | value: n.Value(), 50 | expiration: expiration, 51 | cost: n.Cost(), 52 | } 53 | } 54 | 55 | // GetQuietly returns the value associated with the key in this cache. 56 | // 57 | // Unlike Get in the cache, this function does not produce any side effects 58 | // such as updating statistics or the eviction policy. 59 | func (e Extension[K, V]) GetQuietly(key K) (V, bool) { 60 | n, ok := e.cache.GetNodeQuietly(key) 61 | if !ok { 62 | return zeroValue[V](), false 63 | } 64 | 65 | return n.Value(), true 66 | } 67 | 68 | // GetEntry returns the cache entry associated with the key in this cache. 69 | func (e Extension[K, V]) GetEntry(key K) (Entry[K, V], bool) { 70 | n, ok := e.cache.GetNode(key) 71 | if !ok { 72 | return Entry[K, V]{}, false 73 | } 74 | 75 | return e.createEntry(n), true 76 | } 77 | 78 | // GetEntryQuietly returns the cache entry associated with the key in this cache. 79 | // 80 | // Unlike GetEntry, this function does not produce any side effects 81 | // such as updating statistics or the eviction policy. 82 | func (e Extension[K, V]) GetEntryQuietly(key K) (Entry[K, V], bool) { 83 | n, ok := e.cache.GetNodeQuietly(key) 84 | if !ok { 85 | return Entry[K, V]{}, false 86 | } 87 | 88 | return e.createEntry(n), true 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maypok86/otter 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/dolthub/maphash v0.1.0 7 | github.com/gammazero/deque v0.2.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= 3 | github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 4 | github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= 5 | github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 8 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 9 | -------------------------------------------------------------------------------- /internal/core/cache_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/maypok86/otter/internal/generated/node" 8 | ) 9 | 10 | func TestCache_SetWithCost(t *testing.T) { 11 | size := 10 12 | c := NewCache[int, int](Config[int, int]{ 13 | Capacity: size, 14 | CostFunc: func(key int, value int) uint32 { 15 | return uint32(key) 16 | }, 17 | }) 18 | 19 | goodCost := c.policy.MaxAvailableCost() 20 | badCost := goodCost + 1 21 | 22 | added := c.Set(goodCost, 1) 23 | if !added { 24 | t.Fatalf("Set was dropped, even though it shouldn't have been. Max available cost: %d, actual cost: %d", 25 | c.policy.MaxAvailableCost(), 26 | c.costFunc(goodCost, 1), 27 | ) 28 | } 29 | added = c.Set(badCost, 1) 30 | if added { 31 | t.Fatalf("Set wasn't dropped, though it should have been. Max available cost: %d, actual cost: %d", 32 | c.policy.MaxAvailableCost(), 33 | c.costFunc(badCost, 1), 34 | ) 35 | } 36 | } 37 | 38 | func TestCache_Range(t *testing.T) { 39 | size := 10 40 | ttl := time.Hour 41 | c := NewCache[int, int](Config[int, int]{ 42 | Capacity: size, 43 | CostFunc: func(key int, value int) uint32 { 44 | return 1 45 | }, 46 | TTL: &ttl, 47 | }) 48 | 49 | time.Sleep(3 * time.Second) 50 | 51 | nm := node.NewManager[int, int](node.Config{ 52 | WithExpiration: true, 53 | WithCost: true, 54 | }) 55 | 56 | c.Set(1, 1) 57 | c.hashmap.Set(nm.Create(2, 2, 1, 1)) 58 | c.Set(3, 3) 59 | aliveNodes := 2 60 | iters := 0 61 | c.Range(func(key, value int) bool { 62 | if key != value { 63 | t.Fatalf("got unexpected key/value for iteration %d: %d/%d", iters, key, value) 64 | return false 65 | } 66 | iters++ 67 | return true 68 | }) 69 | if iters != aliveNodes { 70 | t.Fatalf("got unexpected number of iterations: %d", iters) 71 | } 72 | } 73 | 74 | func TestCache_Close(t *testing.T) { 75 | size := 10 76 | c := NewCache[int, int](Config[int, int]{ 77 | Capacity: size, 78 | CostFunc: func(key int, value int) uint32 { 79 | return 1 80 | }, 81 | }) 82 | 83 | for i := 0; i < size; i++ { 84 | c.Set(i, i) 85 | } 86 | 87 | if cacheSize := c.Size(); cacheSize != size { 88 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, size) 89 | } 90 | 91 | c.Close() 92 | 93 | time.Sleep(10 * time.Millisecond) 94 | 95 | if cacheSize := c.Size(); cacheSize != 0 { 96 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 97 | } 98 | if !c.isClosed { 99 | t.Fatalf("cache should be closed") 100 | } 101 | 102 | c.Close() 103 | 104 | if cacheSize := c.Size(); cacheSize != 0 { 105 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 106 | } 107 | if !c.isClosed { 108 | t.Fatalf("cache should be closed") 109 | } 110 | } 111 | 112 | func TestCache_Clear(t *testing.T) { 113 | size := 10 114 | c := NewCache[int, int](Config[int, int]{ 115 | Capacity: size, 116 | CostFunc: func(key int, value int) uint32 { 117 | return 1 118 | }, 119 | }) 120 | 121 | for i := 0; i < size; i++ { 122 | c.Set(i, i) 123 | } 124 | 125 | if cacheSize := c.Size(); cacheSize != size { 126 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, size) 127 | } 128 | 129 | c.Clear() 130 | 131 | time.Sleep(10 * time.Millisecond) 132 | 133 | if cacheSize := c.Size(); cacheSize != 0 { 134 | t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 135 | } 136 | if c.isClosed { 137 | t.Fatalf("cache shouldn't be closed") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /internal/core/task.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package core 16 | 17 | import ( 18 | "github.com/maypok86/otter/internal/generated/node" 19 | ) 20 | 21 | // reason represents the reason for writing the item to the cache. 22 | type reason uint8 23 | 24 | const ( 25 | addReason reason = iota + 1 26 | deleteReason 27 | updateReason 28 | clearReason 29 | closeReason 30 | expiredReason 31 | ) 32 | 33 | // task is a set of information to update the cache: 34 | // node, reason for write, difference after node cost change, etc. 35 | type task[K comparable, V any] struct { 36 | n node.Node[K, V] 37 | old node.Node[K, V] 38 | writeReason reason 39 | } 40 | 41 | // newAddTask creates a task to add a node to policies. 42 | func newAddTask[K comparable, V any](n node.Node[K, V]) task[K, V] { 43 | return task[K, V]{ 44 | n: n, 45 | writeReason: addReason, 46 | } 47 | } 48 | 49 | // newDeleteTask creates a task to delete a node from policies. 50 | func newDeleteTask[K comparable, V any](n node.Node[K, V]) task[K, V] { 51 | return task[K, V]{ 52 | n: n, 53 | writeReason: deleteReason, 54 | } 55 | } 56 | 57 | // newExpireTask creates a task to delete a expired node from policies. 58 | func newExpiredTask[K comparable, V any](n node.Node[K, V]) task[K, V] { 59 | return task[K, V]{ 60 | n: n, 61 | writeReason: expiredReason, 62 | } 63 | } 64 | 65 | // newUpdateTask creates a task to update the node in the policies. 66 | func newUpdateTask[K comparable, V any](n, oldNode node.Node[K, V]) task[K, V] { 67 | return task[K, V]{ 68 | n: n, 69 | old: oldNode, 70 | writeReason: updateReason, 71 | } 72 | } 73 | 74 | // newClearTask creates a task to clear policies. 75 | func newClearTask[K comparable, V any]() task[K, V] { 76 | return task[K, V]{ 77 | writeReason: clearReason, 78 | } 79 | } 80 | 81 | // newCloseTask creates a task to clear policies and stop all goroutines. 82 | func newCloseTask[K comparable, V any]() task[K, V] { 83 | return task[K, V]{ 84 | writeReason: closeReason, 85 | } 86 | } 87 | 88 | // node returns the node contained in the task. If node was not specified, it returns nil. 89 | func (t *task[K, V]) node() node.Node[K, V] { 90 | return t.n 91 | } 92 | 93 | // oldNode returns the old node contained in the task. If old node was not specified, it returns nil. 94 | func (t *task[K, V]) oldNode() node.Node[K, V] { 95 | return t.old 96 | } 97 | 98 | // isAdd returns true if this is an add task. 99 | func (t *task[K, V]) isAdd() bool { 100 | return t.writeReason == addReason 101 | } 102 | 103 | // isDelete returns true if this is a delete task. 104 | func (t *task[K, V]) isDelete() bool { 105 | return t.writeReason == deleteReason 106 | } 107 | 108 | // isExpired returns true if this is an expired task. 109 | func (t *task[K, V]) isExpired() bool { 110 | return t.writeReason == expiredReason 111 | } 112 | 113 | // isUpdate returns true if this is an update task. 114 | func (t *task[K, V]) isUpdate() bool { 115 | return t.writeReason == updateReason 116 | } 117 | 118 | // isClear returns true if this is a clear task. 119 | func (t *task[K, V]) isClear() bool { 120 | return t.writeReason == clearReason 121 | } 122 | 123 | // isClose returns true if this is a close task. 124 | func (t *task[K, V]) isClose() bool { 125 | return t.writeReason == closeReason 126 | } 127 | -------------------------------------------------------------------------------- /internal/core/task_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package core 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/maypok86/otter/internal/generated/node" 21 | ) 22 | 23 | func TestTask(t *testing.T) { 24 | nm := node.NewManager[int, int](node.Config{ 25 | WithExpiration: true, 26 | WithCost: true, 27 | }) 28 | n := nm.Create(1, 2, 6, 4) 29 | oldNode := nm.Create(1, 3, 8, 6) 30 | 31 | addTask := newAddTask(n) 32 | if addTask.node() != n || !addTask.isAdd() { 33 | t.Fatalf("not valid add task %+v", addTask) 34 | } 35 | 36 | deleteTask := newDeleteTask(n) 37 | if deleteTask.node() != n || !deleteTask.isDelete() { 38 | t.Fatalf("not valid delete task %+v", deleteTask) 39 | } 40 | 41 | updateTask := newUpdateTask(n, oldNode) 42 | if updateTask.node() != n || !updateTask.isUpdate() || updateTask.oldNode() != oldNode { 43 | t.Fatalf("not valid update task %+v", updateTask) 44 | } 45 | 46 | clearTask := newClearTask[int, int]() 47 | if clearTask.node() != nil || !clearTask.isClear() { 48 | t.Fatalf("not valid clear task %+v", clearTask) 49 | } 50 | 51 | closeTask := newCloseTask[int, int]() 52 | if closeTask.node() != nil || !closeTask.isClose() { 53 | t.Fatalf("not valid close task %+v", closeTask) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/expiry/disabled.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package expiry 16 | 17 | import "github.com/maypok86/otter/internal/generated/node" 18 | 19 | type Disabled[K comparable, V any] struct{} 20 | 21 | func NewDisabled[K comparable, V any]() *Disabled[K, V] { 22 | return &Disabled[K, V]{} 23 | } 24 | 25 | func (d *Disabled[K, V]) Add(n node.Node[K, V]) { 26 | } 27 | 28 | func (d *Disabled[K, V]) Delete(n node.Node[K, V]) { 29 | } 30 | 31 | func (d *Disabled[K, V]) DeleteExpired() { 32 | } 33 | 34 | func (d *Disabled[K, V]) Clear() { 35 | } 36 | -------------------------------------------------------------------------------- /internal/expiry/fixed.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package expiry 16 | 17 | import "github.com/maypok86/otter/internal/generated/node" 18 | 19 | type Fixed[K comparable, V any] struct { 20 | q *queue[K, V] 21 | deleteNode func(node.Node[K, V]) 22 | } 23 | 24 | func NewFixed[K comparable, V any](deleteNode func(node.Node[K, V])) *Fixed[K, V] { 25 | return &Fixed[K, V]{ 26 | q: newQueue[K, V](), 27 | deleteNode: deleteNode, 28 | } 29 | } 30 | 31 | func (f *Fixed[K, V]) Add(n node.Node[K, V]) { 32 | f.q.push(n) 33 | } 34 | 35 | func (f *Fixed[K, V]) Delete(n node.Node[K, V]) { 36 | f.q.delete(n) 37 | } 38 | 39 | func (f *Fixed[K, V]) DeleteExpired() { 40 | for !f.q.isEmpty() && f.q.head.HasExpired() { 41 | f.deleteNode(f.q.pop()) 42 | } 43 | } 44 | 45 | func (f *Fixed[K, V]) Clear() { 46 | f.q.clear() 47 | } 48 | -------------------------------------------------------------------------------- /internal/expiry/queue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package expiry 16 | 17 | import "github.com/maypok86/otter/internal/generated/node" 18 | 19 | type queue[K comparable, V any] struct { 20 | head node.Node[K, V] 21 | tail node.Node[K, V] 22 | len int 23 | } 24 | 25 | func newQueue[K comparable, V any]() *queue[K, V] { 26 | return &queue[K, V]{} 27 | } 28 | 29 | func (q *queue[K, V]) length() int { 30 | return q.len 31 | } 32 | 33 | func (q *queue[K, V]) isEmpty() bool { 34 | return q.length() == 0 35 | } 36 | 37 | func (q *queue[K, V]) push(n node.Node[K, V]) { 38 | if q.isEmpty() { 39 | q.head = n 40 | q.tail = n 41 | } else { 42 | n.SetPrevExp(q.tail) 43 | q.tail.SetNextExp(n) 44 | q.tail = n 45 | } 46 | 47 | q.len++ 48 | } 49 | 50 | func (q *queue[K, V]) pop() node.Node[K, V] { 51 | if q.isEmpty() { 52 | return nil 53 | } 54 | 55 | result := q.head 56 | q.delete(result) 57 | return result 58 | } 59 | 60 | func (q *queue[K, V]) delete(n node.Node[K, V]) { 61 | next := n.NextExp() 62 | prev := n.PrevExp() 63 | 64 | if node.Equals(prev, nil) { 65 | if node.Equals(next, nil) && !node.Equals(q.head, n) { 66 | return 67 | } 68 | 69 | q.head = next 70 | } else { 71 | prev.SetNextExp(next) 72 | n.SetPrevExp(nil) 73 | } 74 | 75 | if node.Equals(next, nil) { 76 | q.tail = prev 77 | } else { 78 | next.SetPrevExp(prev) 79 | n.SetNextExp(nil) 80 | } 81 | 82 | q.len-- 83 | } 84 | 85 | func (q *queue[K, V]) clear() { 86 | for !q.isEmpty() { 87 | q.pop() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/expiry/queue_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // Copyright 2009 The Go Authors. All rights reserved. 3 | // 4 | // Copyright notice. Initial version of the following tests was based on 5 | // the following file from the Go Programming Language core repo: 6 | // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/container/list/list_test.go 7 | // 8 | // Use of this source code is governed by a BSD-style 9 | // license that can be found in the LICENSE file. 10 | // That can be found at https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:LICENSE 11 | 12 | package expiry 13 | 14 | import ( 15 | "strconv" 16 | "testing" 17 | 18 | "github.com/maypok86/otter/internal/generated/node" 19 | ) 20 | 21 | func checkQueueLen[K comparable, V any](t *testing.T, q *queue[K, V], length int) bool { 22 | t.Helper() 23 | 24 | if n := q.length(); n != length { 25 | t.Errorf("q.length() = %d, want %d", n, length) 26 | return false 27 | } 28 | return true 29 | } 30 | 31 | func checkQueuePointers[K comparable, V any](t *testing.T, q *queue[K, V], nodes []node.Node[K, V]) { 32 | t.Helper() 33 | 34 | if !checkQueueLen(t, q, len(nodes)) { 35 | return 36 | } 37 | 38 | // zero length queues must be the zero value 39 | if len(nodes) == 0 { 40 | if !(node.Equals(q.head, nil) && node.Equals(q.tail, nil)) { 41 | t.Errorf("q.head = %p, q.tail = %p; both should be nil", q.head, q.tail) 42 | } 43 | return 44 | } 45 | 46 | // check internal and external prev/next connections 47 | for i, n := range nodes { 48 | var prev node.Node[K, V] 49 | if i > 0 { 50 | prev = nodes[i-1] 51 | } 52 | if p := n.PrevExp(); !node.Equals(p, prev) { 53 | t.Errorf("elt[%d](%p).prev = %p, want %p", i, n, p, prev) 54 | } 55 | 56 | var next node.Node[K, V] 57 | if i < len(nodes)-1 { 58 | next = nodes[i+1] 59 | } 60 | if nn := n.NextExp(); !node.Equals(nn, next) { 61 | t.Errorf("nodes[%d](%p).next = %p, want %p", i, n, nn, next) 62 | } 63 | } 64 | } 65 | 66 | func newNode[K comparable](e K) node.Node[K, K] { 67 | m := node.NewManager[K, K](node.Config{WithCost: true, WithExpiration: true}) 68 | return m.Create(e, e, 0, 0) 69 | } 70 | 71 | func TestQueue(t *testing.T) { 72 | q := newQueue[string, string]() 73 | checkQueuePointers(t, q, []node.Node[string, string]{}) 74 | 75 | // Single element queue 76 | e := newNode("a") 77 | q.push(e) 78 | checkQueuePointers(t, q, []node.Node[string, string]{e}) 79 | q.delete(e) 80 | q.push(e) 81 | checkQueuePointers(t, q, []node.Node[string, string]{e}) 82 | q.delete(e) 83 | checkQueuePointers(t, q, []node.Node[string, string]{}) 84 | 85 | // Bigger queue 86 | e2 := newNode("2") 87 | e1 := newNode("1") 88 | e3 := newNode("3") 89 | e4 := newNode("4") 90 | q.push(e1) 91 | q.push(e2) 92 | q.push(e3) 93 | q.push(e4) 94 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e2, e3, e4}) 95 | 96 | q.delete(e2) 97 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e3, e4}) 98 | 99 | // move from middle 100 | q.delete(e3) 101 | q.push(e3) 102 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) 103 | 104 | q.clear() 105 | q.push(e3) 106 | q.push(e1) 107 | q.push(e4) 108 | checkQueuePointers(t, q, []node.Node[string, string]{e3, e1, e4}) 109 | 110 | // should be no-op 111 | q.delete(e3) 112 | q.push(e3) 113 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) 114 | 115 | // Check standard iteration. 116 | sum := 0 117 | for e := q.head; !node.Equals(e, nil); e = e.NextExp() { 118 | i, err := strconv.Atoi(e.Value()) 119 | if err != nil { 120 | continue 121 | } 122 | sum += i 123 | } 124 | if sum != 8 { 125 | t.Errorf("sum over l = %d, want 8", sum) 126 | } 127 | 128 | // Clear all elements by iterating 129 | var next node.Node[string, string] 130 | for e := q.head; !node.Equals(e, nil); e = next { 131 | next = e.NextExp() 132 | q.delete(e) 133 | } 134 | checkQueuePointers(t, q, []node.Node[string, string]{}) 135 | } 136 | 137 | func TestQueue_Remove(t *testing.T) { 138 | q := newQueue[int, int]() 139 | 140 | e1 := newNode(1) 141 | e2 := newNode(2) 142 | q.push(e1) 143 | q.push(e2) 144 | checkQueuePointers(t, q, []node.Node[int, int]{e1, e2}) 145 | e := q.head 146 | q.delete(e) 147 | checkQueuePointers(t, q, []node.Node[int, int]{e2}) 148 | q.delete(e) 149 | checkQueuePointers(t, q, []node.Node[int, int]{e2}) 150 | } 151 | -------------------------------------------------------------------------------- /internal/expiry/variable.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package expiry 16 | 17 | import ( 18 | "math" 19 | "math/bits" 20 | "time" 21 | 22 | "github.com/maypok86/otter/internal/generated/node" 23 | "github.com/maypok86/otter/internal/unixtime" 24 | "github.com/maypok86/otter/internal/xmath" 25 | ) 26 | 27 | var ( 28 | buckets = []uint32{64, 64, 32, 4, 1} 29 | spans = []uint32{ 30 | xmath.RoundUpPowerOf2(uint32((1 * time.Second).Seconds())), // 1s 31 | xmath.RoundUpPowerOf2(uint32((1 * time.Minute).Seconds())), // 1.07m 32 | xmath.RoundUpPowerOf2(uint32((1 * time.Hour).Seconds())), // 1.13h 33 | xmath.RoundUpPowerOf2(uint32((24 * time.Hour).Seconds())), // 1.52d 34 | buckets[3] * xmath.RoundUpPowerOf2(uint32((24 * time.Hour).Seconds())), // 6.07d 35 | buckets[3] * xmath.RoundUpPowerOf2(uint32((24 * time.Hour).Seconds())), // 6.07d 36 | } 37 | shift = []uint32{ 38 | uint32(bits.TrailingZeros32(spans[0])), 39 | uint32(bits.TrailingZeros32(spans[1])), 40 | uint32(bits.TrailingZeros32(spans[2])), 41 | uint32(bits.TrailingZeros32(spans[3])), 42 | uint32(bits.TrailingZeros32(spans[4])), 43 | } 44 | ) 45 | 46 | type Variable[K comparable, V any] struct { 47 | wheel [][]node.Node[K, V] 48 | time uint32 49 | deleteNode func(node.Node[K, V]) 50 | } 51 | 52 | func NewVariable[K comparable, V any](nodeManager *node.Manager[K, V], deleteNode func(node.Node[K, V])) *Variable[K, V] { 53 | wheel := make([][]node.Node[K, V], len(buckets)) 54 | for i := 0; i < len(wheel); i++ { 55 | wheel[i] = make([]node.Node[K, V], buckets[i]) 56 | for j := 0; j < len(wheel[i]); j++ { 57 | var k K 58 | var v V 59 | fn := nodeManager.Create(k, v, math.MaxUint32, 1) 60 | fn.SetPrevExp(fn) 61 | fn.SetNextExp(fn) 62 | wheel[i][j] = fn 63 | } 64 | } 65 | return &Variable[K, V]{ 66 | wheel: wheel, 67 | deleteNode: deleteNode, 68 | } 69 | } 70 | 71 | // findBucket determines the bucket that the timer event should be added to. 72 | func (v *Variable[K, V]) findBucket(expiration uint32) node.Node[K, V] { 73 | duration := expiration - v.time 74 | length := len(v.wheel) - 1 75 | for i := 0; i < length; i++ { 76 | if duration < spans[i+1] { 77 | ticks := expiration >> shift[i] 78 | index := ticks & (buckets[i] - 1) 79 | return v.wheel[i][index] 80 | } 81 | } 82 | return v.wheel[length][0] 83 | } 84 | 85 | // Add schedules a timer event for the node. 86 | func (v *Variable[K, V]) Add(n node.Node[K, V]) { 87 | root := v.findBucket(n.Expiration()) 88 | link(root, n) 89 | } 90 | 91 | // Delete removes a timer event for this entry if present. 92 | func (v *Variable[K, V]) Delete(n node.Node[K, V]) { 93 | unlink(n) 94 | n.SetNextExp(nil) 95 | n.SetPrevExp(nil) 96 | } 97 | 98 | func (v *Variable[K, V]) DeleteExpired() { 99 | currentTime := unixtime.Now() 100 | prevTime := v.time 101 | v.time = currentTime 102 | 103 | for i := 0; i < len(shift); i++ { 104 | previousTicks := prevTime >> shift[i] 105 | currentTicks := currentTime >> shift[i] 106 | delta := currentTicks - previousTicks 107 | if delta == 0 { 108 | break 109 | } 110 | 111 | v.deleteExpiredFromBucket(i, previousTicks, delta) 112 | } 113 | } 114 | 115 | func (v *Variable[K, V]) deleteExpiredFromBucket(index int, prevTicks, delta uint32) { 116 | mask := buckets[index] - 1 117 | steps := buckets[index] 118 | if delta < steps { 119 | steps = delta 120 | } 121 | start := prevTicks & mask 122 | end := start + steps 123 | timerWheel := v.wheel[index] 124 | for i := start; i < end; i++ { 125 | root := timerWheel[i&mask] 126 | n := root.NextExp() 127 | root.SetPrevExp(root) 128 | root.SetNextExp(root) 129 | 130 | for !node.Equals(n, root) { 131 | next := n.NextExp() 132 | n.SetPrevExp(nil) 133 | n.SetNextExp(nil) 134 | 135 | if n.Expiration() <= v.time { 136 | v.deleteNode(n) 137 | } else { 138 | v.Add(n) 139 | } 140 | 141 | n = next 142 | } 143 | } 144 | } 145 | 146 | func (v *Variable[K, V]) Clear() { 147 | for i := 0; i < len(v.wheel); i++ { 148 | for j := 0; j < len(v.wheel[i]); j++ { 149 | root := v.wheel[i][j] 150 | n := root.NextExp() 151 | // NOTE(maypok86): Maybe we should use the same approach as in DeleteExpired? 152 | 153 | for !node.Equals(n, root) { 154 | next := n.NextExp() 155 | v.Delete(n) 156 | 157 | n = next 158 | } 159 | } 160 | } 161 | v.time = unixtime.Now() 162 | } 163 | 164 | // link adds the entry at the tail of the bucket's list. 165 | func link[K comparable, V any](root, n node.Node[K, V]) { 166 | n.SetPrevExp(root.PrevExp()) 167 | n.SetNextExp(root) 168 | 169 | root.PrevExp().SetNextExp(n) 170 | root.SetPrevExp(n) 171 | } 172 | 173 | // unlink removes the entry from its bucket, if scheduled. 174 | func unlink[K comparable, V any](n node.Node[K, V]) { 175 | next := n.NextExp() 176 | if !node.Equals(next, nil) { 177 | prev := n.PrevExp() 178 | next.SetPrevExp(prev) 179 | prev.SetNextExp(next) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /internal/expiry/variable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package expiry 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/maypok86/otter/internal/generated/node" 21 | "github.com/maypok86/otter/internal/unixtime" 22 | ) 23 | 24 | func contains[K comparable, V any](root, f node.Node[K, V]) bool { 25 | n := root.NextExp() 26 | for !node.Equals(n, root) { 27 | if node.Equals(n, f) { 28 | return true 29 | } 30 | 31 | n = n.NextExp() 32 | } 33 | return false 34 | } 35 | 36 | func match[K comparable, V any](t *testing.T, nodes []node.Node[K, V], keys []K) { 37 | t.Helper() 38 | 39 | if len(nodes) != len(keys) { 40 | t.Fatalf("Not equals lengths of nodes (%d) and keys (%d)", len(nodes), len(keys)) 41 | } 42 | 43 | for i, k := range keys { 44 | if k != nodes[i].Key() { 45 | t.Fatalf("Not valid entry found: %+v", nodes[i]) 46 | } 47 | } 48 | } 49 | 50 | func TestVariable_Add(t *testing.T) { 51 | nm := node.NewManager[string, string](node.Config{ 52 | WithExpiration: true, 53 | }) 54 | nodes := []node.Node[string, string]{ 55 | nm.Create("k1", "", 1, 1), 56 | nm.Create("k2", "", 69, 1), 57 | nm.Create("k3", "", 4399, 1), 58 | } 59 | v := NewVariable[string, string](nm, func(n node.Node[string, string]) { 60 | }) 61 | 62 | for _, n := range nodes { 63 | v.Add(n) 64 | } 65 | 66 | var found bool 67 | for _, root := range v.wheel[0] { 68 | if contains(root, nodes[0]) { 69 | found = true 70 | } 71 | } 72 | if !found { 73 | t.Fatalf("Not found node %+v in timer wheel", nodes[0]) 74 | } 75 | 76 | found = false 77 | for _, root := range v.wheel[1] { 78 | if contains(root, nodes[1]) { 79 | found = true 80 | } 81 | } 82 | if !found { 83 | t.Fatalf("Not found node %+v in timer wheel", nodes[1]) 84 | } 85 | 86 | found = false 87 | for _, root := range v.wheel[2] { 88 | if contains(root, nodes[2]) { 89 | found = true 90 | } 91 | } 92 | if !found { 93 | t.Fatalf("Not found node %+v in timer wheel", nodes[2]) 94 | } 95 | } 96 | 97 | func TestVariable_DeleteExpired(t *testing.T) { 98 | nm := node.NewManager[string, string](node.Config{ 99 | WithExpiration: true, 100 | }) 101 | nodes := []node.Node[string, string]{ 102 | nm.Create("k1", "", 1, 1), 103 | nm.Create("k2", "", 10, 1), 104 | nm.Create("k3", "", 30, 1), 105 | nm.Create("k4", "", 120, 1), 106 | nm.Create("k5", "", 6500, 1), 107 | nm.Create("k6", "", 142000, 1), 108 | nm.Create("k7", "", 1420000, 1), 109 | } 110 | var expired []node.Node[string, string] 111 | v := NewVariable[string, string](nm, func(n node.Node[string, string]) { 112 | expired = append(expired, n) 113 | }) 114 | 115 | for _, n := range nodes { 116 | v.Add(n) 117 | } 118 | 119 | var keys []string 120 | unixtime.SetNow(64) 121 | v.DeleteExpired() 122 | keys = append(keys, "k1", "k2", "k3") 123 | match(t, expired, keys) 124 | 125 | unixtime.SetNow(200) 126 | v.DeleteExpired() 127 | keys = append(keys, "k4") 128 | match(t, expired, keys) 129 | 130 | unixtime.SetNow(12000) 131 | v.DeleteExpired() 132 | keys = append(keys, "k5") 133 | match(t, expired, keys) 134 | 135 | unixtime.SetNow(350000) 136 | v.DeleteExpired() 137 | keys = append(keys, "k6") 138 | match(t, expired, keys) 139 | 140 | unixtime.SetNow(1520000) 141 | v.DeleteExpired() 142 | keys = append(keys, "k7") 143 | match(t, expired, keys) 144 | } 145 | -------------------------------------------------------------------------------- /internal/generated/node/b.go: -------------------------------------------------------------------------------- 1 | // Code generated by NodeGenerator. DO NOT EDIT. 2 | 3 | // Package node is a generated generator package. 4 | package node 5 | 6 | import ( 7 | "sync/atomic" 8 | "unsafe" 9 | ) 10 | 11 | // B is a cache entry that provide the following features: 12 | // 13 | // 1. Base 14 | type B[K comparable, V any] struct { 15 | key K 16 | value V 17 | prev *B[K, V] 18 | next *B[K, V] 19 | state uint32 20 | frequency uint8 21 | queueType uint8 22 | } 23 | 24 | // NewB creates a new B. 25 | func NewB[K comparable, V any](key K, value V, expiration, cost uint32) Node[K, V] { 26 | return &B[K, V]{ 27 | key: key, 28 | value: value, 29 | state: aliveState, 30 | } 31 | } 32 | 33 | // CastPointerToB casts a pointer to B. 34 | func CastPointerToB[K comparable, V any](ptr unsafe.Pointer) Node[K, V] { 35 | return (*B[K, V])(ptr) 36 | } 37 | 38 | func (n *B[K, V]) Key() K { 39 | return n.key 40 | } 41 | 42 | func (n *B[K, V]) Value() V { 43 | return n.value 44 | } 45 | 46 | func (n *B[K, V]) AsPointer() unsafe.Pointer { 47 | return unsafe.Pointer(n) 48 | } 49 | 50 | func (n *B[K, V]) Prev() Node[K, V] { 51 | return n.prev 52 | } 53 | 54 | func (n *B[K, V]) SetPrev(v Node[K, V]) { 55 | if v == nil { 56 | n.prev = nil 57 | return 58 | } 59 | n.prev = (*B[K, V])(v.AsPointer()) 60 | } 61 | 62 | func (n *B[K, V]) Next() Node[K, V] { 63 | return n.next 64 | } 65 | 66 | func (n *B[K, V]) SetNext(v Node[K, V]) { 67 | if v == nil { 68 | n.next = nil 69 | return 70 | } 71 | n.next = (*B[K, V])(v.AsPointer()) 72 | } 73 | 74 | func (n *B[K, V]) PrevExp() Node[K, V] { 75 | panic("not implemented") 76 | } 77 | 78 | func (n *B[K, V]) SetPrevExp(v Node[K, V]) { 79 | panic("not implemented") 80 | } 81 | 82 | func (n *B[K, V]) NextExp() Node[K, V] { 83 | panic("not implemented") 84 | } 85 | 86 | func (n *B[K, V]) SetNextExp(v Node[K, V]) { 87 | panic("not implemented") 88 | } 89 | 90 | func (n *B[K, V]) HasExpired() bool { 91 | return false 92 | } 93 | 94 | func (n *B[K, V]) Expiration() uint32 { 95 | panic("not implemented") 96 | } 97 | 98 | func (n *B[K, V]) Cost() uint32 { 99 | return 1 100 | } 101 | 102 | func (n *B[K, V]) IsAlive() bool { 103 | return atomic.LoadUint32(&n.state) == aliveState 104 | } 105 | 106 | func (n *B[K, V]) Die() { 107 | atomic.StoreUint32(&n.state, deadState) 108 | } 109 | 110 | func (n *B[K, V]) Frequency() uint8 { 111 | return n.frequency 112 | } 113 | 114 | func (n *B[K, V]) IncrementFrequency() { 115 | n.frequency = minUint8(n.frequency+1, maxFrequency) 116 | } 117 | 118 | func (n *B[K, V]) DecrementFrequency() { 119 | n.frequency-- 120 | } 121 | 122 | func (n *B[K, V]) ResetFrequency() { 123 | n.frequency = 0 124 | } 125 | 126 | func (n *B[K, V]) MarkSmall() { 127 | n.queueType = smallQueueType 128 | } 129 | 130 | func (n *B[K, V]) IsSmall() bool { 131 | return n.queueType == smallQueueType 132 | } 133 | 134 | func (n *B[K, V]) MarkMain() { 135 | n.queueType = mainQueueType 136 | } 137 | 138 | func (n *B[K, V]) IsMain() bool { 139 | return n.queueType == mainQueueType 140 | } 141 | 142 | func (n *B[K, V]) Unmark() { 143 | n.queueType = unknownQueueType 144 | } 145 | -------------------------------------------------------------------------------- /internal/generated/node/bc.go: -------------------------------------------------------------------------------- 1 | // Code generated by NodeGenerator. DO NOT EDIT. 2 | 3 | // Package node is a generated generator package. 4 | package node 5 | 6 | import ( 7 | "sync/atomic" 8 | "unsafe" 9 | ) 10 | 11 | // BC is a cache entry that provide the following features: 12 | // 13 | // 1. Base 14 | // 15 | // 2. Cost 16 | type BC[K comparable, V any] struct { 17 | key K 18 | value V 19 | prev *BC[K, V] 20 | next *BC[K, V] 21 | cost uint32 22 | state uint32 23 | frequency uint8 24 | queueType uint8 25 | } 26 | 27 | // NewBC creates a new BC. 28 | func NewBC[K comparable, V any](key K, value V, expiration, cost uint32) Node[K, V] { 29 | return &BC[K, V]{ 30 | key: key, 31 | value: value, 32 | cost: cost, 33 | state: aliveState, 34 | } 35 | } 36 | 37 | // CastPointerToBC casts a pointer to BC. 38 | func CastPointerToBC[K comparable, V any](ptr unsafe.Pointer) Node[K, V] { 39 | return (*BC[K, V])(ptr) 40 | } 41 | 42 | func (n *BC[K, V]) Key() K { 43 | return n.key 44 | } 45 | 46 | func (n *BC[K, V]) Value() V { 47 | return n.value 48 | } 49 | 50 | func (n *BC[K, V]) AsPointer() unsafe.Pointer { 51 | return unsafe.Pointer(n) 52 | } 53 | 54 | func (n *BC[K, V]) Prev() Node[K, V] { 55 | return n.prev 56 | } 57 | 58 | func (n *BC[K, V]) SetPrev(v Node[K, V]) { 59 | if v == nil { 60 | n.prev = nil 61 | return 62 | } 63 | n.prev = (*BC[K, V])(v.AsPointer()) 64 | } 65 | 66 | func (n *BC[K, V]) Next() Node[K, V] { 67 | return n.next 68 | } 69 | 70 | func (n *BC[K, V]) SetNext(v Node[K, V]) { 71 | if v == nil { 72 | n.next = nil 73 | return 74 | } 75 | n.next = (*BC[K, V])(v.AsPointer()) 76 | } 77 | 78 | func (n *BC[K, V]) PrevExp() Node[K, V] { 79 | panic("not implemented") 80 | } 81 | 82 | func (n *BC[K, V]) SetPrevExp(v Node[K, V]) { 83 | panic("not implemented") 84 | } 85 | 86 | func (n *BC[K, V]) NextExp() Node[K, V] { 87 | panic("not implemented") 88 | } 89 | 90 | func (n *BC[K, V]) SetNextExp(v Node[K, V]) { 91 | panic("not implemented") 92 | } 93 | 94 | func (n *BC[K, V]) HasExpired() bool { 95 | return false 96 | } 97 | 98 | func (n *BC[K, V]) Expiration() uint32 { 99 | panic("not implemented") 100 | } 101 | 102 | func (n *BC[K, V]) Cost() uint32 { 103 | return n.cost 104 | } 105 | 106 | func (n *BC[K, V]) IsAlive() bool { 107 | return atomic.LoadUint32(&n.state) == aliveState 108 | } 109 | 110 | func (n *BC[K, V]) Die() { 111 | atomic.StoreUint32(&n.state, deadState) 112 | } 113 | 114 | func (n *BC[K, V]) Frequency() uint8 { 115 | return n.frequency 116 | } 117 | 118 | func (n *BC[K, V]) IncrementFrequency() { 119 | n.frequency = minUint8(n.frequency+1, maxFrequency) 120 | } 121 | 122 | func (n *BC[K, V]) DecrementFrequency() { 123 | n.frequency-- 124 | } 125 | 126 | func (n *BC[K, V]) ResetFrequency() { 127 | n.frequency = 0 128 | } 129 | 130 | func (n *BC[K, V]) MarkSmall() { 131 | n.queueType = smallQueueType 132 | } 133 | 134 | func (n *BC[K, V]) IsSmall() bool { 135 | return n.queueType == smallQueueType 136 | } 137 | 138 | func (n *BC[K, V]) MarkMain() { 139 | n.queueType = mainQueueType 140 | } 141 | 142 | func (n *BC[K, V]) IsMain() bool { 143 | return n.queueType == mainQueueType 144 | } 145 | 146 | func (n *BC[K, V]) Unmark() { 147 | n.queueType = unknownQueueType 148 | } 149 | -------------------------------------------------------------------------------- /internal/generated/node/be.go: -------------------------------------------------------------------------------- 1 | // Code generated by NodeGenerator. DO NOT EDIT. 2 | 3 | // Package node is a generated generator package. 4 | package node 5 | 6 | import ( 7 | "sync/atomic" 8 | "unsafe" 9 | 10 | "github.com/maypok86/otter/internal/unixtime" 11 | ) 12 | 13 | // BE is a cache entry that provide the following features: 14 | // 15 | // 1. Base 16 | // 17 | // 2. Expiration 18 | type BE[K comparable, V any] struct { 19 | key K 20 | value V 21 | prev *BE[K, V] 22 | next *BE[K, V] 23 | prevExp *BE[K, V] 24 | nextExp *BE[K, V] 25 | expiration uint32 26 | state uint32 27 | frequency uint8 28 | queueType uint8 29 | } 30 | 31 | // NewBE creates a new BE. 32 | func NewBE[K comparable, V any](key K, value V, expiration, cost uint32) Node[K, V] { 33 | return &BE[K, V]{ 34 | key: key, 35 | value: value, 36 | expiration: expiration, 37 | state: aliveState, 38 | } 39 | } 40 | 41 | // CastPointerToBE casts a pointer to BE. 42 | func CastPointerToBE[K comparable, V any](ptr unsafe.Pointer) Node[K, V] { 43 | return (*BE[K, V])(ptr) 44 | } 45 | 46 | func (n *BE[K, V]) Key() K { 47 | return n.key 48 | } 49 | 50 | func (n *BE[K, V]) Value() V { 51 | return n.value 52 | } 53 | 54 | func (n *BE[K, V]) AsPointer() unsafe.Pointer { 55 | return unsafe.Pointer(n) 56 | } 57 | 58 | func (n *BE[K, V]) Prev() Node[K, V] { 59 | return n.prev 60 | } 61 | 62 | func (n *BE[K, V]) SetPrev(v Node[K, V]) { 63 | if v == nil { 64 | n.prev = nil 65 | return 66 | } 67 | n.prev = (*BE[K, V])(v.AsPointer()) 68 | } 69 | 70 | func (n *BE[K, V]) Next() Node[K, V] { 71 | return n.next 72 | } 73 | 74 | func (n *BE[K, V]) SetNext(v Node[K, V]) { 75 | if v == nil { 76 | n.next = nil 77 | return 78 | } 79 | n.next = (*BE[K, V])(v.AsPointer()) 80 | } 81 | 82 | func (n *BE[K, V]) PrevExp() Node[K, V] { 83 | return n.prevExp 84 | } 85 | 86 | func (n *BE[K, V]) SetPrevExp(v Node[K, V]) { 87 | if v == nil { 88 | n.prevExp = nil 89 | return 90 | } 91 | n.prevExp = (*BE[K, V])(v.AsPointer()) 92 | } 93 | 94 | func (n *BE[K, V]) NextExp() Node[K, V] { 95 | return n.nextExp 96 | } 97 | 98 | func (n *BE[K, V]) SetNextExp(v Node[K, V]) { 99 | if v == nil { 100 | n.nextExp = nil 101 | return 102 | } 103 | n.nextExp = (*BE[K, V])(v.AsPointer()) 104 | } 105 | 106 | func (n *BE[K, V]) HasExpired() bool { 107 | return n.expiration <= unixtime.Now() 108 | } 109 | 110 | func (n *BE[K, V]) Expiration() uint32 { 111 | return n.expiration 112 | } 113 | 114 | func (n *BE[K, V]) Cost() uint32 { 115 | return 1 116 | } 117 | 118 | func (n *BE[K, V]) IsAlive() bool { 119 | return atomic.LoadUint32(&n.state) == aliveState 120 | } 121 | 122 | func (n *BE[K, V]) Die() { 123 | atomic.StoreUint32(&n.state, deadState) 124 | } 125 | 126 | func (n *BE[K, V]) Frequency() uint8 { 127 | return n.frequency 128 | } 129 | 130 | func (n *BE[K, V]) IncrementFrequency() { 131 | n.frequency = minUint8(n.frequency+1, maxFrequency) 132 | } 133 | 134 | func (n *BE[K, V]) DecrementFrequency() { 135 | n.frequency-- 136 | } 137 | 138 | func (n *BE[K, V]) ResetFrequency() { 139 | n.frequency = 0 140 | } 141 | 142 | func (n *BE[K, V]) MarkSmall() { 143 | n.queueType = smallQueueType 144 | } 145 | 146 | func (n *BE[K, V]) IsSmall() bool { 147 | return n.queueType == smallQueueType 148 | } 149 | 150 | func (n *BE[K, V]) MarkMain() { 151 | n.queueType = mainQueueType 152 | } 153 | 154 | func (n *BE[K, V]) IsMain() bool { 155 | return n.queueType == mainQueueType 156 | } 157 | 158 | func (n *BE[K, V]) Unmark() { 159 | n.queueType = unknownQueueType 160 | } 161 | -------------------------------------------------------------------------------- /internal/generated/node/bec.go: -------------------------------------------------------------------------------- 1 | // Code generated by NodeGenerator. DO NOT EDIT. 2 | 3 | // Package node is a generated generator package. 4 | package node 5 | 6 | import ( 7 | "sync/atomic" 8 | "unsafe" 9 | 10 | "github.com/maypok86/otter/internal/unixtime" 11 | ) 12 | 13 | // BEC is a cache entry that provide the following features: 14 | // 15 | // 1. Base 16 | // 17 | // 2. Expiration 18 | // 19 | // 3. Cost 20 | type BEC[K comparable, V any] struct { 21 | key K 22 | value V 23 | prev *BEC[K, V] 24 | next *BEC[K, V] 25 | prevExp *BEC[K, V] 26 | nextExp *BEC[K, V] 27 | expiration uint32 28 | cost uint32 29 | state uint32 30 | frequency uint8 31 | queueType uint8 32 | } 33 | 34 | // NewBEC creates a new BEC. 35 | func NewBEC[K comparable, V any](key K, value V, expiration, cost uint32) Node[K, V] { 36 | return &BEC[K, V]{ 37 | key: key, 38 | value: value, 39 | expiration: expiration, 40 | cost: cost, 41 | state: aliveState, 42 | } 43 | } 44 | 45 | // CastPointerToBEC casts a pointer to BEC. 46 | func CastPointerToBEC[K comparable, V any](ptr unsafe.Pointer) Node[K, V] { 47 | return (*BEC[K, V])(ptr) 48 | } 49 | 50 | func (n *BEC[K, V]) Key() K { 51 | return n.key 52 | } 53 | 54 | func (n *BEC[K, V]) Value() V { 55 | return n.value 56 | } 57 | 58 | func (n *BEC[K, V]) AsPointer() unsafe.Pointer { 59 | return unsafe.Pointer(n) 60 | } 61 | 62 | func (n *BEC[K, V]) Prev() Node[K, V] { 63 | return n.prev 64 | } 65 | 66 | func (n *BEC[K, V]) SetPrev(v Node[K, V]) { 67 | if v == nil { 68 | n.prev = nil 69 | return 70 | } 71 | n.prev = (*BEC[K, V])(v.AsPointer()) 72 | } 73 | 74 | func (n *BEC[K, V]) Next() Node[K, V] { 75 | return n.next 76 | } 77 | 78 | func (n *BEC[K, V]) SetNext(v Node[K, V]) { 79 | if v == nil { 80 | n.next = nil 81 | return 82 | } 83 | n.next = (*BEC[K, V])(v.AsPointer()) 84 | } 85 | 86 | func (n *BEC[K, V]) PrevExp() Node[K, V] { 87 | return n.prevExp 88 | } 89 | 90 | func (n *BEC[K, V]) SetPrevExp(v Node[K, V]) { 91 | if v == nil { 92 | n.prevExp = nil 93 | return 94 | } 95 | n.prevExp = (*BEC[K, V])(v.AsPointer()) 96 | } 97 | 98 | func (n *BEC[K, V]) NextExp() Node[K, V] { 99 | return n.nextExp 100 | } 101 | 102 | func (n *BEC[K, V]) SetNextExp(v Node[K, V]) { 103 | if v == nil { 104 | n.nextExp = nil 105 | return 106 | } 107 | n.nextExp = (*BEC[K, V])(v.AsPointer()) 108 | } 109 | 110 | func (n *BEC[K, V]) HasExpired() bool { 111 | return n.expiration <= unixtime.Now() 112 | } 113 | 114 | func (n *BEC[K, V]) Expiration() uint32 { 115 | return n.expiration 116 | } 117 | 118 | func (n *BEC[K, V]) Cost() uint32 { 119 | return n.cost 120 | } 121 | 122 | func (n *BEC[K, V]) IsAlive() bool { 123 | return atomic.LoadUint32(&n.state) == aliveState 124 | } 125 | 126 | func (n *BEC[K, V]) Die() { 127 | atomic.StoreUint32(&n.state, deadState) 128 | } 129 | 130 | func (n *BEC[K, V]) Frequency() uint8 { 131 | return n.frequency 132 | } 133 | 134 | func (n *BEC[K, V]) IncrementFrequency() { 135 | n.frequency = minUint8(n.frequency+1, maxFrequency) 136 | } 137 | 138 | func (n *BEC[K, V]) DecrementFrequency() { 139 | n.frequency-- 140 | } 141 | 142 | func (n *BEC[K, V]) ResetFrequency() { 143 | n.frequency = 0 144 | } 145 | 146 | func (n *BEC[K, V]) MarkSmall() { 147 | n.queueType = smallQueueType 148 | } 149 | 150 | func (n *BEC[K, V]) IsSmall() bool { 151 | return n.queueType == smallQueueType 152 | } 153 | 154 | func (n *BEC[K, V]) MarkMain() { 155 | n.queueType = mainQueueType 156 | } 157 | 158 | func (n *BEC[K, V]) IsMain() bool { 159 | return n.queueType == mainQueueType 160 | } 161 | 162 | func (n *BEC[K, V]) Unmark() { 163 | n.queueType = unknownQueueType 164 | } 165 | -------------------------------------------------------------------------------- /internal/generated/node/manager.go: -------------------------------------------------------------------------------- 1 | // Code generated by NodeGenerator. DO NOT EDIT. 2 | 3 | // Package node is a generated generator package. 4 | package node 5 | 6 | import ( 7 | "strings" 8 | "unsafe" 9 | ) 10 | 11 | const ( 12 | unknownQueueType uint8 = iota 13 | smallQueueType 14 | mainQueueType 15 | 16 | maxFrequency uint8 = 3 17 | ) 18 | 19 | const ( 20 | aliveState uint32 = iota 21 | deadState 22 | ) 23 | 24 | // Node is a cache entry. 25 | type Node[K comparable, V any] interface { 26 | // Key returns the key. 27 | Key() K 28 | // Value returns the value. 29 | Value() V 30 | // AsPointer returns the node as a pointer. 31 | AsPointer() unsafe.Pointer 32 | // Prev returns the previous node in the eviction policy. 33 | Prev() Node[K, V] 34 | // SetPrev sets the previous node in the eviction policy. 35 | SetPrev(v Node[K, V]) 36 | // Next returns the next node in the eviction policy. 37 | Next() Node[K, V] 38 | // SetNext sets the next node in the eviction policy. 39 | SetNext(v Node[K, V]) 40 | // PrevExp returns the previous node in the expiration policy. 41 | PrevExp() Node[K, V] 42 | // SetPrevExp sets the previous node in the expiration policy. 43 | SetPrevExp(v Node[K, V]) 44 | // NextExp returns the next node in the expiration policy. 45 | NextExp() Node[K, V] 46 | // SetNextExp sets the next node in the expiration policy. 47 | SetNextExp(v Node[K, V]) 48 | // HasExpired returns true if node has expired. 49 | HasExpired() bool 50 | // Expiration returns the expiration time. 51 | Expiration() uint32 52 | // Cost returns the cost of the node. 53 | Cost() uint32 54 | // IsAlive returns true if the entry is available in the hash-table. 55 | IsAlive() bool 56 | // Die sets the node to the dead state. 57 | Die() 58 | // Frequency returns the frequency of the node. 59 | Frequency() uint8 60 | // IncrementFrequency increments the frequency of the node. 61 | IncrementFrequency() 62 | // DecrementFrequency decrements the frequency of the node. 63 | DecrementFrequency() 64 | // ResetFrequency resets the frequency. 65 | ResetFrequency() 66 | // MarkSmall sets the status to the small queue. 67 | MarkSmall() 68 | // IsSmall returns true if node is in the small queue. 69 | IsSmall() bool 70 | // MarkMain sets the status to the main queue. 71 | MarkMain() 72 | // IsMain returns true if node is in the main queue. 73 | IsMain() bool 74 | // Unmark sets the status to unknown. 75 | Unmark() 76 | } 77 | 78 | func Equals[K comparable, V any](a, b Node[K, V]) bool { 79 | if a == nil { 80 | return b == nil || b.AsPointer() == nil 81 | } 82 | if b == nil { 83 | return a.AsPointer() == nil 84 | } 85 | return a.AsPointer() == b.AsPointer() 86 | } 87 | 88 | type Config struct { 89 | WithExpiration bool 90 | WithCost bool 91 | } 92 | 93 | type Manager[K comparable, V any] struct { 94 | create func(key K, value V, expiration, cost uint32) Node[K, V] 95 | fromPointer func(ptr unsafe.Pointer) Node[K, V] 96 | } 97 | 98 | func NewManager[K comparable, V any](c Config) *Manager[K, V] { 99 | var sb strings.Builder 100 | sb.WriteString("b") 101 | if c.WithExpiration { 102 | sb.WriteString("e") 103 | } 104 | if c.WithCost { 105 | sb.WriteString("c") 106 | } 107 | nodeType := sb.String() 108 | m := &Manager[K, V]{} 109 | 110 | switch nodeType { 111 | case "bec": 112 | m.create = NewBEC[K, V] 113 | m.fromPointer = CastPointerToBEC[K, V] 114 | case "bc": 115 | m.create = NewBC[K, V] 116 | m.fromPointer = CastPointerToBC[K, V] 117 | case "be": 118 | m.create = NewBE[K, V] 119 | m.fromPointer = CastPointerToBE[K, V] 120 | case "b": 121 | m.create = NewB[K, V] 122 | m.fromPointer = CastPointerToB[K, V] 123 | default: 124 | panic("not valid nodeType") 125 | } 126 | return m 127 | } 128 | 129 | func (m *Manager[K, V]) Create(key K, value V, expiration, cost uint32) Node[K, V] { 130 | return m.create(key, value, expiration, cost) 131 | } 132 | 133 | func (m *Manager[K, V]) FromPointer(ptr unsafe.Pointer) Node[K, V] { 134 | return m.fromPointer(ptr) 135 | } 136 | 137 | func minUint8(a, b uint8) uint8 { 138 | if a < b { 139 | return a 140 | } 141 | 142 | return b 143 | } 144 | -------------------------------------------------------------------------------- /internal/hashtable/bucket.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // Copyright (c) 2021 Andrey Pechkurov 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // Copyright notice. This code is a fork of xsync.MapOf from this file with some changes: 17 | // https://github.com/puzpuzpuz/xsync/blob/main/mapof.go 18 | // 19 | // Use of this source code is governed by a MIT license that can be found 20 | // at https://github.com/puzpuzpuz/xsync/blob/main/LICENSE 21 | 22 | package hashtable 23 | 24 | import ( 25 | "sync" 26 | "unsafe" 27 | 28 | "github.com/maypok86/otter/internal/xruntime" 29 | ) 30 | 31 | // paddedBucket is a CL-sized map bucket holding up to 32 | // bucketSize nodes. 33 | type paddedBucket struct { 34 | // ensure each bucket takes two cache lines on both 32 and 64-bit archs 35 | padding [xruntime.CacheLineSize - unsafe.Sizeof(bucket{})]byte 36 | 37 | bucket 38 | } 39 | 40 | type bucket struct { 41 | hashes [bucketSize]uint64 42 | nodes [bucketSize]unsafe.Pointer 43 | next unsafe.Pointer 44 | mutex sync.Mutex 45 | } 46 | 47 | func (root *paddedBucket) isEmpty() bool { 48 | b := root 49 | for { 50 | for i := 0; i < bucketSize; i++ { 51 | if b.nodes[i] != nil { 52 | return false 53 | } 54 | } 55 | if b.next == nil { 56 | return true 57 | } 58 | b = (*paddedBucket)(b.next) 59 | } 60 | } 61 | 62 | func (root *paddedBucket) add(h uint64, nodePtr unsafe.Pointer) { 63 | b := root 64 | for { 65 | for i := 0; i < bucketSize; i++ { 66 | if b.nodes[i] == nil { 67 | b.hashes[i] = h 68 | b.nodes[i] = nodePtr 69 | return 70 | } 71 | } 72 | if b.next == nil { 73 | newBucket := &paddedBucket{} 74 | newBucket.hashes[0] = h 75 | newBucket.nodes[0] = nodePtr 76 | b.next = unsafe.Pointer(newBucket) 77 | return 78 | } 79 | b = (*paddedBucket)(b.next) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/hashtable/map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // Copyright (c) 2021 Andrey Pechkurov 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // Copyright notice. This code is a fork of tests for xsync.MapOf from this file with some changes: 17 | // https://github.com/puzpuzpuz/xsync/blob/main/mapof_test.go 18 | // 19 | // Use of this source code is governed by a MIT license that can be found 20 | // at https://github.com/puzpuzpuz/xsync/blob/main/LICENSE 21 | 22 | package hashtable 23 | 24 | import ( 25 | "math/rand" 26 | "strconv" 27 | "sync" 28 | "sync/atomic" 29 | "testing" 30 | "time" 31 | "unsafe" 32 | 33 | "github.com/maypok86/otter/internal/generated/node" 34 | "github.com/maypok86/otter/internal/xruntime" 35 | ) 36 | 37 | func TestMap_PaddedBucketSize(t *testing.T) { 38 | size := unsafe.Sizeof(paddedBucket{}) 39 | if size != xruntime.CacheLineSize { 40 | t.Fatalf("size of 64B (one cache line) is expected, got: %d", size) 41 | } 42 | } 43 | 44 | func TestMap_EmptyStringKey(t *testing.T) { 45 | nm := node.NewManager[string, string](node.Config{}) 46 | m := New(nm) 47 | m.Set(nm.Create("", "foobar", 0, 1)) 48 | n, ok := m.Get("") 49 | if !ok { 50 | t.Fatal("value was expected") 51 | } 52 | if n.Value() != "foobar" { 53 | t.Fatalf("value does not match: %v", n.Value()) 54 | } 55 | } 56 | 57 | func TestMap_SetNilValue(t *testing.T) { 58 | nm := node.NewManager[string, *struct{}](node.Config{}) 59 | m := New(nm) 60 | m.Set(nm.Create("foo", nil, 0, 1)) 61 | n, ok := m.Get("foo") 62 | if !ok { 63 | t.Fatal("nil value was expected") 64 | } 65 | if n.Value() != nil { 66 | t.Fatalf("value was not nil: %v", n.Value()) 67 | } 68 | } 69 | 70 | func TestMap_Set(t *testing.T) { 71 | const numberOfNodes = 128 72 | nm := node.NewManager[string, int](node.Config{}) 73 | m := New(nm) 74 | for i := 0; i < numberOfNodes; i++ { 75 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 76 | } 77 | for i := 0; i < numberOfNodes; i++ { 78 | n, ok := m.Get(strconv.Itoa(i)) 79 | if !ok { 80 | t.Fatalf("value not found for %d", i) 81 | } 82 | if n.Value() != i { 83 | t.Fatalf("values do not match for %d: %v", i, n.Value()) 84 | } 85 | } 86 | } 87 | 88 | func TestMap_SetIfAbsent(t *testing.T) { 89 | const numberOfNodes = 128 90 | nm := node.NewManager[string, int](node.Config{}) 91 | m := New(nm) 92 | for i := 0; i < numberOfNodes; i++ { 93 | res := m.SetIfAbsent(nm.Create(strconv.Itoa(i), i, 0, 1)) 94 | if res != nil { 95 | t.Fatalf("set was dropped. got: %+v", res) 96 | } 97 | } 98 | for i := 0; i < numberOfNodes; i++ { 99 | n := nm.Create(strconv.Itoa(i), i, 0, 1) 100 | res := m.SetIfAbsent(n) 101 | if res == nil { 102 | t.Fatalf("set was not dropped. node that was set: %+v", res) 103 | } 104 | } 105 | 106 | for i := 0; i < numberOfNodes; i++ { 107 | n, ok := m.Get(strconv.Itoa(i)) 108 | if !ok { 109 | t.Fatalf("value not found for %d", i) 110 | } 111 | if n.Value() != i { 112 | t.Fatalf("values do not match for %d: %v", i, n.Value()) 113 | } 114 | } 115 | } 116 | 117 | // this code may break if the maphash.Hasher[k] structure changes. 118 | type hasher struct { 119 | hash func(pointer unsafe.Pointer, seed uintptr) uintptr 120 | seed uintptr 121 | } 122 | 123 | func TestMap_SetWithCollisions(t *testing.T) { 124 | const numNodes = 1000 125 | nm := node.NewManager[int, int](node.Config{}) 126 | m := NewWithSize(nm, numNodes) 127 | table := (*table[int])(atomic.LoadPointer(&m.table)) 128 | hasher := (*hasher)((unsafe.Pointer)(&table.hasher)) 129 | hasher.hash = func(ptr unsafe.Pointer, seed uintptr) uintptr { 130 | // We intentionally use an awful hash function here to make sure 131 | // that the map copes with key collisions. 132 | return 42 133 | } 134 | for i := 0; i < numNodes; i++ { 135 | m.Set(nm.Create(i, i, 0, 1)) 136 | } 137 | for i := 0; i < numNodes; i++ { 138 | v, ok := m.Get(i) 139 | if !ok { 140 | t.Fatalf("value not found for %d", i) 141 | } 142 | if v.Value() != i { 143 | t.Fatalf("values do not match for %d: %v", i, v) 144 | } 145 | } 146 | } 147 | 148 | func TestMap_SetThenDelete(t *testing.T) { 149 | const numberOfNodes = 1000 150 | nm := node.NewManager[string, int](node.Config{}) 151 | m := New(nm) 152 | for i := 0; i < numberOfNodes; i++ { 153 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 154 | } 155 | for i := 0; i < numberOfNodes; i++ { 156 | m.Delete(strconv.Itoa(i)) 157 | if _, ok := m.Get(strconv.Itoa(i)); ok { 158 | t.Fatalf("value was not expected for %d", i) 159 | } 160 | } 161 | } 162 | 163 | func TestMap_Range(t *testing.T) { 164 | const numNodes = 1000 165 | nm := node.NewManager[string, int](node.Config{}) 166 | m := New(nm) 167 | for i := 0; i < numNodes; i++ { 168 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 169 | } 170 | iters := 0 171 | met := make(map[string]int) 172 | m.Range(func(n node.Node[string, int]) bool { 173 | if n.Key() != strconv.Itoa(n.Value()) { 174 | t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, n.Key(), n.Value()) 175 | return false 176 | } 177 | met[n.Key()] += 1 178 | iters++ 179 | return true 180 | }) 181 | if iters != numNodes { 182 | t.Fatalf("got unexpected number of iterations: %d", iters) 183 | } 184 | for i := 0; i < numNodes; i++ { 185 | if c := met[strconv.Itoa(i)]; c != 1 { 186 | t.Fatalf("range did not iterate correctly over %d: %d", i, c) 187 | } 188 | } 189 | } 190 | 191 | func TestMap_RangeFalseReturned(t *testing.T) { 192 | nm := node.NewManager[string, int](node.Config{}) 193 | m := New(nm) 194 | for i := 0; i < 100; i++ { 195 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 196 | } 197 | iters := 0 198 | m.Range(func(n node.Node[string, int]) bool { 199 | iters++ 200 | return iters != 13 201 | }) 202 | if iters != 13 { 203 | t.Fatalf("got unexpected number of iterations: %d", iters) 204 | } 205 | } 206 | 207 | func TestMap_RangeNestedDelete(t *testing.T) { 208 | const numNodes = 256 209 | nm := node.NewManager[string, int](node.Config{}) 210 | m := New(nm) 211 | for i := 0; i < numNodes; i++ { 212 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 213 | } 214 | m.Range(func(n node.Node[string, int]) bool { 215 | m.Delete(n.Key()) 216 | return true 217 | }) 218 | for i := 0; i < numNodes; i++ { 219 | if _, ok := m.Get(strconv.Itoa(i)); ok { 220 | t.Fatalf("value found for %d", i) 221 | } 222 | } 223 | } 224 | 225 | func TestMap_Size(t *testing.T) { 226 | const numberOfNodes = 1000 227 | nm := node.NewManager[string, int](node.Config{}) 228 | m := New(nm) 229 | size := m.Size() 230 | if size != 0 { 231 | t.Fatalf("zero size expected: %d", size) 232 | } 233 | expectedSize := 0 234 | for i := 0; i < numberOfNodes; i++ { 235 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 236 | expectedSize++ 237 | size := m.Size() 238 | if size != expectedSize { 239 | t.Fatalf("size of %d was expected, got: %d", expectedSize, size) 240 | } 241 | } 242 | for i := 0; i < numberOfNodes; i++ { 243 | m.Delete(strconv.Itoa(i)) 244 | expectedSize-- 245 | size := m.Size() 246 | if size != expectedSize { 247 | t.Fatalf("size of %d was expected, got: %d", expectedSize, size) 248 | } 249 | } 250 | } 251 | 252 | func TestMap_Clear(t *testing.T) { 253 | const numberOfNodes = 1000 254 | nm := node.NewManager[string, int](node.Config{}) 255 | m := New(nm) 256 | for i := 0; i < numberOfNodes; i++ { 257 | m.Set(nm.Create(strconv.Itoa(i), i, 0, 1)) 258 | } 259 | size := m.Size() 260 | if size != numberOfNodes { 261 | t.Fatalf("size of %d was expected, got: %d", numberOfNodes, size) 262 | } 263 | m.Clear() 264 | size = m.Size() 265 | if size != 0 { 266 | t.Fatalf("zero size was expected, got: %d", size) 267 | } 268 | } 269 | 270 | func parallelSeqSetter(t *testing.T, m *Map[string, int], storers, iterations, nodes int, wg *sync.WaitGroup) { 271 | t.Helper() 272 | 273 | for i := 0; i < iterations; i++ { 274 | for j := 0; j < nodes; j++ { 275 | if storers == 0 || j%storers == 0 { 276 | m.Set(m.nodeManager.Create(strconv.Itoa(j), j, 0, 1)) 277 | n, ok := m.Get(strconv.Itoa(j)) 278 | if !ok { 279 | t.Errorf("value was not found for %d", j) 280 | break 281 | } 282 | if n.Value() != j { 283 | t.Errorf("value was not expected for %d: %d", j, n.Value()) 284 | break 285 | } 286 | } 287 | } 288 | } 289 | wg.Done() 290 | } 291 | 292 | func TestMap_ParallelSets(t *testing.T) { 293 | const storers = 4 294 | const iterations = 10_000 295 | const nodes = 100 296 | nm := node.NewManager[string, int](node.Config{}) 297 | m := New(nm) 298 | 299 | wg := &sync.WaitGroup{} 300 | wg.Add(storers) 301 | for i := 0; i < storers; i++ { 302 | go parallelSeqSetter(t, m, i, iterations, nodes, wg) 303 | } 304 | wg.Wait() 305 | 306 | for i := 0; i < nodes; i++ { 307 | n, ok := m.Get(strconv.Itoa(i)) 308 | if !ok { 309 | t.Fatalf("value not found for %d", i) 310 | } 311 | if n.Value() != i { 312 | t.Fatalf("values do not match for %d: %v", i, n.Value()) 313 | } 314 | } 315 | } 316 | 317 | func parallelRandSetter(t *testing.T, m *Map[string, int], iteratinos, nodes int, wg *sync.WaitGroup) { 318 | t.Helper() 319 | 320 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 321 | for i := 0; i < iteratinos; i++ { 322 | j := r.Intn(nodes) 323 | m.Set(m.nodeManager.Create(strconv.Itoa(j), j, 0, 1)) 324 | } 325 | wg.Done() 326 | } 327 | 328 | func parallelRandDeleter(t *testing.T, m *Map[string, int], iterations, nodes int, wg *sync.WaitGroup) { 329 | t.Helper() 330 | 331 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 332 | for i := 0; i < iterations; i++ { 333 | j := r.Intn(nodes) 334 | if v := m.Delete(strconv.Itoa(j)); v != nil && v.Value() != j { 335 | t.Errorf("value was not expected for %d: %d", j, v.Value()) 336 | } 337 | } 338 | wg.Done() 339 | } 340 | 341 | func parallelGetter(t *testing.T, m *Map[string, int], iterations, nodes int, wg *sync.WaitGroup) { 342 | t.Helper() 343 | 344 | for i := 0; i < iterations; i++ { 345 | for j := 0; j < nodes; j++ { 346 | if n, ok := m.Get(strconv.Itoa(j)); ok && n.Value() != j { 347 | t.Errorf("value was not expected for %d: %d", j, n.Value()) 348 | } 349 | } 350 | } 351 | wg.Done() 352 | } 353 | 354 | func TestMap_ParallelGet(t *testing.T) { 355 | const iterations = 100_000 356 | const nodes = 100 357 | nm := node.NewManager[string, int](node.Config{}) 358 | m := New(nm) 359 | 360 | wg := &sync.WaitGroup{} 361 | wg.Add(3) 362 | go parallelRandSetter(t, m, iterations, nodes, wg) 363 | go parallelRandDeleter(t, m, iterations, nodes, wg) 364 | go parallelGetter(t, m, iterations, nodes, wg) 365 | 366 | wg.Wait() 367 | } 368 | 369 | func TestMap_ParallelSetsAndDeletes(t *testing.T) { 370 | const workers = 2 371 | const iterations = 100_000 372 | const nodes = 1000 373 | nm := node.NewManager[string, int](node.Config{}) 374 | m := New(nm) 375 | wg := &sync.WaitGroup{} 376 | wg.Add(2 * workers) 377 | for i := 0; i < workers; i++ { 378 | go parallelRandSetter(t, m, iterations, nodes, wg) 379 | go parallelRandDeleter(t, m, iterations, nodes, wg) 380 | } 381 | 382 | wg.Wait() 383 | } 384 | 385 | func parallelTypedRangeSetter(t *testing.T, m *Map[int, int], numNodes int, stopFlag *int64, cdone chan bool) { 386 | t.Helper() 387 | 388 | for { 389 | for i := 0; i < numNodes; i++ { 390 | m.Set(m.nodeManager.Create(i, i, 0, 1)) 391 | } 392 | if atomic.LoadInt64(stopFlag) != 0 { 393 | break 394 | } 395 | } 396 | cdone <- true 397 | } 398 | 399 | func parallelTypedRangeDeleter(t *testing.T, m *Map[int, int], numNodes int, stopFlag *int64, cdone chan bool) { 400 | t.Helper() 401 | 402 | for { 403 | for i := 0; i < numNodes; i++ { 404 | m.Delete(i) 405 | } 406 | if atomic.LoadInt64(stopFlag) != 0 { 407 | break 408 | } 409 | } 410 | cdone <- true 411 | } 412 | 413 | func TestMap_ParallelRange(t *testing.T) { 414 | const numNodes = 10_000 415 | nm := node.NewManager[int, int](node.Config{}) 416 | m := New(nm) 417 | for i := 0; i < numNodes; i++ { 418 | m.Set(nm.Create(i, i, 0, 1)) 419 | } 420 | // Start goroutines that would be storing and deleting items in parallel. 421 | cdone := make(chan bool) 422 | stopFlag := int64(0) 423 | go parallelTypedRangeSetter(t, m, numNodes, &stopFlag, cdone) 424 | go parallelTypedRangeDeleter(t, m, numNodes, &stopFlag, cdone) 425 | // Iterate the map and verify that no duplicate keys were met. 426 | met := make(map[int]int) 427 | m.Range(func(n node.Node[int, int]) bool { 428 | if n.Key() != n.Value() { 429 | t.Fatalf("got unexpected value for key %d: %d", n.Key(), n.Value()) 430 | return false 431 | } 432 | met[n.Key()] += 1 433 | return true 434 | }) 435 | if len(met) == 0 { 436 | t.Fatal("no nodes were met when iterating") 437 | } 438 | for k, c := range met { 439 | if c != 1 { 440 | t.Fatalf("met key %d multiple times: %d", k, c) 441 | } 442 | } 443 | // Make sure that both goroutines finish. 444 | atomic.StoreInt64(&stopFlag, 1) 445 | <-cdone 446 | <-cdone 447 | } 448 | -------------------------------------------------------------------------------- /internal/lossy/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package lossy 16 | 17 | import ( 18 | "runtime" 19 | "sync/atomic" 20 | "unsafe" 21 | 22 | "github.com/maypok86/otter/internal/generated/node" 23 | "github.com/maypok86/otter/internal/xruntime" 24 | ) 25 | 26 | const ( 27 | // The maximum number of elements per buffer. 28 | capacity = 16 29 | mask = uint64(capacity - 1) 30 | ) 31 | 32 | // PolicyBuffers is the set of buffers returned by the lossy buffer. 33 | type PolicyBuffers[K comparable, V any] struct { 34 | Returned []node.Node[K, V] 35 | } 36 | 37 | // Buffer is a circular ring buffer stores the elements being transferred by the producers to the consumer. 38 | // The monotonically increasing count of reads and writes allow indexing sequentially to the next 39 | // element location based upon a power-of-two sizing. 40 | // 41 | // The producers race to read the counts, check if there is available capacity, and if so then try 42 | // once to CAS to the next write count. If the increment is successful then the producer lazily 43 | // publishes the element. The producer does not retry or block when unsuccessful due to a failed 44 | // CAS or the buffer being full. 45 | // 46 | // The consumer reads the counts and takes the available elements. The clearing of the elements 47 | // and the next read count are lazily set. 48 | // 49 | // This implementation is striped to further increase concurrency. 50 | type Buffer[K comparable, V any] struct { 51 | head atomic.Uint64 52 | headPadding [xruntime.CacheLineSize - unsafe.Sizeof(atomic.Uint64{})]byte 53 | tail atomic.Uint64 54 | tailPadding [xruntime.CacheLineSize - unsafe.Sizeof(atomic.Uint64{})]byte 55 | nodeManager *node.Manager[K, V] 56 | returned unsafe.Pointer 57 | returnedPadding [xruntime.CacheLineSize - 2*8]byte 58 | policyBuffers unsafe.Pointer 59 | returnedSlicePadding [xruntime.CacheLineSize - 8]byte 60 | buffer [capacity]unsafe.Pointer 61 | } 62 | 63 | // New creates a new lossy Buffer. 64 | func New[K comparable, V any](nodeManager *node.Manager[K, V]) *Buffer[K, V] { 65 | pb := &PolicyBuffers[K, V]{ 66 | Returned: make([]node.Node[K, V], 0, capacity), 67 | } 68 | b := &Buffer[K, V]{ 69 | nodeManager: nodeManager, 70 | policyBuffers: unsafe.Pointer(pb), 71 | } 72 | b.returned = b.policyBuffers 73 | return b 74 | } 75 | 76 | // Add lazily publishes the item to the consumer. 77 | // 78 | // item may be lost due to contention. 79 | func (b *Buffer[K, V]) Add(n node.Node[K, V]) *PolicyBuffers[K, V] { 80 | head := b.head.Load() 81 | tail := b.tail.Load() 82 | size := tail - head 83 | if size >= capacity { 84 | // full buffer 85 | return nil 86 | } 87 | if b.tail.CompareAndSwap(tail, tail+1) { 88 | // success 89 | //nolint:gosec // there will never be an overflow 90 | index := int(tail & mask) 91 | atomic.StorePointer(&b.buffer[index], n.AsPointer()) 92 | if size == capacity-1 { 93 | // try return new buffer 94 | if !atomic.CompareAndSwapPointer(&b.returned, b.policyBuffers, nil) { 95 | // somebody already get buffer 96 | return nil 97 | } 98 | 99 | pb := (*PolicyBuffers[K, V])(b.policyBuffers) 100 | for i := 0; i < capacity; i++ { 101 | //nolint:gosec // there will never be an overflow 102 | index := int(head & mask) 103 | v := atomic.LoadPointer(&b.buffer[index]) 104 | if v != nil { 105 | // published 106 | pb.Returned = append(pb.Returned, b.nodeManager.FromPointer(v)) 107 | // release 108 | atomic.StorePointer(&b.buffer[index], nil) 109 | } 110 | head++ 111 | } 112 | 113 | b.head.Store(head) 114 | return pb 115 | } 116 | } 117 | 118 | // failed 119 | return nil 120 | } 121 | 122 | // Free returns the processed buffer back and also clears it. 123 | func (b *Buffer[K, V]) Free() { 124 | pb := (*PolicyBuffers[K, V])(b.policyBuffers) 125 | for i := 0; i < len(pb.Returned); i++ { 126 | pb.Returned[i] = nil 127 | } 128 | pb.Returned = pb.Returned[:0] 129 | atomic.StorePointer(&b.returned, b.policyBuffers) 130 | } 131 | 132 | // Clear clears the lossy Buffer and returns it to the default state. 133 | func (b *Buffer[K, V]) Clear() { 134 | for !atomic.CompareAndSwapPointer(&b.returned, b.policyBuffers, nil) { 135 | runtime.Gosched() 136 | } 137 | for i := 0; i < capacity; i++ { 138 | atomic.StorePointer(&b.buffer[i], nil) 139 | } 140 | b.Free() 141 | b.tail.Store(0) 142 | b.head.Store(0) 143 | } 144 | -------------------------------------------------------------------------------- /internal/queue/growable.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package queue 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/maypok86/otter/internal/xmath" 21 | ) 22 | 23 | type Growable[T any] struct { 24 | mutex sync.Mutex 25 | notEmpty sync.Cond 26 | notFull sync.Cond 27 | buf []T 28 | head int 29 | tail int 30 | count int 31 | minCap int 32 | maxCap int 33 | } 34 | 35 | func NewGrowable[T any](minCap, maxCap uint32) *Growable[T] { 36 | minCap = xmath.RoundUpPowerOf2(minCap) 37 | maxCap = xmath.RoundUpPowerOf2(maxCap) 38 | 39 | g := &Growable[T]{ 40 | buf: make([]T, minCap), 41 | minCap: int(minCap), 42 | maxCap: int(maxCap), 43 | } 44 | 45 | g.notEmpty = *sync.NewCond(&g.mutex) 46 | g.notFull = *sync.NewCond(&g.mutex) 47 | 48 | return g 49 | } 50 | 51 | func (g *Growable[T]) Push(item T) { 52 | g.mutex.Lock() 53 | for g.count == g.maxCap { 54 | g.notFull.Wait() 55 | } 56 | g.push(item) 57 | g.mutex.Unlock() 58 | } 59 | 60 | func (g *Growable[T]) push(item T) { 61 | g.grow() 62 | g.buf[g.tail] = item 63 | g.tail = g.next(g.tail) 64 | g.count++ 65 | g.notEmpty.Signal() 66 | } 67 | 68 | func (g *Growable[T]) Pop() T { 69 | g.mutex.Lock() 70 | for g.count == 0 { 71 | g.notEmpty.Wait() 72 | } 73 | item := g.pop() 74 | g.mutex.Unlock() 75 | return item 76 | } 77 | 78 | func (g *Growable[T]) TryPop() (T, bool) { 79 | var zero T 80 | g.mutex.Lock() 81 | if g.count == 0 { 82 | g.mutex.Unlock() 83 | return zero, false 84 | } 85 | item := g.pop() 86 | g.mutex.Unlock() 87 | return item, true 88 | } 89 | 90 | func (g *Growable[T]) pop() T { 91 | var zero T 92 | 93 | item := g.buf[g.head] 94 | g.buf[g.head] = zero 95 | 96 | g.head = g.next(g.head) 97 | g.count-- 98 | 99 | g.notFull.Signal() 100 | 101 | return item 102 | } 103 | 104 | func (g *Growable[T]) Clear() { 105 | g.mutex.Lock() 106 | for g.count > 0 { 107 | g.pop() 108 | } 109 | g.mutex.Unlock() 110 | } 111 | 112 | func (g *Growable[T]) grow() { 113 | if g.count != len(g.buf) { 114 | return 115 | } 116 | g.resize() 117 | } 118 | 119 | func (g *Growable[T]) resize() { 120 | newBuf := make([]T, g.count<<1) 121 | if g.tail > g.head { 122 | copy(newBuf, g.buf[g.head:g.tail]) 123 | } else { 124 | n := copy(newBuf, g.buf[g.head:]) 125 | copy(newBuf[n:], g.buf[:g.tail]) 126 | } 127 | 128 | g.head = 0 129 | g.tail = g.count 130 | g.buf = newBuf 131 | } 132 | 133 | func (g *Growable[T]) next(i int) int { 134 | return (i + 1) & (len(g.buf) - 1) 135 | } 136 | -------------------------------------------------------------------------------- /internal/queue/growable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package queue 16 | 17 | import ( 18 | "fmt" 19 | "runtime" 20 | "sync" 21 | "sync/atomic" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | const minCapacity = 4 27 | 28 | func TestGrowable_PushPop(t *testing.T) { 29 | const capacity = 10 30 | g := NewGrowable[int](minCapacity, capacity) 31 | for i := 0; i < capacity; i++ { 32 | g.Push(i) 33 | } 34 | for i := 0; i < capacity; i++ { 35 | if got := g.Pop(); got != i { 36 | t.Fatalf("got %v, want %d", got, i) 37 | } 38 | } 39 | } 40 | 41 | func TestGrowable_ClearAndPopBlocksOnEmpty(t *testing.T) { 42 | const capacity = 10 43 | g := NewGrowable[int](minCapacity, capacity) 44 | for i := 0; i < capacity; i++ { 45 | g.Push(i) 46 | } 47 | 48 | g.Clear() 49 | 50 | cdone := make(chan bool) 51 | flag := int32(0) 52 | go func() { 53 | g.Pop() 54 | if atomic.LoadInt32(&flag) == 0 { 55 | t.Error("pop on empty queue didn't wait for pop") 56 | } 57 | cdone <- true 58 | }() 59 | time.Sleep(50 * time.Millisecond) 60 | atomic.StoreInt32(&flag, 1) 61 | g.Push(-1) 62 | <-cdone 63 | } 64 | 65 | func TestGrowable_PushBlocksOnFull(t *testing.T) { 66 | g := NewGrowable[string](1, 1) 67 | g.Push("foo") 68 | 69 | done := make(chan struct{}) 70 | flag := int32(0) 71 | go func() { 72 | g.Push("bar") 73 | if atomic.LoadInt32(&flag) == 0 { 74 | t.Error("push on full queue didn't wait for pop") 75 | } 76 | done <- struct{}{} 77 | }() 78 | 79 | time.Sleep(50 * time.Millisecond) 80 | atomic.StoreInt32(&flag, 1) 81 | if got := g.Pop(); got != "foo" { 82 | t.Fatalf("got %v, want foo", got) 83 | } 84 | <-done 85 | } 86 | 87 | func TestGrowable_PopBlocksOnEmpty(t *testing.T) { 88 | g := NewGrowable[string](2, 2) 89 | 90 | done := make(chan struct{}) 91 | flag := int32(0) 92 | go func() { 93 | g.Pop() 94 | if atomic.LoadInt32(&flag) == 0 { 95 | t.Error("pop on empty queue didn't wait for push") 96 | } 97 | done <- struct{}{} 98 | }() 99 | 100 | time.Sleep(50 * time.Millisecond) 101 | atomic.StoreInt32(&flag, 1) 102 | g.Push("foobar") 103 | <-done 104 | } 105 | 106 | func testGrowableConcurrent(t *testing.T, parallelism, ops, goroutines int) { 107 | t.Helper() 108 | runtime.GOMAXPROCS(parallelism) 109 | 110 | g := NewGrowable[int](minCapacity, uint32(goroutines)) 111 | var wg sync.WaitGroup 112 | wg.Add(1) 113 | csum := make(chan int, goroutines) 114 | 115 | // run producers. 116 | for i := 0; i < goroutines; i++ { 117 | go func(n int) { 118 | wg.Wait() 119 | for j := n; j < ops; j += goroutines { 120 | g.Push(j) 121 | } 122 | }(i) 123 | } 124 | 125 | // run consumers. 126 | for i := 0; i < goroutines; i++ { 127 | go func(n int) { 128 | wg.Wait() 129 | sum := 0 130 | for j := n; j < ops; j += goroutines { 131 | item := g.Pop() 132 | sum += item 133 | } 134 | csum <- sum 135 | }(i) 136 | } 137 | wg.Done() 138 | // Wait for all the sums from producers. 139 | sum := 0 140 | for i := 0; i < goroutines; i++ { 141 | s := <-csum 142 | sum += s 143 | } 144 | 145 | expectedSum := ops * (ops - 1) / 2 146 | if sum != expectedSum { 147 | t.Errorf("calculated sum is wrong. got %d, want %d", sum, expectedSum) 148 | } 149 | } 150 | 151 | func TestGrowable_Concurrent(t *testing.T) { 152 | defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) 153 | 154 | n := 100 155 | if testing.Short() { 156 | n = 10 157 | } 158 | 159 | tests := []struct { 160 | parallelism int 161 | ops int 162 | goroutines int 163 | }{ 164 | { 165 | parallelism: 1, 166 | ops: 100 * n, 167 | goroutines: n, 168 | }, 169 | { 170 | parallelism: 1, 171 | ops: 1000 * n, 172 | goroutines: 10 * n, 173 | }, 174 | { 175 | parallelism: 4, 176 | ops: 100 * n, 177 | goroutines: n, 178 | }, 179 | { 180 | parallelism: 4, 181 | ops: 1000 * n, 182 | goroutines: 10 * n, 183 | }, 184 | { 185 | parallelism: 8, 186 | ops: 100 * n, 187 | goroutines: n, 188 | }, 189 | { 190 | parallelism: 8, 191 | ops: 1000 * n, 192 | goroutines: 10 * n, 193 | }, 194 | } 195 | 196 | for _, tt := range tests { 197 | t.Run(fmt.Sprintf("testConcurrent-%d-%d", tt.parallelism, tt.ops), func(t *testing.T) { 198 | testGrowableConcurrent(t, tt.parallelism, tt.ops, tt.goroutines) 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /internal/queue/queue_bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package queue 16 | 17 | import ( 18 | "runtime" 19 | "sync/atomic" 20 | "testing" 21 | ) 22 | 23 | func benchmarkProdCons(b *testing.B, push func(int), pop func() int, queueSize, localWork int) { 24 | b.Helper() 25 | 26 | callsPerSched := queueSize 27 | procs := runtime.GOMAXPROCS(-1) / 2 28 | if procs == 0 { 29 | procs = 1 30 | } 31 | N := int32(b.N / callsPerSched) 32 | c := make(chan bool, 2*procs) 33 | for p := 0; p < procs; p++ { 34 | go func() { 35 | foo := 0 36 | for atomic.AddInt32(&N, -1) >= 0 { 37 | for g := 0; g < callsPerSched; g++ { 38 | for i := 0; i < localWork; i++ { 39 | foo *= 2 40 | foo /= 2 41 | } 42 | push(1) 43 | } 44 | } 45 | push(0) 46 | c <- foo == 42 47 | }() 48 | go func() { 49 | foo := 0 50 | for { 51 | v := pop() 52 | if v == 0 { 53 | break 54 | } 55 | for i := 0; i < localWork; i++ { 56 | foo *= 2 57 | foo /= 2 58 | } 59 | } 60 | c <- foo == 42 61 | }() 62 | } 63 | for p := 0; p < procs; p++ { 64 | <-c 65 | <-c 66 | } 67 | } 68 | 69 | func BenchmarkGrowableProdConsWork100(b *testing.B) { 70 | length := 128 * 16 71 | g := NewGrowable[int](uint32(length), uint32(length)) 72 | b.ResetTimer() 73 | benchmarkProdCons(b, func(i int) { 74 | g.Push(i) 75 | }, func() int { 76 | return g.Pop() 77 | }, length, 100) 78 | } 79 | 80 | func BenchmarkChanProdConsWork100(b *testing.B) { 81 | length := 128 * 16 82 | c := make(chan int, length) 83 | benchmarkProdCons(b, func(i int) { 84 | c <- i 85 | }, func() int { 86 | return <-c 87 | }, length, 100) 88 | } 89 | -------------------------------------------------------------------------------- /internal/s3fifo/ghost.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s3fifo 16 | 17 | import ( 18 | "github.com/dolthub/maphash" 19 | "github.com/gammazero/deque" 20 | 21 | "github.com/maypok86/otter/internal/generated/node" 22 | ) 23 | 24 | type ghost[K comparable, V any] struct { 25 | q *deque.Deque[uint64] 26 | m map[uint64]struct{} 27 | main *main[K, V] 28 | small *small[K, V] 29 | hasher maphash.Hasher[K] 30 | evictNode func(node.Node[K, V]) 31 | } 32 | 33 | func newGhost[K comparable, V any](main *main[K, V], evictNode func(node.Node[K, V])) *ghost[K, V] { 34 | return &ghost[K, V]{ 35 | q: &deque.Deque[uint64]{}, 36 | m: make(map[uint64]struct{}), 37 | main: main, 38 | hasher: maphash.NewHasher[K](), 39 | evictNode: evictNode, 40 | } 41 | } 42 | 43 | func (g *ghost[K, V]) isGhost(n node.Node[K, V]) bool { 44 | h := g.hasher.Hash(n.Key()) 45 | _, ok := g.m[h] 46 | return ok 47 | } 48 | 49 | func (g *ghost[K, V]) insert(n node.Node[K, V]) { 50 | g.evictNode(n) 51 | 52 | h := g.hasher.Hash(n.Key()) 53 | 54 | if _, ok := g.m[h]; ok { 55 | return 56 | } 57 | 58 | maxLength := g.small.length() + g.main.length() 59 | if maxLength == 0 { 60 | return 61 | } 62 | 63 | for g.q.Len() >= maxLength { 64 | v := g.q.PopFront() 65 | delete(g.m, v) 66 | } 67 | 68 | g.q.PushBack(h) 69 | g.m[h] = struct{}{} 70 | } 71 | 72 | func (g *ghost[K, V]) clear() { 73 | g.q.Clear() 74 | for k := range g.m { 75 | delete(g.m, k) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/s3fifo/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s3fifo 16 | 17 | import ( 18 | "github.com/maypok86/otter/internal/generated/node" 19 | ) 20 | 21 | const maxReinsertions = 20 22 | 23 | type main[K comparable, V any] struct { 24 | q *queue[K, V] 25 | cost int 26 | maxCost int 27 | evictNode func(node.Node[K, V]) 28 | } 29 | 30 | func newMain[K comparable, V any](maxCost int, evictNode func(node.Node[K, V])) *main[K, V] { 31 | return &main[K, V]{ 32 | q: newQueue[K, V](), 33 | maxCost: maxCost, 34 | evictNode: evictNode, 35 | } 36 | } 37 | 38 | func (m *main[K, V]) insert(n node.Node[K, V]) { 39 | m.q.push(n) 40 | n.MarkMain() 41 | m.cost += int(n.Cost()) 42 | } 43 | 44 | func (m *main[K, V]) evict() { 45 | reinsertions := 0 46 | for m.cost > 0 { 47 | n := m.q.pop() 48 | 49 | if !n.IsAlive() || n.HasExpired() || n.Frequency() == 0 { 50 | n.Unmark() 51 | m.cost -= int(n.Cost()) 52 | m.evictNode(n) 53 | return 54 | } 55 | 56 | // to avoid the worst case O(n), we remove the 20th reinserted consecutive element. 57 | reinsertions++ 58 | if reinsertions >= maxReinsertions { 59 | n.Unmark() 60 | m.cost -= int(n.Cost()) 61 | m.evictNode(n) 62 | return 63 | } 64 | 65 | m.q.push(n) 66 | n.DecrementFrequency() 67 | } 68 | } 69 | 70 | func (m *main[K, V]) delete(n node.Node[K, V]) { 71 | m.cost -= int(n.Cost()) 72 | n.Unmark() 73 | m.q.delete(n) 74 | } 75 | 76 | func (m *main[K, V]) length() int { 77 | return m.q.length() 78 | } 79 | 80 | func (m *main[K, V]) clear() { 81 | m.q.clear() 82 | m.cost = 0 83 | } 84 | 85 | func (m *main[K, V]) isFull() bool { 86 | return m.cost >= m.maxCost 87 | } 88 | -------------------------------------------------------------------------------- /internal/s3fifo/policy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s3fifo 16 | 17 | import ( 18 | "github.com/maypok86/otter/internal/generated/node" 19 | ) 20 | 21 | // Policy is an eviction policy based on S3-FIFO eviction algorithm 22 | // from the following paper: https://dl.acm.org/doi/10.1145/3600006.3613147. 23 | type Policy[K comparable, V any] struct { 24 | small *small[K, V] 25 | main *main[K, V] 26 | ghost *ghost[K, V] 27 | maxCost int 28 | maxAvailableNodeCost int 29 | } 30 | 31 | // NewPolicy creates a new Policy. 32 | func NewPolicy[K comparable, V any](maxCost int, evictNode func(node.Node[K, V])) *Policy[K, V] { 33 | smallMaxCost := maxCost / 10 34 | mainMaxCost := maxCost - smallMaxCost 35 | 36 | main := newMain[K, V](mainMaxCost, evictNode) 37 | ghost := newGhost(main, evictNode) 38 | small := newSmall(smallMaxCost, main, ghost, evictNode) 39 | ghost.small = small 40 | 41 | return &Policy[K, V]{ 42 | small: small, 43 | main: main, 44 | ghost: ghost, 45 | maxCost: maxCost, 46 | maxAvailableNodeCost: smallMaxCost, 47 | } 48 | } 49 | 50 | // Read updates the eviction policy based on node accesses. 51 | func (p *Policy[K, V]) Read(nodes []node.Node[K, V]) { 52 | for _, n := range nodes { 53 | n.IncrementFrequency() 54 | } 55 | } 56 | 57 | // Add adds node to the eviction policy. 58 | func (p *Policy[K, V]) Add(n node.Node[K, V]) { 59 | if p.ghost.isGhost(n) { 60 | p.main.insert(n) 61 | n.ResetFrequency() 62 | } else { 63 | p.small.insert(n) 64 | } 65 | 66 | for p.isFull() { 67 | p.evict() 68 | } 69 | } 70 | 71 | func (p *Policy[K, V]) evict() { 72 | if p.small.cost >= p.maxCost/10 { 73 | p.small.evict() 74 | return 75 | } 76 | 77 | p.main.evict() 78 | } 79 | 80 | func (p *Policy[K, V]) isFull() bool { 81 | return p.small.cost+p.main.cost > p.maxCost 82 | } 83 | 84 | // Delete deletes node from the eviction policy. 85 | func (p *Policy[K, V]) Delete(n node.Node[K, V]) { 86 | if n.IsSmall() { 87 | p.small.delete(n) 88 | return 89 | } 90 | 91 | if n.IsMain() { 92 | p.main.delete(n) 93 | } 94 | } 95 | 96 | // MaxAvailableCost returns the maximum available cost of the node. 97 | func (p *Policy[K, V]) MaxAvailableCost() int { 98 | return p.maxAvailableNodeCost 99 | } 100 | 101 | // Clear clears the eviction policy and returns it to the default state. 102 | func (p *Policy[K, V]) Clear() { 103 | p.ghost.clear() 104 | p.main.clear() 105 | p.small.clear() 106 | } 107 | -------------------------------------------------------------------------------- /internal/s3fifo/policy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s3fifo 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/maypok86/otter/internal/generated/node" 21 | ) 22 | 23 | func newNode(k int) node.Node[int, int] { 24 | m := node.NewManager[int, int](node.Config{}) 25 | n := m.Create(k, k, 0, 1) 26 | return n 27 | } 28 | 29 | func TestPolicy_ReadAndWrite(t *testing.T) { 30 | n := newNode(2) 31 | p := NewPolicy[int, int](10, func(n node.Node[int, int]) { 32 | }) 33 | p.Add(n) 34 | if !n.IsSmall() { 35 | t.Fatalf("not valid node state: %+v", n) 36 | } 37 | } 38 | 39 | func TestPolicy_OneHitWonders(t *testing.T) { 40 | p := NewPolicy[int, int](10, func(n node.Node[int, int]) { 41 | }) 42 | 43 | oneHitWonders := make([]node.Node[int, int], 0, 2) 44 | for i := 0; i < cap(oneHitWonders); i++ { 45 | oneHitWonders = append(oneHitWonders, newNode(i+1)) 46 | } 47 | 48 | popular := make([]node.Node[int, int], 0, 8) 49 | for i := 0; i < cap(popular); i++ { 50 | popular = append(popular, newNode(i+3)) 51 | } 52 | 53 | for _, n := range oneHitWonders { 54 | p.Add(n) 55 | } 56 | 57 | for _, n := range popular { 58 | p.Add(n) 59 | } 60 | 61 | p.Read(oneHitWonders) 62 | for i := 0; i < 3; i++ { 63 | p.Read(popular) 64 | } 65 | 66 | newNodes := make([]node.Node[int, int], 0, 11) 67 | for i := 0; i < cap(newNodes); i++ { 68 | newNodes = append(newNodes, newNode(i+12)) 69 | } 70 | 71 | for _, n := range newNodes { 72 | p.Add(n) 73 | } 74 | 75 | for _, n := range oneHitWonders { 76 | if n.IsSmall() || n.IsMain() { 77 | t.Fatalf("one hit wonder should be evicted: %+v", n) 78 | } 79 | } 80 | 81 | for _, n := range popular { 82 | if !n.IsMain() { 83 | t.Fatalf("popular objects should be in main queue: %+v", n) 84 | } 85 | } 86 | 87 | for _, n := range oneHitWonders { 88 | p.Delete(n) 89 | } 90 | for _, n := range popular { 91 | p.Delete(n) 92 | } 93 | for _, n := range newNodes { 94 | p.Delete(n) 95 | } 96 | 97 | if p.small.cost+p.main.cost != 0 { 98 | t.Fatalf("queues should be empty, but small size: %d, main size: %d", p.small.cost, p.main.cost) 99 | } 100 | } 101 | 102 | func TestPolicy_Update(t *testing.T) { 103 | collect := false 104 | var deleted []node.Node[int, int] 105 | p := NewPolicy[int, int](100, func(n node.Node[int, int]) { 106 | if collect { 107 | deleted = deleted[:0] 108 | deleted = append(deleted, n) 109 | } 110 | }) 111 | 112 | n := newNode(1) 113 | m := node.NewManager[int, int](node.Config{WithCost: true}) 114 | n1 := m.Create(1, 1, 0, n.Cost()+8) 115 | 116 | p.Add(n) 117 | p.Delete(n) 118 | p.Add(n1) 119 | 120 | p.Read([]node.Node[int, int]{n1, n1}) 121 | 122 | n2 := m.Create(2, 1, 0, 92) 123 | collect = true 124 | p.Add(n2) 125 | 126 | if !n1.IsMain() { 127 | t.Fatalf("updated node should be in main queue: %+v", n1) 128 | } 129 | 130 | if n2.IsSmall() || n2.IsMain() || len(deleted) != 1 || deleted[0] != n2 { 131 | t.Fatalf("inserted node should be evicted: %+v", n2) 132 | } 133 | 134 | n3 := m.Create(1, 1, 0, 109) 135 | p.Delete(n1) 136 | p.Add(n3) 137 | if n3.IsSmall() || n3.IsMain() || len(deleted) != 1 || deleted[0] != n3 { 138 | t.Fatalf("updated node should be evicted: %+v", n3) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /internal/s3fifo/queue.go: -------------------------------------------------------------------------------- 1 | package s3fifo 2 | 3 | import "github.com/maypok86/otter/internal/generated/node" 4 | 5 | type queue[K comparable, V any] struct { 6 | head node.Node[K, V] 7 | tail node.Node[K, V] 8 | len int 9 | } 10 | 11 | func newQueue[K comparable, V any]() *queue[K, V] { 12 | return &queue[K, V]{} 13 | } 14 | 15 | func (q *queue[K, V]) length() int { 16 | return q.len 17 | } 18 | 19 | func (q *queue[K, V]) isEmpty() bool { 20 | return q.length() == 0 21 | } 22 | 23 | func (q *queue[K, V]) push(n node.Node[K, V]) { 24 | if q.isEmpty() { 25 | q.head = n 26 | q.tail = n 27 | } else { 28 | n.SetPrev(q.tail) 29 | q.tail.SetNext(n) 30 | q.tail = n 31 | } 32 | 33 | q.len++ 34 | } 35 | 36 | func (q *queue[K, V]) pop() node.Node[K, V] { 37 | if q.isEmpty() { 38 | return nil 39 | } 40 | 41 | result := q.head 42 | q.delete(result) 43 | return result 44 | } 45 | 46 | func (q *queue[K, V]) delete(n node.Node[K, V]) { 47 | next := n.Next() 48 | prev := n.Prev() 49 | 50 | if node.Equals(prev, nil) { 51 | if node.Equals(next, nil) && !node.Equals(q.head, n) { 52 | return 53 | } 54 | 55 | q.head = next 56 | } else { 57 | prev.SetNext(next) 58 | n.SetPrev(nil) 59 | } 60 | 61 | if node.Equals(next, nil) { 62 | q.tail = prev 63 | } else { 64 | next.SetPrev(prev) 65 | n.SetNext(nil) 66 | } 67 | 68 | q.len-- 69 | } 70 | 71 | func (q *queue[K, V]) clear() { 72 | for !q.isEmpty() { 73 | q.pop() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/s3fifo/queue_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // Copyright 2009 The Go Authors. All rights reserved. 3 | // 4 | // Copyright notice. Initial version of the following tests was based on 5 | // the following file from the Go Programming Language core repo: 6 | // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/container/list/list_test.go 7 | // 8 | // Use of this source code is governed by a BSD-style 9 | // license that can be found in the LICENSE file. 10 | // That can be found at https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:LICENSE 11 | 12 | package s3fifo 13 | 14 | import ( 15 | "strconv" 16 | "testing" 17 | 18 | "github.com/maypok86/otter/internal/generated/node" 19 | ) 20 | 21 | func checkQueueLen[K comparable, V any](t *testing.T, q *queue[K, V], length int) bool { 22 | t.Helper() 23 | 24 | if n := q.length(); n != length { 25 | t.Errorf("q.length() = %d, want %d", n, length) 26 | return false 27 | } 28 | return true 29 | } 30 | 31 | func checkQueuePointers[K comparable, V any](t *testing.T, q *queue[K, V], nodes []node.Node[K, V]) { 32 | t.Helper() 33 | 34 | if !checkQueueLen(t, q, len(nodes)) { 35 | return 36 | } 37 | 38 | // zero length queues must be the zero value 39 | if len(nodes) == 0 { 40 | if !(node.Equals(q.head, nil) && node.Equals(q.tail, nil)) { 41 | t.Errorf("q.head = %p, q.tail = %p; both should be nil", q.head, q.tail) 42 | } 43 | return 44 | } 45 | 46 | // check internal and external prev/next connections 47 | for i, n := range nodes { 48 | var prev node.Node[K, V] 49 | if i > 0 { 50 | prev = nodes[i-1] 51 | } 52 | if p := n.Prev(); !node.Equals(p, prev) { 53 | t.Errorf("elt[%d](%p).prev = %p, want %p", i, n, p, prev) 54 | } 55 | 56 | var next node.Node[K, V] 57 | if i < len(nodes)-1 { 58 | next = nodes[i+1] 59 | } 60 | if nn := n.Next(); !node.Equals(nn, next) { 61 | t.Errorf("nodes[%d](%p).next = %p, want %p", i, n, nn, next) 62 | } 63 | } 64 | } 65 | 66 | func newFakeNode[K comparable](e K) node.Node[K, K] { 67 | m := node.NewManager[K, K](node.Config{WithCost: true}) 68 | return m.Create(e, e, 0, 0) 69 | } 70 | 71 | func TestQueue(t *testing.T) { 72 | q := newQueue[string, string]() 73 | checkQueuePointers(t, q, []node.Node[string, string]{}) 74 | 75 | // Single element queue 76 | e := newFakeNode("a") 77 | q.push(e) 78 | checkQueuePointers(t, q, []node.Node[string, string]{e}) 79 | q.delete(e) 80 | q.push(e) 81 | checkQueuePointers(t, q, []node.Node[string, string]{e}) 82 | q.delete(e) 83 | checkQueuePointers(t, q, []node.Node[string, string]{}) 84 | 85 | // Bigger queue 86 | e2 := newFakeNode("2") 87 | e1 := newFakeNode("1") 88 | e3 := newFakeNode("3") 89 | e4 := newFakeNode("4") 90 | q.push(e1) 91 | q.push(e2) 92 | q.push(e3) 93 | q.push(e4) 94 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e2, e3, e4}) 95 | 96 | q.delete(e2) 97 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e3, e4}) 98 | 99 | // move from middle 100 | q.delete(e3) 101 | q.push(e3) 102 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) 103 | 104 | q.clear() 105 | q.push(e3) 106 | q.push(e1) 107 | q.push(e4) 108 | checkQueuePointers(t, q, []node.Node[string, string]{e3, e1, e4}) 109 | 110 | // should be no-op 111 | q.delete(e3) 112 | q.push(e3) 113 | checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) 114 | 115 | // Check standard iteration. 116 | sum := 0 117 | for e := q.head; !node.Equals(e, nil); e = e.Next() { 118 | i, err := strconv.Atoi(e.Value()) 119 | if err != nil { 120 | continue 121 | } 122 | sum += i 123 | } 124 | if sum != 8 { 125 | t.Errorf("sum over l = %d, want 8", sum) 126 | } 127 | 128 | // Clear all elements by iterating 129 | var next node.Node[string, string] 130 | for e := q.head; !node.Equals(e, nil); e = next { 131 | next = e.Next() 132 | q.delete(e) 133 | } 134 | checkQueuePointers(t, q, []node.Node[string, string]{}) 135 | } 136 | 137 | func TestQueue_Remove(t *testing.T) { 138 | q := newQueue[int, int]() 139 | 140 | e1 := newFakeNode(1) 141 | e2 := newFakeNode(2) 142 | q.push(e1) 143 | q.push(e2) 144 | checkQueuePointers(t, q, []node.Node[int, int]{e1, e2}) 145 | e := q.head 146 | q.delete(e) 147 | checkQueuePointers(t, q, []node.Node[int, int]{e2}) 148 | q.delete(e) 149 | checkQueuePointers(t, q, []node.Node[int, int]{e2}) 150 | } 151 | -------------------------------------------------------------------------------- /internal/s3fifo/small.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s3fifo 16 | 17 | import ( 18 | "github.com/maypok86/otter/internal/generated/node" 19 | ) 20 | 21 | type small[K comparable, V any] struct { 22 | q *queue[K, V] 23 | main *main[K, V] 24 | ghost *ghost[K, V] 25 | cost int 26 | maxCost int 27 | evictNode func(node.Node[K, V]) 28 | } 29 | 30 | func newSmall[K comparable, V any]( 31 | maxCost int, 32 | main *main[K, V], 33 | ghost *ghost[K, V], 34 | evictNode func(node.Node[K, V]), 35 | ) *small[K, V] { 36 | return &small[K, V]{ 37 | q: newQueue[K, V](), 38 | main: main, 39 | ghost: ghost, 40 | maxCost: maxCost, 41 | evictNode: evictNode, 42 | } 43 | } 44 | 45 | func (s *small[K, V]) insert(n node.Node[K, V]) { 46 | s.q.push(n) 47 | n.MarkSmall() 48 | s.cost += int(n.Cost()) 49 | } 50 | 51 | func (s *small[K, V]) evict() { 52 | if s.cost == 0 { 53 | return 54 | } 55 | 56 | n := s.q.pop() 57 | s.cost -= int(n.Cost()) 58 | n.Unmark() 59 | if !n.IsAlive() || n.HasExpired() { 60 | s.evictNode(n) 61 | return 62 | } 63 | 64 | if n.Frequency() > 1 { 65 | s.main.insert(n) 66 | for s.main.isFull() { 67 | s.main.evict() 68 | } 69 | n.ResetFrequency() 70 | return 71 | } 72 | 73 | s.ghost.insert(n) 74 | } 75 | 76 | func (s *small[K, V]) delete(n node.Node[K, V]) { 77 | s.cost -= int(n.Cost()) 78 | n.Unmark() 79 | s.q.delete(n) 80 | } 81 | 82 | func (s *small[K, V]) length() int { 83 | return s.q.length() 84 | } 85 | 86 | func (s *small[K, V]) clear() { 87 | s.q.clear() 88 | s.cost = 0 89 | } 90 | -------------------------------------------------------------------------------- /internal/stats/counter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // Copyright (c) 2021 Andrey Pechkurov 3 | // 4 | // Copyright notice. This code is a fork of xsync.Counter from this file with some changes: 5 | // https://github.com/puzpuzpuz/xsync/blob/main/counter.go 6 | // 7 | // Use of this source code is governed by a MIT license that can be found 8 | // at https://github.com/puzpuzpuz/xsync/blob/main/LICENSE 9 | 10 | package stats 11 | 12 | import ( 13 | "sync" 14 | "sync/atomic" 15 | 16 | "github.com/maypok86/otter/internal/xmath" 17 | "github.com/maypok86/otter/internal/xruntime" 18 | ) 19 | 20 | // pool for P tokens. 21 | var tokenPool sync.Pool 22 | 23 | // a P token is used to point at the current OS thread (P) 24 | // on which the goroutine is run; exact identity of the thread, 25 | // as well as P migration tolerance, is not important since 26 | // it's used to as a best effort mechanism for assigning 27 | // concurrent operations (goroutines) to different stripes of 28 | // the counter. 29 | type token struct { 30 | idx uint32 31 | padding [xruntime.CacheLineSize - 4]byte 32 | } 33 | 34 | // A counter is a striped int64 counter. 35 | // 36 | // Should be preferred over a single atomically updated int64 37 | // counter in high contention scenarios. 38 | // 39 | // A counter must not be copied after first use. 40 | type counter struct { 41 | shards []cshard 42 | mask uint32 43 | } 44 | 45 | type cshard struct { 46 | c int64 47 | padding [xruntime.CacheLineSize - 8]byte 48 | } 49 | 50 | // newCounter creates a new counter instance. 51 | func newCounter() *counter { 52 | nshards := xmath.RoundUpPowerOf2(xruntime.Parallelism()) 53 | return &counter{ 54 | shards: make([]cshard, nshards), 55 | mask: nshards - 1, 56 | } 57 | } 58 | 59 | // increment increments the counter by 1. 60 | func (c *counter) increment() { 61 | c.add(1) 62 | } 63 | 64 | // decrement decrements the counter by 1. 65 | func (c *counter) decrement() { 66 | c.add(-1) 67 | } 68 | 69 | // add adds the delta to the counter. 70 | func (c *counter) add(delta int64) { 71 | t, ok := tokenPool.Get().(*token) 72 | if !ok { 73 | t = &token{} 74 | t.idx = xruntime.Fastrand() 75 | } 76 | for { 77 | shard := &c.shards[t.idx&c.mask] 78 | cnt := atomic.LoadInt64(&shard.c) 79 | if atomic.CompareAndSwapInt64(&shard.c, cnt, cnt+delta) { 80 | break 81 | } 82 | // Give a try with another randomly selected shard. 83 | t.idx = xruntime.Fastrand() 84 | } 85 | tokenPool.Put(t) 86 | } 87 | 88 | // value returns the current counter value. 89 | // The returned value may not include all of the latest operations in 90 | // presence of concurrent modifications of the counter. 91 | func (c *counter) value() int64 { 92 | v := int64(0) 93 | for i := 0; i < len(c.shards); i++ { 94 | shard := &c.shards[i] 95 | v += atomic.LoadInt64(&shard.c) 96 | } 97 | return v 98 | } 99 | 100 | // reset resets the counter to zero. 101 | // This method should only be used when it is known that there are 102 | // no concurrent modifications of the counter. 103 | func (c *counter) reset() { 104 | for i := 0; i < len(c.shards); i++ { 105 | shard := &c.shards[i] 106 | atomic.StoreInt64(&shard.c, 0) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/stats/counter_bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // Copyright (c) 2021 Andrey Pechkurov 3 | // 4 | // Copyright notice. This code is a fork of benchmarks for xsync.Counter from this file with some changes: 5 | // https://github.com/puzpuzpuz/xsync/blob/main/counter_test.go 6 | // 7 | // Use of this source code is governed by a MIT license that can be found 8 | // at https://github.com/puzpuzpuz/xsync/blob/main/LICENSE 9 | 10 | package stats 11 | 12 | import ( 13 | "sync/atomic" 14 | "testing" 15 | ) 16 | 17 | func runBenchCounter(b *testing.B, value func() int64, increment func(), writeRatio int) { 18 | b.Helper() 19 | b.ResetTimer() 20 | b.ReportAllocs() 21 | b.RunParallel(func(pb *testing.PB) { 22 | sink := 0 23 | for pb.Next() { 24 | sink++ 25 | if writeRatio > 0 && sink%writeRatio == 0 { 26 | value() 27 | } else { 28 | increment() 29 | } 30 | } 31 | _ = sink 32 | }) 33 | } 34 | 35 | func benchmarkCounter(b *testing.B, writeRatio int) { 36 | b.Helper() 37 | c := newCounter() 38 | runBenchCounter(b, func() int64 { 39 | return c.value() 40 | }, func() { 41 | c.increment() 42 | }, writeRatio) 43 | } 44 | 45 | func BenchmarkCounter(b *testing.B) { 46 | benchmarkCounter(b, 10000) 47 | } 48 | 49 | func benchmarkAtomicInt64(b *testing.B, writeRatio int) { 50 | b.Helper() 51 | var c int64 52 | runBenchCounter(b, func() int64 { 53 | return atomic.LoadInt64(&c) 54 | }, func() { 55 | atomic.AddInt64(&c, 1) 56 | }, writeRatio) 57 | } 58 | 59 | func BenchmarkAtomicInt64(b *testing.B) { 60 | benchmarkAtomicInt64(b, 10000) 61 | } 62 | -------------------------------------------------------------------------------- /internal/stats/counter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // Copyright (c) 2021 Andrey Pechkurov 3 | // 4 | // Copyright notice. This code is a fork of tests for xsync.Counter from this file with some changes: 5 | // https://github.com/puzpuzpuz/xsync/blob/main/counter_test.go 6 | // 7 | // Use of this source code is governed by a MIT license that can be found 8 | // at https://github.com/puzpuzpuz/xsync/blob/main/LICENSE 9 | 10 | package stats 11 | 12 | import ( 13 | "fmt" 14 | "runtime" 15 | "sync" 16 | "testing" 17 | ) 18 | 19 | func TestCounterIncrement(t *testing.T) { 20 | c := newCounter() 21 | for i := 0; i < 1000; i++ { 22 | if v := c.value(); v != int64(i) { 23 | t.Fatalf("got %v, want %d", v, i) 24 | } 25 | c.increment() 26 | } 27 | } 28 | 29 | func TestCounterDecrement(t *testing.T) { 30 | c := newCounter() 31 | for i := 0; i < 1000; i++ { 32 | if v := c.value(); v != int64(-i) { 33 | t.Fatalf("got %v, want %d", v, -i) 34 | } 35 | c.decrement() 36 | } 37 | } 38 | 39 | func TestCounterAdd(t *testing.T) { 40 | c := newCounter() 41 | for i := 0; i < 100; i++ { 42 | if v := c.value(); v != int64(i*42) { 43 | t.Fatalf("got %v, want %d", v, i*42) 44 | } 45 | c.add(42) 46 | } 47 | } 48 | 49 | func TestCounterReset(t *testing.T) { 50 | c := newCounter() 51 | c.add(42) 52 | if v := c.value(); v != 42 { 53 | t.Fatalf("got %v, want %d", v, 42) 54 | } 55 | c.reset() 56 | if v := c.value(); v != 0 { 57 | t.Fatalf("got %v, want %d", v, 0) 58 | } 59 | } 60 | 61 | func parallelIncrement(c *counter, incs int, wg *sync.WaitGroup) { 62 | for i := 0; i < incs; i++ { 63 | c.increment() 64 | } 65 | wg.Done() 66 | } 67 | 68 | func testParallelIncrement(t *testing.T, modifiers, gomaxprocs int) { 69 | t.Helper() 70 | runtime.GOMAXPROCS(gomaxprocs) 71 | c := newCounter() 72 | wg := &sync.WaitGroup{} 73 | incs := 10_000 74 | wg.Add(modifiers) 75 | for i := 0; i < modifiers; i++ { 76 | go parallelIncrement(c, incs, wg) 77 | } 78 | wg.Wait() 79 | expected := int64(modifiers * incs) 80 | if v := c.value(); v != expected { 81 | t.Fatalf("got %d, want %d", v, expected) 82 | } 83 | } 84 | 85 | func TestCounterParallelIncrementors(t *testing.T) { 86 | defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) 87 | 88 | tests := []struct { 89 | modifiers int 90 | gomaxprocs int 91 | }{ 92 | { 93 | modifiers: 4, 94 | gomaxprocs: 2, 95 | }, 96 | { 97 | modifiers: 16, 98 | gomaxprocs: 4, 99 | }, 100 | { 101 | modifiers: 64, 102 | gomaxprocs: 8, 103 | }, 104 | } 105 | 106 | for i, tt := range tests { 107 | t.Run(fmt.Sprintf("parallelIncrement-%d", i+1), func(t *testing.T) { 108 | testParallelIncrement(t, tt.modifiers, tt.gomaxprocs) 109 | testParallelIncrement(t, tt.modifiers, tt.gomaxprocs) 110 | testParallelIncrement(t, tt.modifiers, tt.gomaxprocs) 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/stats/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package stats 16 | 17 | import ( 18 | "sync/atomic" 19 | "unsafe" 20 | 21 | "github.com/maypok86/otter/internal/xruntime" 22 | ) 23 | 24 | // Stats is a thread-safe statistics collector. 25 | type Stats struct { 26 | hits *counter 27 | misses *counter 28 | rejectedSets *counter 29 | evictedCountersPadding [xruntime.CacheLineSize - 2*unsafe.Sizeof(atomic.Int64{})]byte 30 | evictedCount atomic.Int64 31 | evictedCost atomic.Int64 32 | } 33 | 34 | // New creates a new Stats collector. 35 | func New() *Stats { 36 | return &Stats{ 37 | hits: newCounter(), 38 | misses: newCounter(), 39 | rejectedSets: newCounter(), 40 | } 41 | } 42 | 43 | // IncHits increments the hits counter. 44 | func (s *Stats) IncHits() { 45 | if s == nil { 46 | return 47 | } 48 | 49 | s.hits.increment() 50 | } 51 | 52 | // Hits returns the number of cache hits. 53 | func (s *Stats) Hits() int64 { 54 | if s == nil { 55 | return 0 56 | } 57 | 58 | return s.hits.value() 59 | } 60 | 61 | // IncMisses increments the misses counter. 62 | func (s *Stats) IncMisses() { 63 | if s == nil { 64 | return 65 | } 66 | 67 | s.misses.increment() 68 | } 69 | 70 | // Misses returns the number of cache misses. 71 | func (s *Stats) Misses() int64 { 72 | if s == nil { 73 | return 0 74 | } 75 | 76 | return s.misses.value() 77 | } 78 | 79 | // IncRejectedSets increments the rejectedSets counter. 80 | func (s *Stats) IncRejectedSets() { 81 | if s == nil { 82 | return 83 | } 84 | 85 | s.rejectedSets.increment() 86 | } 87 | 88 | // RejectedSets returns the number of rejected sets. 89 | func (s *Stats) RejectedSets() int64 { 90 | if s == nil { 91 | return 0 92 | } 93 | 94 | return s.rejectedSets.value() 95 | } 96 | 97 | // IncEvictedCount increments the evictedCount counter. 98 | func (s *Stats) IncEvictedCount() { 99 | if s == nil { 100 | return 101 | } 102 | 103 | s.evictedCount.Add(1) 104 | } 105 | 106 | // EvictedCount returns the number of evicted entries. 107 | func (s *Stats) EvictedCount() int64 { 108 | if s == nil { 109 | return 0 110 | } 111 | 112 | return s.evictedCount.Load() 113 | } 114 | 115 | // AddEvictedCost adds cost to the evictedCost counter. 116 | func (s *Stats) AddEvictedCost(cost uint32) { 117 | if s == nil { 118 | return 119 | } 120 | 121 | s.evictedCost.Add(int64(cost)) 122 | } 123 | 124 | // EvictedCost returns the sum of costs of evicted entries. 125 | func (s *Stats) EvictedCost() int64 { 126 | if s == nil { 127 | return 0 128 | } 129 | 130 | return s.evictedCost.Load() 131 | } 132 | 133 | func (s *Stats) Clear() { 134 | if s == nil { 135 | return 136 | } 137 | 138 | s.hits.reset() 139 | s.misses.reset() 140 | s.rejectedSets.reset() 141 | s.evictedCount.Store(0) 142 | s.evictedCost.Store(0) 143 | } 144 | -------------------------------------------------------------------------------- /internal/stats/stats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package stats 16 | 17 | import ( 18 | "math/rand" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | const ( 24 | maxCount = 10_000 25 | ) 26 | 27 | func generateCount(t *testing.T) int64 { 28 | t.Helper() 29 | 30 | r := rand.NewSource(time.Now().UnixNano()) 31 | count := r.Int63() % maxCount 32 | if count == 0 { 33 | return 1 34 | } 35 | 36 | return count 37 | } 38 | 39 | func TestStats_Nil(t *testing.T) { 40 | var s *Stats 41 | expected := int64(0) 42 | for _, inc := range []func(){ 43 | s.IncHits, 44 | s.IncMisses, 45 | s.IncRejectedSets, 46 | s.IncEvictedCount, 47 | func() { 48 | s.AddEvictedCost(1) 49 | }, 50 | s.IncHits, 51 | s.IncMisses, 52 | } { 53 | inc() 54 | } 55 | for _, f := range []func() int64{ 56 | s.Hits, 57 | s.Misses, 58 | s.RejectedSets, 59 | s.EvictedCount, 60 | s.EvictedCost, 61 | } { 62 | if expected != f() { 63 | t.Fatalf("hits and misses for nil stats should always be %d", expected) 64 | } 65 | } 66 | s.Clear() 67 | } 68 | 69 | func TestStats_Hits(t *testing.T) { 70 | expected := generateCount(t) 71 | 72 | s := New() 73 | for i := int64(0); i < expected; i++ { 74 | s.IncHits() 75 | } 76 | 77 | hits := s.Hits() 78 | if expected != hits { 79 | t.Fatalf("number of hits should be %d, but got %d", expected, hits) 80 | } 81 | } 82 | 83 | func TestStats_Misses(t *testing.T) { 84 | expected := generateCount(t) 85 | 86 | s := New() 87 | for i := int64(0); i < expected; i++ { 88 | s.IncMisses() 89 | } 90 | 91 | misses := s.Misses() 92 | if expected != misses { 93 | t.Fatalf("number of misses should be %d, but got %d", expected, misses) 94 | } 95 | } 96 | 97 | func TestStats_RejectedSets(t *testing.T) { 98 | expected := generateCount(t) 99 | 100 | s := New() 101 | for i := int64(0); i < expected; i++ { 102 | s.IncRejectedSets() 103 | } 104 | 105 | rejectedSets := s.RejectedSets() 106 | if expected != rejectedSets { 107 | t.Fatalf("number of rejected sets should be %d, but got %d", expected, rejectedSets) 108 | } 109 | } 110 | 111 | func TestStats_EvictedCount(t *testing.T) { 112 | expected := generateCount(t) 113 | 114 | s := New() 115 | for i := int64(0); i < expected; i++ { 116 | s.IncEvictedCount() 117 | } 118 | 119 | evictedCount := s.EvictedCount() 120 | if expected != evictedCount { 121 | t.Fatalf("number of evicted entries should be %d, but got %d", expected, evictedCount) 122 | } 123 | } 124 | 125 | func TestStats_EvictedCost(t *testing.T) { 126 | expected := generateCount(t) 127 | 128 | s := New() 129 | k := int64(0) 130 | for k < expected { 131 | add := 2 132 | if expected-k < 2 { 133 | add = 1 134 | } 135 | k += int64(add) 136 | s.AddEvictedCost(uint32(add)) 137 | } 138 | 139 | evictedCost := s.EvictedCost() 140 | if expected != evictedCost { 141 | t.Fatalf("sum of costs of evicted entries should be %d, but got %d", expected, evictedCost) 142 | } 143 | } 144 | 145 | func TestStats_Clear(t *testing.T) { 146 | s := New() 147 | 148 | count := generateCount(t) 149 | for i := int64(0); i < count; i++ { 150 | s.IncHits() 151 | } 152 | for i := int64(0); i < count; i++ { 153 | s.IncMisses() 154 | } 155 | 156 | misses := s.Misses() 157 | if count != misses { 158 | t.Fatalf("number of misses should be %d, but got %d", count, misses) 159 | } 160 | hits := s.Hits() 161 | if count != hits { 162 | t.Fatalf("number of hits should be %d, but got %d", count, hits) 163 | } 164 | 165 | s.Clear() 166 | 167 | hits = s.Hits() 168 | misses = s.Misses() 169 | if hits != 0 || misses != 0 { 170 | t.Fatalf("hits and misses after clear should be 0, but got hits: %d and misses: %d", hits, misses) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /internal/unixtime/unixtime.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unixtime 16 | 17 | import ( 18 | "sync" 19 | "sync/atomic" 20 | "time" 21 | ) 22 | 23 | var ( 24 | // We need this package because time.Now() is slower, allocates memory, 25 | // and we don't need a more precise time for the expiry time (and most other operations). 26 | now uint32 27 | startTime int64 28 | 29 | mutex sync.Mutex 30 | countInstance int 31 | done chan struct{} 32 | ) 33 | 34 | func startTimer() { 35 | done = make(chan struct{}) 36 | atomic.StoreInt64(&startTime, time.Now().Unix()) 37 | atomic.StoreUint32(&now, uint32(0)) 38 | 39 | go func() { 40 | ticker := time.NewTicker(time.Second) 41 | defer ticker.Stop() 42 | for { 43 | select { 44 | case t := <-ticker.C: 45 | //nolint:gosec // there will never be an overflow 46 | atomic.StoreUint32(&now, uint32(t.Unix()-StartTime())) 47 | case <-done: 48 | return 49 | } 50 | } 51 | }() 52 | } 53 | 54 | // Start should be called when the cache instance is created to initialize the timer. 55 | func Start() { 56 | mutex.Lock() 57 | defer mutex.Unlock() 58 | 59 | if countInstance == 0 { 60 | startTimer() 61 | } 62 | 63 | countInstance++ 64 | } 65 | 66 | // Stop should be called when closing and stopping the cache instance to stop the timer. 67 | func Stop() { 68 | mutex.Lock() 69 | defer mutex.Unlock() 70 | 71 | countInstance-- 72 | if countInstance == 0 { 73 | done <- struct{}{} 74 | close(done) 75 | } 76 | } 77 | 78 | // Now returns time as a Unix time, the number of seconds elapsed since program start. 79 | func Now() uint32 { 80 | return atomic.LoadUint32(&now) 81 | } 82 | 83 | // SetNow sets the current time. 84 | // 85 | // NOTE: use only for testing and debugging. 86 | func SetNow(t uint32) { 87 | atomic.StoreUint32(&now, t) 88 | } 89 | 90 | // StartTime returns the start time of the program. 91 | func StartTime() int64 { 92 | return atomic.LoadInt64(&startTime) 93 | } 94 | -------------------------------------------------------------------------------- /internal/unixtime/unixtime_bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unixtime 16 | 17 | import ( 18 | "sync/atomic" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func BenchmarkNow(b *testing.B) { 24 | Start() 25 | 26 | b.ReportAllocs() 27 | b.RunParallel(func(pb *testing.PB) { 28 | var ts uint32 29 | for pb.Next() { 30 | ts += Now() 31 | } 32 | atomic.StoreUint32(&sink, ts) 33 | }) 34 | 35 | Stop() 36 | } 37 | 38 | func BenchmarkTimeNowUnix(b *testing.B) { 39 | b.ReportAllocs() 40 | b.RunParallel(func(pb *testing.PB) { 41 | var ts uint32 42 | for pb.Next() { 43 | ts += uint32(time.Now().Unix()) 44 | } 45 | atomic.StoreUint32(&sink, ts) 46 | }) 47 | } 48 | 49 | // sink should prevent from code elimination by optimizing compiler. 50 | var sink uint32 51 | -------------------------------------------------------------------------------- /internal/unixtime/unixtime_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unixtime 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestNow(t *testing.T) { 23 | Start() 24 | 25 | got := Now() 26 | if got != 0 { 27 | t.Fatalf("unexpected time since program start; got %d; want %d", got, 0) 28 | } 29 | 30 | time.Sleep(3 * time.Second) 31 | 32 | got = Now() 33 | if got != 2 && got != 3 { 34 | t.Fatalf("unexpected time since program start; got %d; want %d", got, 3) 35 | } 36 | 37 | Stop() 38 | 39 | time.Sleep(3 * time.Second) 40 | 41 | if Now()-got > 1 { 42 | t.Fatal("timer should have stopped") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/xmath/power.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package xmath 16 | 17 | // RoundUpPowerOf2 is based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2. 18 | func RoundUpPowerOf2(v uint32) uint32 { 19 | if v == 0 { 20 | return 1 21 | } 22 | v-- 23 | v |= v >> 1 24 | v |= v >> 2 25 | v |= v >> 4 26 | v |= v >> 8 27 | v |= v >> 16 28 | v++ 29 | return v 30 | } 31 | -------------------------------------------------------------------------------- /internal/xruntime/runtime.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.22 2 | 3 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package xruntime 18 | 19 | import ( 20 | _ "unsafe" 21 | ) 22 | 23 | //go:noescape 24 | //go:linkname Fastrand runtime.fastrand 25 | func Fastrand() uint32 26 | -------------------------------------------------------------------------------- /internal/xruntime/runtime_1.22.go: -------------------------------------------------------------------------------- 1 | //go:build go1.22 2 | 3 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package xruntime 18 | 19 | import ( 20 | "math/rand/v2" 21 | ) 22 | 23 | func Fastrand() uint32 { 24 | //nolint:gosec // we don't need a cryptographically secure random number generator 25 | return rand.Uint32() 26 | } 27 | -------------------------------------------------------------------------------- /internal/xruntime/xruntime.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package xruntime 16 | 17 | import ( 18 | "runtime" 19 | ) 20 | 21 | const ( 22 | // CacheLineSize is useful for preventing false sharing. 23 | CacheLineSize = 64 24 | ) 25 | 26 | // Parallelism returns the maximum possible number of concurrently running goroutines. 27 | func Parallelism() uint32 { 28 | //nolint:gosec // there will never be an overflow 29 | maxProcs := uint32(runtime.GOMAXPROCS(0)) 30 | //nolint:gosec // there will never be an overflow 31 | numCPU := uint32(runtime.NumCPU()) 32 | if maxProcs < numCPU { 33 | return maxProcs 34 | } 35 | return numCPU 36 | } 37 | -------------------------------------------------------------------------------- /scripts/deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | (which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | 6 | bash -s -- -b "$(go env GOPATH)"/bin v1.62.0 7 | 8 | go install github.com/daixiang0/gci@latest 9 | GO111MODULE=on go install mvdan.cc/gofumpt@latest 10 | -------------------------------------------------------------------------------- /scripts/pre-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run CI tests 4 | readonly ci_cmd="make ci" 5 | eval "$ci_cmd" 6 | 7 | readonly result=$? 8 | if [ $result -ne 0 ]; then 9 | echo -e "CI tests Failed!\n CMD: $ci_cmd" 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "math" 19 | 20 | "github.com/maypok86/otter/internal/stats" 21 | ) 22 | 23 | // Stats is a statistics snapshot. 24 | type Stats struct { 25 | hits int64 26 | misses int64 27 | rejectedSets int64 28 | evictedCount int64 29 | evictedCost int64 30 | } 31 | 32 | func newStats(s *stats.Stats) Stats { 33 | return Stats{ 34 | hits: negativeToMax(s.Hits()), 35 | misses: negativeToMax(s.Misses()), 36 | rejectedSets: negativeToMax(s.RejectedSets()), 37 | evictedCount: negativeToMax(s.EvictedCount()), 38 | evictedCost: negativeToMax(s.EvictedCost()), 39 | } 40 | } 41 | 42 | // Hits returns the number of cache hits. 43 | func (s Stats) Hits() int64 { 44 | return s.hits 45 | } 46 | 47 | // Misses returns the number of cache misses. 48 | func (s Stats) Misses() int64 { 49 | return s.misses 50 | } 51 | 52 | // Ratio returns the cache hit ratio. 53 | func (s Stats) Ratio() float64 { 54 | requests := checkedAdd(s.hits, s.misses) 55 | if requests == 0 { 56 | return 0.0 57 | } 58 | return float64(s.hits) / float64(requests) 59 | } 60 | 61 | // RejectedSets returns the number of rejected sets. 62 | func (s Stats) RejectedSets() int64 { 63 | return s.rejectedSets 64 | } 65 | 66 | // EvictedCount returns the number of evicted entries. 67 | func (s Stats) EvictedCount() int64 { 68 | return s.evictedCount 69 | } 70 | 71 | // EvictedCost returns the sum of costs of evicted entries. 72 | func (s Stats) EvictedCost() int64 { 73 | return s.evictedCost 74 | } 75 | 76 | func checkedAdd(a, b int64) int64 { 77 | naiveSum := a + b 78 | if (a^b) < 0 || (a^naiveSum) >= 0 { 79 | // If a and b have different signs or a has the same sign as the result then there was no overflow, return. 80 | return naiveSum 81 | } 82 | // we did over/under flow, if the sign is negative we should return math.MaxInt64 otherwise math.MinInt64. 83 | if naiveSum < 0 { 84 | return math.MaxInt64 85 | } 86 | return math.MinInt64 87 | } 88 | 89 | func negativeToMax(v int64) int64 { 90 | if v < 0 { 91 | return math.MaxInt64 92 | } 93 | 94 | return v 95 | } 96 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Alexey Mayshev. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package otter 16 | 17 | import ( 18 | "math" 19 | "testing" 20 | "unsafe" 21 | ) 22 | 23 | func TestStats(t *testing.T) { 24 | var s Stats 25 | if s.Ratio() != 0.0 { 26 | t.Fatalf("not valid hit ratio. want 0.0, got %.2f", s.Ratio()) 27 | } 28 | 29 | expected := int64(math.MaxInt64) 30 | 31 | s = Stats{ 32 | hits: math.MaxInt64, 33 | misses: math.MaxInt64, 34 | rejectedSets: math.MaxInt64, 35 | evictedCount: math.MaxInt64, 36 | evictedCost: math.MaxInt64, 37 | } 38 | 39 | if s.Hits() != expected { 40 | t.Fatalf("not valid hits. want %d, got %d", expected, s.Hits()) 41 | } 42 | 43 | if s.Misses() != expected { 44 | t.Fatalf("not valid misses. want %d, got %d", expected, s.Misses()) 45 | } 46 | 47 | if s.Ratio() != 1.0 { 48 | t.Fatalf("not valid hit ratio. want 1.0, got %.2f", s.Ratio()) 49 | } 50 | 51 | if s.RejectedSets() != expected { 52 | t.Fatalf("not valid rejected sets. want %d, got %d", expected, s.RejectedSets()) 53 | } 54 | 55 | if s.EvictedCount() != expected { 56 | t.Fatalf("not valid evicted count. want %d, got %d", expected, s.EvictedCount()) 57 | } 58 | 59 | if s.EvictedCost() != expected { 60 | t.Fatalf("not valid evicted cost. want %d, got %d", expected, s.EvictedCost()) 61 | } 62 | } 63 | 64 | func TestCheckedAdd_Overflow(t *testing.T) { 65 | if unsafe.Sizeof(t) != 8 { 66 | t.Skip() 67 | } 68 | 69 | a := int64(math.MaxInt64) 70 | b := int64(23) 71 | 72 | if got := checkedAdd(a, b); got != math.MaxInt64 { 73 | t.Fatalf("wrong overflow in checkedAdd. want %d, got %d", int64(math.MaxInt64), got) 74 | } 75 | } 76 | 77 | func TestCheckedAdd_Underflow(t *testing.T) { 78 | if unsafe.Sizeof(t) != 8 { 79 | t.Skip() 80 | } 81 | 82 | a := int64(math.MinInt64 + 10) 83 | b := int64(-23) 84 | 85 | if got := checkedAdd(a, b); got != math.MinInt64 { 86 | t.Fatalf("wrong underflow in checkedAdd. want %d, got %d", int64(math.MinInt64), got) 87 | } 88 | } 89 | --------------------------------------------------------------------------------