Skip to content

Commit 4f0dc8d

Browse files
authored
Merge pull request #4 from praserx/v2
feat: minor fixes and improvements, enhanced readme, updated workflows
2 parents 0abef74 + 4dca9ef commit 4f0dc8d

7 files changed

Lines changed: 136 additions & 126 deletions

File tree

.github/copilot-instructions.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copilot Instructions
2+
3+
This project implements a high-performance in-memory cache in Go, optimized for low-latency data retrieval in performance-critical systems. The codebase primarily leverages the Go standard library and select well-maintained packages to ensure operational stability, maintainability, and ease of integration into production environments.
4+
5+
## Coding Standards
6+
7+
- This is a Go (Golang) project.
8+
- Write idiomatic, clean, and readable Go with clear inline comments where necessary.
9+
- Prioritize performance and simplicity, with a primary focus on efficient caching mechanisms.
10+
- Develop composable, testable functions to facilitate maintainability and reuse.
11+
- Write unit tests using the testing package for all handlers and core logic.
12+
- Use the Go standard library and select well-maintained packages to ensure stability and reduce dependency surface area.
13+
- Ensure the codebase is clear, consistent, and approachable for contributors.
14+
- Ensure go build ./... and go test ./... pass without errors before submitting changes.
15+
- Include structured log output with contextual information to support debugging and observability.
16+
- Write clear, descriptive commit messages outlining the purpose and scope of changes.
17+
- If implementation guidance is needed, seek clarification promptly. Do not fabricate or assume correctness without validation.

.github/workflows/atomiccache-test.yml

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,32 @@ on: [push, pull_request]
33
jobs:
44
test:
55
strategy:
6+
fail-fast: false
67
matrix:
7-
go-version: [1.16.x, 1.17.x]
8+
go-version: [1.20.x, 1.21.x, 1.22.x]
89
os: [ubuntu-latest, macos-latest, windows-latest]
910
runs-on: ${{ matrix.os }}
1011
steps:
11-
- name: Install Go
12-
uses: actions/setup-go@v2
13-
with:
14-
go-version: ${{ matrix.go-version }}
15-
- name: Checkout code
16-
uses: actions/checkout@v2
17-
- name: Install staticcheck
18-
run: go install honnef.co/go/tools/cmd/staticcheck@latest
19-
- name: Test
20-
run: go test ./...
21-
- name: Staticcheck test
22-
run: staticcheck ./...
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
- name: Set up Go
15+
uses: actions/setup-go@v4
16+
with:
17+
go-version: ${{ matrix.go-version }}
18+
- name: Cache Go modules
19+
uses: actions/cache@v4
20+
with:
21+
path: |
22+
~/.cache/go-build
23+
~/go/pkg/mod
24+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
25+
restore-keys: |
26+
${{ runner.os }}-go-
27+
- name: Vet
28+
run: go vet ./...
29+
- name: Test
30+
run: go test ./...
31+
- name: Test (race detector)
32+
if: matrix.os == 'ubuntu-latest'
33+
run: go test -race ./...
2334

.github/workflows/codeql-analysis.yml

Lines changed: 0 additions & 70 deletions
This file was deleted.

README.md

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,88 @@
11
# Atomic Cache
22

3-
Atomic cache is Golang fast in-memory cache (it wants to be fast - if you want to help, go ahead). Cache using limited number of shards with limited number of containing records. So the memory is limited, but the limit depends on you.
3+
Atomic Cache is a high-performance, in-memory caching library for Go, designed for low-latency data retrieval in performance-critical systems. It uses a sharded architecture with configurable memory limits, supporting efficient storage and retrieval of byte slices with per-record expiration.
44

5-
After cache initialization, only one shard is allocated. After that, if there is no left space in shard, new one is allocated. If shard is empty, memory is freed.
5+
Atomic Cache is ideal for applications that require predictable memory usage, fast access times, and robust cache eviction strategies.
66

7-
There is also support for record expiration. You can set expire time for every record in cache memory.
7+
The library is production-ready, leverages only the Go standard library and select well-maintained dependencies, and is easy to integrate into any Go project.
8+
9+
## Installation
10+
11+
```go
12+
go get github.com/praserx/atomic-cache/v2
13+
```
814

915
## Configuration
1016

