├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------