mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
fix(memorycache): add a cloneable interface (#8414)
This commit is contained in:
parent
7d5e14abb6
commit
8274ebfe37
73
pkg/cache/memorycache/provider.go
vendored
73
pkg/cache/memorycache/provider.go
vendored
@ -11,11 +11,11 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
go_cache "github.com/patrickmn/go-cache"
|
gocache "github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
type provider struct {
|
type provider struct {
|
||||||
cc *go_cache.Cache
|
cc *gocache.Cache
|
||||||
config cache.Config
|
config cache.Config
|
||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
}
|
}
|
||||||
@ -26,50 +26,75 @@ func NewFactory() factory.ProviderFactory[cache.Cache, cache.Config] {
|
|||||||
|
|
||||||
func New(ctx context.Context, settings factory.ProviderSettings, config cache.Config) (cache.Cache, error) {
|
func New(ctx context.Context, settings factory.ProviderSettings, config cache.Config) (cache.Cache, error) {
|
||||||
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/cache/memorycache")
|
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/cache/memorycache")
|
||||||
return &provider{cc: go_cache.New(config.Memory.TTL, config.Memory.CleanupInterval), settings: scopedProviderSettings, config: config}, nil
|
|
||||||
|
return &provider{
|
||||||
|
cc: gocache.New(config.Memory.TTL, config.Memory.CleanupInterval),
|
||||||
|
settings: scopedProviderSettings,
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Set(ctx context.Context, orgID valuer.UUID, cacheKey string, data cachetypes.Cacheable, ttl time.Duration) error {
|
func (provider *provider) Set(ctx context.Context, orgID valuer.UUID, cacheKey string, data cachetypes.Cacheable, ttl time.Duration) error {
|
||||||
// check if the data being passed is a pointer and is not nil
|
err := cachetypes.CheckCacheablePointer(data)
|
||||||
err := cachetypes.ValidatePointer(data, "inmemory")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ttl == 0 {
|
if cloneable, ok := data.(cachetypes.Cloneable); ok {
|
||||||
provider.settings.Logger().WarnContext(ctx, "zero value for TTL found. defaulting to the base TTL", "cache_key", cacheKey, "default_ttl", provider.config.Memory.TTL)
|
toCache := cloneable.Clone()
|
||||||
|
provider.cc.Set(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, ttl)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
provider.cc.Set(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), data, ttl)
|
|
||||||
|
toCache, err := data.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.cc.Set(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"), toCache, ttl)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Get(_ context.Context, orgID valuer.UUID, cacheKey string, dest cachetypes.Cacheable, allowExpired bool) error {
|
func (provider *provider) Get(_ context.Context, orgID valuer.UUID, cacheKey string, dest cachetypes.Cacheable, allowExpired bool) error {
|
||||||
// check if the destination being passed is a pointer and is not nil
|
err := cachetypes.CheckCacheablePointer(dest)
|
||||||
err := cachetypes.ValidatePointer(dest, "inmemory")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the destination value is settable
|
cachedData, found := provider.cc.Get(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"))
|
||||||
dstv := reflect.ValueOf(dest)
|
|
||||||
if !dstv.Elem().CanSet() {
|
|
||||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "destination value is not settable, %s", dstv.Elem())
|
|
||||||
}
|
|
||||||
|
|
||||||
data, found := provider.cc.Get(strings.Join([]string{orgID.StringValue(), cacheKey}, "::"))
|
|
||||||
if !found {
|
if !found {
|
||||||
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "key miss")
|
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "key miss")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the type compatbility between the src and dest
|
if cloneable, ok := cachedData.(cachetypes.Cloneable); ok {
|
||||||
srcv := reflect.ValueOf(data)
|
// check if the destination value is settable
|
||||||
if !srcv.Type().AssignableTo(dstv.Type()) {
|
dstv := reflect.ValueOf(dest)
|
||||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "src type is not assignable to dst type")
|
if !dstv.Elem().CanSet() {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "unsettable: (value: \"%s\")", dstv.Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
fromCache := cloneable.Clone()
|
||||||
|
|
||||||
|
// check the type compatbility between the src and dest
|
||||||
|
srcv := reflect.ValueOf(fromCache)
|
||||||
|
if !srcv.Type().AssignableTo(dstv.Type()) {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "unassignable: (src: \"%s\", dst: \"%s\")", srcv.Type().String(), dstv.Type().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the value to from src to dest
|
||||||
|
dstv.Elem().Set(srcv.Elem())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the value to from src to dest
|
if fromCache, ok := cachedData.([]byte); ok {
|
||||||
dstv.Elem().Set(srcv.Elem())
|
if err = dest.UnmarshalBinary(fromCache); err != nil {
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.NewInternalf(errors.CodeInternal, "unrecognized: (value: \"%s\")", reflect.TypeOf(cachedData).String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Delete(_ context.Context, orgID valuer.UUID, cacheKey string) {
|
func (provider *provider) Delete(_ context.Context, orgID valuer.UUID, cacheKey string) {
|
||||||
|
|||||||
348
pkg/cache/memorycache/provider_test.go
vendored
348
pkg/cache/memorycache/provider_test.go
vendored
@ -3,247 +3,217 @@ package memorycache
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestNew tests the New function
|
type CloneableA struct {
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
|
||||||
CleanupInterval: 10 * time.Second,
|
|
||||||
}
|
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotNil(t, c)
|
|
||||||
assert.NotNil(t, c.(*provider).cc)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CacheableEntity struct {
|
|
||||||
Key string
|
Key string
|
||||||
Value int
|
Value int
|
||||||
Expiry time.Duration
|
Expiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ce CacheableEntity) MarshalBinary() ([]byte, error) {
|
func (cloneable *CloneableA) Clone() cachetypes.Cacheable {
|
||||||
return json.Marshal(ce)
|
return &CloneableA{
|
||||||
|
Key: cloneable.Key,
|
||||||
|
Value: cloneable.Value,
|
||||||
|
Expiry: cloneable.Expiry,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ce CacheableEntity) UnmarshalBinary(data []byte) error {
|
func (cloneable *CloneableA) MarshalBinary() ([]byte, error) {
|
||||||
return nil
|
return json.Marshal(cloneable)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DCacheableEntity struct {
|
func (cloneable *CloneableA) UnmarshalBinary(data []byte) error {
|
||||||
|
return json.Unmarshal(data, cloneable)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CacheableB struct {
|
||||||
Key string
|
Key string
|
||||||
Value int
|
Value int
|
||||||
Expiry time.Duration
|
Expiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dce DCacheableEntity) MarshalBinary() ([]byte, error) {
|
func (cacheable *CacheableB) MarshalBinary() ([]byte, error) {
|
||||||
return json.Marshal(dce)
|
return json.Marshal(cacheable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dce DCacheableEntity) UnmarshalBinary(data []byte) error {
|
func (cacheable *CacheableB) UnmarshalBinary(data []byte) error {
|
||||||
return nil
|
return json.Unmarshal(data, cacheable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestStore tests the Store function
|
func TestCloneableSetWithNilPointer(t *testing.T) {
|
||||||
// this should fail because of nil pointer error
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
func TestStoreWithNilPointer(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
TTL: 10 * time.Second,
|
||||||
CleanupInterval: 10 * time.Second,
|
CleanupInterval: 10 * time.Second,
|
||||||
}
|
}})
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
var storeCacheableEntity *CacheableEntity
|
|
||||||
assert.Error(t, c.Set(context.Background(), valuer.GenerateUUID(), "key", storeCacheableEntity, 10*time.Second))
|
var cloneable *CloneableA
|
||||||
|
assert.Error(t, cache.Set(context.Background(), valuer.GenerateUUID(), "key", cloneable, 10*time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
// this should fail because of no pointer error
|
func TestCacheableSetWithNilPointer(t *testing.T) {
|
||||||
func TestStoreWithStruct(t *testing.T) {
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
TTL: 10 * time.Second,
|
||||||
CleanupInterval: 10 * time.Second,
|
CleanupInterval: 10 * time.Second,
|
||||||
}
|
}})
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
var storeCacheableEntity CacheableEntity
|
|
||||||
assert.Error(t, c.Set(context.Background(), valuer.GenerateUUID(), "key", storeCacheableEntity, 10*time.Second))
|
var cacheable *CacheableB
|
||||||
|
assert.Error(t, cache.Set(context.Background(), valuer.GenerateUUID(), "key", cacheable, 10*time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoreWithNonNilPointer(t *testing.T) {
|
func TestCloneableSetGet(t *testing.T) {
|
||||||
opts := cache.Memory{
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
TTL: 10 * time.Second,
|
TTL: 10 * time.Second,
|
||||||
CleanupInterval: 10 * time.Second,
|
CleanupInterval: 10 * time.Second,
|
||||||
}
|
}})
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
assert.NoError(t, c.Set(context.Background(), valuer.GenerateUUID(), "key", storeCacheableEntity, 10*time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRetrieve tests the Retrieve function
|
|
||||||
func TestRetrieveWithNilPointer(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
|
||||||
CleanupInterval: 10 * time.Second,
|
|
||||||
}
|
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID := valuer.GenerateUUID()
|
orgID := valuer.GenerateUUID()
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second))
|
cloneable := &CloneableA{
|
||||||
|
Key: "some-random-key",
|
||||||
|
Value: 1,
|
||||||
|
Expiry: time.Microsecond,
|
||||||
|
}
|
||||||
|
|
||||||
var retrieveCacheableEntity *CacheableEntity
|
assert.NoError(t, cache.Set(context.Background(), orgID, "key", cloneable, 10*time.Second))
|
||||||
|
|
||||||
err = c.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
provider := cache.(*provider)
|
||||||
|
insideCache, found := provider.cc.Get(strings.Join([]string{orgID.StringValue(), "key"}, "::"))
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.IsType(t, &CloneableA{}, insideCache)
|
||||||
|
|
||||||
|
cached := new(CloneableA)
|
||||||
|
assert.NoError(t, cache.Get(context.Background(), orgID, "key", cached, false))
|
||||||
|
|
||||||
|
assert.Equal(t, cloneable, cached)
|
||||||
|
// confirm that the cached cloneable is a different pointer
|
||||||
|
assert.NotSame(t, cloneable, cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheableSetGet(t *testing.T) {
|
||||||
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
|
TTL: 10 * time.Second,
|
||||||
|
CleanupInterval: 10 * time.Second,
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
cacheable := &CacheableB{
|
||||||
|
Key: "some-random-key",
|
||||||
|
Value: 1,
|
||||||
|
Expiry: time.Microsecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, cache.Set(context.Background(), orgID, "key", cacheable, 10*time.Second))
|
||||||
|
|
||||||
|
provider := cache.(*provider)
|
||||||
|
insideCache, found := provider.cc.Get(strings.Join([]string{orgID.StringValue(), "key"}, "::"))
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.IsType(t, []byte{}, insideCache)
|
||||||
|
assert.Equal(t, "{\"Key\":\"some-random-key\",\"Value\":1,\"Expiry\":1000}", string(insideCache.([]byte)))
|
||||||
|
|
||||||
|
cached := new(CacheableB)
|
||||||
|
assert.NoError(t, cache.Get(context.Background(), orgID, "key", cached, false))
|
||||||
|
|
||||||
|
assert.Equal(t, cacheable, cached)
|
||||||
|
assert.NotSame(t, cacheable, cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWithNilPointer(t *testing.T) {
|
||||||
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
|
TTL: 10 * time.Second,
|
||||||
|
CleanupInterval: 10 * time.Second,
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var cloneable *CloneableA
|
||||||
|
assert.Error(t, cache.Get(context.Background(), valuer.GenerateUUID(), "key", cloneable, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetGetWithDifferentTypes(t *testing.T) {
|
||||||
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
|
TTL: 10 * time.Second,
|
||||||
|
CleanupInterval: 10 * time.Second,
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
|
||||||
|
cloneable := &CloneableA{
|
||||||
|
Key: "some-random-key",
|
||||||
|
Value: 1,
|
||||||
|
Expiry: time.Microsecond,
|
||||||
|
}
|
||||||
|
assert.NoError(t, cache.Set(context.Background(), orgID, "key", cloneable, 10*time.Second))
|
||||||
|
|
||||||
|
cachedCacheable := new(CacheableB)
|
||||||
|
err = cache.Get(context.Background(), orgID, "key", cachedCacheable, false)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRetrieveWitNonPointer(t *testing.T) {
|
func TestCloneableConcurrentSetGet(t *testing.T) {
|
||||||
opts := cache.Memory{
|
cache, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: cache.Memory{
|
||||||
TTL: 10 * time.Second,
|
TTL: 10 * time.Second,
|
||||||
CleanupInterval: 10 * time.Second,
|
CleanupInterval: 10 * time.Second,
|
||||||
}
|
}})
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
orgID := valuer.GenerateUUID()
|
orgID := valuer.GenerateUUID()
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second))
|
numGoroutines := 100
|
||||||
|
done := make(chan bool, numGoroutines*2)
|
||||||
|
cloneables := make([]*CloneableA, numGoroutines)
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
|
||||||
var retrieveCacheableEntity CacheableEntity
|
for i := 0; i < numGoroutines; i++ {
|
||||||
|
go func(id int) {
|
||||||
|
cloneable := &CloneableA{
|
||||||
|
Key: fmt.Sprintf("key-%d", id),
|
||||||
|
Value: id,
|
||||||
|
Expiry: 50 * time.Second,
|
||||||
|
}
|
||||||
|
err := cache.Set(context.Background(), orgID, fmt.Sprintf("key-%d", id), cloneable, 10*time.Second)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mu.Lock()
|
||||||
|
cloneables[id] = cloneable
|
||||||
|
mu.Unlock()
|
||||||
|
done <- true
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
err = c.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
for i := 0; i < numGoroutines; i++ {
|
||||||
assert.Error(t, err)
|
go func(id int) {
|
||||||
}
|
cachedCloneable := new(CloneableA)
|
||||||
|
err := cache.Get(context.Background(), orgID, fmt.Sprintf("key-%d", id), cachedCloneable, false)
|
||||||
func TestRetrieveWithDifferentTypes(t *testing.T) {
|
// Some keys might not exist due to concurrent access, which is expected
|
||||||
opts := cache.Memory{
|
_ = err
|
||||||
TTL: 10 * time.Second,
|
done <- true
|
||||||
CleanupInterval: 10 * time.Second,
|
}(i)
|
||||||
}
|
}
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
for i := 0; i < numGoroutines*2; i++ {
|
||||||
orgID := valuer.GenerateUUID()
|
<-done
|
||||||
storeCacheableEntity := &CacheableEntity{
|
}
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
for i := 0; i < numGoroutines; i++ {
|
||||||
Expiry: time.Microsecond,
|
cachedCloneable := new(CloneableA)
|
||||||
}
|
assert.NoError(t, cache.Get(context.Background(), orgID, fmt.Sprintf("key-%d", i), cachedCloneable, false))
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second))
|
assert.Equal(t, fmt.Sprintf("key-%d", i), cachedCloneable.Key)
|
||||||
|
assert.Equal(t, i, cachedCloneable.Value)
|
||||||
retrieveCacheableEntity := new(DCacheableEntity)
|
// confirm that the cached cacheable is a different pointer
|
||||||
err = c.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
assert.NotSame(t, cachedCloneable, cloneables[i])
|
||||||
assert.Error(t, err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetrieveWithSameTypes(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
|
||||||
CleanupInterval: 10 * time.Second,
|
|
||||||
}
|
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second))
|
|
||||||
|
|
||||||
retrieveCacheableEntity := new(CacheableEntity)
|
|
||||||
err = c.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, storeCacheableEntity, retrieveCacheableEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRemove tests the Remove function
|
|
||||||
func TestRemove(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
|
||||||
CleanupInterval: 10 * time.Second,
|
|
||||||
}
|
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
retrieveCacheableEntity := new(CacheableEntity)
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second))
|
|
||||||
c.Delete(context.Background(), orgID, "key")
|
|
||||||
|
|
||||||
err = c.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBulkRemove tests the BulkRemove function
|
|
||||||
func TestBulkRemove(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
|
||||||
CleanupInterval: 10 * time.Second,
|
|
||||||
}
|
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
retrieveCacheableEntity := new(CacheableEntity)
|
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key1", storeCacheableEntity, 10*time.Second))
|
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key2", storeCacheableEntity, 10*time.Second))
|
|
||||||
c.DeleteMany(context.Background(), orgID, []string{"key1", "key2"})
|
|
||||||
|
|
||||||
err = c.Get(context.Background(), orgID, "key1", retrieveCacheableEntity, false)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
err = c.Get(context.Background(), orgID, "key2", retrieveCacheableEntity, false)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCache tests the cache
|
|
||||||
func TestCache(t *testing.T) {
|
|
||||||
opts := cache.Memory{
|
|
||||||
TTL: 10 * time.Second,
|
|
||||||
CleanupInterval: 10 * time.Second,
|
|
||||||
}
|
|
||||||
c, err := New(context.Background(), factorytest.NewSettings(), cache.Config{Provider: "memory", Memory: opts})
|
|
||||||
require.NoError(t, err)
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
retrieveCacheableEntity := new(CacheableEntity)
|
|
||||||
assert.NoError(t, c.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second))
|
|
||||||
err = c.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, storeCacheableEntity, retrieveCacheableEntity)
|
|
||||||
c.Delete(context.Background(), orgID, "key")
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
pkg/cache/rediscache/provider.go
vendored
7
pkg/cache/rediscache/provider.go
vendored
@ -2,14 +2,13 @@ package rediscache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
@ -48,10 +47,12 @@ func (c *provider) Get(ctx context.Context, orgID valuer.UUID, cacheKey string,
|
|||||||
err := c.client.Get(ctx, strings.Join([]string{orgID.StringValue(), cacheKey}, "::")).Scan(dest)
|
err := c.client.Get(ctx, strings.Join([]string{orgID.StringValue(), cacheKey}, "::")).Scan(dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, redis.Nil) {
|
if errors.Is(err, redis.Nil) {
|
||||||
return errorsV2.Newf(errorsV2.TypeNotFound, errorsV2.CodeNotFound, "key miss")
|
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "key miss")
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
109
pkg/cache/rediscache/provider_test.go
vendored
109
pkg/cache/rediscache/provider_test.go
vendored
@ -8,114 +8,49 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/go-redis/redismock/v8"
|
"github.com/go-redis/redismock/v8"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CacheableEntity struct {
|
type CacheableA struct {
|
||||||
Key string
|
Key string
|
||||||
Value int
|
Value int
|
||||||
Expiry time.Duration
|
Expiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ce *CacheableEntity) MarshalBinary() ([]byte, error) {
|
func (cacheable *CacheableA) Clone() cachetypes.Cacheable {
|
||||||
return json.Marshal(ce)
|
return &CacheableA{
|
||||||
|
Key: cacheable.Key,
|
||||||
|
Value: cacheable.Value,
|
||||||
|
Expiry: cacheable.Expiry,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ce *CacheableEntity) UnmarshalBinary(data []byte) error {
|
func (cacheable *CacheableA) MarshalBinary() ([]byte, error) {
|
||||||
return json.Unmarshal(data, ce)
|
return json.Marshal(cacheable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cacheable *CacheableA) UnmarshalBinary(data []byte) error {
|
||||||
|
return json.Unmarshal(data, cacheable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSet(t *testing.T) {
|
func TestSet(t *testing.T) {
|
||||||
db, mock := redismock.NewClientMock()
|
db, mock := redismock.NewClientMock()
|
||||||
cache := &provider{client: db, settings: factory.NewScopedProviderSettings(factorytest.NewSettings(), "github.com/SigNoz/signoz/pkg/cache/rediscache")}
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
storeCacheableEntity := &CacheableEntity{
|
cache := &provider{client: db, settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/cache/rediscache")}
|
||||||
|
|
||||||
|
cacheable := &CacheableA{
|
||||||
Key: "some-random-key",
|
Key: "some-random-key",
|
||||||
Value: 1,
|
Value: 1,
|
||||||
Expiry: time.Microsecond,
|
Expiry: time.Microsecond,
|
||||||
}
|
}
|
||||||
|
|
||||||
orgID := valuer.GenerateUUID()
|
orgID := valuer.GenerateUUID()
|
||||||
mock.ExpectSet(strings.Join([]string{orgID.StringValue(), "key"}, "::"), storeCacheableEntity, 10*time.Second).RedisNil()
|
mock.ExpectSet(strings.Join([]string{orgID.StringValue(), "key"}, "::"), cacheable, 10*time.Second).SetVal("ok")
|
||||||
_ = cache.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second)
|
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
assert.NoError(t, cache.Set(context.Background(), orgID, "key", cacheable, 10*time.Second))
|
||||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
|
||||||
db, mock := redismock.NewClientMock()
|
|
||||||
cache := &provider{client: db, settings: factory.NewScopedProviderSettings(factorytest.NewSettings(), "github.com/SigNoz/signoz/pkg/cache/rediscache")}
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
retrieveCacheableEntity := new(CacheableEntity)
|
|
||||||
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
mock.ExpectSet(strings.Join([]string{orgID.StringValue(), "key"}, "::"), storeCacheableEntity, 10*time.Second).RedisNil()
|
|
||||||
_ = cache.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second)
|
|
||||||
|
|
||||||
data, err := storeCacheableEntity.MarshalBinary()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
mock.ExpectGet(strings.Join([]string{orgID.StringValue(), "key"}, "::")).SetVal(string(data))
|
|
||||||
err = cache.Get(context.Background(), orgID, "key", retrieveCacheableEntity, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, storeCacheableEntity, retrieveCacheableEntity)
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
|
||||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDelete(t *testing.T) {
|
|
||||||
db, mock := redismock.NewClientMock()
|
|
||||||
cache := &provider{client: db, settings: factory.NewScopedProviderSettings(factorytest.NewSettings(), "github.com/SigNoz/signoz/pkg/cache/rediscache")}
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
|
|
||||||
mock.ExpectSet(strings.Join([]string{orgID.StringValue(), "key"}, "::"), storeCacheableEntity, 10*time.Second).RedisNil()
|
|
||||||
_ = cache.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second)
|
|
||||||
|
|
||||||
mock.ExpectDel(strings.Join([]string{orgID.StringValue(), "key"}, "::")).RedisNil()
|
|
||||||
cache.Delete(context.Background(), orgID, "key")
|
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
|
||||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteMany(t *testing.T) {
|
|
||||||
db, mock := redismock.NewClientMock()
|
|
||||||
cache := &provider{client: db, settings: factory.NewScopedProviderSettings(factorytest.NewSettings(), "github.com/SigNoz/signoz/pkg/cache/rediscache")}
|
|
||||||
storeCacheableEntity := &CacheableEntity{
|
|
||||||
Key: "some-random-key",
|
|
||||||
Value: 1,
|
|
||||||
Expiry: time.Microsecond,
|
|
||||||
}
|
|
||||||
orgID := valuer.GenerateUUID()
|
|
||||||
|
|
||||||
mock.ExpectSet(strings.Join([]string{orgID.StringValue(), "key"}, "::"), storeCacheableEntity, 10*time.Second).RedisNil()
|
|
||||||
_ = cache.Set(context.Background(), orgID, "key", storeCacheableEntity, 10*time.Second)
|
|
||||||
|
|
||||||
mock.ExpectSet(strings.Join([]string{orgID.StringValue(), "key2"}, "::"), storeCacheableEntity, 10*time.Second).RedisNil()
|
|
||||||
_ = cache.Set(context.Background(), orgID, "key2", storeCacheableEntity, 10*time.Second)
|
|
||||||
|
|
||||||
mock.ExpectDel(strings.Join([]string{orgID.StringValue(), "key"}, "::"), strings.Join([]string{orgID.StringValue(), "key2"}, "::")).RedisNil()
|
|
||||||
cache.DeleteMany(context.Background(), orgID, []string{"key", "key2"})
|
|
||||||
|
|
||||||
if err := mock.ExpectationsWereMet(); err != nil {
|
|
||||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "encoding/json"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"maps"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||||
|
)
|
||||||
|
|
||||||
type GetWaterfallSpansForTraceWithMetadataCache struct {
|
type GetWaterfallSpansForTraceWithMetadataCache struct {
|
||||||
StartTime uint64 `json:"startTime"`
|
StartTime uint64 `json:"startTime"`
|
||||||
@ -14,6 +19,28 @@ type GetWaterfallSpansForTraceWithMetadataCache struct {
|
|||||||
HasMissingSpans bool `json:"hasMissingSpans"`
|
HasMissingSpans bool `json:"hasMissingSpans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GetWaterfallSpansForTraceWithMetadataCache) Clone() cachetypes.Cacheable {
|
||||||
|
copyOfServiceNameToTotalDurationMap := make(map[string]uint64)
|
||||||
|
maps.Copy(copyOfServiceNameToTotalDurationMap, c.ServiceNameToTotalDurationMap)
|
||||||
|
|
||||||
|
copyOfSpanIdToSpanNodeMap := make(map[string]*Span)
|
||||||
|
maps.Copy(copyOfSpanIdToSpanNodeMap, c.SpanIdToSpanNodeMap)
|
||||||
|
|
||||||
|
copyOfTraceRoots := make([]*Span, len(c.TraceRoots))
|
||||||
|
copy(copyOfTraceRoots, c.TraceRoots)
|
||||||
|
return &GetWaterfallSpansForTraceWithMetadataCache{
|
||||||
|
StartTime: c.StartTime,
|
||||||
|
EndTime: c.EndTime,
|
||||||
|
DurationNano: c.DurationNano,
|
||||||
|
TotalSpans: c.TotalSpans,
|
||||||
|
TotalErrorSpans: c.TotalErrorSpans,
|
||||||
|
ServiceNameToTotalDurationMap: copyOfServiceNameToTotalDurationMap,
|
||||||
|
SpanIdToSpanNodeMap: copyOfSpanIdToSpanNodeMap,
|
||||||
|
TraceRoots: copyOfTraceRoots,
|
||||||
|
HasMissingSpans: c.HasMissingSpans,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GetWaterfallSpansForTraceWithMetadataCache) MarshalBinary() (data []byte, err error) {
|
func (c *GetWaterfallSpansForTraceWithMetadataCache) MarshalBinary() (data []byte, err error) {
|
||||||
return json.Marshal(c)
|
return json.Marshal(c)
|
||||||
}
|
}
|
||||||
@ -29,6 +56,16 @@ type GetFlamegraphSpansForTraceCache struct {
|
|||||||
TraceRoots []*FlamegraphSpan `json:"traceRoots"`
|
TraceRoots []*FlamegraphSpan `json:"traceRoots"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GetFlamegraphSpansForTraceCache) Clone() cachetypes.Cacheable {
|
||||||
|
return &GetFlamegraphSpansForTraceCache{
|
||||||
|
StartTime: c.StartTime,
|
||||||
|
EndTime: c.EndTime,
|
||||||
|
DurationNano: c.DurationNano,
|
||||||
|
SelectedSpans: c.SelectedSpans,
|
||||||
|
TraceRoots: c.TraceRoots,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) {
|
func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) {
|
||||||
return json.Marshal(c)
|
return json.Marshal(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,9 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdateMetricsMetadata struct {
|
type UpdateMetricsMetadata struct {
|
||||||
|
|||||||
@ -12,22 +12,26 @@ type Cacheable interface {
|
|||||||
encoding.BinaryUnmarshaler
|
encoding.BinaryUnmarshaler
|
||||||
}
|
}
|
||||||
|
|
||||||
func WrapCacheableErrors(rt reflect.Type, caller string) error {
|
type Cloneable interface {
|
||||||
if rt == nil {
|
// Creates a deep copy of the Cacheable. This method is useful for memory caches to avoid the need for serialization/deserialization. It also prevents
|
||||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "%s: (nil)", caller)
|
// race conditions in the memory cache.
|
||||||
}
|
Clone() Cacheable
|
||||||
|
|
||||||
if rt.Kind() != reflect.Pointer {
|
|
||||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "%s: (non-pointer \"%s\")", caller, rt.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "%s: (nil \"%s\")", caller, rt.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidatePointer(dest any, caller string) error {
|
func CheckCacheablePointer(dest any) error {
|
||||||
rv := reflect.ValueOf(dest)
|
rv := reflect.ValueOf(dest)
|
||||||
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||||
return WrapCacheableErrors(reflect.TypeOf(dest), caller)
|
rt := reflect.TypeOf(dest)
|
||||||
|
if rt == nil {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cacheable: (nil)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.Kind() != reflect.Pointer {
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cacheable: (non-pointer \"%s\")", rt.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cacheable: (nil \"%s\")", rt.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user