11-
| Option | Type | Description |
12-
| ---------------- | ------ | ---------------------------------------------------------------------- |
13-
| RecordSizeSmall | int | Size of byte array used for memory allocation at small shard section. |
14-
| RecordSizeMedium | int | Size of byte array used for memory allocation at medium shard section. |
15-
| RecordSizeLarge | int | Size of byte array used for memory allocation at large shard section. |
16-
| MaxRecords | int | Maximum records per shard. |
17-
| MaxShardsSmall | int | Maximum small shards which can be allocated in cache memory. |
18-
| MaxShardsMedium | int | Maximum medium shards which can be allocated in cache memory. |
19-
| MaxShardsLarge | int | Maximum large shards which can be allocated in cache memory. |
20-
| GcStarter | uint32 | Garbage collector starter (run garbage collection every X sets). |
21-
17+
| Option | Type | Description |
18+
| ----------------------| ------- | ---------------------------------------------------------------------- |
19+
| RecordSizeSmall | int | Size of byte array used for memory allocation at small shard section. |
20+
| RecordSizeMedium | int | Size of byte array used for memory allocation at medium shard section. |
21+
| RecordSizeLarge | int | Size of byte array used for memory allocation at large shard section. |
22+
| MaxRecords | int | Maximum records per shard. |
23+
| MaxShardsSmall | int | Maximum small shards which can be allocated in cache memory. |
24+
| MaxShardsMedium | int | Maximum medium shards which can be allocated in cache memory. |
25+
| MaxShardsLarge | int | Maximum large shards which can be allocated in cache memory. |
26+
| GcStarter | uint32 | Garbage collector starter (run garbage collection every X sets). |
27+
28+
### Option Functions
29+
30+
All options are set using functional options when calling `atomiccache.New`. Available option functions:
31+
32+
- `atomiccache.OptionRecordSizeSmall(int)`
33+
- `atomiccache.OptionRecordSizeMedium(int)`
34+
- `atomiccache.OptionRecordSizeLarge(int)`
35+
- `atomiccache.OptionMaxRecords(int)`
36+
- `atomiccache.OptionMaxShardsSmall(int)`
37+
- `atomiccache.OptionMaxShardsMedium(int)`
38+
- `atomiccache.OptionMaxShardsLarge(int)`
39+
- `atomiccache.OptionGcStarter(uint32)`
40+
2241
## Example usage
2342

2443
```go
25-
// Initialize cache memory (ac == atomiccache)
26-
cache := ac.New(OptionMaxRecords(512), OptionRecordSize(2048), OptionMaxShards(48))
44+
package main
45+
46+
import (
47+
"github.com/praserx/atomic-cache"
48+
"fmt"
49+
"os"
50+
"time"
51+
)
2752

28-
// Store data in cache memory - key, data, record valid time
29-
cache.Set("key", []byte("data"), 500*time.Millisecond)
53+
func main() {
54+
// Initialize cache memory with custom options
55+
cache := atomiccache.New(
56+
atomiccache.OptionMaxRecords(512),
57+
atomiccache.OptionRecordSizeSmall(2048),
58+
atomiccache.OptionMaxShardsSmall(48),
59+
)
3060

31-
// Get data from cache memory
32-
if _, err := cache.Get("key"); err != nil {
33-
fmt.Fprintf(os.Stderr, "Cache is empty, but expecting some data: %v", err)
34-
os.Exit(1)
61+
// Store data in cache memory - key, data, record valid time
62+
if err := atomiccache.Set("key", []byte("data"), 500*time.Millisecond); err != nil {
63+
fmt.Fprintf(os.Stderr, "Set failed: %v\n", err)
64+
os.Exit(1)
65+
}
66+
67+
// Get data from cache memory
68+
data, err := atomiccache.Get("key")
69+
if err != nil {
70+
fmt.Fprintf(os.Stderr, "Cache miss: %v\n", err)
71+
os.Exit(1)
72+
}
73+
fmt.Printf("Got: %s\n", data)
3574
}
3675
```
3776

3877
## Benchmark
3978

40-
For this benchmark was created memory with following specs: `1024 bytes per record`, `4096 records per shard`, `256 shards (max)`. The 1024 bytes was set.
79+
To run benchmarks, use:
80+
81+
```sh
82+
go test -bench=. -benchmem
83+
```
84+
85+
For this benchmark, memory was created with the following specs: `1024 bytes per record`, `4096 records per shard`, `256 shards (max)`.
4186

