fix: 修复空提交问题
This commit is contained in:
@@ -2,13 +2,13 @@ package share
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math/rand"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newSessionStore 创建会话缓存实例
|
// newSessionStore 创建会话缓存实例
|
||||||
@@ -94,8 +94,8 @@ func (m *sessionManager) UserOnlineExists(userID uint32) (bool, error) {
|
|||||||
// generate6DigitCode 生成6位数字注册码(100000-999999)
|
// generate6DigitCode 生成6位数字注册码(100000-999999)
|
||||||
func (m *sessionManager) generate6DigitCode() int {
|
func (m *sessionManager) generate6DigitCode() int {
|
||||||
// 初始化随机数生成器(确保每次调用生成不同序列)
|
// 初始化随机数生成器(确保每次调用生成不同序列)
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
return r.Intn(900000) + 100000
|
return grand.Intn(900000) + 100000
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveEmailCode 生成并保存邮件注册码(返回生成的验证码,支持设置过期时间)
|
// SaveEmailCode 生成并保存邮件注册码(返回生成的验证码,支持设置过期时间)
|
||||||
|
|||||||
13
common/utils/golang-lru-main/.github/CODEOWNERS
vendored
13
common/utils/golang-lru-main/.github/CODEOWNERS
vendored
@@ -1,13 +0,0 @@
|
|||||||
# Each line is a file pattern followed by one or more owners.
|
|
||||||
# More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
|
|
||||||
|
|
||||||
# Default owner
|
|
||||||
* @hashicorp/team-ip-compliance @hashicorp/raft-force
|
|
||||||
|
|
||||||
# Add override rules below. Each line is a file/folder pattern followed by one or more owners.
|
|
||||||
# Being an owner means those groups or individuals will be added as reviewers to PRs affecting
|
|
||||||
# those areas of the code.
|
|
||||||
# Examples:
|
|
||||||
# /docs/ @docs-team
|
|
||||||
# *.js @js-team
|
|
||||||
# *.go @go-team
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Copyright (c) HashiCorp, Inc.
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
day: "sunday"
|
|
||||||
commit-message:
|
|
||||||
prefix: "[chore] : "
|
|
||||||
groups:
|
|
||||||
actions:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directories:
|
|
||||||
- "/"
|
|
||||||
- "/arc"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
day: "sunday"
|
|
||||||
commit-message:
|
|
||||||
prefix: "[chore] : "
|
|
||||||
groups:
|
|
||||||
go:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
applies-to: "version-updates"
|
|
||||||
go-security:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
applies-to: "security-updates"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- Provide a summary of what the PR does and why it is being submitted. -->
|
|
||||||
|
|
||||||
## Related Issue
|
|
||||||
|
|
||||||
<!-- If this PR is linked to any issue, provide the issue number or description here. Any related JIRA tickets can also be added here. -->
|
|
||||||
|
|
||||||
## How Has This Been Tested?
|
|
||||||
|
|
||||||
<!-- Describe how the changes have been tested. Provide test instructions or details. -->
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
tags: ["*"]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: set up go 1.19
|
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
|
||||||
with:
|
|
||||||
go-version: 1.19
|
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
|
|
||||||
- name: build, test and generate coverage report
|
|
||||||
run: |
|
|
||||||
go test -timeout=60s -race -v ./... -coverprofile=coverage.out
|
|
||||||
go build -race ./...
|
|
||||||
|
|
||||||
- name: build and test ARC
|
|
||||||
working-directory: ./arc
|
|
||||||
run: |
|
|
||||||
go test -timeout=60s -race
|
|
||||||
go build -race
|
|
||||||
|
|
||||||
- name: Upload the coverage report
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
path: coverage.out
|
|
||||||
name: Coverage-report
|
|
||||||
|
|
||||||
- name: Display the coverage report
|
|
||||||
run: go tool cover -func=coverage.out
|
|
||||||
|
|
||||||
- name: install golangci-lint
|
|
||||||
run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.53.3
|
|
||||||
|
|
||||||
- name: run golangci-lint
|
|
||||||
run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions ./... ./simplelru/... ./expirable/...
|
|
||||||
|
|
||||||
- name: run golangci-lint on ARC
|
|
||||||
working-directory: ./arc
|
|
||||||
run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions ./...
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: Two-Stage PR Review Process
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review, converted_to_draft]
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
manage-pr-status:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.0.0
|
|
||||||
|
|
||||||
- name: Two stage PR review
|
|
||||||
uses: hashicorp/two-stage-pr-approval@v0.1.0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1.19
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2014, 2025
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
linters:
|
|
||||||
fast: false
|
|
||||||
disable-all: true
|
|
||||||
enable:
|
|
||||||
- revive
|
|
||||||
- megacheck
|
|
||||||
- govet
|
|
||||||
- unconvert
|
|
||||||
- gas
|
|
||||||
- gocyclo
|
|
||||||
- dupl
|
|
||||||
- misspell
|
|
||||||
- unparam
|
|
||||||
- unused
|
|
||||||
- typecheck
|
|
||||||
- ineffassign
|
|
||||||
# - stylecheck
|
|
||||||
- exportloopref
|
|
||||||
- gocritic
|
|
||||||
- nakedret
|
|
||||||
- gosimple
|
|
||||||
- prealloc
|
|
||||||
|
|
||||||
# golangci-lint configuration file
|
|
||||||
linters-settings:
|
|
||||||
revive:
|
|
||||||
ignore-generated-header: true
|
|
||||||
severity: warning
|
|
||||||
rules:
|
|
||||||
- name: package-comments
|
|
||||||
severity: warning
|
|
||||||
disabled: true
|
|
||||||
- name: exported
|
|
||||||
severity: warning
|
|
||||||
disabled: false
|
|
||||||
arguments: ["checkPrivateReceivers", "disableStutteringCheck"]
|
|
||||||
|
|
||||||
issues:
|
|
||||||
exclude-use-default: false
|
|
||||||
exclude-rules:
|
|
||||||
- path: _test\.go
|
|
||||||
linters:
|
|
||||||
- dupl
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package lru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Default2QRecentRatio is the ratio of the 2Q cache dedicated
|
|
||||||
// to recently added entries that have only been accessed once.
|
|
||||||
Default2QRecentRatio = 0.25
|
|
||||||
|
|
||||||
// Default2QGhostEntries is the default ratio of ghost
|
|
||||||
// entries kept to track entries recently evicted
|
|
||||||
Default2QGhostEntries = 0.50
|
|
||||||
)
|
|
||||||
|
|
||||||
// TwoQueueCache is a thread-safe fixed size 2Q cache.
|
|
||||||
// 2Q is an enhancement over the standard LRU cache
|
|
||||||
// in that it tracks both frequently and recently used
|
|
||||||
// entries separately. This avoids a burst in access to new
|
|
||||||
// entries from evicting frequently used entries. It adds some
|
|
||||||
// additional tracking overhead to the standard LRU cache, and is
|
|
||||||
// computationally about 2x the cost, and adds some metadata over
|
|
||||||
// head. The ARCCache is similar, but does not require setting any
|
|
||||||
// parameters.
|
|
||||||
type TwoQueueCache[K comparable, V any] struct {
|
|
||||||
size int
|
|
||||||
recentSize int
|
|
||||||
recentRatio float64
|
|
||||||
ghostRatio float64
|
|
||||||
|
|
||||||
recent simplelru.LRUCache[K, V]
|
|
||||||
frequent simplelru.LRUCache[K, V]
|
|
||||||
recentEvict simplelru.LRUCache[K, struct{}]
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New2Q creates a new TwoQueueCache using the default
|
|
||||||
// values for the parameters.
|
|
||||||
func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {
|
|
||||||
return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New2QParams creates a new TwoQueueCache using the provided
|
|
||||||
// parameter values.
|
|
||||||
func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {
|
|
||||||
if size <= 0 {
|
|
||||||
return nil, errors.New("invalid size")
|
|
||||||
}
|
|
||||||
if recentRatio < 0.0 || recentRatio > 1.0 {
|
|
||||||
return nil, errors.New("invalid recent ratio")
|
|
||||||
}
|
|
||||||
if ghostRatio < 0.0 || ghostRatio > 1.0 {
|
|
||||||
return nil, errors.New("invalid ghost ratio")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the sub-sizes
|
|
||||||
recentSize := int(float64(size) * recentRatio)
|
|
||||||
evictSize := int(float64(size) * ghostRatio)
|
|
||||||
|
|
||||||
// Allocate the LRUs
|
|
||||||
recent, err := simplelru.NewLRU[K, V](size, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
frequent, err := simplelru.NewLRU[K, V](size, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the cache
|
|
||||||
c := &TwoQueueCache[K, V]{
|
|
||||||
size: size,
|
|
||||||
recentSize: recentSize,
|
|
||||||
recentRatio: recentRatio,
|
|
||||||
ghostRatio: ghostRatio,
|
|
||||||
recent: recent,
|
|
||||||
frequent: frequent,
|
|
||||||
recentEvict: recentEvict,
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// Check if this is a frequent value
|
|
||||||
if val, ok := c.frequent.Get(key); ok {
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the value is contained in recent, then we
|
|
||||||
// promote it to frequent
|
|
||||||
if val, ok := c.recent.Peek(key); ok {
|
|
||||||
c.recent.Remove(key)
|
|
||||||
c.frequent.Add(key, val)
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// No hit
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache.
|
|
||||||
func (c *TwoQueueCache[K, V]) Add(key K, value V) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// Check if the value is frequently used already,
|
|
||||||
// and just update the value
|
|
||||||
if c.frequent.Contains(key) {
|
|
||||||
c.frequent.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the value is recently used, and promote
|
|
||||||
// the value into the frequent list
|
|
||||||
if c.recent.Contains(key) {
|
|
||||||
c.recent.Remove(key)
|
|
||||||
c.frequent.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the value was recently evicted, add it to the
|
|
||||||
// frequently used list
|
|
||||||
if c.recentEvict.Contains(key) {
|
|
||||||
c.ensureSpace(true)
|
|
||||||
c.recentEvict.Remove(key)
|
|
||||||
c.frequent.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to the recently seen list
|
|
||||||
c.ensureSpace(false)
|
|
||||||
c.recent.Add(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureSpace is used to ensure we have space in the cache
|
|
||||||
func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {
|
|
||||||
// If we have space, nothing to do
|
|
||||||
recentLen := c.recent.Len()
|
|
||||||
freqLen := c.frequent.Len()
|
|
||||||
if recentLen+freqLen < c.size {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the recent buffer is larger than
|
|
||||||
// the target, evict from there
|
|
||||||
if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
|
|
||||||
k, _, _ := c.recent.RemoveOldest()
|
|
||||||
c.recentEvict.Add(k, struct{}{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from the frequent list otherwise
|
|
||||||
c.frequent.RemoveOldest()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of items in the cache.
|
|
||||||
func (c *TwoQueueCache[K, V]) Len() int {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
return c.recent.Len() + c.frequent.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap returns the capacity of the cache
|
|
||||||
func (c *TwoQueueCache[K, V]) Cap() int {
|
|
||||||
return c.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize changes the cache size.
|
|
||||||
func (c *TwoQueueCache[K, V]) Resize(size int) (evicted int) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// Recalculate the sub-sizes
|
|
||||||
recentSize := int(float64(size) * c.recentRatio)
|
|
||||||
evictSize := int(float64(size) * c.ghostRatio)
|
|
||||||
c.size = size
|
|
||||||
c.recentSize = recentSize
|
|
||||||
|
|
||||||
// ensureSpace
|
|
||||||
diff := c.recent.Len() + c.frequent.Len() - size
|
|
||||||
if diff < 0 {
|
|
||||||
diff = 0
|
|
||||||
}
|
|
||||||
for i := 0; i < diff; i++ {
|
|
||||||
c.ensureSpace(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reallocate the LRUs
|
|
||||||
c.recent.Resize(size)
|
|
||||||
c.frequent.Resize(size)
|
|
||||||
c.recentEvict.Resize(evictSize)
|
|
||||||
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of the keys in the cache.
|
|
||||||
// The frequently used keys are first in the returned slice.
|
|
||||||
func (c *TwoQueueCache[K, V]) Keys() []K {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
k1 := c.frequent.Keys()
|
|
||||||
k2 := c.recent.Keys()
|
|
||||||
return append(k1, k2...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns a slice of the values in the cache.
|
|
||||||
// The frequently used values are first in the returned slice.
|
|
||||||
func (c *TwoQueueCache[K, V]) Values() []V {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
v1 := c.frequent.Values()
|
|
||||||
v2 := c.recent.Values()
|
|
||||||
return append(v1, v2...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the provided key from the cache.
|
|
||||||
func (c *TwoQueueCache[K, V]) Remove(key K) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
if c.frequent.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.recent.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.recentEvict.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge is used to completely clear the cache.
|
|
||||||
func (c *TwoQueueCache[K, V]) Purge() {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
c.recent.Purge()
|
|
||||||
c.frequent.Purge()
|
|
||||||
c.recentEvict.Purge()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains is used to check if the cache contains a key
|
|
||||||
// without updating recency or frequency.
|
|
||||||
func (c *TwoQueueCache[K, V]) Contains(key K) bool {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
return c.frequent.Contains(key) || c.recent.Contains(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek is used to inspect the cache value of a key
|
|
||||||
// without updating recency or frequency.
|
|
||||||
func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
if val, ok := c.frequent.Peek(key); ok {
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
return c.recent.Peek(key)
|
|
||||||
}
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package lru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Benchmark2Q_Rand(b *testing.B) {
|
|
||||||
l, err := New2Q[int64, int64](8192)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < 2*b.N; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
} else {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Benchmark2Q_Freq(b *testing.B) {
|
|
||||||
l, err := New2Q[int64, int64](8192)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
trace[i] = getRand(b) % 16384
|
|
||||||
} else {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
}
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test2Q_RandomOps(t *testing.T) {
|
|
||||||
size := 128
|
|
||||||
l, err := New2Q[int64, int64](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n := 200000
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
key := getRand(t) % 512
|
|
||||||
r := getRand(t)
|
|
||||||
switch r % 3 {
|
|
||||||
case 0:
|
|
||||||
l.Add(key, key)
|
|
||||||
case 1:
|
|
||||||
l.Get(key)
|
|
||||||
case 2:
|
|
||||||
l.Remove(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.recent.Len()+l.frequent.Len() > size {
|
|
||||||
t.Fatalf("bad: recent: %d freq: %d",
|
|
||||||
l.recent.Len(), l.frequent.Len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test2Q_Get_RecentToFrequent(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch all the entries, should be in t1
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if n := l.recent.Len(); n != 128 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get should upgrade to t2
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("missing: %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n := l.recent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 128 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get be from t2
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("missing: %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n := l.recent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 128 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test2Q_Add_RecentToFrequent(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add initially to recent
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.recent.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add should upgrade to frequent
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.recent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add should remain in frequent
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.recent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test2Q_Add_RecentEvict(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](4)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 1,2,3,4,5 -> Evict 1
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
l.Add(3, 3)
|
|
||||||
l.Add(4, 4)
|
|
||||||
l.Add(5, 5)
|
|
||||||
if n := l.recent.Len(); n != 4 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.recentEvict.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull in the recently evicted
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.recent.Len(); n != 3 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.recentEvict.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 6, should cause another recent evict
|
|
||||||
l.Add(6, 6)
|
|
||||||
if n := l.recent.Len(); n != 3 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.recentEvict.Len(); n != 2 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test2Q_Resize(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch all the entries, should be in t1
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
evicted := l.Resize(50)
|
|
||||||
if evicted != 50 {
|
|
||||||
t.Fatalf("bad: %d", evicted)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := l.recent.Len(); n != 50 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err = New2Q[int, int](100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 50; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
evicted = l.Resize(50)
|
|
||||||
if evicted != 50 {
|
|
||||||
t.Fatalf("bad: %d", evicted)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := l.recent.Len(); n != 12 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 38 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err = New2Q[int, int](100)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
evicted = l.Resize(50)
|
|
||||||
if evicted != 50 {
|
|
||||||
t.Fatalf("bad: %d", evicted)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := l.recent.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.frequent.Len(); n != 50 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test2Q(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if l.Len() != 128 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if l.Cap() != 128 {
|
|
||||||
t.Fatalf("expect %d, but %d", 128, l.Cap())
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, k := range l.Keys() {
|
|
||||||
if v, ok := l.Get(k); !ok || v != k || v != i+128 {
|
|
||||||
t.Fatalf("bad key: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, v := range l.Values() {
|
|
||||||
if v != i+128 {
|
|
||||||
t.Fatalf("bad key: %v", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 256; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("should not be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 192; i++ {
|
|
||||||
l.Remove(i)
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be deleted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Purge()
|
|
||||||
if l.Len() != 0 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if _, ok := l.Get(200); ok {
|
|
||||||
t.Fatalf("should contain nothing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Contains doesn't update recent-ness
|
|
||||||
func Test2Q_Contains(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if !l.Contains(1) {
|
|
||||||
t.Errorf("1 should be contained")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("Contains should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Peek doesn't update recent-ness
|
|
||||||
func Test2Q_Peek(t *testing.T) {
|
|
||||||
l, err := New2Q[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if v, ok := l.Peek(1); !ok || v != 1 {
|
|
||||||
t.Errorf("1 should be set to 1: %v, %v", v, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
## Unreleased
|
|
||||||
|
|
||||||
### Improvements
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
### Security
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
Copyright IBM Corp. 2014, 2025
|
|
||||||
|
|
||||||
Mozilla Public License, version 2.0
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
|
|
||||||
means each individual or legal entity that creates, contributes to the
|
|
||||||
creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
|
|
||||||
means the combination of the Contributions of others (if any) used by a
|
|
||||||
Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
|
|
||||||
means Source Code Form to which the initial Contributor has attached the
|
|
||||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
|
||||||
Modifications of such Source Code Form, in each case including portions
|
|
||||||
thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
a. that the initial Contributor has attached the notice described in
|
|
||||||
Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
b. that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the terms of
|
|
||||||
a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
|
|
||||||
means a work that combines Covered Software with other material, in a
|
|
||||||
separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
|
|
||||||
means having the right to grant, to the maximum extent possible, whether
|
|
||||||
at the time of the initial grant or subsequently, any and all of the
|
|
||||||
rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
a. any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered Software; or
|
|
||||||
|
|
||||||
b. any new file in Source Code Form that contains any Covered Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the License,
|
|
||||||
by the making, using, selling, offering for sale, having made, import,
|
|
||||||
or transfer of either its Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
|
||||||
General Public License, Version 2.1, the GNU Affero General Public
|
|
||||||
License, Version 3.0, or any later versions of those licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that controls, is
|
|
||||||
controlled by, or is under common control with You. For purposes of this
|
|
||||||
definition, "control" means (a) the power, direct or indirect, to cause
|
|
||||||
the direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
|
||||||
outstanding shares or beneficial ownership of such entity.
|
|
||||||
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
a. under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
|
||||||
sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
a. for any code that a Contributor has removed from Covered Software; or
|
|
||||||
|
|
||||||
b. for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
c. under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights to
|
|
||||||
grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
|
||||||
Section 2.1.
|
|
||||||
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
a. such Covered Software must also be made available in Source Code Form,
|
|
||||||
as described in Section 3.1, and You must inform recipients of the
|
|
||||||
Executable Form how they can obtain a copy of such Source Code Form by
|
|
||||||
reasonable means in a timely manner, at a charge no more than the cost
|
|
||||||
of distribution to the recipient; and
|
|
||||||
|
|
||||||
b. You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter the
|
|
||||||
recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
|
||||||
limitations of liability) contained within the Source Code Form of the
|
|
||||||
Covered Software, except that You may alter any license notices to the
|
|
||||||
extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this License
|
|
||||||
with respect to some or all of the Covered Software due to statute,
|
|
||||||
judicial order, or regulation then You must: (a) comply with the terms of
|
|
||||||
this License to the maximum extent possible; and (b) describe the
|
|
||||||
limitations and the code they affect. Such description must be placed in a
|
|
||||||
text file included with all distributions of the Covered Software under
|
|
||||||
this License. Except to the extent prohibited by statute or regulation,
|
|
||||||
such description must be sufficiently detailed for a recipient of ordinary
|
|
||||||
skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically if You
|
|
||||||
fail to comply with any of its terms. However, if You become compliant,
|
|
||||||
then the rights granted under this License from a particular Contributor
|
|
||||||
are reinstated (a) provisionally, unless and until such Contributor
|
|
||||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
|
||||||
basis, if such Contributor fails to notify You of the non-compliance by
|
|
||||||
some reasonable means prior to 60 days after You have come back into
|
|
||||||
compliance. Moreover, Your grants from a particular Contributor are
|
|
||||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
|
||||||
non-compliance by some reasonable means, this is the first time You have
|
|
||||||
received notice of non-compliance with this License from such
|
|
||||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
|
||||||
of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
|
||||||
license agreements (excluding distributors and resellers) which have been
|
|
||||||
validly granted by You or Your distributors under this License prior to
|
|
||||||
termination shall survive termination.
|
|
||||||
|
|
||||||
6. Disclaimer of Warranty
|
|
||||||
|
|
||||||
Covered Software is provided under this License on an "as is" basis,
|
|
||||||
without warranty of any kind, either expressed, implied, or statutory,
|
|
||||||
including, without limitation, warranties that the Covered Software is free
|
|
||||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
|
||||||
The entire risk as to the quality and performance of the Covered Software
|
|
||||||
is with You. Should any Covered Software prove defective in any respect,
|
|
||||||
You (not any Contributor) assume the cost of any necessary servicing,
|
|
||||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
|
||||||
part of this License. No use of any Covered Software is authorized under
|
|
||||||
this License except under this disclaimer.
|
|
||||||
|
|
||||||
7. Limitation of Liability
|
|
||||||
|
|
||||||
Under no circumstances and under no legal theory, whether tort (including
|
|
||||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
|
||||||
distributes Covered Software as permitted above, be liable to You for any
|
|
||||||
direct, indirect, special, incidental, or consequential damages of any
|
|
||||||
character including, without limitation, damages for lost profits, loss of
|
|
||||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses, even if such party shall have been
|
|
||||||
informed of the possibility of such damages. This limitation of liability
|
|
||||||
shall not apply to liability for death or personal injury resulting from
|
|
||||||
such party's negligence to the extent applicable law prohibits such
|
|
||||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
|
||||||
incidental or consequential damages, so this exclusion and limitation may
|
|
||||||
not apply to You.
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the courts
|
|
||||||
of a jurisdiction where the defendant maintains its principal place of
|
|
||||||
business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
|
||||||
in this Section shall prevent a party's ability to bring cross-claims or
|
|
||||||
counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides that
|
|
||||||
the language of a contract shall be construed against the drafter shall not
|
|
||||||
be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses If You choose to distribute Source Code Form that is
|
|
||||||
Incompatible With Secondary Licenses under the terms of this version of
|
|
||||||
the License, the notice described in Exhibit B of this License must be
|
|
||||||
attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
|
|
||||||
This Source Code Form is subject to the
|
|
||||||
terms of the Mozilla Public License, v.
|
|
||||||
2.0. If a copy of the MPL was not
|
|
||||||
distributed with this file, You can
|
|
||||||
obtain one at
|
|
||||||
http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular file,
|
|
||||||
then You may include the notice in a location (such as a LICENSE file in a
|
|
||||||
relevant directory) where a recipient would be likely to look for such a
|
|
||||||
notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible
|
|
||||||
With Secondary Licenses", as defined by
|
|
||||||
the Mozilla Public License, v. 2.0.
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
golang-lru
|
|
||||||
==========
|
|
||||||
|
|
||||||
This provides the `lru` package which implements a fixed-size
|
|
||||||
thread safe LRU cache. It is based on the cache in Groupcache.
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
=============
|
|
||||||
|
|
||||||
Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2)
|
|
||||||
|
|
||||||
LRU cache example
|
|
||||||
=================
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/hashicorp/golang-lru/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
l, _ := lru.New[int, any](128)
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
l.Add(i, nil)
|
|
||||||
}
|
|
||||||
if l.Len() != 128 {
|
|
||||||
panic(fmt.Sprintf("bad len: %v", l.Len()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Expirable LRU cache example
|
|
||||||
===========================
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// make cache with 10ms TTL and 5 max keys
|
|
||||||
cache := expirable.NewLRU[string, string](5, nil, time.Millisecond*10)
|
|
||||||
|
|
||||||
|
|
||||||
// set value under key1.
|
|
||||||
cache.Add("key1", "val1")
|
|
||||||
|
|
||||||
// get value under key1
|
|
||||||
r, ok := cache.Get("key1")
|
|
||||||
|
|
||||||
// check for OK value
|
|
||||||
if ok {
|
|
||||||
fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for cache to expire
|
|
||||||
time.Sleep(time.Millisecond * 12)
|
|
||||||
|
|
||||||
// get value under key1 after key expiration
|
|
||||||
r, ok = cache.Get("key1")
|
|
||||||
fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r)
|
|
||||||
|
|
||||||
// set value under key2, would evict old entry because it is already expired.
|
|
||||||
cache.Add("key2", "val2")
|
|
||||||
|
|
||||||
fmt.Printf("Cache len: %d\n", cache.Len())
|
|
||||||
// Output:
|
|
||||||
// value before expiration is found: true, value: "val1"
|
|
||||||
// value after expiration is found: false, value: ""
|
|
||||||
// Cache len: 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package arc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ARCCache is a thread-safe fixed size Adaptive Replacement Cache (ARC).
|
|
||||||
// ARC is an enhancement over the standard LRU cache in that tracks both
|
|
||||||
// frequency and recency of use. This avoids a burst in access to new
|
|
||||||
// entries from evicting the frequently used older entries. It adds some
|
|
||||||
// additional tracking overhead to a standard LRU cache, computationally
|
|
||||||
// it is roughly 2x the cost, and the extra memory overhead is linear
|
|
||||||
// with the size of the cache. ARC has been patented by IBM, but is
|
|
||||||
// similar to the TwoQueueCache (2Q) which requires setting parameters.
|
|
||||||
type ARCCache[K comparable, V any] struct {
|
|
||||||
size int // Size is the total capacity of the cache
|
|
||||||
p int // P is the dynamic preference towards T1 or T2
|
|
||||||
|
|
||||||
t1 simplelru.LRUCache[K, V] // T1 is the LRU for recently accessed items
|
|
||||||
b1 simplelru.LRUCache[K, struct{}] // B1 is the LRU for evictions from t1
|
|
||||||
|
|
||||||
t2 simplelru.LRUCache[K, V] // T2 is the LRU for frequently accessed items
|
|
||||||
b2 simplelru.LRUCache[K, struct{}] // B2 is the LRU for evictions from t2
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewARC creates an ARC of the given size
|
|
||||||
func NewARC[K comparable, V any](size int) (*ARCCache[K, V], error) {
|
|
||||||
// Create the sub LRUs
|
|
||||||
b1, err := simplelru.NewLRU[K, struct{}](size, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b2, err := simplelru.NewLRU[K, struct{}](size, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t1, err := simplelru.NewLRU[K, V](size, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t2, err := simplelru.NewLRU[K, V](size, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the ARC
|
|
||||||
c := &ARCCache[K, V]{
|
|
||||||
size: size,
|
|
||||||
p: 0,
|
|
||||||
t1: t1,
|
|
||||||
b1: b1,
|
|
||||||
t2: t2,
|
|
||||||
b2: b2,
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *ARCCache[K, V]) Get(key K) (value V, ok bool) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// If the value is contained in T1 (recent), then
|
|
||||||
// promote it to T2 (frequent)
|
|
||||||
if val, ok := c.t1.Peek(key); ok {
|
|
||||||
c.t1.Remove(key)
|
|
||||||
c.t2.Add(key, val)
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the value is contained in T2 (frequent)
|
|
||||||
if val, ok := c.t2.Get(key); ok {
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// No hit
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache.
|
|
||||||
func (c *ARCCache[K, V]) Add(key K, value V) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// Check if the value is contained in T1 (recent), and potentially
|
|
||||||
// promote it to frequent T2
|
|
||||||
if c.t1.Contains(key) {
|
|
||||||
c.t1.Remove(key)
|
|
||||||
c.t2.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the value is already in T2 (frequent) and update it
|
|
||||||
if c.t2.Contains(key) {
|
|
||||||
c.t2.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this value was recently evicted as part of the
|
|
||||||
// recently used list
|
|
||||||
if c.b1.Contains(key) {
|
|
||||||
// T1 set is too small, increase P appropriately
|
|
||||||
delta := 1
|
|
||||||
b1Len := c.b1.Len()
|
|
||||||
b2Len := c.b2.Len()
|
|
||||||
if b2Len > b1Len {
|
|
||||||
delta = b2Len / b1Len
|
|
||||||
}
|
|
||||||
if c.p+delta >= c.size {
|
|
||||||
c.p = c.size
|
|
||||||
} else {
|
|
||||||
c.p += delta
|
|
||||||
}
|
|
||||||
|
|
||||||
// Potentially need to make room in the cache
|
|
||||||
if c.t1.Len()+c.t2.Len() >= c.size {
|
|
||||||
c.replace(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from B1
|
|
||||||
c.b1.Remove(key)
|
|
||||||
|
|
||||||
// Add the key to the frequently used list
|
|
||||||
c.t2.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this value was recently evicted as part of the
|
|
||||||
// frequently used list
|
|
||||||
if c.b2.Contains(key) {
|
|
||||||
// T2 set is too small, decrease P appropriately
|
|
||||||
delta := 1
|
|
||||||
b1Len := c.b1.Len()
|
|
||||||
b2Len := c.b2.Len()
|
|
||||||
if b1Len > b2Len {
|
|
||||||
delta = b1Len / b2Len
|
|
||||||
}
|
|
||||||
if delta >= c.p {
|
|
||||||
c.p = 0
|
|
||||||
} else {
|
|
||||||
c.p -= delta
|
|
||||||
}
|
|
||||||
|
|
||||||
// Potentially need to make room in the cache
|
|
||||||
if c.t1.Len()+c.t2.Len() >= c.size {
|
|
||||||
c.replace(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from B2
|
|
||||||
c.b2.Remove(key)
|
|
||||||
|
|
||||||
// Add the key to the frequently used list
|
|
||||||
c.t2.Add(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Potentially need to make room in the cache
|
|
||||||
if c.t1.Len()+c.t2.Len() >= c.size {
|
|
||||||
c.replace(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the size of the ghost buffers trim
|
|
||||||
if c.b1.Len() > c.size-c.p {
|
|
||||||
c.b1.RemoveOldest()
|
|
||||||
}
|
|
||||||
if c.b2.Len() > c.p {
|
|
||||||
c.b2.RemoveOldest()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to the recently seen list
|
|
||||||
c.t1.Add(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace is used to adaptively evict from either T1 or T2
|
|
||||||
// based on the current learned value of P
|
|
||||||
func (c *ARCCache[K, V]) replace(b2ContainsKey bool) {
|
|
||||||
t1Len := c.t1.Len()
|
|
||||||
if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) {
|
|
||||||
k, _, ok := c.t1.RemoveOldest()
|
|
||||||
if ok {
|
|
||||||
c.b1.Add(k, struct{}{})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
k, _, ok := c.t2.RemoveOldest()
|
|
||||||
if ok {
|
|
||||||
c.b2.Add(k, struct{}{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of cached entries
|
|
||||||
func (c *ARCCache[K, V]) Len() int {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
return c.t1.Len() + c.t2.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap returns the capacity of the cache
|
|
||||||
func (c *ARCCache[K, V]) Cap() int {
|
|
||||||
return c.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns all the cached keys
|
|
||||||
func (c *ARCCache[K, V]) Keys() []K {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
k1 := c.t1.Keys()
|
|
||||||
k2 := c.t2.Keys()
|
|
||||||
return append(k1, k2...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns all the cached values
|
|
||||||
func (c *ARCCache[K, V]) Values() []V {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
v1 := c.t1.Values()
|
|
||||||
v2 := c.t2.Values()
|
|
||||||
return append(v1, v2...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove is used to purge a key from the cache
|
|
||||||
func (c *ARCCache[K, V]) Remove(key K) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
if c.t1.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.t2.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.b1.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.b2.Remove(key) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge is used to clear the cache
|
|
||||||
func (c *ARCCache[K, V]) Purge() {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
c.t1.Purge()
|
|
||||||
c.t2.Purge()
|
|
||||||
c.b1.Purge()
|
|
||||||
c.b2.Purge()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains is used to check if the cache contains a key
|
|
||||||
// without updating recency or frequency.
|
|
||||||
func (c *ARCCache[K, V]) Contains(key K) bool {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
return c.t1.Contains(key) || c.t2.Contains(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek is used to inspect the cache value of a key
|
|
||||||
// without updating recency or frequency.
|
|
||||||
func (c *ARCCache[K, V]) Peek(key K) (value V, ok bool) {
|
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
if val, ok := c.t1.Peek(key); ok {
|
|
||||||
return val, ok
|
|
||||||
}
|
|
||||||
return c.t2.Peek(key)
|
|
||||||
}
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package arc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
mathrand "math/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getRand(tb testing.TB) int64 {
|
|
||||||
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
|
||||||
if err != nil {
|
|
||||||
tb.Fatal(err)
|
|
||||||
}
|
|
||||||
return out.Int64()
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
mathrand.Seed(time.Now().Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkARC_Rand(b *testing.B) {
|
|
||||||
l, err := NewARC[int64, int64](8192)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < 2*b.N; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
} else {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkARC_Freq(b *testing.B) {
|
|
||||||
l, err := NewARC[int64, int64](8192)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
trace[i] = getRand(b) % 16384
|
|
||||||
} else {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
}
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestARC_RandomOps(t *testing.T) {
|
|
||||||
size := 128
|
|
||||||
l, err := NewARC[int64, int64](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n := 200000
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
key := getRand(t) % 512
|
|
||||||
r := getRand(t)
|
|
||||||
switch r % 3 {
|
|
||||||
case 0:
|
|
||||||
l.Add(key, key)
|
|
||||||
case 1:
|
|
||||||
l.Get(key)
|
|
||||||
case 2:
|
|
||||||
l.Remove(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.t1.Len()+l.t2.Len() > size {
|
|
||||||
t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d",
|
|
||||||
l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p)
|
|
||||||
}
|
|
||||||
if l.b1.Len()+l.b2.Len() > size {
|
|
||||||
t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d",
|
|
||||||
l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestARC_Get_RecentToFrequent(t *testing.T) {
|
|
||||||
l, err := NewARC[int, int](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch all the entries, should be in t1
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if n := l.t1.Len(); n != 128 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get should upgrade to t2
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("missing: %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n := l.t1.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 128 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get be from t2
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("missing: %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if n := l.t1.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 128 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestARC_Add_RecentToFrequent(t *testing.T) {
|
|
||||||
l, err := NewARC[int, int](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add initially to t1
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.t1.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add should upgrade to t2
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.t1.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add should remain in t2
|
|
||||||
l.Add(1, 1)
|
|
||||||
if n := l.t1.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestARC_Adaptive(t *testing.T) {
|
|
||||||
l, err := NewARC[int, int](4)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill t1
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if n := l.t1.Len(); n != 4 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to t2
|
|
||||||
l.Get(0)
|
|
||||||
l.Get(1)
|
|
||||||
if n := l.t2.Len(); n != 2 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict from t1
|
|
||||||
l.Add(4, 4)
|
|
||||||
if n := l.b1.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current state
|
|
||||||
// t1 : (MRU) [4, 3] (LRU)
|
|
||||||
// t2 : (MRU) [1, 0] (LRU)
|
|
||||||
// b1 : (MRU) [2] (LRU)
|
|
||||||
// b2 : (MRU) [] (LRU)
|
|
||||||
|
|
||||||
// Add 2, should cause hit on b1
|
|
||||||
l.Add(2, 2)
|
|
||||||
if n := l.b1.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if l.p != 1 {
|
|
||||||
t.Fatalf("bad: %d", l.p)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 3 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current state
|
|
||||||
// t1 : (MRU) [4] (LRU)
|
|
||||||
// t2 : (MRU) [2, 1, 0] (LRU)
|
|
||||||
// b1 : (MRU) [3] (LRU)
|
|
||||||
// b2 : (MRU) [] (LRU)
|
|
||||||
|
|
||||||
// Add 4, should migrate to t2
|
|
||||||
l.Add(4, 4)
|
|
||||||
if n := l.t1.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 4 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current state
|
|
||||||
// t1 : (MRU) [] (LRU)
|
|
||||||
// t2 : (MRU) [4, 2, 1, 0] (LRU)
|
|
||||||
// b1 : (MRU) [3] (LRU)
|
|
||||||
// b2 : (MRU) [] (LRU)
|
|
||||||
|
|
||||||
// Add 4, should evict to b2
|
|
||||||
l.Add(5, 5)
|
|
||||||
if n := l.t1.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 3 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.b2.Len(); n != 1 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current state
|
|
||||||
// t1 : (MRU) [5] (LRU)
|
|
||||||
// t2 : (MRU) [4, 2, 1] (LRU)
|
|
||||||
// b1 : (MRU) [3] (LRU)
|
|
||||||
// b2 : (MRU) [0] (LRU)
|
|
||||||
|
|
||||||
// Add 0, should decrease p
|
|
||||||
l.Add(0, 0)
|
|
||||||
if n := l.t1.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.t2.Len(); n != 4 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.b1.Len(); n != 2 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if n := l.b2.Len(); n != 0 {
|
|
||||||
t.Fatalf("bad: %d", n)
|
|
||||||
}
|
|
||||||
if l.p != 0 {
|
|
||||||
t.Fatalf("bad: %d", l.p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current state
|
|
||||||
// t1 : (MRU) [] (LRU)
|
|
||||||
// t2 : (MRU) [0, 4, 2, 1] (LRU)
|
|
||||||
// b1 : (MRU) [5, 3] (LRU)
|
|
||||||
// b2 : (MRU) [0] (LRU)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestARC(t *testing.T) {
|
|
||||||
l, err := NewARC[int, int](128)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if l.Len() != 128 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if l.Cap() != 128 {
|
|
||||||
t.Fatalf("expect %d, but %d", 128, l.Cap())
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, k := range l.Keys() {
|
|
||||||
if v, ok := l.Get(k); !ok || v != k || v != i+128 {
|
|
||||||
t.Fatalf("bad key: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, v := range l.Values() {
|
|
||||||
if v != i+128 {
|
|
||||||
t.Fatalf("bad value: %v", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 256; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("should not be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 192; i++ {
|
|
||||||
l.Remove(i)
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be deleted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if l.Cap() != 128 {
|
|
||||||
t.Fatalf("expect %d, but %d", 128, l.Cap())
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Purge()
|
|
||||||
if l.Len() != 0 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if _, ok := l.Get(200); ok {
|
|
||||||
t.Fatalf("should contain nothing")
|
|
||||||
}
|
|
||||||
if l.Cap() != 128 {
|
|
||||||
t.Fatalf("expect %d, but %d", 128, l.Cap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Contains doesn't update recent-ness
|
|
||||||
func TestARC_Contains(t *testing.T) {
|
|
||||||
l, err := NewARC[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if !l.Contains(1) {
|
|
||||||
t.Errorf("1 should be contained")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("Contains should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Peek doesn't update recent-ness
|
|
||||||
func TestARC_Peek(t *testing.T) {
|
|
||||||
l, err := NewARC[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if v, ok := l.Peek(1); !ok || v != 1 {
|
|
||||||
t.Errorf("1 should be set to 1: %v, %v", v, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module github.com/hashicorp/golang-lru/arc/v2
|
|
||||||
|
|
||||||
go 1.18
|
|
||||||
|
|
||||||
require github.com/hashicorp/golang-lru/v2 v2.0.7
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
// Package lru provides three different LRU caches of varying sophistication.
|
|
||||||
//
|
|
||||||
// Cache is a simple LRU cache. It is based on the LRU implementation in
|
|
||||||
// groupcache: https://github.com/golang/groupcache/tree/master/lru
|
|
||||||
//
|
|
||||||
// TwoQueueCache tracks frequently used and recently used entries separately.
|
|
||||||
// This avoids a burst of accesses from taking out frequently used entries, at
|
|
||||||
// the cost of about 2x computational overhead and some extra bookkeeping.
|
|
||||||
//
|
|
||||||
// ARCCache is an adaptive replacement cache. It tracks recent evictions as well
|
|
||||||
// as recent usage in both the frequent and recent caches. Its computational
|
|
||||||
// overhead is comparable to TwoQueueCache, but the memory overhead is linear
|
|
||||||
// with the size of the cache.
|
|
||||||
//
|
|
||||||
// ARC has been patented by IBM, so do not use it if that is problematic for
|
|
||||||
// your program. For this reason, it is in a separate go module contained within
|
|
||||||
// this repository.
|
|
||||||
//
|
|
||||||
// All caches in this package take locks while operating, and are therefore
|
|
||||||
// thread-safe for consumers.
|
|
||||||
package lru
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package expirable
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
|
||||||
type EvictCallback[K comparable, V any] func(key K, value V)
|
|
||||||
|
|
||||||
// LRU implements a thread-safe LRU with expirable entries.
|
|
||||||
type LRU[K comparable, V any] struct {
|
|
||||||
size int
|
|
||||||
evictList *internal.LruList[K, V]
|
|
||||||
items map[K]*internal.Entry[K, V]
|
|
||||||
onEvict EvictCallback[K, V]
|
|
||||||
|
|
||||||
// expirable options
|
|
||||||
mu sync.Mutex
|
|
||||||
ttl time.Duration
|
|
||||||
done chan struct{}
|
|
||||||
|
|
||||||
// buckets for expiration
|
|
||||||
buckets []bucket[K, V]
|
|
||||||
// uint8 because it's number between 0 and numBuckets
|
|
||||||
nextCleanupBucket uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// bucket is a container for holding entries to be expired
|
|
||||||
type bucket[K comparable, V any] struct {
|
|
||||||
entries map[K]*internal.Entry[K, V]
|
|
||||||
newestEntry time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// noEvictionTTL - very long ttl to prevent eviction
|
|
||||||
const noEvictionTTL = time.Hour * 24 * 365 * 10
|
|
||||||
|
|
||||||
// because of uint8 usage for nextCleanupBucket, should not exceed 256.
|
|
||||||
// casting it as uint8 explicitly requires type conversions in multiple places
|
|
||||||
const numBuckets = 100
|
|
||||||
|
|
||||||
// NewLRU returns a new thread-safe cache with expirable entries.
|
|
||||||
//
|
|
||||||
// Size parameter set to 0 makes cache of unlimited size, e.g. turns LRU mechanism off.
|
|
||||||
//
|
|
||||||
// Providing 0 TTL turns expiring off.
|
|
||||||
//
|
|
||||||
// Delete expired entries every 1/100th of ttl value. Goroutine which deletes expired entries runs indefinitely.
|
|
||||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time.Duration) *LRU[K, V] {
|
|
||||||
if size < 0 {
|
|
||||||
size = 0
|
|
||||||
}
|
|
||||||
if ttl <= 0 {
|
|
||||||
ttl = noEvictionTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
res := LRU[K, V]{
|
|
||||||
ttl: ttl,
|
|
||||||
size: size,
|
|
||||||
evictList: internal.NewList[K, V](),
|
|
||||||
items: make(map[K]*internal.Entry[K, V]),
|
|
||||||
onEvict: onEvict,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the buckets
|
|
||||||
res.buckets = make([]bucket[K, V], numBuckets)
|
|
||||||
for i := 0; i < numBuckets; i++ {
|
|
||||||
res.buckets[i] = bucket[K, V]{entries: make(map[K]*internal.Entry[K, V])}
|
|
||||||
}
|
|
||||||
|
|
||||||
// enable deleteExpired() running in separate goroutine for cache with non-zero TTL
|
|
||||||
//
|
|
||||||
// Important: done channel is never closed, so deleteExpired() goroutine will never exit,
|
|
||||||
// it's decided to add functionality to close it in the version later than v2.
|
|
||||||
if res.ttl != noEvictionTTL {
|
|
||||||
go func(done <-chan struct{}) {
|
|
||||||
ticker := time.NewTicker(res.ttl / numBuckets)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
res.deleteExpired()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(res.done)
|
|
||||||
}
|
|
||||||
return &res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge clears the cache completely.
|
|
||||||
// onEvict is called for each evicted key.
|
|
||||||
func (c *LRU[K, V]) Purge() {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
for k, v := range c.items {
|
|
||||||
if c.onEvict != nil {
|
|
||||||
c.onEvict(k, v.Value)
|
|
||||||
}
|
|
||||||
delete(c.items, k)
|
|
||||||
}
|
|
||||||
for _, b := range c.buckets {
|
|
||||||
for _, ent := range b.entries {
|
|
||||||
delete(b.entries, ent.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.evictList.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
|
||||||
// Returns false if there was no eviction: the item was already in the cache,
|
|
||||||
// or the size was not exceeded.
|
|
||||||
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
// Check for existing item
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.evictList.MoveToFront(ent)
|
|
||||||
c.removeFromBucket(ent) // remove the entry from its current bucket as expiresAt is renewed
|
|
||||||
ent.Value = value
|
|
||||||
ent.ExpiresAt = now.Add(c.ttl)
|
|
||||||
c.addToBucket(ent)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new item
|
|
||||||
ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))
|
|
||||||
c.items[key] = ent
|
|
||||||
c.addToBucket(ent) // adds the entry to the appropriate bucket and sets entry.expireBucket
|
|
||||||
|
|
||||||
evict := c.size > 0 && c.evictList.Length() > c.size
|
|
||||||
// Verify size not exceeded
|
|
||||||
if evict {
|
|
||||||
c.removeOldest()
|
|
||||||
}
|
|
||||||
return evict
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
var ent *internal.Entry[K, V]
|
|
||||||
if ent, ok = c.items[key]; ok {
|
|
||||||
// Expired item check
|
|
||||||
if time.Now().After(ent.ExpiresAt) {
|
|
||||||
return value, false
|
|
||||||
}
|
|
||||||
c.evictList.MoveToFront(ent)
|
|
||||||
return ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
|
||||||
// or deleting it for being stale.
|
|
||||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
_, ok = c.items[key]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek returns the key value (or undefined if not found) without updating
|
|
||||||
// the "recently used"-ness of the key.
|
|
||||||
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
var ent *internal.Entry[K, V]
|
|
||||||
if ent, ok = c.items[key]; ok {
|
|
||||||
// Expired item check
|
|
||||||
if time.Now().After(ent.ExpiresAt) {
|
|
||||||
return value, false
|
|
||||||
}
|
|
||||||
return ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the provided key from the cache, returning if the
|
|
||||||
// key was contained.
|
|
||||||
func (c *LRU[K, V]) Remove(key K) bool {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.removeElement(ent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveOldest removes the oldest item from the cache.
|
|
||||||
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if ent := c.evictList.Back(); ent != nil {
|
|
||||||
c.removeElement(ent)
|
|
||||||
return ent.Key, ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOldest returns the oldest entry
|
|
||||||
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if ent := c.evictList.Back(); ent != nil {
|
|
||||||
return ent.Key, ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
||||||
// Expired entries are filtered out.
|
|
||||||
func (c *LRU[K, V]) Keys() []K {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
keys := make([]K, 0, len(c.items))
|
|
||||||
now := time.Now()
|
|
||||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
|
||||||
if now.After(ent.ExpiresAt) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
keys = append(keys, ent.Key)
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
|
||||||
// Expired entries are filtered out.
|
|
||||||
func (c *LRU[K, V]) Values() []V {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
values := make([]V, 0, len(c.items))
|
|
||||||
now := time.Now()
|
|
||||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
|
||||||
if now.After(ent.ExpiresAt) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
values = append(values, ent.Value)
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of items in the cache.
|
|
||||||
func (c *LRU[K, V]) Len() int {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.evictList.Length()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize changes the cache size. Size of 0 means unlimited.
|
|
||||||
func (c *LRU[K, V]) Resize(size int) (evicted int) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if size <= 0 {
|
|
||||||
c.size = 0
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
diff := c.evictList.Length() - size
|
|
||||||
if diff < 0 {
|
|
||||||
diff = 0
|
|
||||||
}
|
|
||||||
for i := 0; i < diff; i++ {
|
|
||||||
c.removeOldest()
|
|
||||||
}
|
|
||||||
c.size = size
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().
|
|
||||||
// func (c *LRU[K, V]) Close() {
|
|
||||||
// c.mu.Lock()
|
|
||||||
// defer c.mu.Unlock()
|
|
||||||
// select {
|
|
||||||
// case <-c.done:
|
|
||||||
// return
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
// close(c.done)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// removeOldest removes the oldest item from the cache. Has to be called with lock!
|
|
||||||
func (c *LRU[K, V]) removeOldest() {
|
|
||||||
if ent := c.evictList.Back(); ent != nil {
|
|
||||||
c.removeElement(ent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
|
|
||||||
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
|
|
||||||
c.evictList.Remove(e)
|
|
||||||
delete(c.items, e.Key)
|
|
||||||
c.removeFromBucket(e)
|
|
||||||
if c.onEvict != nil {
|
|
||||||
c.onEvict(e.Key, e.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteExpired deletes expired records from the oldest bucket, waiting for the newest entry
|
|
||||||
// in it to expire first.
|
|
||||||
func (c *LRU[K, V]) deleteExpired() {
|
|
||||||
c.mu.Lock()
|
|
||||||
bucketIdx := c.nextCleanupBucket
|
|
||||||
timeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)
|
|
||||||
// wait for newest entry to expire before cleanup without holding lock
|
|
||||||
if timeToExpire > 0 {
|
|
||||||
c.mu.Unlock()
|
|
||||||
time.Sleep(timeToExpire)
|
|
||||||
c.mu.Lock()
|
|
||||||
}
|
|
||||||
for _, ent := range c.buckets[bucketIdx].entries {
|
|
||||||
c.removeElement(ent)
|
|
||||||
}
|
|
||||||
c.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBuckets
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// addToBucket adds entry to expire bucket so that it will be cleaned up when the time comes. Has to be called with lock!
|
|
||||||
func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {
|
|
||||||
bucketID := (numBuckets + c.nextCleanupBucket - 1) % numBuckets
|
|
||||||
e.ExpireBucket = bucketID
|
|
||||||
c.buckets[bucketID].entries[e.Key] = e
|
|
||||||
if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {
|
|
||||||
c.buckets[bucketID].newestEntry = e.ExpiresAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeFromBucket removes the entry from its corresponding bucket. Has to be called with lock!
|
|
||||||
func (c *LRU[K, V]) removeFromBucket(e *internal.Entry[K, V]) {
|
|
||||||
delete(c.buckets[e.ExpireBucket].entries, e.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap returns the capacity of the cache
|
|
||||||
func (c *LRU[K, V]) Cap() int {
|
|
||||||
return c.size
|
|
||||||
}
|
|
||||||
@@ -1,577 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package expirable
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkLRU_Rand_NoExpire(b *testing.B) {
|
|
||||||
l := NewLRU[int64, int64](8192, nil, 0)
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < 2*b.N; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
} else {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkLRU_Freq_NoExpire(b *testing.B) {
|
|
||||||
l := NewLRU[int64, int64](8192, nil, 0)
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
trace[i] = getRand(b) % 16384
|
|
||||||
} else {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
}
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkLRU_Rand_WithExpire(b *testing.B) {
|
|
||||||
l := NewLRU[int64, int64](8192, nil, time.Millisecond*10)
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < 2*b.N; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
} else {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkLRU_Freq_WithExpire(b *testing.B) {
|
|
||||||
l := NewLRU[int64, int64](8192, nil, time.Millisecond*10)
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
trace[i] = getRand(b) % 16384
|
|
||||||
} else {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
}
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRUInterface(_ *testing.T) {
|
|
||||||
var _ simplelru.LRUCache[int, int] = &LRU[int, int]{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRUNoPurge(t *testing.T) {
|
|
||||||
lc := NewLRU[string, string](10, nil, 0)
|
|
||||||
|
|
||||||
lc.Add("key1", "val1")
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := lc.Peek("key1")
|
|
||||||
if v != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lc.Contains("key1") {
|
|
||||||
t.Fatalf("should contain key1")
|
|
||||||
}
|
|
||||||
if lc.Contains("key2") {
|
|
||||||
t.Fatalf("should not contain key2")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok = lc.Peek("key2")
|
|
||||||
if v != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(lc.Keys(), []string{"key1"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
if lc.Resize(0) != 0 {
|
|
||||||
t.Fatalf("evicted count differs from expected")
|
|
||||||
}
|
|
||||||
if lc.Resize(2) != 0 {
|
|
||||||
t.Fatalf("evicted count differs from expected")
|
|
||||||
}
|
|
||||||
lc.Add("key2", "val2")
|
|
||||||
if lc.Resize(1) != 1 {
|
|
||||||
t.Fatalf("evicted count differs from expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRUEdgeCases(t *testing.T) {
|
|
||||||
lc := NewLRU[string, *string](2, nil, 0)
|
|
||||||
|
|
||||||
// Adding a nil value
|
|
||||||
lc.Add("key1", nil)
|
|
||||||
|
|
||||||
value, exists := lc.Get("key1")
|
|
||||||
if value != nil || !exists {
|
|
||||||
t.Fatalf("unexpected value or existence flag for key1: value=%v, exists=%v", value, exists)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding an entry with the same key but different value
|
|
||||||
newVal := "val1"
|
|
||||||
lc.Add("key1", &newVal)
|
|
||||||
|
|
||||||
value, exists = lc.Get("key1")
|
|
||||||
if value != &newVal || !exists {
|
|
||||||
t.Fatalf("unexpected value or existence flag for key1: value=%v, exists=%v", value, exists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRU_Values(t *testing.T) {
|
|
||||||
lc := NewLRU[string, string](3, nil, 0)
|
|
||||||
|
|
||||||
lc.Add("key1", "val1")
|
|
||||||
lc.Add("key2", "val2")
|
|
||||||
lc.Add("key3", "val3")
|
|
||||||
|
|
||||||
values := lc.Values()
|
|
||||||
if !reflect.DeepEqual(values, []string{"val1", "val2", "val3"}) {
|
|
||||||
t.Fatalf("values differs from expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func TestExpirableMultipleClose(_ *testing.T) {
|
|
||||||
// lc := NewLRU[string, string](10, nil, 0)
|
|
||||||
// lc.Close()
|
|
||||||
// // should not panic
|
|
||||||
// lc.Close()
|
|
||||||
// }
|
|
||||||
|
|
||||||
func TestLRUWithPurge(t *testing.T) {
|
|
||||||
var evicted []string
|
|
||||||
lc := NewLRU(10, func(key string, value string) { evicted = append(evicted, key, value) }, 150*time.Millisecond)
|
|
||||||
|
|
||||||
k, v, ok := lc.GetOldest()
|
|
||||||
if k != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if v != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.Add("key1", "val1")
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond) // not enough to expire
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok = lc.Get("key1")
|
|
||||||
if v != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(200 * time.Millisecond) // expire
|
|
||||||
v, ok = lc.Get("key1")
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
if v != "" {
|
|
||||||
t.Fatalf("should be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if lc.Len() != 0 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(evicted, []string{"key1", "val1"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new entry
|
|
||||||
lc.Add("key2", "val2")
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
k, v, ok = lc.GetOldest()
|
|
||||||
if k != "key2" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if v != "val2" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExpired, nothing deleted
|
|
||||||
lc.deleteExpired()
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(evicted, []string{"key1", "val1"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge, cache should be clean
|
|
||||||
lc.Purge()
|
|
||||||
if lc.Len() != 0 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(evicted, []string{"key1", "val1", "key2", "val2"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRUWithPurgeEnforcedBySize(t *testing.T) {
|
|
||||||
lc := NewLRU[string, string](10, nil, time.Hour)
|
|
||||||
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
i := i
|
|
||||||
lc.Add(fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i))
|
|
||||||
v, ok := lc.Get(fmt.Sprintf("key%d", i))
|
|
||||||
if v != fmt.Sprintf("val%d", i) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
if lc.Len() > 20 {
|
|
||||||
t.Fatalf("length should be less than 20")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lc.Len() != 10 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRUConcurrency(t *testing.T) {
|
|
||||||
lc := NewLRU[string, string](0, nil, 0)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(1000)
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
go func(i int) {
|
|
||||||
lc.Add(fmt.Sprintf("key-%d", i/10), fmt.Sprintf("val-%d", i/10))
|
|
||||||
wg.Done()
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
if lc.Len() != 100 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRUInvalidateAndEvict(t *testing.T) {
|
|
||||||
var evicted int
|
|
||||||
lc := NewLRU(-1, func(_, _ string) { evicted++ }, 0)
|
|
||||||
|
|
||||||
lc.Add("key1", "val1")
|
|
||||||
lc.Add("key2", "val2")
|
|
||||||
|
|
||||||
val, ok := lc.Get("key1")
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
if val != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if evicted != 0 {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.Remove("key1")
|
|
||||||
if evicted != 1 {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
val, ok = lc.Get("key1")
|
|
||||||
if val != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadingExpired(t *testing.T) {
|
|
||||||
lc := NewLRU[string, string](0, nil, time.Millisecond*5)
|
|
||||||
|
|
||||||
lc.Add("key1", "val1")
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := lc.Peek("key1")
|
|
||||||
if v != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok = lc.Get("key1")
|
|
||||||
if v != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
result, ok := lc.Get("key1")
|
|
||||||
if ok && result == "" {
|
|
||||||
t.Fatalf("ok should return a result")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 100) // wait for expiration reaper
|
|
||||||
if lc.Len() != 0 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok = lc.Peek("key1")
|
|
||||||
if v != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok = lc.Get("key1")
|
|
||||||
if v != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRURemoveOldest(t *testing.T) {
|
|
||||||
lc := NewLRU[string, string](2, nil, 0)
|
|
||||||
|
|
||||||
if lc.Cap() != 2 {
|
|
||||||
t.Fatalf("expect cap is 2")
|
|
||||||
}
|
|
||||||
|
|
||||||
k, v, ok := lc.RemoveOldest()
|
|
||||||
if k != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if v != "" {
|
|
||||||
t.Fatalf("should be empty")
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = lc.Remove("non_existent")
|
|
||||||
if ok {
|
|
||||||
t.Fatalf("should be false")
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.Add("key1", "val1")
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok = lc.Get("key1")
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
if v != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(lc.Keys(), []string{"key1"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.Add("key2", "val2")
|
|
||||||
if !reflect.DeepEqual(lc.Keys(), []string{"key1", "key2"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if lc.Len() != 2 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
k, v, ok = lc.RemoveOldest()
|
|
||||||
if k != "key1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if v != "val1" {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("should be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(lc.Keys(), []string{"key2"}) {
|
|
||||||
t.Fatalf("value differs from expected")
|
|
||||||
}
|
|
||||||
if lc.Len() != 1 {
|
|
||||||
t.Fatalf("length differs from expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleLRU() {
|
|
||||||
// make cache with 10ms TTL and 5 max keys
|
|
||||||
cache := NewLRU[string, string](5, nil, time.Millisecond*10)
|
|
||||||
|
|
||||||
// set value under key1.
|
|
||||||
cache.Add("key1", "val1")
|
|
||||||
|
|
||||||
// get value under key1
|
|
||||||
r, ok := cache.Get("key1")
|
|
||||||
|
|
||||||
// check for OK value
|
|
||||||
if ok {
|
|
||||||
fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for cache to expire
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
|
|
||||||
// get value under key1 after key expiration
|
|
||||||
r, ok = cache.Get("key1")
|
|
||||||
fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r)
|
|
||||||
|
|
||||||
// set value under key2, would evict old entry because it is already expired.
|
|
||||||
cache.Add("key2", "val2")
|
|
||||||
|
|
||||||
fmt.Printf("Cache len: %d\n", cache.Len())
|
|
||||||
// Output:
|
|
||||||
// value before expiration is found: true, value: "val1"
|
|
||||||
// value after expiration is found: false, value: ""
|
|
||||||
// Cache len: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRand(tb testing.TB) int64 {
|
|
||||||
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
|
||||||
if err != nil {
|
|
||||||
tb.Fatal(err)
|
|
||||||
}
|
|
||||||
return out.Int64()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *LRU[K, V]) wantKeys(t *testing.T, want []K) {
|
|
||||||
t.Helper()
|
|
||||||
got := c.Keys()
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("wrong keys got: %v, want: %v ", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCache_EvictionSameKey(t *testing.T) {
|
|
||||||
var evictedKeys []int
|
|
||||||
|
|
||||||
cache := NewLRU[int, struct{}](
|
|
||||||
2,
|
|
||||||
func(key int, _ struct{}) {
|
|
||||||
evictedKeys = append(evictedKeys, key)
|
|
||||||
},
|
|
||||||
0)
|
|
||||||
|
|
||||||
if evicted := cache.Add(1, struct{}{}); evicted {
|
|
||||||
t.Error("First 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1})
|
|
||||||
|
|
||||||
if evicted := cache.Add(2, struct{}{}); evicted {
|
|
||||||
t.Error("2: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
if evicted := cache.Add(1, struct{}{}); evicted {
|
|
||||||
t.Error("Second 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{2, 1})
|
|
||||||
|
|
||||||
if evicted := cache.Add(3, struct{}{}); !evicted {
|
|
||||||
t.Error("3: did not get expected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 3})
|
|
||||||
|
|
||||||
want := []int{2}
|
|
||||||
if !reflect.DeepEqual(evictedKeys, want) {
|
|
||||||
t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module github.com/hashicorp/golang-lru/v2
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// Copyright 2009 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE_list file.
|
|
||||||
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Entry is an LRU Entry
|
|
||||||
type Entry[K comparable, V any] struct {
|
|
||||||
// Next and previous pointers in the doubly-linked list of elements.
|
|
||||||
// To simplify the implementation, internally a list l is implemented
|
|
||||||
// as a ring, such that &l.root is both the next element of the last
|
|
||||||
// list element (l.Back()) and the previous element of the first list
|
|
||||||
// element (l.Front()).
|
|
||||||
next, prev *Entry[K, V]
|
|
||||||
|
|
||||||
// The list to which this element belongs.
|
|
||||||
list *LruList[K, V]
|
|
||||||
|
|
||||||
// The LRU Key of this element.
|
|
||||||
Key K
|
|
||||||
|
|
||||||
// The Value stored with this element.
|
|
||||||
Value V
|
|
||||||
|
|
||||||
// The time this element would be cleaned up, optional
|
|
||||||
ExpiresAt time.Time
|
|
||||||
|
|
||||||
// The expiry bucket item was put in, optional
|
|
||||||
ExpireBucket uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrevEntry returns the previous list element or nil.
|
|
||||||
func (e *Entry[K, V]) PrevEntry() *Entry[K, V] {
|
|
||||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LruList represents a doubly linked list.
|
|
||||||
// The zero Value for LruList is an empty list ready to use.
|
|
||||||
type LruList[K comparable, V any] struct {
|
|
||||||
root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used
|
|
||||||
len int // current list Length excluding (this) sentinel element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes or clears list l.
|
|
||||||
func (l *LruList[K, V]) Init() *LruList[K, V] {
|
|
||||||
l.root.next = &l.root
|
|
||||||
l.root.prev = &l.root
|
|
||||||
l.len = 0
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewList returns an initialized list.
|
|
||||||
func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() }
|
|
||||||
|
|
||||||
// Length returns the number of elements of list l.
|
|
||||||
// The complexity is O(1).
|
|
||||||
func (l *LruList[K, V]) Length() int { return l.len }
|
|
||||||
|
|
||||||
// Back returns the last element of list l or nil if the list is empty.
|
|
||||||
func (l *LruList[K, V]) Back() *Entry[K, V] {
|
|
||||||
if l.len == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.root.prev
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazyInit lazily initializes a zero List Value.
|
|
||||||
func (l *LruList[K, V]) lazyInit() {
|
|
||||||
if l.root.next == nil {
|
|
||||||
l.Init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert inserts e after at, increments l.len, and returns e.
|
|
||||||
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
|
|
||||||
e.prev = at
|
|
||||||
e.next = at.next
|
|
||||||
e.prev.next = e
|
|
||||||
e.next.prev = e
|
|
||||||
e.list = l
|
|
||||||
l.len++
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at).
|
|
||||||
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
|
|
||||||
return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes e from its list, decrements l.len
|
|
||||||
func (l *LruList[K, V]) Remove(e *Entry[K, V]) V {
|
|
||||||
e.prev.next = e.next
|
|
||||||
e.next.prev = e.prev
|
|
||||||
e.next = nil // avoid memory leaks
|
|
||||||
e.prev = nil // avoid memory leaks
|
|
||||||
e.list = nil
|
|
||||||
l.len--
|
|
||||||
|
|
||||||
return e.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// move moves e to next to at.
|
|
||||||
func (l *LruList[K, V]) move(e, at *Entry[K, V]) {
|
|
||||||
if e == at {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.prev.next = e.next
|
|
||||||
e.next.prev = e.prev
|
|
||||||
|
|
||||||
e.prev = at
|
|
||||||
e.next = at.next
|
|
||||||
e.prev.next = e
|
|
||||||
e.next.prev = e
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
|
||||||
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
|
|
||||||
l.lazyInit()
|
|
||||||
return l.insertValue(k, v, time.Time{}, &l.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e.
|
|
||||||
func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] {
|
|
||||||
l.lazyInit()
|
|
||||||
return l.insertValue(k, v, expiresAt, &l.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveToFront moves element e to the front of list l.
|
|
||||||
// If e is not an element of l, the list is not modified.
|
|
||||||
// The element must not be nil.
|
|
||||||
func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {
|
|
||||||
if e.list != l || l.root.next == e {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// see comment in List.Remove about initialization of l
|
|
||||||
l.move(e, &l.root)
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package lru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
|
|
||||||
DefaultEvictedBufferSize = 16
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache is a thread-safe fixed size LRU cache.
|
|
||||||
type Cache[K comparable, V any] struct {
|
|
||||||
lru *simplelru.LRU[K, V]
|
|
||||||
evictedKeys []K
|
|
||||||
evictedVals []V
|
|
||||||
onEvictedCB func(k K, v V)
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates an LRU of the given size.
|
|
||||||
func New[K comparable, V any](size int) (*Cache[K, V], error) {
|
|
||||||
return NewWithEvict[K, V](size, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithEvict constructs a fixed size cache with the given eviction
|
|
||||||
// callback.
|
|
||||||
func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) {
|
|
||||||
// create a cache with default settings
|
|
||||||
c = &Cache[K, V]{
|
|
||||||
onEvictedCB: onEvicted,
|
|
||||||
}
|
|
||||||
if onEvicted != nil {
|
|
||||||
c.initEvictBuffers()
|
|
||||||
onEvicted = c.onEvicted
|
|
||||||
}
|
|
||||||
c.lru, err = simplelru.NewLRU(size, onEvicted)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache[K, V]) initEvictBuffers() {
|
|
||||||
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
|
|
||||||
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// onEvicted save evicted key/val and sent in externally registered callback
|
|
||||||
// outside of critical section
|
|
||||||
func (c *Cache[K, V]) onEvicted(k K, v V) {
|
|
||||||
c.evictedKeys = append(c.evictedKeys, k)
|
|
||||||
c.evictedVals = append(c.evictedVals, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge is used to completely clear the cache.
|
|
||||||
func (c *Cache[K, V]) Purge() {
|
|
||||||
var ks []K
|
|
||||||
var vs []V
|
|
||||||
c.lock.Lock()
|
|
||||||
c.lru.Purge()
|
|
||||||
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
|
|
||||||
ks, vs = c.evictedKeys, c.evictedVals
|
|
||||||
c.initEvictBuffers()
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
// invoke callback outside of critical section
|
|
||||||
if c.onEvictedCB != nil {
|
|
||||||
for i := 0; i < len(ks); i++ {
|
|
||||||
c.onEvictedCB(ks[i], vs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
|
||||||
func (c *Cache[K, V]) Add(key K, value V) (evicted bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
evicted = c.lru.Add(key, value)
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
c.onEvictedCB(k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
|
|
||||||
c.lock.Lock()
|
|
||||||
value, ok = c.lru.Get(key)
|
|
||||||
c.lock.Unlock()
|
|
||||||
return value, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks if a key is in the cache, without updating the
|
|
||||||
// recent-ness or deleting it for being stale.
|
|
||||||
func (c *Cache[K, V]) Contains(key K) bool {
|
|
||||||
c.lock.RLock()
|
|
||||||
containKey := c.lru.Contains(key)
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return containKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek returns the key value (or undefined if not found) without updating
|
|
||||||
// the "recently used"-ness of the key.
|
|
||||||
func (c *Cache[K, V]) Peek(key K) (value V, ok bool) {
|
|
||||||
c.lock.RLock()
|
|
||||||
value, ok = c.lru.Peek(key)
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return value, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
|
||||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
|
||||||
// Returns whether found and whether an eviction occurred.
|
|
||||||
func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
if c.lru.Contains(key) {
|
|
||||||
c.lock.Unlock()
|
|
||||||
return true, false
|
|
||||||
}
|
|
||||||
evicted = c.lru.Add(key, value)
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
c.onEvictedCB(k, v)
|
|
||||||
}
|
|
||||||
return false, evicted
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekOrAdd checks if a key is in the cache without updating the
|
|
||||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
|
||||||
// Returns whether found and whether an eviction occurred.
|
|
||||||
func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
previous, ok = c.lru.Peek(key)
|
|
||||||
if ok {
|
|
||||||
c.lock.Unlock()
|
|
||||||
return previous, true, false
|
|
||||||
}
|
|
||||||
evicted = c.lru.Add(key, value)
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted {
|
|
||||||
c.onEvictedCB(k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the provided key from the cache.
|
|
||||||
func (c *Cache[K, V]) Remove(key K) (present bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
present = c.lru.Remove(key)
|
|
||||||
if c.onEvictedCB != nil && present {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && present {
|
|
||||||
c.onEvictedCB(k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize changes the cache size.
|
|
||||||
func (c *Cache[K, V]) Resize(size int) (evicted int) {
|
|
||||||
var ks []K
|
|
||||||
var vs []V
|
|
||||||
c.lock.Lock()
|
|
||||||
evicted = c.lru.Resize(size)
|
|
||||||
if c.onEvictedCB != nil && evicted > 0 {
|
|
||||||
ks, vs = c.evictedKeys, c.evictedVals
|
|
||||||
c.initEvictBuffers()
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && evicted > 0 {
|
|
||||||
for i := 0; i < len(ks); i++ {
|
|
||||||
c.onEvictedCB(ks[i], vs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evicted
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveOldest removes the oldest item from the cache.
|
|
||||||
func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
|
||||||
var k K
|
|
||||||
var v V
|
|
||||||
c.lock.Lock()
|
|
||||||
key, value, ok = c.lru.RemoveOldest()
|
|
||||||
if c.onEvictedCB != nil && ok {
|
|
||||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
|
||||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
if c.onEvictedCB != nil && ok {
|
|
||||||
c.onEvictedCB(k, v)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOldest returns the oldest entry
|
|
||||||
func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) {
|
|
||||||
c.lock.RLock()
|
|
||||||
key, value, ok = c.lru.GetOldest()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
||||||
func (c *Cache[K, V]) Keys() []K {
|
|
||||||
c.lock.RLock()
|
|
||||||
keys := c.lru.Keys()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
|
||||||
func (c *Cache[K, V]) Values() []V {
|
|
||||||
c.lock.RLock()
|
|
||||||
values := c.lru.Values()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of items in the cache.
|
|
||||||
func (c *Cache[K, V]) Len() int {
|
|
||||||
c.lock.RLock()
|
|
||||||
length := c.lru.Len()
|
|
||||||
c.lock.RUnlock()
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap returns the capacity of the cache
|
|
||||||
func (c *Cache[K, V]) Cap() int {
|
|
||||||
return c.lru.Cap()
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package lru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkLRU_Rand(b *testing.B) {
|
|
||||||
l, err := New[int64, int64](8192)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < 2*b.N; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
} else {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkLRU_Freq(b *testing.B) {
|
|
||||||
l, err := New[int64, int64](8192)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trace := make([]int64, b.N*2)
|
|
||||||
for i := 0; i < b.N*2; i++ {
|
|
||||||
if i%2 == 0 {
|
|
||||||
trace[i] = getRand(b) % 16384
|
|
||||||
} else {
|
|
||||||
trace[i] = getRand(b) % 32768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
l.Add(trace[i], trace[i])
|
|
||||||
}
|
|
||||||
var hit, miss int
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if _, ok := l.Get(trace[i]); ok {
|
|
||||||
hit++
|
|
||||||
} else {
|
|
||||||
miss++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRU(t *testing.T) {
|
|
||||||
evictCounter := 0
|
|
||||||
onEvicted := func(k int, v int) {
|
|
||||||
if k != v {
|
|
||||||
t.Fatalf("Evict values not equal (%v!=%v)", k, v)
|
|
||||||
}
|
|
||||||
evictCounter++
|
|
||||||
}
|
|
||||||
l, err := NewWithEvict(128, onEvicted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if l.Len() != 128 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if l.Cap() != 128 {
|
|
||||||
t.Fatalf("expect %d, but %d", 128, l.Cap())
|
|
||||||
}
|
|
||||||
|
|
||||||
if evictCounter != 128 {
|
|
||||||
t.Fatalf("bad evict count: %v", evictCounter)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, k := range l.Keys() {
|
|
||||||
if v, ok := l.Get(k); !ok || v != k || v != i+128 {
|
|
||||||
t.Fatalf("bad key: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, v := range l.Values() {
|
|
||||||
if v != i+128 {
|
|
||||||
t.Fatalf("bad value: %v", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 256; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("should not be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 192; i++ {
|
|
||||||
l.Remove(i)
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be deleted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Get(192) // expect 192 to be last key in l.Keys()
|
|
||||||
|
|
||||||
for i, k := range l.Keys() {
|
|
||||||
if (i < 63 && k != i+193) || (i == 63 && k != 192) {
|
|
||||||
t.Fatalf("out of order key: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Purge()
|
|
||||||
if l.Len() != 0 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if _, ok := l.Get(200); ok {
|
|
||||||
t.Fatalf("should contain nothing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that Add returns true/false if an eviction occurred
|
|
||||||
func TestLRUAdd(t *testing.T) {
|
|
||||||
evictCounter := 0
|
|
||||||
onEvicted := func(k int, v int) {
|
|
||||||
evictCounter++
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := NewWithEvict(1, onEvicted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.Add(1, 1) == true || evictCounter != 0 {
|
|
||||||
t.Errorf("should not have an eviction")
|
|
||||||
}
|
|
||||||
if l.Add(2, 2) == false || evictCounter != 1 {
|
|
||||||
t.Errorf("should have an eviction")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that Contains doesn't update recent-ness
|
|
||||||
func TestLRUContains(t *testing.T) {
|
|
||||||
l, err := New[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if !l.Contains(1) {
|
|
||||||
t.Errorf("1 should be contained")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("Contains should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that ContainsOrAdd doesn't update recent-ness
|
|
||||||
func TestLRUContainsOrAdd(t *testing.T) {
|
|
||||||
l, err := New[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
contains, evict := l.ContainsOrAdd(1, 1)
|
|
||||||
if !contains {
|
|
||||||
t.Errorf("1 should be contained")
|
|
||||||
}
|
|
||||||
if evict {
|
|
||||||
t.Errorf("nothing should be evicted here")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
contains, evict = l.ContainsOrAdd(1, 1)
|
|
||||||
if contains {
|
|
||||||
t.Errorf("1 should not have been contained")
|
|
||||||
}
|
|
||||||
if !evict {
|
|
||||||
t.Errorf("an eviction should have occurred")
|
|
||||||
}
|
|
||||||
if !l.Contains(1) {
|
|
||||||
t.Errorf("now 1 should be contained")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that PeekOrAdd doesn't update recent-ness
|
|
||||||
func TestLRUPeekOrAdd(t *testing.T) {
|
|
||||||
l, err := New[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
previous, contains, evict := l.PeekOrAdd(1, 1)
|
|
||||||
if !contains {
|
|
||||||
t.Errorf("1 should be contained")
|
|
||||||
}
|
|
||||||
if evict {
|
|
||||||
t.Errorf("nothing should be evicted here")
|
|
||||||
}
|
|
||||||
if previous != 1 {
|
|
||||||
t.Errorf("previous is not equal to 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
contains, evict = l.ContainsOrAdd(1, 1)
|
|
||||||
if contains {
|
|
||||||
t.Errorf("1 should not have been contained")
|
|
||||||
}
|
|
||||||
if !evict {
|
|
||||||
t.Errorf("an eviction should have occurred")
|
|
||||||
}
|
|
||||||
if !l.Contains(1) {
|
|
||||||
t.Errorf("now 1 should be contained")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that Peek doesn't update recent-ness
|
|
||||||
func TestLRUPeek(t *testing.T) {
|
|
||||||
l, err := New[int, int](2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if v, ok := l.Peek(1); !ok || v != 1 {
|
|
||||||
t.Errorf("1 should be set to 1: %v, %v", v, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that Resize can upsize and downsize
|
|
||||||
func TestLRUResize(t *testing.T) {
|
|
||||||
onEvictCounter := 0
|
|
||||||
onEvicted := func(k int, v int) {
|
|
||||||
onEvictCounter++
|
|
||||||
}
|
|
||||||
l, err := NewWithEvict(2, onEvicted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downsize
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
evicted := l.Resize(1)
|
|
||||||
if evicted != 1 {
|
|
||||||
t.Errorf("1 element should have been evicted: %v", evicted)
|
|
||||||
}
|
|
||||||
if onEvictCounter != 1 {
|
|
||||||
t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("Element 1 should have been evicted")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upsize
|
|
||||||
evicted = l.Resize(2)
|
|
||||||
if evicted != 0 {
|
|
||||||
t.Errorf("0 elements should have been evicted: %v", evicted)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(4, 4)
|
|
||||||
if !l.Contains(3) || !l.Contains(4) {
|
|
||||||
t.Errorf("Cache should have contained 2 elements")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache[K, V]) wantKeys(t *testing.T, want []K) {
|
|
||||||
t.Helper()
|
|
||||||
got := c.Keys()
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("wrong keys got: %v, want: %v ", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCache_EvictionSameKey(t *testing.T) {
|
|
||||||
t.Run("Add", func(t *testing.T) {
|
|
||||||
var evictedKeys []int
|
|
||||||
|
|
||||||
cache, _ := NewWithEvict(
|
|
||||||
2,
|
|
||||||
func(key int, _ struct{}) {
|
|
||||||
evictedKeys = append(evictedKeys, key)
|
|
||||||
})
|
|
||||||
|
|
||||||
if evicted := cache.Add(1, struct{}{}); evicted {
|
|
||||||
t.Error("First 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1})
|
|
||||||
|
|
||||||
if evicted := cache.Add(2, struct{}{}); evicted {
|
|
||||||
t.Error("2: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
if evicted := cache.Add(1, struct{}{}); evicted {
|
|
||||||
t.Error("Second 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{2, 1})
|
|
||||||
|
|
||||||
if evicted := cache.Add(3, struct{}{}); !evicted {
|
|
||||||
t.Error("3: did not get expected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 3})
|
|
||||||
|
|
||||||
want := []int{2}
|
|
||||||
if !reflect.DeepEqual(evictedKeys, want) {
|
|
||||||
t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ContainsOrAdd", func(t *testing.T) {
|
|
||||||
var evictedKeys []int
|
|
||||||
|
|
||||||
cache, _ := NewWithEvict(
|
|
||||||
2,
|
|
||||||
func(key int, _ struct{}) {
|
|
||||||
evictedKeys = append(evictedKeys, key)
|
|
||||||
})
|
|
||||||
|
|
||||||
contained, evicted := cache.ContainsOrAdd(1, struct{}{})
|
|
||||||
if contained {
|
|
||||||
t.Error("First 1: got unexpected contained")
|
|
||||||
}
|
|
||||||
if evicted {
|
|
||||||
t.Error("First 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1})
|
|
||||||
|
|
||||||
contained, evicted = cache.ContainsOrAdd(2, struct{}{})
|
|
||||||
if contained {
|
|
||||||
t.Error("2: got unexpected contained")
|
|
||||||
}
|
|
||||||
if evicted {
|
|
||||||
t.Error("2: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
contained, evicted = cache.ContainsOrAdd(1, struct{}{})
|
|
||||||
if !contained {
|
|
||||||
t.Error("Second 1: did not get expected contained")
|
|
||||||
}
|
|
||||||
if evicted {
|
|
||||||
t.Error("Second 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
contained, evicted = cache.ContainsOrAdd(3, struct{}{})
|
|
||||||
if contained {
|
|
||||||
t.Error("3: got unexpected contained")
|
|
||||||
}
|
|
||||||
if !evicted {
|
|
||||||
t.Error("3: did not get expected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{2, 3})
|
|
||||||
|
|
||||||
want := []int{1}
|
|
||||||
if !reflect.DeepEqual(evictedKeys, want) {
|
|
||||||
t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("PeekOrAdd", func(t *testing.T) {
|
|
||||||
var evictedKeys []int
|
|
||||||
|
|
||||||
cache, _ := NewWithEvict(
|
|
||||||
2,
|
|
||||||
func(key int, _ struct{}) {
|
|
||||||
evictedKeys = append(evictedKeys, key)
|
|
||||||
})
|
|
||||||
|
|
||||||
_, contained, evicted := cache.PeekOrAdd(1, struct{}{})
|
|
||||||
if contained {
|
|
||||||
t.Error("First 1: got unexpected contained")
|
|
||||||
}
|
|
||||||
if evicted {
|
|
||||||
t.Error("First 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1})
|
|
||||||
|
|
||||||
_, contained, evicted = cache.PeekOrAdd(2, struct{}{})
|
|
||||||
if contained {
|
|
||||||
t.Error("2: got unexpected contained")
|
|
||||||
}
|
|
||||||
if evicted {
|
|
||||||
t.Error("2: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
_, contained, evicted = cache.PeekOrAdd(1, struct{}{})
|
|
||||||
if !contained {
|
|
||||||
t.Error("Second 1: did not get expected contained")
|
|
||||||
}
|
|
||||||
if evicted {
|
|
||||||
t.Error("Second 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
_, contained, evicted = cache.PeekOrAdd(3, struct{}{})
|
|
||||||
if contained {
|
|
||||||
t.Error("3: got unexpected contained")
|
|
||||||
}
|
|
||||||
if !evicted {
|
|
||||||
t.Error("3: did not get expected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{2, 3})
|
|
||||||
|
|
||||||
want := []int{1}
|
|
||||||
if !reflect.DeepEqual(evictedKeys, want) {
|
|
||||||
t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
This license applies to simplelru/list.go
|
|
||||||
|
|
||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package simplelru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru/v2/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
|
||||||
type EvictCallback[K comparable, V any] func(key K, value V)
|
|
||||||
|
|
||||||
// LRU implements a non-thread safe fixed size LRU cache
|
|
||||||
type LRU[K comparable, V any] struct {
|
|
||||||
size int
|
|
||||||
evictList *internal.LruList[K, V]
|
|
||||||
items map[K]*internal.Entry[K, V]
|
|
||||||
onEvict EvictCallback[K, V]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLRU constructs an LRU of the given size
|
|
||||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
|
|
||||||
if size <= 0 {
|
|
||||||
return nil, errors.New("must provide a positive size")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &LRU[K, V]{
|
|
||||||
size: size,
|
|
||||||
evictList: internal.NewList[K, V](),
|
|
||||||
items: make(map[K]*internal.Entry[K, V]),
|
|
||||||
onEvict: onEvict,
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge is used to completely clear the cache.
|
|
||||||
func (c *LRU[K, V]) Purge() {
|
|
||||||
for k, v := range c.items {
|
|
||||||
if c.onEvict != nil {
|
|
||||||
c.onEvict(k, v.Value)
|
|
||||||
}
|
|
||||||
delete(c.items, k)
|
|
||||||
}
|
|
||||||
c.evictList.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
|
||||||
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
|
|
||||||
// Check for existing item
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.evictList.MoveToFront(ent)
|
|
||||||
ent.Value = value
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new item
|
|
||||||
ent := c.evictList.PushFront(key, value)
|
|
||||||
c.items[key] = ent
|
|
||||||
|
|
||||||
evict := c.evictList.Length() > c.size
|
|
||||||
// Verify size not exceeded
|
|
||||||
if evict {
|
|
||||||
c.removeOldest()
|
|
||||||
}
|
|
||||||
return evict
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get looks up a key's value from the cache.
|
|
||||||
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.evictList.MoveToFront(ent)
|
|
||||||
return ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
|
||||||
// or deleting it for being stale.
|
|
||||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
|
||||||
_, ok = c.items[key]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peek returns the key value (or undefined if not found) without updating
|
|
||||||
// the "recently used"-ness of the key.
|
|
||||||
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
|
|
||||||
var ent *internal.Entry[K, V]
|
|
||||||
if ent, ok = c.items[key]; ok {
|
|
||||||
return ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the provided key from the cache, returning if the
|
|
||||||
// key was contained.
|
|
||||||
func (c *LRU[K, V]) Remove(key K) (present bool) {
|
|
||||||
if ent, ok := c.items[key]; ok {
|
|
||||||
c.removeElement(ent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveOldest removes the oldest item from the cache.
|
|
||||||
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
|
||||||
if ent := c.evictList.Back(); ent != nil {
|
|
||||||
c.removeElement(ent)
|
|
||||||
return ent.Key, ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOldest returns the oldest entry
|
|
||||||
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
|
|
||||||
if ent := c.evictList.Back(); ent != nil {
|
|
||||||
return ent.Key, ent.Value, true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
||||||
func (c *LRU[K, V]) Keys() []K {
|
|
||||||
keys := make([]K, c.evictList.Length())
|
|
||||||
i := 0
|
|
||||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
|
||||||
keys[i] = ent.Key
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
|
||||||
func (c *LRU[K, V]) Values() []V {
|
|
||||||
values := make([]V, len(c.items))
|
|
||||||
i := 0
|
|
||||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
|
||||||
values[i] = ent.Value
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of items in the cache.
|
|
||||||
func (c *LRU[K, V]) Len() int {
|
|
||||||
return c.evictList.Length()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap returns the capacity of the cache
|
|
||||||
func (c *LRU[K, V]) Cap() int {
|
|
||||||
return c.size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize changes the cache size.
|
|
||||||
func (c *LRU[K, V]) Resize(size int) (evicted int) {
|
|
||||||
diff := c.Len() - size
|
|
||||||
if diff < 0 {
|
|
||||||
diff = 0
|
|
||||||
}
|
|
||||||
for i := 0; i < diff; i++ {
|
|
||||||
c.removeOldest()
|
|
||||||
}
|
|
||||||
c.size = size
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeOldest removes the oldest item from the cache.
|
|
||||||
func (c *LRU[K, V]) removeOldest() {
|
|
||||||
if ent := c.evictList.Back(); ent != nil {
|
|
||||||
c.removeElement(ent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeElement is used to remove a given list element from the cache
|
|
||||||
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
|
|
||||||
c.evictList.Remove(e)
|
|
||||||
delete(c.items, e.Key)
|
|
||||||
if c.onEvict != nil {
|
|
||||||
c.onEvict(e.Key, e.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
// Package simplelru provides simple LRU implementation based on build-in container/list.
|
|
||||||
package simplelru
|
|
||||||
|
|
||||||
// LRUCache is the interface for simple LRU cache.
|
|
||||||
type LRUCache[K comparable, V any] interface {
|
|
||||||
// Adds a value to the cache, returns true if an eviction occurred and
|
|
||||||
// updates the "recently used"-ness of the key.
|
|
||||||
Add(key K, value V) bool
|
|
||||||
|
|
||||||
// Returns key's value from the cache and
|
|
||||||
// updates the "recently used"-ness of the key. #value, isFound
|
|
||||||
Get(key K) (value V, ok bool)
|
|
||||||
|
|
||||||
// Checks if a key exists in cache without updating the recent-ness.
|
|
||||||
Contains(key K) (ok bool)
|
|
||||||
|
|
||||||
// Returns key's value without updating the "recently used"-ness of the key.
|
|
||||||
Peek(key K) (value V, ok bool)
|
|
||||||
|
|
||||||
// Removes a key from the cache.
|
|
||||||
Remove(key K) bool
|
|
||||||
|
|
||||||
// Removes the oldest entry from cache.
|
|
||||||
RemoveOldest() (K, V, bool)
|
|
||||||
|
|
||||||
// Returns the oldest entry from the cache. #key, value, isFound
|
|
||||||
GetOldest() (K, V, bool)
|
|
||||||
|
|
||||||
// Returns a slice of the keys in the cache, from oldest to newest.
|
|
||||||
Keys() []K
|
|
||||||
|
|
||||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
|
||||||
Values() []V
|
|
||||||
|
|
||||||
// Returns the number of items in the cache.
|
|
||||||
Len() int
|
|
||||||
|
|
||||||
// Returns the capacity of the cache.
|
|
||||||
Cap() int
|
|
||||||
|
|
||||||
// Clears all cache entries.
|
|
||||||
Purge()
|
|
||||||
|
|
||||||
// Resizes cache, returning number evicted
|
|
||||||
Resize(int) int
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package simplelru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLRU(t *testing.T) {
|
|
||||||
evictCounter := 0
|
|
||||||
onEvicted := func(k int, v int) {
|
|
||||||
if k != v {
|
|
||||||
t.Fatalf("Evict values not equal (%v!=%v)", k, v)
|
|
||||||
}
|
|
||||||
evictCounter++
|
|
||||||
}
|
|
||||||
l, err := NewLRU(128, onEvicted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
if l.Len() != 128 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if l.Cap() != 128 {
|
|
||||||
t.Fatalf("expect %d, but %d", 128, l.Cap())
|
|
||||||
}
|
|
||||||
|
|
||||||
if evictCounter != 128 {
|
|
||||||
t.Fatalf("bad evict count: %v", evictCounter)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, k := range l.Keys() {
|
|
||||||
if v, ok := l.Get(k); !ok || v != k || v != i+128 {
|
|
||||||
t.Fatalf("bad key: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, v := range l.Values() {
|
|
||||||
if v != i+128 {
|
|
||||||
t.Fatalf("bad value: %v", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 256; i++ {
|
|
||||||
if _, ok := l.Get(i); !ok {
|
|
||||||
t.Fatalf("should not be evicted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 128; i < 192; i++ {
|
|
||||||
if ok := l.Remove(i); !ok {
|
|
||||||
t.Fatalf("should be contained")
|
|
||||||
}
|
|
||||||
if ok := l.Remove(i); ok {
|
|
||||||
t.Fatalf("should not be contained")
|
|
||||||
}
|
|
||||||
if _, ok := l.Get(i); ok {
|
|
||||||
t.Fatalf("should be deleted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Get(192) // expect 192 to be last key in l.Keys()
|
|
||||||
|
|
||||||
for i, k := range l.Keys() {
|
|
||||||
if (i < 63 && k != i+193) || (i == 63 && k != 192) {
|
|
||||||
t.Fatalf("out of order key: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Purge()
|
|
||||||
if l.Len() != 0 {
|
|
||||||
t.Fatalf("bad len: %v", l.Len())
|
|
||||||
}
|
|
||||||
if _, ok := l.Get(200); ok {
|
|
||||||
t.Fatalf("should contain nothing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLRU_GetOldest_RemoveOldest(t *testing.T) {
|
|
||||||
l, err := NewLRU[int, int](128, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
for i := 0; i < 256; i++ {
|
|
||||||
l.Add(i, i)
|
|
||||||
}
|
|
||||||
k, _, ok := l.GetOldest()
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("missing")
|
|
||||||
}
|
|
||||||
if k != 128 {
|
|
||||||
t.Fatalf("bad: %v", k)
|
|
||||||
}
|
|
||||||
|
|
||||||
k, _, ok = l.RemoveOldest()
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("missing")
|
|
||||||
}
|
|
||||||
if k != 128 {
|
|
||||||
t.Fatalf("bad: %v", k)
|
|
||||||
}
|
|
||||||
|
|
||||||
k, _, ok = l.RemoveOldest()
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("missing")
|
|
||||||
}
|
|
||||||
if k != 129 {
|
|
||||||
t.Fatalf("bad: %v", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Add returns true/false if an eviction occurred
|
|
||||||
func TestLRU_Add(t *testing.T) {
|
|
||||||
evictCounter := 0
|
|
||||||
onEvicted := func(k int, v int) {
|
|
||||||
evictCounter++
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := NewLRU(1, onEvicted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.Add(1, 1) == true || evictCounter != 0 {
|
|
||||||
t.Errorf("should not have an eviction")
|
|
||||||
}
|
|
||||||
if l.Add(2, 2) == false || evictCounter != 1 {
|
|
||||||
t.Errorf("should have an eviction")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Contains doesn't update recent-ness
|
|
||||||
func TestLRU_Contains(t *testing.T) {
|
|
||||||
l, err := NewLRU[int, int](2, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if !l.Contains(1) {
|
|
||||||
t.Errorf("1 should be contained")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("Contains should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Peek doesn't update recent-ness
|
|
||||||
func TestLRU_Peek(t *testing.T) {
|
|
||||||
l, err := NewLRU[int, int](2, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
if v, ok := l.Peek(1); !ok || v != 1 {
|
|
||||||
t.Errorf("1 should be set to 1: %v, %v", v, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("should not have updated recent-ness of 1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Resize can upsize and downsize
|
|
||||||
func TestLRU_Resize(t *testing.T) {
|
|
||||||
onEvictCounter := 0
|
|
||||||
onEvicted := func(k int, v int) {
|
|
||||||
onEvictCounter++
|
|
||||||
}
|
|
||||||
l, err := NewLRU(2, onEvicted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downsize
|
|
||||||
l.Add(1, 1)
|
|
||||||
l.Add(2, 2)
|
|
||||||
evicted := l.Resize(1)
|
|
||||||
if evicted != 1 {
|
|
||||||
t.Errorf("1 element should have been evicted: %v", evicted)
|
|
||||||
}
|
|
||||||
if onEvictCounter != 1 {
|
|
||||||
t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(3, 3)
|
|
||||||
if l.Contains(1) {
|
|
||||||
t.Errorf("Element 1 should have been evicted")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upsize
|
|
||||||
evicted = l.Resize(2)
|
|
||||||
if evicted != 0 {
|
|
||||||
t.Errorf("0 elements should have been evicted: %v", evicted)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Add(4, 4)
|
|
||||||
if !l.Contains(3) || !l.Contains(4) {
|
|
||||||
t.Errorf("Cache should have contained 2 elements")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *LRU[K, V]) wantKeys(t *testing.T, want []K) {
|
|
||||||
t.Helper()
|
|
||||||
got := c.Keys()
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("wrong keys got: %v, want: %v ", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCache_EvictionSameKey(t *testing.T) {
|
|
||||||
var evictedKeys []int
|
|
||||||
|
|
||||||
cache, _ := NewLRU(
|
|
||||||
2,
|
|
||||||
func(key int, _ struct{}) {
|
|
||||||
evictedKeys = append(evictedKeys, key)
|
|
||||||
})
|
|
||||||
|
|
||||||
if evicted := cache.Add(1, struct{}{}); evicted {
|
|
||||||
t.Error("First 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1})
|
|
||||||
|
|
||||||
if evicted := cache.Add(2, struct{}{}); evicted {
|
|
||||||
t.Error("2: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 2})
|
|
||||||
|
|
||||||
if evicted := cache.Add(1, struct{}{}); evicted {
|
|
||||||
t.Error("Second 1: got unexpected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{2, 1})
|
|
||||||
|
|
||||||
if evicted := cache.Add(3, struct{}{}); !evicted {
|
|
||||||
t.Error("3: did not get expected eviction")
|
|
||||||
}
|
|
||||||
cache.wantKeys(t, []int{1, 3})
|
|
||||||
|
|
||||||
want := []int{2}
|
|
||||||
if !reflect.DeepEqual(evictedKeys, want) {
|
|
||||||
t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2014, 2025
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package lru
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getRand(tb testing.TB) int64 {
|
|
||||||
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
|
||||||
if err != nil {
|
|
||||||
tb.Fatal(err)
|
|
||||||
}
|
|
||||||
return out.Int64()
|
|
||||||
}
|
|
||||||
@@ -8,3 +8,12 @@ func FindWithIndex[T any](slice []T, predicate func(item T) bool) (int, *T, bool
|
|||||||
}
|
}
|
||||||
return -1, nil, false
|
return -1, nil, false
|
||||||
}
|
}
|
||||||
|
func LastFourElements[T any](s []T, n1 int) []T {
|
||||||
|
n := len(s)
|
||||||
|
if n <= n1 {
|
||||||
|
// 切片长度小于等于4时,返回整个切片
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
// 切片长度大于4时,返回最后4个元素(从n-4索引到末尾)
|
||||||
|
return s[n-n1:]
|
||||||
|
}
|
||||||
|
|||||||
1
go.work
1
go.work
@@ -13,7 +13,6 @@ use (
|
|||||||
./common/utils/go-jsonrpc
|
./common/utils/go-jsonrpc
|
||||||
./common/utils/go-sensitive-word-1.3.3
|
./common/utils/go-sensitive-word-1.3.3
|
||||||
./common/utils/goja
|
./common/utils/goja
|
||||||
./common/utils/golang-lru-main
|
|
||||||
./common/utils/limit
|
./common/utils/limit
|
||||||
./common/utils/log
|
./common/utils/log
|
||||||
./common/utils/qqwry
|
./common/utils/qqwry
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"blazing/common/data/xmlres"
|
"blazing/common/data/xmlres"
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
"math/rand"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"blazing/logic/service/fight"
|
"blazing/logic/service/fight"
|
||||||
@@ -39,7 +38,7 @@ func processMonID(bm string) string {
|
|||||||
selected = monid[0]
|
selected = monid[0]
|
||||||
default:
|
default:
|
||||||
// 长度大于1时,随机选取一个
|
// 长度大于1时,随机选取一个
|
||||||
randomIdx := rand.Intn(len(monid))
|
randomIdx := grand.Intn(len(monid))
|
||||||
selected = monid[randomIdx]
|
selected = monid[randomIdx]
|
||||||
}
|
}
|
||||||
return selected
|
return selected
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"blazing/common/data/xmlres"
|
"blazing/common/data/xmlres"
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"blazing/logic/service/item"
|
"blazing/logic/service/item"
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
"blazing/modules/blazing/model"
|
"blazing/modules/blazing/model"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h Controller) UserItemList(data *item.ItemListInboundInfo, c *player.Player) (result *item.ItemListOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) UserItemList(data *item.ItemListInboundInfo, c *player.Player) (result *item.ItemListOutboundInfo, err errorcode.ErrorCode) {
|
||||||
@@ -113,10 +113,7 @@ func (h Controller) TalkCate(data *item.TalkCateInboundInfo, c *player.Player) (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano()) // UnixNano 精度更高,避免短时间内种子重复
|
randomNum := grand.Intn(10) + 1
|
||||||
|
|
||||||
// 2. 生成 1-10 的随机数:rand.Intn(10) → 0-9,+1 后范围变为 1-10
|
|
||||||
randomNum := rand.Intn(10) + 1
|
|
||||||
c.Service.Talk.Exec(func(t map[uint32]uint32) bool {
|
c.Service.Talk.Exec(func(t map[uint32]uint32) bool {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
t = make(map[uint32]uint32)
|
t = make(map[uint32]uint32)
|
||||||
|
|||||||
@@ -17,24 +17,20 @@ func (h *Controller) GetPetInfo(
|
|||||||
data *pet.InInfo,
|
data *pet.InInfo,
|
||||||
c *player.Player) (result *pet.OutInfo,
|
c *player.Player) (result *pet.OutInfo,
|
||||||
err errorcode.ErrorCode) { //这个时候player应该是空的
|
err errorcode.ErrorCode) { //这个时候player应该是空的
|
||||||
|
_, tet, ok := c.FindPet(data.CatchTime)
|
||||||
|
|
||||||
for _, pi := range c.Info.PetList {
|
if ok {
|
||||||
if pi.CatchTime == data.CatchTime {
|
result = &pet.OutInfo{
|
||||||
result = &pet.OutInfo{
|
PetInfo: *tet,
|
||||||
PetInfo: pi,
|
|
||||||
}
|
|
||||||
return result, 0
|
|
||||||
}
|
}
|
||||||
|
return result, 0
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
|
|
||||||
result = &pet.OutInfo{
|
result = &pet.OutInfo{
|
||||||
PetInfo: c.Service.Pet.PetInfo_One(data.CatchTime).Data,
|
PetInfo: c.Service.Pet.PetInfo_One(data.CatchTime).Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,46 +69,36 @@ func (h *Controller) PetRelease(
|
|||||||
}
|
}
|
||||||
switch data.Flag {
|
switch data.Flag {
|
||||||
case 0:
|
case 0:
|
||||||
var temp []model.PetInfo
|
|
||||||
|
|
||||||
for _, v := range c.Info.PetList {
|
|
||||||
if v.CatchTime == uint32(data.CatchTime) {
|
|
||||||
c.Service.Pet.PetInfo_One_exec(data.CatchTime, func(t *model.PetEX) {
|
|
||||||
t.Data = v
|
|
||||||
//t.InBag = 0
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
} else {
|
|
||||||
temp = append(temp, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
index, _, ok := c.FindPet(data.CatchTime)
|
||||||
|
if ok {
|
||||||
|
c.Info.PetList = append(c.Info.PetList[:index], c.Info.PetList[index+1:]...)
|
||||||
}
|
}
|
||||||
c.Info.PetList = temp
|
|
||||||
// break // 只移除第一个匹配值,若需移除所有,可省略 break 继续循环
|
// break // 只移除第一个匹配值,若需移除所有,可省略 break 继续循环
|
||||||
case 1:
|
case 1:
|
||||||
//todo 背包
|
if len(c.Info.PetList) < 6 {
|
||||||
c.Service.Pet.PetInfo_One_exec(data.CatchTime, func(t *model.PetEX) {
|
//todo 背包
|
||||||
|
c.Service.Pet.PetInfo_One_exec(data.CatchTime, func(t *model.PetEX) {
|
||||||
|
|
||||||
_, _, ok := utils.FindWithIndex(c.Info.PetList, func(item model.PetInfo) bool {
|
_, _, ok := c.FindPet(data.CatchTime)
|
||||||
return item.CatchTime == uint32(data.CatchTime)
|
|
||||||
|
//如果背包没找到,再放入背包
|
||||||
|
if !ok && t.CatchTime != 0 {
|
||||||
|
//t.InBag = 1
|
||||||
|
c.Info.PetList = append(c.Info.PetList, t.Data)
|
||||||
|
result.PetInfo = t.Data
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
//如果背包没找到,再放入背包
|
|
||||||
if !ok && t.CatchTime != 0 {
|
|
||||||
//t.InBag = 1
|
|
||||||
c.Info.PetList = append(c.Info.PetList, t.Data)
|
|
||||||
result.PetInfo = t.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.Info.PetList) > 0 {
|
if len(c.Info.PetList) > 0 {
|
||||||
result.FirstPetTime = c.Info.PetList[0].CatchTime //设置首发
|
result.FirstPetTime = c.Info.PetList[0].CatchTime //设置首发
|
||||||
}
|
}
|
||||||
//service.NewUserService(c.Info.UserID).PetAdd( *r)
|
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,16 +106,13 @@ func (h *Controller) PetRelease(
|
|||||||
func (h *Controller) PlayerShowPet(
|
func (h *Controller) PlayerShowPet(
|
||||||
data *pet.PetShowInboundInfo, c *player.Player) (result *pet.PetShowOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
|
data *pet.PetShowInboundInfo, c *player.Player) (result *pet.PetShowOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
|
||||||
result = &pet.PetShowOutboundInfo{}
|
result = &pet.PetShowOutboundInfo{}
|
||||||
|
_, onpet, ok := c.FindPet(data.CatchTime)
|
||||||
|
|
||||||
for _, pi := range c.Info.PetList {
|
if ok {
|
||||||
if pi.CatchTime == data.CatchTime {
|
copier.Copy(&result, onpet)
|
||||||
copier.Copy(&result, pi)
|
result.Flag = data.Flag
|
||||||
result.Flag = data.Flag
|
result.UserID = data.Head.UserID
|
||||||
result.UserID = data.Head.UserID
|
c.GetSpace().Broadcast(c, data.Head.CMD, result)
|
||||||
c.GetSpace().Broadcast(c, data.Head.CMD, result)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -139,9 +122,7 @@ func (h *Controller) PetOneCure(
|
|||||||
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
||||||
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
|
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
|
||||||
}
|
}
|
||||||
_, onpet, ok := utils.FindWithIndex(c.Info.PetList, func(item model.PetInfo) bool {
|
_, onpet, ok := c.FindPet(data.CatchTime)
|
||||||
return item.CatchTime == data.CatchTime
|
|
||||||
})
|
|
||||||
if ok {
|
if ok {
|
||||||
onpet.Cure()
|
onpet.Cure()
|
||||||
}
|
}
|
||||||
@@ -155,20 +136,21 @@ func (h *Controller) PetOneCure(
|
|||||||
// 精灵首发
|
// 精灵首发
|
||||||
func (h *Controller) PetFirst(
|
func (h *Controller) PetFirst(
|
||||||
data *pet.PetDefaultInboundInfo, c *player.Player) (result *pet.PetDefaultOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
|
data *pet.PetDefaultInboundInfo, c *player.Player) (result *pet.PetDefaultOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
|
||||||
result = &pet.PetDefaultOutboundInfo{}
|
//擂台住不能换精灵
|
||||||
var ttt []model.PetInfo
|
if c.GetSpace().Owner.UserID == c.Info.UserID {
|
||||||
|
return result, errorcode.ErrorCodes.ErrChampionCannotSwitch
|
||||||
for index, pi := range c.Info.PetList {
|
|
||||||
if pi.CatchTime == data.CatchTime {
|
|
||||||
ttt = append(ttt, pi)
|
|
||||||
ttt = append(ttt, c.Info.PetList[:index]...)
|
|
||||||
ttt = append(ttt, c.Info.PetList[index+1:]...)
|
|
||||||
result.IsDefault = 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
c.Info.PetList = ttt
|
|
||||||
|
result = &pet.PetDefaultOutboundInfo{}
|
||||||
|
|
||||||
|
index, _, ok := c.FindPet(data.CatchTime)
|
||||||
|
if ok && index != 0 {
|
||||||
|
|
||||||
|
c.Info.PetList[index], c.Info.PetList[0] = c.Info.PetList[0], c.Info.PetList[index]
|
||||||
|
|
||||||
|
result.IsDefault = 1
|
||||||
|
}
|
||||||
|
|
||||||
return result, 0
|
return result, 0
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -177,9 +159,7 @@ func (h *Controller) PetFirst(
|
|||||||
// 返回:索引、元素指针、是否找到
|
// 返回:索引、元素指针、是否找到
|
||||||
|
|
||||||
func (h Controller) SetPetExp(data *pet.PetSetExpInboundInfo, c *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) SetPetExp(data *pet.PetSetExpInboundInfo, c *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) {
|
||||||
_, onpet, ok := utils.FindWithIndex(c.Info.PetList, func(item model.PetInfo) bool {
|
_, onpet, ok := c.FindPet(data.CatchTime)
|
||||||
return item.CatchTime == data.CatchTime
|
|
||||||
})
|
|
||||||
if ok {
|
if ok {
|
||||||
|
|
||||||
c.AddPetExp(onpet, data.Exp)
|
c.AddPetExp(onpet, data.Exp)
|
||||||
@@ -190,28 +170,18 @@ func (h Controller) SetPetExp(data *pet.PetSetExpInboundInfo, c *player.Player)
|
|||||||
}, 0
|
}, 0
|
||||||
}
|
}
|
||||||
func (h Controller) SetPetSkill(data *pet.ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
|
func (h Controller) SetPetSkill(data *pet.ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
|
||||||
_, onpet, ok := utils.FindWithIndex(c.Info.PetList, func(item model.PetInfo) bool {
|
_, onpet, ok := c.FindPet(data.CatchTime)
|
||||||
return item.CatchTime == data.CatchTime
|
|
||||||
})
|
|
||||||
if ok {
|
if ok {
|
||||||
_, _, ok := utils.FindWithIndex(onpet.SkillList, func(item model.SkillInfo) bool { //已经存在技能
|
_, HasSkill, ok := utils.FindWithIndex(onpet.SkillList, func(item model.SkillInfo) bool { //已经存在技能
|
||||||
return item.ID == data.ReplaceSkill
|
return item.ID == data.ReplaceSkill
|
||||||
})
|
})
|
||||||
if ok {
|
if !ok {
|
||||||
return
|
HasSkill.ID = data.ReplaceSkill
|
||||||
}
|
HasSkill.PP = uint32(xmlres.SkillMap[int(HasSkill.ID)].MaxPP)
|
||||||
for i := 0; i < len(onpet.SkillList); i++ {
|
|
||||||
if onpet.SkillList[i].ID == data.HasSkill {
|
|
||||||
onpet.SkillList[i].ID = data.ReplaceSkill
|
|
||||||
onpet.SkillList[i].PP = uint32(xmlres.SkillMap[int(onpet.SkillList[i].ID)].MaxPP)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// onpet.SkillList = lo.UniqBy(onpet.SkillList, func(s model.SkillInfo) int {
|
|
||||||
// return int(s.ID)
|
|
||||||
// })
|
|
||||||
return &pet.ChangeSkillOutInfo{
|
return &pet.ChangeSkillOutInfo{
|
||||||
CatchTime: data.CatchTime,
|
CatchTime: data.CatchTime,
|
||||||
}, 0
|
}, 0
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package effect
|
|||||||
import (
|
import (
|
||||||
"blazing/logic/service/fight/input"
|
"blazing/logic/service/fight/input"
|
||||||
"blazing/logic/service/fight/node"
|
"blazing/logic/service/fight/node"
|
||||||
"math/rand"
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
// -----------------------------------------------------------
|
||||||
@@ -49,7 +50,7 @@ func (e *EffectRandomPower) Skill_Hit() bool {
|
|||||||
if e.Input != nil && e.Input.FightC != nil {
|
if e.Input != nil && e.Input.FightC != nil {
|
||||||
n = int(e.Input.FightC.GetRand().Int31n(int32(e.Max-e.Min+1))) + e.Min
|
n = int(e.Input.FightC.GetRand().Int31n(int32(e.Max-e.Min+1))) + e.Min
|
||||||
} else {
|
} else {
|
||||||
n = rand.Intn(e.Max-e.Min+1) + e.Min
|
n = grand.Intn(e.Max-e.Min+1) + e.Min
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Ctx().SkillEntity.Power = n
|
e.Ctx().SkillEntity.Power = n
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package effect
|
package effect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
element "blazing/common/data/Element"
|
||||||
"blazing/logic/service/fight/info"
|
"blazing/logic/service/fight/info"
|
||||||
"blazing/logic/service/fight/input"
|
"blazing/logic/service/fight/input"
|
||||||
"blazing/logic/service/fight/node"
|
"blazing/logic/service/fight/node"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
// -----------------------------------------------------------
|
||||||
@@ -26,7 +29,9 @@ func (e *Effect13) OnSkill() bool {
|
|||||||
if !e.Hit() {
|
if !e.Hit() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if gconv.Int(e.Ctx().Opp.CurrentPet.PetInfo.Type) == int(element.ElementTypeGrass) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
duration := e.EffectNode.SideEffectArgs[0] - 1
|
duration := e.EffectNode.SideEffectArgs[0] - 1
|
||||||
//duration++
|
//duration++
|
||||||
// 获取状态效果
|
// 获取状态效果
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"blazing/logic/service/fight/input"
|
"blazing/logic/service/fight/input"
|
||||||
"blazing/logic/service/fight/node"
|
"blazing/logic/service/fight/node"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -100,9 +99,6 @@ type ParasiticSeed struct {
|
|||||||
// 技能命中前触发寄生效果
|
// 技能命中前触发寄生效果
|
||||||
func (e *ParasiticSeed) Action_start_ex(attacker, defender *action.SelectSkillAction) bool {
|
func (e *ParasiticSeed) Action_start_ex(attacker, defender *action.SelectSkillAction) bool {
|
||||||
// 过滤特定类型单位(假设1是植物类型,使用枚举替代魔法数字)
|
// 过滤特定类型单位(假设1是植物类型,使用枚举替代魔法数字)
|
||||||
if gconv.Int(e.Ctx().Our.CurrentPet.Type) == int(element.ElementTypeGrass) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
damage := decimal.NewFromUint64(uint64(e.Ctx().Our.CurrentPet.Info.MaxHp)).
|
damage := decimal.NewFromUint64(uint64(e.Ctx().Our.CurrentPet.Info.MaxHp)).
|
||||||
Div(decimal.NewFromInt(8))
|
Div(decimal.NewFromInt(8))
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ type BattlePetEntity struct {
|
|||||||
Info model.PetInfo //通过偏移赋值
|
Info model.PetInfo //通过偏移赋值
|
||||||
//*input.Input
|
//*input.Input
|
||||||
//PType int
|
//PType int
|
||||||
statusConditions sync.Map // key: StatusCondition, value: int (剩余回合)
|
statusConditions sync.Map // key: StatusCondition, value: int (剩余回合)
|
||||||
Skills [4]*SkillEntity // 技能槽(最多4个技能)
|
Skills map[uint32]*SkillEntity // 技能槽(最多4个技能)
|
||||||
//Status StatusDict //精灵的状态
|
//Status StatusDict //精灵的状态
|
||||||
//能力提升属性
|
//能力提升属性
|
||||||
//Prop PropDict
|
//Prop PropDict
|
||||||
@@ -42,10 +42,12 @@ func CreateBattlePetEntity(info model.PetInfo, rand *rand.Rand) *BattlePetEntity
|
|||||||
|
|
||||||
ret.PetInfo = xmlres.PetMAP[int(info.ID)] //注入精灵信息
|
ret.PetInfo = xmlres.PetMAP[int(info.ID)] //注入精灵信息
|
||||||
ret.Info = info
|
ret.Info = info
|
||||||
|
ret.Skills = make(map[uint32]*SkillEntity)
|
||||||
|
|
||||||
for i := 0; i < len(info.SkillList); i++ {
|
for i := 0; i < len(info.SkillList); i++ {
|
||||||
//todo 技能信息应该每回合进行深拷贝,保证每次的技能效果都是不一样的
|
//todo 技能信息应该每回合进行深拷贝,保证每次的技能效果都是不一样的
|
||||||
|
|
||||||
ret.Skills[i] = CreateSkill(&info.SkillList[i], rand, ret)
|
ret.Skills[info.SkillList[i].ID] = CreateSkill(&info.SkillList[i], rand, ret)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,7 +157,7 @@ func RandomElfIDs(n int) []int {
|
|||||||
|
|
||||||
for len(ids) < n {
|
for len(ids) < n {
|
||||||
// 生成1-2000的随机数
|
// 生成1-2000的随机数
|
||||||
id := rand.Intn(2000) + 1 // rand.Intn(2000)生成0-1999,+1后为1-2000
|
id := grand.Intn(2000) + 1 // rand.Intn(2000)生成0-1999,+1后为1-2000
|
||||||
|
|
||||||
// 检查是否已抽取
|
// 检查是否已抽取
|
||||||
if _, exists := used[id]; !exists {
|
if _, exists := used[id]; !exists {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"blazing/logic/service/fight/action"
|
"blazing/logic/service/fight/action"
|
||||||
"blazing/logic/service/fight/info"
|
"blazing/logic/service/fight/info"
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -262,10 +262,14 @@ func (our *Input) GetAction(opp *Input) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
randomIdx := rand.Intn(len(allSkills))
|
randomIdx := grand.Intn(len(allSkills))
|
||||||
chosenSkill := skills[randomIdx]
|
for i, v := range skills {
|
||||||
our.FightC.UseSkill(our.Player, int32(chosenSkill.ID))
|
if randomIdx == int(i) {
|
||||||
// i.FightC.UseSkill(i.Player, int32(bestSkill.skill.ID))
|
our.FightC.UseSkill(our.Player, int32(v.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
our.FightC.UseSkill(our.Player, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算技能威力
|
// 计算技能威力
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package player
|
package player
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blazing/common/utils"
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/fight/info"
|
"blazing/logic/service/fight/info"
|
||||||
"blazing/modules/blazing/model"
|
"blazing/modules/blazing/model"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type baseplayer struct {
|
type baseplayer struct {
|
||||||
@@ -17,7 +20,7 @@ type baseplayer struct {
|
|||||||
|
|
||||||
// NewPlayerCaptureContext 创建用户捕捉上下文(每次登录调用)
|
// NewPlayerCaptureContext 创建用户捕捉上下文(每次登录调用)
|
||||||
func newbaseplayer() baseplayer {
|
func newbaseplayer() baseplayer {
|
||||||
rng := rand.New(rand.NewSource(time.Now().UnixNano() + int64(rand.Intn(1000000))))
|
rng := rand.New(rand.NewSource(time.Now().UnixNano() + int64(grand.Intn(1000000))))
|
||||||
ret := baseplayer{}
|
ret := baseplayer{}
|
||||||
ret.PlayerCaptureContext = &info.PlayerCaptureContext{
|
ret.PlayerCaptureContext = &info.PlayerCaptureContext{
|
||||||
rng,
|
rng,
|
||||||
@@ -41,6 +44,13 @@ func (f *baseplayer) GetPlayerCaptureContext() *info.PlayerCaptureContext {
|
|||||||
return f.PlayerCaptureContext
|
return f.PlayerCaptureContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *baseplayer) FindPet(CatchTime uint32) (int, *model.PetInfo, bool) {
|
||||||
|
|
||||||
|
return utils.FindWithIndex(f.Info.PetList, func(item model.PetInfo) bool {
|
||||||
|
return item.CatchTime == CatchTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// // 计算整数的二进制1的个数(Integer.bitCount)
|
// // 计算整数的二进制1的个数(Integer.bitCount)
|
||||||
// func bitsCount(n int) int {
|
// func bitsCount(n int) int {
|
||||||
// count := 0
|
// count := 0
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (p *Player) AddPetExp(petinfo *model.PetInfo, addExp uint32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理技能学习
|
// 处理技能学习
|
||||||
canLearnSkillList := LastFourElements(petinfo.GetLevelRangeCanLearningSkills(originalLevel, petinfo.Level)) //获取最后四个技能,如果不足,那就取全部技能
|
canLearnSkillList := utils.LastFourElements(petinfo.GetLevelRangeCanLearningSkills(originalLevel, petinfo.Level), 4) //获取最后四个技能,如果不足,那就取全部技能
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
|
|
||||||
@@ -86,16 +86,6 @@ func (p *Player) AddPetExp(petinfo *model.PetInfo, addExp uint32) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LastFourElements[T any](s []T) []T {
|
|
||||||
n := len(s)
|
|
||||||
if n <= 4 {
|
|
||||||
// 切片长度小于等于4时,返回整个切片
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// 切片长度大于4时,返回最后4个元素(从n-4索引到末尾)
|
|
||||||
return s[n-4:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenPetInfo 生成一个新的精灵实例
|
// GenPetInfo 生成一个新的精灵实例
|
||||||
// - 参数为 -1 时表示随机生成对应属性
|
// - 参数为 -1 时表示随机生成对应属性
|
||||||
// * @param petTypeId 精灵类型ID
|
// * @param petTypeId 精灵类型ID
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/antlabs/timer"
|
"github.com/antlabs/timer"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
"github.com/panjf2000/gnet/v2"
|
"github.com/panjf2000/gnet/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,13 +225,13 @@ func (p *Player) genMonster(mapid uint32) {
|
|||||||
|
|
||||||
// 生成0-9之间三个不重复的随机数 进地图5s
|
// 生成0-9之间三个不重复的随机数 进地图5s
|
||||||
func generateThreeUniqueNumbers() [3]int {
|
func generateThreeUniqueNumbers() [3]int {
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
selected := make(map[int]bool)
|
selected := make(map[int]bool)
|
||||||
var result [3]int
|
var result [3]int
|
||||||
index := 0
|
index := 0
|
||||||
|
|
||||||
for index < 3 {
|
for index < 3 {
|
||||||
num := rand.Intn(9)
|
num := grand.Intn(9)
|
||||||
if !selected[num] {
|
if !selected[num] {
|
||||||
selected[num] = true
|
selected[num] = true
|
||||||
result[index] = num
|
result[index] = num
|
||||||
@@ -243,7 +244,7 @@ func generateThreeUniqueNumbers() [3]int {
|
|||||||
// 从三个数字中移除一个,并从剩余6个数字中选一个补充 10s
|
// 从三个数字中移除一个,并从剩余6个数字中选一个补充 10s
|
||||||
func replaceOneNumber(original [3]int) ([3]int, int, int) {
|
func replaceOneNumber(original [3]int) ([3]int, int, int) {
|
||||||
// 随机选择要移除的索引(0-2)
|
// 随机选择要移除的索引(0-2)
|
||||||
removeIndex := rand.Intn(3)
|
removeIndex := grand.Intn(3)
|
||||||
removedNum := original[removeIndex]
|
removedNum := original[removeIndex]
|
||||||
|
|
||||||
// 找出所有不在原始数组中的数字(候选数字)
|
// 找出所有不在原始数组中的数字(候选数字)
|
||||||
@@ -260,7 +261,7 @@ func replaceOneNumber(original [3]int) ([3]int, int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从候选数字中随机选择一个
|
// 从候选数字中随机选择一个
|
||||||
newNum := candidates[rand.Intn(len(candidates))]
|
newNum := candidates[grand.Intn(len(candidates))]
|
||||||
|
|
||||||
// 创建新数组并替换数字
|
// 创建新数组并替换数字
|
||||||
newNumbers := original
|
newNumbers := original
|
||||||
|
|||||||
@@ -90,13 +90,3 @@ func (s *Space) Walk(c common.PlayerI, info *info.WalkOutInfo) {
|
|||||||
s.Broadcast(c, 2101, info)
|
s.Broadcast(c, 2101, info)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LastFourElements[T any](s []T) []T {
|
|
||||||
n := len(s)
|
|
||||||
if n <= 30 {
|
|
||||||
// 切片长度小于等于4时,返回整个切片
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// 切片长度大于4时,返回最后4个元素(从n-4索引到末尾)
|
|
||||||
return s[n-30:]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/data/xmlres"
|
"blazing/common/data/xmlres"
|
||||||
|
"blazing/common/utils"
|
||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -401,7 +402,7 @@ func GenPetInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 技能学习 ----
|
// ---- 技能学习 ----
|
||||||
skills := LastFourElements(p.GetLevelRangeCanLearningSkills(0, p.Level)) // 最后四个技能
|
skills := utils.LastFourElements(p.GetLevelRangeCanLearningSkills(0, p.Level), 4) // 最后四个技能
|
||||||
|
|
||||||
for i := 0; i < len(skills) && i < 4; i++ {
|
for i := 0; i < len(skills) && i < 4; i++ {
|
||||||
skillID := skills[i]
|
skillID := skills[i]
|
||||||
@@ -422,16 +423,6 @@ func GenPetInfo(
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func LastFourElements[T any](s []T) []T {
|
|
||||||
n := len(s)
|
|
||||||
if n <= 4 {
|
|
||||||
// 切片长度小于等于4时,返回整个切片
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// 切片长度大于4时,返回最后4个元素(从n-4索引到末尾)
|
|
||||||
return s[n-4:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 除数数组(放大100倍)
|
// 除数数组(放大100倍)
|
||||||
// 数组按递增顺序排列,用于判断个体值等级
|
// 数组按递增顺序排列,用于判断个体值等级
|
||||||
var divisors = []int{
|
var divisors = []int{
|
||||||
|
|||||||
Reference in New Issue
Block a user