chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:33 +08:00
commit e071084ebe
982 changed files with 160368 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
package cache
import (
"context"
"errors"
"time"
)
var (
// DefaultCache is the default cache.
DefaultCache Cache = NewCache()
// DefaultExpiration is the default duration for items stored in
// the cache to expire.
DefaultExpiration time.Duration = 0
// ErrItemExpired is returned in Cache.Get when the item found in the cache
// has expired.
ErrItemExpired error = errors.New("item has expired")
// ErrKeyNotFound is returned in Cache.Get and Cache.Delete when the
// provided key could not be found in cache.
ErrKeyNotFound error = errors.New("key not found in cache")
)
// Cache is the interface that wraps the cache.
type Cache interface {
// Get gets a cached value by key.
Get(ctx context.Context, key string) (interface{}, time.Time, error)
// Put stores a key-value pair into cache.
Put(ctx context.Context, key string, val interface{}, d time.Duration) error
// Delete removes a key from cache.
Delete(ctx context.Context, key string) error
// String returns the name of the implementation.
String() string
}
// Item represents an item stored in the cache.
type Item struct {
Value interface{}
Expiration int64
}
// Expired returns true if the item has expired.
func (i *Item) Expired() bool {
if i.Expiration == 0 {
return false
}
return time.Now().UnixNano() > i.Expiration
}
// NewCache returns a new cache.
func NewCache(opts ...Option) Cache {
options := NewOptions(opts...)
items := make(map[string]Item)
if len(options.Items) > 0 {
items = options.Items
}
return &memCache{
opts: options,
items: items,
}
}
+66
View File
@@ -0,0 +1,66 @@
package cache
import (
"context"
"sync"
"time"
)
type memCache struct {
opts Options
items map[string]Item
sync.RWMutex
}
func (c *memCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) {
c.RLock()
defer c.RUnlock()
item, found := c.items[key]
if !found {
return nil, time.Time{}, ErrKeyNotFound
}
if item.Expired() {
return nil, time.Time{}, ErrItemExpired
}
return item.Value, time.Unix(0, item.Expiration), nil
}
func (c *memCache) Put(ctx context.Context, key string, val interface{}, d time.Duration) error {
var e int64
if d == DefaultExpiration {
d = c.opts.Expiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
c.Lock()
defer c.Unlock()
c.items[key] = Item{
Value: val,
Expiration: e,
}
return nil
}
func (c *memCache) Delete(ctx context.Context, key string) error {
c.Lock()
defer c.Unlock()
_, found := c.items[key]
if !found {
return ErrKeyNotFound
}
delete(c.items, key)
return nil
}
func (c *memCache) String() string {
return "memory"
}
+111
View File
@@ -0,0 +1,111 @@
package cache
import (
"context"
"testing"
"time"
)
var (
ctx = context.TODO()
key string = "test"
val interface{} = "hello go-micro"
)
// TestMemCache tests the in-memory cache implementation.
func TestCache(t *testing.T) {
t.Run("CacheGetMiss", func(t *testing.T) {
if _, _, err := NewCache().Get(ctx, key); err == nil {
t.Error("expected to get no value from cache")
}
})
t.Run("CacheGetHit", func(t *testing.T) {
c := NewCache()
if err := c.Put(ctx, key, val, 0); err != nil {
t.Error(err)
}
if a, _, err := c.Get(ctx, key); err != nil {
t.Errorf("Expected a value, got err: %s", err)
} else if a != val {
t.Errorf("Expected '%v', got '%v'", val, a)
}
})
t.Run("CacheGetExpired", func(t *testing.T) {
c := NewCache()
e := 20 * time.Millisecond
if err := c.Put(ctx, key, val, e); err != nil {
t.Error(err)
}
<-time.After(25 * time.Millisecond)
if _, _, err := c.Get(ctx, key); err == nil {
t.Error("expected to get no value from cache")
}
})
t.Run("CacheGetValid", func(t *testing.T) {
c := NewCache()
e := 25 * time.Millisecond
if err := c.Put(ctx, key, val, e); err != nil {
t.Error(err)
}
<-time.After(20 * time.Millisecond)
if _, _, err := c.Get(ctx, key); err != nil {
t.Errorf("expected a value, got err: %s", err)
}
})
t.Run("CacheDeleteMiss", func(t *testing.T) {
if err := NewCache().Delete(ctx, key); err == nil {
t.Error("expected to delete no value from cache")
}
})
t.Run("CacheDeleteHit", func(t *testing.T) {
c := NewCache()
if err := c.Put(ctx, key, val, 0); err != nil {
t.Error(err)
}
if err := c.Delete(ctx, key); err != nil {
t.Errorf("Expected to delete an item, got err: %s", err)
}
if _, _, err := c.Get(ctx, key); err == nil {
t.Errorf("Expected error")
}
})
}
func TestCacheWithOptions(t *testing.T) {
t.Run("CacheWithExpiration", func(t *testing.T) {
c := NewCache(Expiration(20 * time.Millisecond))
if err := c.Put(ctx, key, val, 0); err != nil {
t.Error(err)
}
<-time.After(25 * time.Millisecond)
if _, _, err := c.Get(ctx, key); err == nil {
t.Error("expected to get no value from cache")
}
})
t.Run("CacheWithItems", func(t *testing.T) {
c := NewCache(Items(map[string]Item{key: {val, 0}}))
if a, _, err := c.Get(ctx, key); err != nil {
t.Errorf("Expected a value, got err: %s", err)
} else if a != val {
t.Errorf("Expected '%v', got '%v'", val, a)
}
})
}
+73
View File
@@ -0,0 +1,73 @@
package cache
import (
"context"
"time"
"go-micro.dev/v6/logger"
)
// Options represents the options for the cache.
type Options struct {
// Context should contain all implementation specific options, using context.WithValue.
Context context.Context
// Logger is the be used logger
Logger logger.Logger
Items map[string]Item
// Address represents the address or other connection information of the cache service.
Address string
Expiration time.Duration
}
// Option manipulates the Options passed.
type Option func(o *Options)
// Expiration sets the duration for items stored in the cache to expire.
func Expiration(d time.Duration) Option {
return func(o *Options) {
o.Expiration = d
}
}
// Items initializes the cache with preconfigured items.
func Items(i map[string]Item) Option {
return func(o *Options) {
o.Items = i
}
}
// WithAddress sets the cache service address or connection information.
func WithAddress(addr string) Option {
return func(o *Options) {
o.Address = addr
}
}
// WithContext sets the cache context, for any extra configuration.
func WithContext(c context.Context) Option {
return func(o *Options) {
o.Context = c
}
}
// WithLogger sets underline logger.
func WithLogger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// NewOptions returns a new options struct.
func NewOptions(opts ...Option) Options {
options := Options{
Expiration: DefaultExpiration,
Items: make(map[string]Item),
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
return options
}
+43
View File
@@ -0,0 +1,43 @@
package cache
import (
"testing"
"time"
)
func TestOptions(t *testing.T) {
testData := map[string]struct {
set bool
expiration time.Duration
items map[string]Item
}{
"DefaultOptions": {false, DefaultExpiration, map[string]Item{}},
"ModifiedOptions": {true, time.Second, map[string]Item{"test": {"hello go-micro", 0}}},
}
for k, d := range testData {
t.Run(k, func(t *testing.T) {
var opts Options
if d.set {
opts = NewOptions(
Expiration(d.expiration),
Items(d.items),
)
} else {
opts = NewOptions()
}
// test options
for _, o := range []Options{opts} {
if o.Expiration != d.expiration {
t.Fatalf("Expected expiration '%v', got '%v'", d.expiration, o.Expiration)
}
if o.Items["test"] != d.items["test"] {
t.Fatalf("Expected items %#v, got %#v", d.items, o.Items)
}
}
})
}
}
+48
View File
@@ -0,0 +1,48 @@
package redis
import (
"context"
rclient "github.com/go-redis/redis/v8"
"go-micro.dev/v6/cache"
)
type redisOptionsContextKey struct{}
// WithRedisOptions sets advanced options for redis.
func WithRedisOptions(options rclient.UniversalOptions) cache.Option {
return func(o *cache.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, redisOptionsContextKey{}, options)
}
}
func newUniversalClient(options cache.Options) rclient.UniversalClient {
if options.Context == nil {
options.Context = context.Background()
}
opts, ok := options.Context.Value(redisOptionsContextKey{}).(rclient.UniversalOptions)
if !ok {
addr := "redis://127.0.0.1:6379"
if len(options.Address) > 0 {
addr = options.Address
}
redisOptions, err := rclient.ParseURL(addr)
if err != nil {
redisOptions = &rclient.Options{Addr: addr}
}
return rclient.NewClient(redisOptions)
}
if len(opts.Addrs) == 0 && len(options.Address) > 0 {
opts.Addrs = []string{options.Address}
}
return rclient.NewUniversalClient(&opts)
}
+139
View File
@@ -0,0 +1,139 @@
package redis
import (
"context"
"reflect"
"testing"
rclient "github.com/go-redis/redis/v8"
"go-micro.dev/v6/cache"
)
func Test_newUniversalClient(t *testing.T) {
type fields struct {
options cache.Options
}
type wantValues struct {
username string
password string
address string
}
tests := []struct {
name string
fields fields
want wantValues
}{
{name: "No Url", fields: fields{options: cache.Options{}},
want: wantValues{
username: "",
password: "",
address: "127.0.0.1:6379",
}},
{name: "legacy Url", fields: fields{options: cache.Options{Address: "127.0.0.1:6379"}},
want: wantValues{
username: "",
password: "",
address: "127.0.0.1:6379",
}},
{name: "New Url", fields: fields{options: cache.Options{Address: "redis://127.0.0.1:6379"}},
want: wantValues{
username: "",
password: "",
address: "127.0.0.1:6379",
}},
{name: "Url with Pwd", fields: fields{options: cache.Options{Address: "redis://:password@redis:6379"}},
want: wantValues{
username: "",
password: "password",
address: "redis:6379",
}},
{name: "Url with username and Pwd", fields: fields{
options: cache.Options{Address: "redis://username:password@redis:6379"}},
want: wantValues{
username: "username",
password: "password",
address: "redis:6379",
}},
{name: "Sentinel Failover client", fields: fields{
options: cache.Options{
Context: context.WithValue(
context.TODO(), redisOptionsContextKey{},
rclient.UniversalOptions{MasterName: "master-name"}),
}},
want: wantValues{
username: "",
password: "",
address: "FailoverClient", // <- Placeholder set by NewFailoverClient
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
univClient := newUniversalClient(tt.fields.options)
client, ok := univClient.(*rclient.Client)
if !ok {
t.Errorf("newUniversalClient() expect a *redis.Client")
return
}
if client.Options().Addr != tt.want.address {
t.Errorf("newUniversalClient() Address = %v, want address %v", client.Options().Addr, tt.want.address)
}
if client.Options().Password != tt.want.password {
t.Errorf("newUniversalClient() password = %v, want password %v", client.Options().Password, tt.want.password)
}
if client.Options().Username != tt.want.username {
t.Errorf("newUniversalClient() username = %v, want username %v", client.Options().Username, tt.want.username)
}
})
}
}
func Test_newUniversalClientCluster(t *testing.T) {
type fields struct {
options cache.Options
}
type wantValues struct {
username string
password string
addrs []string
}
tests := []struct {
name string
fields fields
want wantValues
}{
{name: "Addrs in redis options", fields: fields{
options: cache.Options{
Address: "127.0.0.1:6379", // <- ignored
Context: context.WithValue(
context.TODO(), redisOptionsContextKey{},
rclient.UniversalOptions{Addrs: []string{"127.0.0.1:6381", "127.0.0.1:6382"}}),
}},
want: wantValues{
username: "",
password: "",
addrs: []string{"127.0.0.1:6381", "127.0.0.1:6382"},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
univClient := newUniversalClient(tt.fields.options)
client, ok := univClient.(*rclient.ClusterClient)
if !ok {
t.Errorf("newUniversalClient() expect a *redis.ClusterClient")
return
}
if !reflect.DeepEqual(client.Options().Addrs, tt.want.addrs) {
t.Errorf("newUniversalClient() Addrs = %v, want addrs %v", client.Options().Addrs, tt.want.addrs)
}
if client.Options().Password != tt.want.password {
t.Errorf("newUniversalClient() password = %v, want password %v", client.Options().Password, tt.want.password)
}
if client.Options().Username != tt.want.username {
t.Errorf("newUniversalClient() username = %v, want username %v", client.Options().Username, tt.want.username)
}
})
}
}
+57
View File
@@ -0,0 +1,57 @@
package redis
import (
"context"
"time"
rclient "github.com/go-redis/redis/v8"
"go-micro.dev/v6/cache"
)
// NewRedisCache returns a new redis cache.
func NewRedisCache(opts ...cache.Option) cache.Cache {
options := cache.NewOptions(opts...)
return &redisCache{
opts: options,
client: newUniversalClient(options),
}
}
type redisCache struct {
opts cache.Options
client rclient.UniversalClient
}
func (c *redisCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) {
val, err := c.client.Get(ctx, key).Bytes()
if err != nil && err == rclient.Nil {
return nil, time.Time{}, cache.ErrKeyNotFound
} else if err != nil {
return nil, time.Time{}, err
}
dur, err := c.client.TTL(ctx, key).Result()
if err != nil {
return nil, time.Time{}, err
}
if dur == -1 {
return val, time.Unix(1<<63-1, 0), nil
}
if dur == -2 {
return val, time.Time{}, cache.ErrItemExpired
}
return val, time.Now().Add(dur), nil
}
func (c *redisCache) Put(ctx context.Context, key string, val interface{}, dur time.Duration) error {
return c.client.Set(ctx, key, val, dur).Err()
}
func (c *redisCache) Delete(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}
func (c *redisCache) String() string {
return "redis"
}
+88
View File
@@ -0,0 +1,88 @@
package redis
import (
"context"
"os"
"testing"
"time"
"go-micro.dev/v6/cache"
)
var (
ctx = context.TODO()
key string = "redistestkey"
val interface{} = "hello go-micro"
addr = cache.WithAddress("redis://127.0.0.1:6379")
)
// TestMemCache tests the in-memory cache implementation.
func TestCache(t *testing.T) {
if len(os.Getenv("LOCAL")) == 0 {
t.Skip()
}
t.Run("CacheGetMiss", func(t *testing.T) {
if _, _, err := NewRedisCache(addr).Get(ctx, key); err == nil {
t.Error("expected to get no value from cache")
}
})
t.Run("CacheGetHit", func(t *testing.T) {
c := NewRedisCache(addr)
if err := c.Put(ctx, key, val, 0); err != nil {
t.Error(err)
}
if a, _, err := c.Get(ctx, key); err != nil {
t.Errorf("Expected a value, got err: %s", err)
} else if string(a.([]byte)) != val {
t.Errorf("Expected '%v', got '%v'", val, a)
}
})
t.Run("CacheGetExpired", func(t *testing.T) {
c := NewRedisCache(addr)
d := 20 * time.Millisecond
if err := c.Put(ctx, key, val, d); err != nil {
t.Error(err)
}
<-time.After(25 * time.Millisecond)
if _, _, err := c.Get(ctx, key); err == nil {
t.Error("expected to get no value from cache")
}
})
t.Run("CacheGetValid", func(t *testing.T) {
c := NewRedisCache(addr)
e := 25 * time.Millisecond
if err := c.Put(ctx, key, val, e); err != nil {
t.Error(err)
}
<-time.After(20 * time.Millisecond)
if _, _, err := c.Get(ctx, key); err != nil {
t.Errorf("expected a value, got err: %s", err)
}
})
t.Run("CacheDeleteHit", func(t *testing.T) {
c := NewRedisCache(addr)
if err := c.Put(ctx, key, val, 0); err != nil {
t.Error(err)
}
if err := c.Delete(ctx, key); err != nil {
t.Errorf("Expected to delete an item, got err: %s", err)
}
if _, _, err := c.Get(ctx, key); err == nil {
t.Errorf("Expected error")
}
})
}