diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ee8f92..0ea8641 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.23.8' ] + go: [ '1.23.11' ] steps: - uses: actions/checkout@v3 diff --git a/.golangci.yml b/.golangci.yml index c468e41..55562ba 100755 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ run: - go: "1.23.8" + go: "1.23.11" concurrency: 4 timeout: 5m tests: false diff --git a/cache/cache_simple.go b/cache/cache.go similarity index 56% rename from cache/cache_simple.go rename to cache/cache.go index 2adb7a6..55ff972 100644 --- a/cache/cache_simple.go +++ b/cache/cache.go @@ -10,19 +10,25 @@ import ( ) type ( - cacheReplace[K comparable, V any] struct { + _cache[K comparable, V any] struct { list map[K]V mux sync.RWMutex } ) -func NewWithReplace[K comparable, V any]() TCacheReplace[K, V] { - return &cacheReplace[K, V]{ - list: make(map[K]V, 1000), +func New[K comparable, V any](opts ...Option[K, V]) Cache[K, V] { + obj := &_cache[K, V]{ + list: make(map[K]V, 100), } + + for _, opt := range opts { + opt(obj) + } + + return obj } -func (v *cacheReplace[K, V]) Has(key K) bool { +func (v *_cache[K, V]) Has(key K) bool { v.mux.RLock() defer v.mux.RUnlock() @@ -31,7 +37,7 @@ func (v *cacheReplace[K, V]) Has(key K) bool { return ok } -func (v *cacheReplace[K, V]) Get(key K) (V, bool) { +func (v *_cache[K, V]) Get(key K) (V, bool) { v.mux.RLock() defer v.mux.RUnlock() @@ -44,28 +50,43 @@ func (v *cacheReplace[K, V]) Get(key K) (V, bool) { return item, true } -func (v *cacheReplace[K, V]) Set(key K, value V) { +func (v *_cache[K, V]) Extract(key K) (V, bool) { + v.mux.Lock() + defer v.mux.Unlock() + + item, ok := v.list[key] + if !ok { + var zeroValue V + return zeroValue, false + } + + delete(v.list, key) + + return item, true +} + +func (v *_cache[K, V]) Set(key K, value V) { v.mux.Lock() defer v.mux.Unlock() v.list[key] = value } -func (v *cacheReplace[K, V]) Replace(data map[K]V) { +func (v *_cache[K, V]) Replace(data map[K]V) { v.mux.Lock() defer v.mux.Unlock() v.list = data } -func (v *cacheReplace[K, V]) Del(key K) { +func (v *_cache[K, V]) Del(key K) { v.mux.Lock() defer v.mux.Unlock() delete(v.list, key) } -func (v *cacheReplace[K, V]) Keys() []K { +func (v *_cache[K, V]) Keys() []K { v.mux.RLock() defer v.mux.RUnlock() @@ -77,7 +98,7 @@ func (v *cacheReplace[K, V]) Keys() []K { return result } -func (v *cacheReplace[K, V]) Flush() { +func (v *_cache[K, V]) Flush() { v.mux.Lock() defer v.mux.Unlock() diff --git a/cache/cache_test.go b/cache/cache_test.go new file mode 100644 index 0000000..f2992c6 --- /dev/null +++ b/cache/cache_test.go @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package cache_test + +import ( + "context" + "testing" + "time" + + "go.osspkg.com/casecheck" + + "go.osspkg.com/ioutils/cache" +) + +func TestUnit_New(t *testing.T) { + c := cache.New[string, string]() + + c.Set("foo", "bar") + casecheck.True(t, c.Has("foo")) + + casecheck.Equal(t, []string{"foo"}, c.Keys()) + + v, ok := c.Get("foo") + casecheck.True(t, ok) + casecheck.Equal(t, v, "bar") + + v, ok = c.Extract("foo") + casecheck.True(t, ok) + casecheck.Equal(t, v, "bar") + + v, ok = c.Extract("foo") + casecheck.False(t, ok) + casecheck.Equal(t, v, "") + + v, ok = c.Get("foo") + casecheck.False(t, ok) + casecheck.Equal(t, v, "") + + casecheck.False(t, c.Has("foo")) + casecheck.Equal(t, []string{}, c.Keys()) + + c.Set("foo", "bar") + casecheck.True(t, c.Has("foo")) + + c.Del("foo") + casecheck.False(t, c.Has("foo")) + + c.Replace(map[string]string{"foo": "bar"}) + casecheck.Equal(t, []string{"foo"}, c.Keys()) + + c.Flush() + casecheck.Equal(t, []string{}, c.Keys()) +} + +type testValue struct { + Val string + TS int64 +} + +func (v testValue) Timestamp() int64 { return v.TS } + +func TestUnit_AutoClean(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c := cache.New[string, testValue]( + cache.AutoClean[string, testValue](ctx, time.Millisecond*100), + ) + + c.Set("foo", testValue{Val: "bar", TS: time.Now().Add(time.Millisecond * 200).Unix()}) + casecheck.True(t, c.Has("foo")) + + time.Sleep(time.Second) + + casecheck.False(t, c.Has("foo")) +} + +func Benchmark_New(b *testing.B) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c := cache.New[string, testValue]( + cache.AutoClean[string, testValue](ctx, time.Millisecond*100), + ) + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + + c.Set("foo", testValue{Val: "bar", TS: time.Now().Add(time.Millisecond * 200).Unix()}) + c.Get("foo") + c.Has("foo") + c.Extract("foo") + c.Replace(map[string]testValue{"foo": {Val: "bar", TS: time.Now().Add(time.Millisecond * 200).Unix()}}) + c.Keys() + c.Del("foo") + c.Set("foo", testValue{Val: "bar", TS: time.Now().Add(time.Millisecond * 200).Unix()}) + c.Flush() + } + }) +} diff --git a/cache/cache_time.go b/cache/cache_time.go deleted file mode 100644 index 9a31468..0000000 --- a/cache/cache_time.go +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package cache - -import ( - "context" - "sync" - "time" - - "go.osspkg.com/routine" -) - -type ( - cacheWithTTL[K comparable, V any] struct { - ttl time.Duration - list map[K]*itemCacheTTL[V] - mux sync.RWMutex - } - - itemCacheTTL[V interface{}] struct { - link V - ts int64 - } -) - -func NewWithTTL[K comparable, V any](ctx context.Context, ttl time.Duration) TCacheTTL[K, V] { - cache := &cacheWithTTL[K, V]{ - ttl: ttl, - list: make(map[K]*itemCacheTTL[V], 1000), - } - go cache.cleaner(ctx) - return cache -} - -func (v *cacheWithTTL[K, V]) cleaner(ctx context.Context) { - routine.Interval(ctx, v.ttl, func(ctx context.Context) { - curr := time.Now().Unix() - - for k, t := range v.list { - if t.ts < curr { - delete(v.list, k) - } - } - }) -} - -func (v *cacheWithTTL[K, V]) Has(key K) bool { - v.mux.RLock() - defer v.mux.RUnlock() - - _, ok := v.list[key] - - return ok -} - -func (v *cacheWithTTL[K, V]) Get(key K) (V, bool) { - v.mux.RLock() - defer v.mux.RUnlock() - - item, ok := v.list[key] - if !ok { - var zeroValue V - return zeroValue, false - } - - return item.link, true -} - -func (v *cacheWithTTL[K, V]) Set(key K, value V) { - v.mux.Lock() - defer v.mux.Unlock() - - v.list[key] = &itemCacheTTL[V]{ - link: value, - ts: time.Now().Add(v.ttl).Unix(), - } -} - -func (v *cacheWithTTL[K, V]) SetWithTTL(key K, value V, ttl time.Time) { - v.mux.Lock() - defer v.mux.Unlock() - - v.list[key] = &itemCacheTTL[V]{ - link: value, - ts: ttl.Unix(), - } -} - -func (v *cacheWithTTL[K, V]) Del(key K) { - v.mux.Lock() - defer v.mux.Unlock() - - delete(v.list, key) -} - -func (v *cacheWithTTL[K, V]) Keys() []K { - v.mux.RLock() - defer v.mux.RUnlock() - - result := make([]K, 0, len(v.list)) - for k := range v.list { - result = append(result, k) - } - - return result -} - -func (v *cacheWithTTL[K, V]) Flush() { - v.mux.Lock() - defer v.mux.Unlock() - - for k := range v.list { - delete(v.list, k) - } -} diff --git a/cache/cache_time_test.go b/cache/cache_time_test.go deleted file mode 100644 index cc4f20e..0000000 --- a/cache/cache_time_test.go +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package cache - -import ( - "context" - "testing" - "time" - - "go.osspkg.com/casecheck" -) - -func TestUnit_WithTTL_Pointer(t *testing.T) { - type A struct { - Data uint64 - } - - ctx, cncl := context.WithTimeout(context.TODO(), time.Second) - defer cncl() - c := NewWithTTL[string, *A](ctx, time.Minute) - v, ok := c.Get("a") - casecheck.False(t, ok) - casecheck.Nil(t, v) -} diff --git a/cache/options.go b/cache/options.go new file mode 100644 index 0000000..04e1b88 --- /dev/null +++ b/cache/options.go @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package cache + +import ( + "context" + "time" + + "go.osspkg.com/routine" +) + +func AutoClean[K comparable, V Timestamp](ctx context.Context, interval time.Duration) Option[K, V] { + return func(v *_cache[K, V]) { + routine.Interval(ctx, interval, func(ctx context.Context) { + curr := time.Now().Unix() + keys := make([]K, 0, 10) + + v.mux.RLock() + for key, value := range v.list { + if value.Timestamp() < curr { + keys = append(keys, key) + } + } + v.mux.RUnlock() + + if len(keys) == 0 { + return + } + + v.mux.Lock() + defer v.mux.Unlock() + + for _, key := range keys { + delete(v.list, key) + } + }) + } +} diff --git a/cache/common.go b/cache/types.go similarity index 56% rename from cache/common.go rename to cache/types.go index 36c56d6..31ee40c 100644 --- a/cache/common.go +++ b/cache/types.go @@ -5,23 +5,19 @@ package cache -import "time" - -type TCache[K comparable, V interface{}] interface { +type Cache[K comparable, V any] interface { Has(key K) bool Get(key K) (V, bool) + Extract(key K) (V, bool) Set(key K, value V) + Replace(data map[K]V) Del(key K) Keys() []K Flush() } -type TCacheTTL[K comparable, V interface{}] interface { - TCache[K, V] - SetWithTTL(key K, value V, ttl time.Time) -} +type Option[K comparable, V any] func(*_cache[K, V]) -type TCacheReplace[K comparable, V interface{}] interface { - TCache[K, V] - Replace(data map[K]V) +type Timestamp interface { + Timestamp() int64 } diff --git a/fs/files.go b/fs/files.go index 95541fb..9051c4b 100644 --- a/fs/files.go +++ b/fs/files.go @@ -6,6 +6,7 @@ package fs import ( + "fmt" "io" "io/fs" "os" @@ -62,6 +63,22 @@ func SearchFilesByExt(dir, ext string) ([]string, error) { return files, err } +func ListFiles(dir string, handler func(path string, fi fs.FileInfo)) error { + if handler == nil { + return fmt.Errorf("handler is nil") + } + return filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + handler(path, info) + return nil + }) +} + func RewriteFile(filename string, call func([]byte) ([]byte, error)) error { var mode fs.FileMode = 0755 diff --git a/go.mod b/go.mod index d8beeaa..be8399f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.osspkg.com/ioutils -go 1.23.8 +go 1.23.11 require ( go.osspkg.com/casecheck v0.3.0