4287
```
4388
BenchmarkCacheNewMedium-12 291 3670372 ns/op 22776481 B/op 12408 allocs/op
@@ -63,4 +108,8 @@ BenchmarkAtomicCacheGet-12 9697010 121.7 ns/op 0 B/op
63108
BenchmarkBigCacheGet-12 4031352 295.3 ns/op 88 B/op 2 allocs/op
64109
BenchmarkFreeCacheGet-12 4813386 276.8 ns/op 88 B/op 2 allocs/op
65110
BenchmarkHashicorpCacheGet-12 11071472 107.4 ns/op 16 B/op 1 allocs/op
66-
```
111+
```
112+
113+
## License
114+
115+
This project is licensed under the terms of the MIT License. See the [LICENSE](LICENSE) file for details.

cache.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
var (
1212
ErrNotFound = errors.New("record not found")
1313
ErrDataLimit = errors.New("cannot create new record: it violates data limit")
14-
ErrFullMemory = errors.New("cannot create new rocord: memory is full")
14+
ErrFullMemory = errors.New("cannot create new record: memory is full")
1515
)
1616

1717
// Constans below are used for shard section identification.
@@ -32,7 +32,7 @@ type AtomicCache struct {
3232
// deadlock.RWMutex
3333

3434
// Lookup structure used for global index.
35-
lookup map[interface{}]LookupRecord
35+
lookup map[string]LookupRecord
3636

3737
// Shards lookup tables which contains information about shards sections.
3838
smallShards, mediumShards, largeShards ShardsLookup
@@ -88,7 +88,7 @@ type LookupRecord struct {
8888
// BufferItem is used for buffer, which contains all unattended cache set
8989
// request.
9090
type BufferItem struct {
91-
Key interface{}
91+
Key string
9292
Data []byte
9393
Expire time.Duration
9494
}
@@ -114,7 +114,7 @@ func New(opts ...Option) *AtomicCache {
114114
cache := &AtomicCache{}
115115

116116
// Init lookup table
117-
cache.lookup = make(map[interface{}]LookupRecord)
117+
cache.lookup = make(map[string]LookupRecord)
118118

119119
// Init small shards section
120120
initShardsSection(&cache.smallShards, options.MaxShardsSmall, options.MaxRecords, options.RecordSizeSmall)
@@ -153,7 +153,7 @@ func initShardsSection(shardsSection *ShardsLookup, maxShards, maxRecords, recor
153153
// are replaced. If not, it checks if there are some allocated shard with empty
154154
// space for data. If there is no empty space, new shard is allocated. Otherwise
155155
// some valid record (FIFO queue) is deleted and new one is stored.
156-
func (a *AtomicCache) Set(key interface{}, data []byte, expire time.Duration) error {
156+
func (a *AtomicCache) Set(key string, data []byte, expire time.Duration) error {
157157
if len(data) > int(a.RecordSizeLarge) {
158158
return ErrDataLimit
159159
}
@@ -208,7 +208,7 @@ func (a *AtomicCache) Set(key interface{}, data []byte, expire time.Duration) er
208208

209209
// Get returns list of bytes if record is present in cache memory. If record is
210210
// not found, then error is returned and list is nil.
211-
func (a *AtomicCache) Get(key interface{}) ([]byte, error) {
211+
func (a *AtomicCache) Get(key string) ([]byte, error) {
212212
a.RLock()
213213
val, ok := a.lookup[key]
214214
a.RUnlock()
@@ -314,11 +314,12 @@ func (a *AtomicCache) getShardsSectionBySize(dataSize int) (*ShardsLookup, int)
314314
// is returned.
315315
// This method is not thread safe and additional locks are required.
316316
func (a *AtomicCache) getShardsSectionByID(sectionID int) *ShardsLookup {
317-
if sectionID == SMSH {
317+
switch sectionID {
318+
case SMSH:
318319
return &a.smallShards
319-
} else if sectionID == MDSH {
320+
case MDSH:
320321
return &a.mediumShards
321-
} else if sectionID == LGSH {
322+
case LGSH:
322323
return &a.largeShards
323324
}
324325

@@ -329,11 +330,12 @@ func (a *AtomicCache) getShardsSectionByID(sectionID int) *ShardsLookup {
329330
// shard section ID. It returns 0 if there is not known section ID on input.
330331
// This method is not thread safe and additional locks are required.
331332
func (a *AtomicCache) getRecordSizeByShardSectionID(sectionID int) int {
332-
if sectionID == SMSH {
333+
switch sectionID {
334+
case SMSH:
333335
return a.RecordSizeSmall
334-
} else if sectionID == MDSH {
336+
case MDSH:
335337
return a.RecordSizeMedium
336-
} else if sectionID == LGSH {
338+
case LGSH:
337339
return a.RecordSizeLarge
338340
}
339341

@@ -367,9 +369,9 @@ func (a *AtomicCache) collectGarbage() {
367369
}
368370
}
369371

370-
var localBuffer []BufferItem
371-
copy(localBuffer, a.buffer)
372-
a.buffer = []BufferItem{}
372+
// Properly copy buffer to avoid concurrency issues
373+
localBuffer := append([]BufferItem(nil), a.buffer...)
374+
a.buffer = nil
373375

374376
a.Unlock()
375377

cache_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package atomiccache
22

33
import (
4-
big "github.com/allegro/bigcache"
5-
fre "github.com/coocood/freecache"
6-
has "github.com/hashicorp/golang-lru"
74
"math/rand"
85
"reflect"
96
"strconv"
107
"testing"
118
"time"
9+
10+
big "github.com/allegro/bigcache"
11+
fre "github.com/coocood/freecache"
12+
has "github.com/hashicorp/golang-lru"
1213
)
1314

1415
func TestCacheFuncGetShardsSectionBySize(t *testing.T) {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/praserx/atomic-cache
1+
module github.com/praserx/atomic-cache/v2
22

33
go 1.17
44

0 commit comments

Comments
 (0)