mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
refactor(layout): consolidate backend codebase into backend/ package and clean root directory
- Moved cmd/, core/, plugins/, pkg/, downstream/, and main.go into backend/ directory - Batch updated all Go source files to import github.com/Rain-kl/Wavelet/backend/... - Updated Makefile, scripts/swagger.sh, architecture guards, and platform skills - Passed all quality gates (100% tests, 0 lint issues, clean build)
This commit is contained in:
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package ram provides a thin wrapper around Otter v2 for process-local caching.
|
||||
package ram
|
||||
|
||||
import (
|
||||
"github.com/maypok86/otter/v2"
|
||||
)
|
||||
|
||||
const defaultMaximumSize = 256
|
||||
|
||||
// Options configures a RAM cache instance.
|
||||
type Options struct {
|
||||
// MaximumSize bounds the number of entries. Zero uses a small default.
|
||||
MaximumSize int
|
||||
}
|
||||
|
||||
// Cache is a concurrency-safe in-memory cache backed by Otter.
|
||||
type Cache[K comparable, V any] struct {
|
||||
inner *otter.Cache[K, V]
|
||||
}
|
||||
|
||||
// New creates a RAM cache from the provided options.
|
||||
func New[K comparable, V any](opts Options) (*Cache[K, V], error) {
|
||||
maximumSize := opts.MaximumSize
|
||||
if maximumSize == 0 {
|
||||
maximumSize = defaultMaximumSize
|
||||
}
|
||||
|
||||
inner, err := otter.New(&otter.Options[K, V]{
|
||||
MaximumSize: maximumSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Cache[K, V]{inner: inner}, nil
|
||||
}
|
||||
|
||||
// MustNew creates a RAM cache and panics when configuration is invalid.
|
||||
func MustNew[K comparable, V any](opts Options) *Cache[K, V] {
|
||||
cache, err := New[K, V](opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// GetIfPresent returns the cached value when present.
|
||||
func (c *Cache[K, V]) GetIfPresent(key K) (V, bool) {
|
||||
return c.inner.GetIfPresent(key)
|
||||
}
|
||||
|
||||
// Set stores a value in the cache.
|
||||
func (c *Cache[K, V]) Set(key K, value V) {
|
||||
c.inner.Set(key, value)
|
||||
}
|
||||
|
||||
// Invalidate removes one entry from the cache.
|
||||
func (c *Cache[K, V]) Invalidate(key K) {
|
||||
c.inner.Invalidate(key)
|
||||
}
|
||||
|
||||
// InvalidateAll removes every entry from the cache.
|
||||
func (c *Cache[K, V]) InvalidateAll() {
|
||||
c.inner.InvalidateAll()
|
||||
}
|
||||
|
||||
// EstimatedSize returns the approximate number of cached entries.
|
||||
func (c *Cache[K, V]) EstimatedSize() int {
|
||||
return c.inner.EstimatedSize()
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ram
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCacheSetGetInvalidate(t *testing.T) {
|
||||
cache := MustNew[string, int](Options{MaximumSize: 8})
|
||||
|
||||
cache.Set("count", 3)
|
||||
|
||||
got, ok := cache.GetIfPresent("count")
|
||||
if !ok {
|
||||
t.Fatal("GetIfPresent(count) ok = false, want true")
|
||||
}
|
||||
if got != 3 {
|
||||
t.Fatalf("GetIfPresent(count) = %d, want %d", got, 3)
|
||||
}
|
||||
|
||||
cache.Invalidate("count")
|
||||
if _, ok := cache.GetIfPresent("count"); ok {
|
||||
t.Fatal("GetIfPresent(count) after Invalidate ok = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheInvalidateAll(t *testing.T) {
|
||||
cache := MustNew[string, string](Options{MaximumSize: 8})
|
||||
|
||||
cache.Set("a", "1")
|
||||
cache.Set("b", "2")
|
||||
|
||||
cache.InvalidateAll()
|
||||
|
||||
if cache.EstimatedSize() != 0 {
|
||||
t.Fatalf("EstimatedSize() after InvalidateAll = %d, want 0", cache.EstimatedSize())
|
||||
}
|
||||
}
|
||||
Vendored
+233
@@ -0,0 +1,233 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound is returned by the Loader when the requested item is not found.
|
||||
ErrNotFound = errors.New("cache item not found in data source")
|
||||
|
||||
managerCache *Cache[string, map[string]cacheEntry]
|
||||
|
||||
writeLocks = make(map[string]*sync.Mutex)
|
||||
writeLocksMu sync.Mutex
|
||||
)
|
||||
|
||||
// CacheItem represents a unified cache entity.
|
||||
type CacheItem struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
TTL time.Duration `json:"ttl"` // -1 means never expire
|
||||
}
|
||||
|
||||
// Loader is an interface that the cache client must implement to handle database retrieval.
|
||||
type Loader interface {
|
||||
LoadAll(ctx context.Context, configType string) ([]CacheItem, error)
|
||||
LoadOne(ctx context.Context, configType string, key string) (CacheItem, error)
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
item CacheItem
|
||||
expireAt time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Initialize with a large maximum size since it only stores one entry per configType
|
||||
managerCache = MustNew[string, map[string]cacheEntry](Options{
|
||||
MaximumSize: 1000,
|
||||
})
|
||||
}
|
||||
|
||||
func getWriteLock(configType string) *sync.Mutex {
|
||||
writeLocksMu.Lock()
|
||||
defer writeLocksMu.Unlock()
|
||||
lock, found := writeLocks[configType]
|
||||
if !found {
|
||||
lock = &sync.Mutex{}
|
||||
writeLocks[configType] = lock
|
||||
}
|
||||
return lock
|
||||
}
|
||||
|
||||
// Get retrieves a cache item from the local cache store, checking for expiration.
|
||||
// Reads are completely lock-free because maps stored in Otter are immutable.
|
||||
func Get(configType string, key string) (CacheItem, bool) {
|
||||
m, ok := managerCache.GetIfPresent(configType)
|
||||
if !ok {
|
||||
return CacheItem{}, false
|
||||
}
|
||||
|
||||
entry, found := m[key]
|
||||
if !found {
|
||||
return CacheItem{}, false
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if entry.item.TTL != -1 && !entry.expireAt.IsZero() && time.Now().After(entry.expireAt) {
|
||||
// Asynchronously remove the expired item from the map and write back
|
||||
go deleteKeyIfExpired(configType, key, entry.expireAt)
|
||||
return CacheItem{}, false
|
||||
}
|
||||
|
||||
return entry.item, true
|
||||
}
|
||||
|
||||
func deleteKeyIfExpired(configType string, key string, expireAt time.Time) {
|
||||
lock := getWriteLock(configType)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
currentMap, ok := managerCache.GetIfPresent(configType)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
entry, found := currentMap[key]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
// Double-check expiration time to ensure we don't delete a newly updated key
|
||||
if entry.expireAt != expireAt || !time.Now().After(entry.expireAt) {
|
||||
return
|
||||
}
|
||||
|
||||
newMap := make(map[string]cacheEntry, len(currentMap)-1)
|
||||
for k, v := range currentMap {
|
||||
if k != key {
|
||||
newMap[k] = v
|
||||
}
|
||||
}
|
||||
managerCache.Set(configType, newMap)
|
||||
}
|
||||
|
||||
// Set stores a cache item in the local cache store.
|
||||
// Writes are protected by a fine-grained lock per configType.
|
||||
func Set(item CacheItem) {
|
||||
lock := getWriteLock(item.Type)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
currentMap, ok := managerCache.GetIfPresent(item.Type)
|
||||
newMap := make(map[string]cacheEntry)
|
||||
if ok {
|
||||
for k, v := range currentMap {
|
||||
newMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
var expireAt time.Time
|
||||
if item.TTL != -1 {
|
||||
expireAt = time.Now().Add(item.TTL)
|
||||
}
|
||||
|
||||
newMap[item.Key] = cacheEntry{
|
||||
item: item,
|
||||
expireAt: expireAt,
|
||||
}
|
||||
managerCache.Set(item.Type, newMap)
|
||||
}
|
||||
|
||||
// Delete removes a single item from the local cache store.
|
||||
// Writes are protected by a fine-grained lock per configType.
|
||||
func Delete(configType string, key string) {
|
||||
lock := getWriteLock(configType)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
currentMap, ok := managerCache.GetIfPresent(configType)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
newMap := make(map[string]cacheEntry, len(currentMap))
|
||||
for k, v := range currentMap {
|
||||
if k != key {
|
||||
newMap[k] = v
|
||||
}
|
||||
}
|
||||
managerCache.Set(configType, newMap)
|
||||
}
|
||||
|
||||
// UpdateTypeItems replaces all cache items of a specific type atomically.
|
||||
// Writes are protected by a fine-grained lock per configType.
|
||||
func UpdateTypeItems(configType string, items []CacheItem) {
|
||||
lock := getWriteLock(configType)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
newMap := make(map[string]cacheEntry, len(items))
|
||||
for _, item := range items {
|
||||
var expireAt time.Time
|
||||
if item.TTL != -1 {
|
||||
expireAt = time.Now().Add(item.TTL)
|
||||
}
|
||||
newMap[item.Key] = cacheEntry{
|
||||
item: item,
|
||||
expireAt: expireAt,
|
||||
}
|
||||
}
|
||||
managerCache.Set(configType, newMap)
|
||||
}
|
||||
|
||||
// GetTypeItems retrieves all unexpired cache items of a specific type.
|
||||
// Reads are completely lock-free because maps stored in Otter are immutable.
|
||||
func GetTypeItems(configType string) []CacheItem {
|
||||
currentMap, ok := managerCache.GetIfPresent(configType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var list []CacheItem
|
||||
for _, entry := range currentMap {
|
||||
if entry.item.TTL == -1 || entry.expireAt.IsZero() || time.Now().Before(entry.expireAt) {
|
||||
list = append(list, entry.item)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Refresh reloads configuration cache from database via the Loader.
|
||||
func Refresh(ctx context.Context, configType string, key string, loader Loader) error {
|
||||
if configType == "" {
|
||||
return errors.New("type is required")
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
// Single key refresh: first fetch latest value from database
|
||||
item, err := loader.LoadOne(ctx, configType, key)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
Delete(configType, key)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
Set(item)
|
||||
return nil
|
||||
}
|
||||
|
||||
// All keys refresh: load all of that type from database first, then replace cache
|
||||
items, err := loader.LoadAll(ctx, configType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
UpdateTypeItems(configType, items)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetForTest clears the local store and locks.
|
||||
func ResetForTest() {
|
||||
writeLocksMu.Lock()
|
||||
writeLocks = make(map[string]*sync.Mutex)
|
||||
writeLocksMu.Unlock()
|
||||
managerCache.InvalidateAll()
|
||||
}
|
||||
Reference in New Issue
Block a user