diff --git a/auth/auth.go b/auth/auth.go index 3e9afbc8..fee5792a 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -42,7 +42,7 @@ type Auth interface { // Revoke access to a resource Revoke(rule *Rule) error // Rules returns all the rules used to verify requests - Rules() ([]*Rule, error) + Rules(...RulesOption) ([]*Rule, error) // String returns the name of the implementation String() string } diff --git a/auth/default.go b/auth/default.go index cf9c8cb6..e66983a0 100644 --- a/auth/default.go +++ b/auth/default.go @@ -68,7 +68,7 @@ func (n *noop) Revoke(rule *Rule) error { } // Rules used to verify requests -func (n *noop) Rules() ([]*Rule, error) { +func (n *noop) Rules(opts ...RulesOption) ([]*Rule, error) { return []*Rule{}, nil } diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go index 616d24c5..492c5b7e 100644 --- a/auth/jwt/jwt.go +++ b/auth/jwt/jwt.go @@ -105,7 +105,7 @@ func (j *jwt) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyO return rules.Verify(j.rules, acc, res) } -func (j *jwt) Rules() ([]*auth.Rule, error) { +func (j *jwt) Rules(opts ...auth.RulesOption) ([]*auth.Rule, error) { j.Lock() defer j.Unlock() return j.rules, nil diff --git a/auth/options.go b/auth/options.go index ed450709..11de5c60 100644 --- a/auth/options.go +++ b/auth/options.go @@ -1,6 +1,7 @@ package auth import ( + "context" "time" "github.com/micro/go-micro/v2/auth/provider" @@ -226,13 +227,25 @@ func NewTokenOptions(opts ...TokenOption) TokenOptions { } type VerifyOptions struct { - Namespace string + Context context.Context } type VerifyOption func(o *VerifyOptions) -func VerifyNamespace(ns string) VerifyOption { +func VerifyContext(ctx context.Context) VerifyOption { return func(o *VerifyOptions) { - o.Namespace = ns + o.Context = ctx + } +} + +type RulesOptions struct { + Context context.Context +} + +type RulesOption func(o *RulesOptions) + +func RulesContext(ctx context.Context) RulesOption { + return func(o *RulesOptions) { + o.Context = ctx } } diff --git a/auth/service/service.go b/auth/service/service.go index f43e863a..eb6dc004 100644 --- a/auth/service/service.go +++ b/auth/service/service.go @@ -3,7 +3,6 @@ package service import ( "context" "strings" - "sync" "time" "github.com/micro/go-micro/v2/auth" @@ -12,19 +11,14 @@ import ( "github.com/micro/go-micro/v2/auth/token" "github.com/micro/go-micro/v2/auth/token/jwt" "github.com/micro/go-micro/v2/client" - log "github.com/micro/go-micro/v2/logger" - "github.com/micro/go-micro/v2/metadata" - "github.com/micro/go-micro/v2/util/jitter" ) // svc is the service implementation of the Auth interface type svc struct { options auth.Options auth pb.AuthService - rule pb.RulesService + rules pb.RulesService jwt token.Provider - rules map[string][]*auth.Rule - sync.Mutex } func (s *svc) String() string { @@ -41,7 +35,7 @@ func (s *svc) Init(opts ...auth.Option) { } s.auth = pb.NewAuthService("go.micro.auth", s.options.Client) - s.rule = pb.NewRulesService("go.micro.auth", s.options.Client) + s.rules = pb.NewRulesService("go.micro.auth", s.options.Client) // if we have a JWT public key passed as an option, // we can decode tokens with the type "JWT" locally @@ -52,8 +46,6 @@ func (s *svc) Init(opts ...auth.Option) { } func (s *svc) Options() auth.Options { - s.Lock() - defer s.Unlock() return s.options } @@ -85,7 +77,7 @@ func (s *svc) Grant(rule *auth.Rule) error { access = pb.Access_DENIED } - _, err := s.rule.Create(context.TODO(), &pb.CreateRequest{ + _, err := s.rules.Create(context.TODO(), &pb.CreateRequest{ Rule: &pb.Rule{ Id: rule.ID, Scope: rule.Scope, @@ -99,25 +91,38 @@ func (s *svc) Grant(rule *auth.Rule) error { }, }) - if err == nil { - go s.loadRules(s.options.Namespace) - } - return err } // Revoke access to a resource func (s *svc) Revoke(rule *auth.Rule) error { - _, err := s.rule.Delete(context.TODO(), &pb.DeleteRequest{ + _, err := s.rules.Delete(context.TODO(), &pb.DeleteRequest{ Id: rule.ID, }) - go s.loadRules(s.options.Namespace) return err } -func (s *svc) Rules() ([]*auth.Rule, error) { - return s.rules[s.options.Namespace], nil +func (s *svc) Rules(opts ...auth.RulesOption) ([]*auth.Rule, error) { + var options auth.RulesOptions + for _, o := range opts { + o(&options) + } + if options.Context == nil { + options.Context = context.TODO() + } + + rsp, err := s.rules.List(options.Context, &pb.ListRequest{}) + if err != nil { + return nil, err + } + + rules := make([]*auth.Rule, len(rsp.Rules)) + for i, r := range rsp.Rules { + rules[i] = serializeRule(r) + } + + return rules, nil } // Verify an account has access to a resource @@ -126,15 +131,13 @@ func (s *svc) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyO for _, o := range opts { o(&options) } - if len(options.Namespace) == 0 { - options.Namespace = s.options.Namespace + + rs, err := s.Rules(auth.RulesContext(options.Context)) + if err != nil { + return err } - // load the rules if none are loaded - s.loadRulesIfEmpty(options.Namespace) - - // verify the request using the rules - return rules.Verify(s.rules[options.Namespace], acc, res) + return rules.Verify(rs, acc, res) } // Inspect a token @@ -170,53 +173,6 @@ func (s *svc) Token(opts ...auth.TokenOption) (*auth.Token, error) { return serializeToken(rsp.Token), nil } -// loadRules retrieves the rules from the auth service. Since this implementation is used by micro -// clients, which support muti-tenancy we may have to persist rules in multiple namespaces. -func (s *svc) loadRules(namespace string) { - ctx := metadata.Set(context.TODO(), "Micro-Namespace", namespace) - rsp, err := s.rule.List(ctx, &pb.ListRequest{}) - if err != nil { - log.Errorf("Error listing rules: %v", err) - return - } - - rules := make([]*auth.Rule, 0, len(rsp.Rules)) - for _, r := range rsp.Rules { - var access auth.Access - if r.Access == pb.Access_GRANTED { - access = auth.AccessGranted - } else { - access = auth.AccessDenied - } - - rules = append(rules, &auth.Rule{ - ID: r.Id, - Scope: r.Scope, - Access: access, - Priority: r.Priority, - Resource: &auth.Resource{ - Type: r.Resource.Type, - Name: r.Resource.Name, - Endpoint: r.Resource.Endpoint, - }, - }) - } - - s.Lock() - s.rules[namespace] = rules - s.Unlock() -} - -func (s *svc) loadRulesIfEmpty(namespace string) { - s.Lock() - rules := s.rules - s.Unlock() - - if _, ok := rules[namespace]; !ok { - s.loadRules(namespace) - } -} - func serializeToken(t *pb.Token) *auth.Token { return &auth.Token{ AccessToken: t.AccessToken, @@ -236,6 +192,27 @@ func serializeAccount(a *pb.Account) *auth.Account { } } +func serializeRule(r *pb.Rule) *auth.Rule { + var access auth.Access + if r.Access == pb.Access_GRANTED { + access = auth.AccessGranted + } else { + access = auth.AccessDenied + } + + return &auth.Rule{ + ID: r.Id, + Scope: r.Scope, + Access: access, + Priority: r.Priority, + Resource: &auth.Resource{ + Type: r.Resource.Type, + Name: r.Resource.Name, + Endpoint: r.Resource.Endpoint, + }, + } +} + // NewAuth returns a new instance of the Auth service func NewAuth(opts ...auth.Option) auth.Auth { options := auth.NewOptions(opts...) @@ -243,26 +220,9 @@ func NewAuth(opts ...auth.Option) auth.Auth { options.Client = client.DefaultClient } - service := &svc{ + return &svc{ auth: pb.NewAuthService("go.micro.auth", options.Client), - rule: pb.NewRulesService("go.micro.auth", options.Client), - rules: make(map[string][]*auth.Rule), + rules: pb.NewRulesService("go.micro.auth", options.Client), options: options, } - - // load rules periodically from the auth service - go func() { - ruleTimer := time.NewTicker(time.Second * 30) - - for { - <-ruleTimer.C - time.Sleep(jitter.Do(time.Second * 5)) - - for ns := range service.rules { - service.loadRules(ns) - } - } - }() - - return service } diff --git a/client/cache.go b/client/cache.go new file mode 100644 index 00000000..16bd9c70 --- /dev/null +++ b/client/cache.go @@ -0,0 +1,66 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "hash/fnv" + "time" + + "github.com/micro/go-micro/v2/metadata" + cache "github.com/patrickmn/go-cache" +) + +// NewCache returns an initialised cache. +func NewCache() *Cache { + return &Cache{ + cache: cache.New(cache.NoExpiration, 30*time.Second), + } +} + +// Cache for responses +type Cache struct { + cache *cache.Cache +} + +// Get a response from the cache +func (c *Cache) Get(ctx context.Context, req *Request) (interface{}, bool) { + return c.cache.Get(key(ctx, req)) +} + +// Set a response in the cache +func (c *Cache) Set(ctx context.Context, req *Request, rsp interface{}, expiry time.Duration) { + c.cache.Set(key(ctx, req), rsp, expiry) +} + +// List the key value pairs in the cache +func (c *Cache) List() map[string]string { + items := c.cache.Items() + + rsp := make(map[string]string, len(items)) + for k, v := range items { + bytes, _ := json.Marshal(v.Object) + rsp[k] = string(bytes) + } + + return rsp +} + +// key returns a hash for the context and request +func key(ctx context.Context, req *Request) string { + md, _ := metadata.FromContext(ctx) + + bytes, _ := json.Marshal(map[string]interface{}{ + "metadata": md, + "request": map[string]interface{}{ + "service": (*req).Service(), + "endpoint": (*req).Endpoint(), + "method": (*req).Method(), + "body": (*req).Body(), + }, + }) + + h := fnv.New64() + h.Write(bytes) + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/client/cache_test.go b/client/cache_test.go new file mode 100644 index 00000000..337312c1 --- /dev/null +++ b/client/cache_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "context" + "testing" + "time" + + "github.com/micro/go-micro/v2/metadata" +) + +func TestCache(t *testing.T) { + ctx := context.TODO() + req := NewRequest("go.micro.service.foo", "Foo.Bar", nil) + + t.Run("CacheMiss", func(t *testing.T) { + if _, ok := NewCache().Get(ctx, &req); ok { + t.Errorf("Expected to get no result from Get") + } + }) + + t.Run("CacheHit", func(t *testing.T) { + c := NewCache() + + rsp := "theresponse" + c.Set(ctx, &req, rsp, time.Minute) + + if res, ok := c.Get(ctx, &req); !ok { + t.Errorf("Expected a result, got nothing") + } else if res != rsp { + t.Errorf("Expected '%v' result, got '%v'", rsp, res) + } + }) +} + +func TestCacheKey(t *testing.T) { + ctx := context.TODO() + req1 := NewRequest("go.micro.service.foo", "Foo.Bar", nil) + req2 := NewRequest("go.micro.service.foo", "Foo.Baz", nil) + req3 := NewRequest("go.micro.service.foo", "Foo.Baz", "customquery") + + t.Run("IdenticalRequests", func(t *testing.T) { + key1 := key(ctx, &req1) + key2 := key(ctx, &req1) + if key1 != key2 { + t.Errorf("Expected the keys to match for identical requests and context") + } + }) + + t.Run("DifferentRequestEndpoints", func(t *testing.T) { + key1 := key(ctx, &req1) + key2 := key(ctx, &req2) + + if key1 == key2 { + t.Errorf("Expected the keys to differ for different request endpoints") + } + }) + + t.Run("DifferentRequestBody", func(t *testing.T) { + key1 := key(ctx, &req2) + key2 := key(ctx, &req3) + + if key1 == key2 { + t.Errorf("Expected the keys to differ for different request bodies") + } + }) + + t.Run("DifferentMetadata", func(t *testing.T) { + mdCtx := metadata.Set(context.TODO(), "foo", "bar") + key1 := key(mdCtx, &req1) + key2 := key(ctx, &req1) + + if key1 == key2 { + t.Errorf("Expected the keys to differ for different metadata") + } + }) +} diff --git a/client/options.go b/client/options.go index 5c8f833d..8ba14698 100644 --- a/client/options.go +++ b/client/options.go @@ -29,6 +29,9 @@ type Options struct { PoolSize int PoolTTL time.Duration + // Response cache + Cache *Cache + // Middleware for client Wrappers []Wrapper @@ -59,6 +62,8 @@ type CallOptions struct { StreamTimeout time.Duration // Use the services own auth token ServiceToken bool + // Duration to cache the response for + CacheExpiry time.Duration // Middleware for low level call func CallWrappers []CallWrapper @@ -91,6 +96,7 @@ type RequestOptions struct { func NewOptions(options ...Option) Options { opts := Options{ + Cache: NewCache(), Context: context.Background(), ContentType: DefaultContentType, Codecs: make(map[string]codec.NewCodec), @@ -324,6 +330,14 @@ func WithServiceToken() CallOption { } } +// WithCache is a CallOption which sets the duration the response +// shoull be cached for +func WithCache(c time.Duration) CallOption { + return func(o *CallOptions) { + o.CacheExpiry = c + } +} + func WithMessageContentType(ct string) MessageOption { return func(o *MessageOptions) { o.ContentType = ct diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 33b2493e..7d8841cf 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -472,9 +472,13 @@ func (c *cmd) Before(ctx *cli.Context) error { var serverOpts []server.Option var clientOpts []client.Option - // setup a client to use when calling the runtime + // setup a client to use when calling the runtime. It is important the auth client is wrapped + // after the cache client since the wrappers are applied in reverse order and the cache will use + // some of the headers set by the auth client. authFn := func() auth.Auth { return *c.opts.Auth } - microClient := wrapper.AuthClient(authFn, grpc.NewClient()) + cacheFn := func() *client.Cache { return (*c.opts.Client).Options().Cache } + microClient := wrapper.CacheClient(cacheFn, grpc.NewClient()) + microClient = wrapper.AuthClient(authFn, microClient) // Set the store if name := ctx.String("store"); len(name) > 0 { diff --git a/debug/service/handler/debug.go b/debug/service/handler/debug.go index ffe1a922..cf97cdc0 100644 --- a/debug/service/handler/debug.go +++ b/debug/service/handler/debug.go @@ -5,6 +5,7 @@ import ( "context" "time" + "github.com/micro/go-micro/v2/client" "github.com/micro/go-micro/v2/debug/log" proto "github.com/micro/go-micro/v2/debug/service/proto" "github.com/micro/go-micro/v2/debug/stats" @@ -13,11 +14,12 @@ import ( ) // NewHandler returns an instance of the Debug Handler -func NewHandler() *Debug { +func NewHandler(c client.Client) *Debug { return &Debug{ log: log.DefaultLog, stats: stats.DefaultStats, trace: trace.DefaultTracer, + cache: c.Options().Cache, } } @@ -30,6 +32,8 @@ type Debug struct { stats stats.Stats // the tracer trace trace.Tracer + // the cache + cache *client.Cache } func (d *Debug) Health(ctx context.Context, req *proto.HealthRequest, rsp *proto.HealthResponse) error { @@ -164,3 +168,9 @@ func (d *Debug) Log(ctx context.Context, stream server.Stream) error { return nil } + +// Cache returns all the key value pairs in the client cache +func (d *Debug) Cache(ctx context.Context, req *proto.CacheRequest, rsp *proto.CacheResponse) error { + rsp.Values = d.cache.List() + return nil +} diff --git a/debug/service/proto/debug.pb.go b/debug/service/proto/debug.pb.go index 41e6aebe..859d44cd 100644 --- a/debug/service/proto/debug.pb.go +++ b/debug/service/proto/debug.pb.go @@ -582,6 +582,76 @@ func (m *Span) GetType() SpanType { return SpanType_INBOUND } +type CacheRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CacheRequest) Reset() { *m = CacheRequest{} } +func (m *CacheRequest) String() string { return proto.CompactTextString(m) } +func (*CacheRequest) ProtoMessage() {} +func (*CacheRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_df91f41a5db378e6, []int{9} +} + +func (m *CacheRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CacheRequest.Unmarshal(m, b) +} +func (m *CacheRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CacheRequest.Marshal(b, m, deterministic) +} +func (m *CacheRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CacheRequest.Merge(m, src) +} +func (m *CacheRequest) XXX_Size() int { + return xxx_messageInfo_CacheRequest.Size(m) +} +func (m *CacheRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CacheRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CacheRequest proto.InternalMessageInfo + +type CacheResponse struct { + Values map[string]string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CacheResponse) Reset() { *m = CacheResponse{} } +func (m *CacheResponse) String() string { return proto.CompactTextString(m) } +func (*CacheResponse) ProtoMessage() {} +func (*CacheResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_df91f41a5db378e6, []int{10} +} + +func (m *CacheResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CacheResponse.Unmarshal(m, b) +} +func (m *CacheResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CacheResponse.Marshal(b, m, deterministic) +} +func (m *CacheResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CacheResponse.Merge(m, src) +} +func (m *CacheResponse) XXX_Size() int { + return xxx_messageInfo_CacheResponse.Size(m) +} +func (m *CacheResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CacheResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CacheResponse proto.InternalMessageInfo + +func (m *CacheResponse) GetValues() map[string]string { + if m != nil { + return m.Values + } + return nil +} + func init() { proto.RegisterEnum("SpanType", SpanType_name, SpanType_value) proto.RegisterType((*HealthRequest)(nil), "HealthRequest") @@ -595,50 +665,56 @@ func init() { proto.RegisterType((*TraceResponse)(nil), "TraceResponse") proto.RegisterType((*Span)(nil), "Span") proto.RegisterMapType((map[string]string)(nil), "Span.MetadataEntry") + proto.RegisterType((*CacheRequest)(nil), "CacheRequest") + proto.RegisterType((*CacheResponse)(nil), "CacheResponse") + proto.RegisterMapType((map[string]string)(nil), "CacheResponse.ValuesEntry") } func init() { proto.RegisterFile("debug/service/proto/debug.proto", fileDescriptor_df91f41a5db378e6) } var fileDescriptor_df91f41a5db378e6 = []byte{ - // 594 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0xdb, 0x6e, 0xd3, 0x40, - 0x10, 0x8d, 0xed, 0x38, 0xb1, 0xa7, 0x8d, 0xa9, 0x96, 0x8b, 0x2c, 0x73, 0x69, 0x65, 0x09, 0x29, - 0x5c, 0xe4, 0x40, 0x79, 0x41, 0xf0, 0x86, 0x8a, 0x04, 0x52, 0x69, 0xa5, 0x6d, 0xfb, 0x01, 0x5b, - 0x7b, 0xe4, 0x1a, 0xea, 0x0b, 0xbb, 0xeb, 0x4a, 0xf9, 0x16, 0xbe, 0x80, 0x37, 0x7e, 0x86, 0xff, - 0x41, 0x7b, 0x71, 0x1b, 0x0b, 0xa1, 0x3e, 0xf0, 0xb6, 0xe7, 0xec, 0xec, 0xc9, 0xcc, 0xc9, 0xf1, - 0xc0, 0x6e, 0x81, 0xe7, 0x7d, 0xb9, 0x12, 0xc8, 0xaf, 0xaa, 0x1c, 0x57, 0x1d, 0x6f, 0x65, 0xbb, - 0xd2, 0x5c, 0xa6, 0xcf, 0xe9, 0x33, 0x58, 0x7c, 0x42, 0x76, 0x29, 0x2f, 0x28, 0x7e, 0xef, 0x51, - 0x48, 0x12, 0xc3, 0xdc, 0x56, 0xc7, 0xce, 0x9e, 0xb3, 0x0c, 0xe9, 0x00, 0xd3, 0x25, 0x44, 0x43, - 0xa9, 0xe8, 0xda, 0x46, 0x20, 0x79, 0x00, 0x33, 0x21, 0x99, 0xec, 0x85, 0x2d, 0xb5, 0x28, 0x5d, - 0xc2, 0xf6, 0x89, 0x64, 0x52, 0xdc, 0xae, 0xf9, 0xdb, 0x81, 0x85, 0x2d, 0xb5, 0x9a, 0x8f, 0x20, - 0x94, 0x55, 0x8d, 0x42, 0xb2, 0xba, 0xd3, 0xd5, 0x53, 0x7a, 0x43, 0x68, 0x25, 0xc9, 0xb8, 0xc4, - 0x22, 0x76, 0xf5, 0xdd, 0x00, 0x55, 0x2f, 0x7d, 0xa7, 0x0a, 0x63, 0x4f, 0x5f, 0x58, 0xa4, 0xf8, - 0x1a, 0xeb, 0x96, 0xaf, 0xe3, 0xa9, 0xe1, 0x0d, 0x52, 0x4a, 0xf2, 0x82, 0x23, 0x2b, 0x44, 0xec, - 0x1b, 0x25, 0x0b, 0x49, 0x04, 0x6e, 0x99, 0xc7, 0x33, 0x4d, 0xba, 0x65, 0x4e, 0x12, 0x08, 0xb8, - 0x19, 0x44, 0xc4, 0x73, 0xcd, 0x5e, 0x63, 0xa5, 0x8e, 0x9c, 0xb7, 0x5c, 0xc4, 0x81, 0x51, 0x37, - 0x28, 0xfd, 0x0a, 0x70, 0xd8, 0x96, 0xb7, 0xce, 0x6f, 0x1c, 0xe4, 0xc8, 0x6a, 0x3d, 0x4e, 0x40, - 0x2d, 0x22, 0xf7, 0xc0, 0xcf, 0xdb, 0xbe, 0x91, 0x7a, 0x18, 0x8f, 0x1a, 0xa0, 0x58, 0x51, 0x35, - 0x39, 0xea, 0x51, 0x3c, 0x6a, 0x40, 0xfa, 0xcb, 0x81, 0x19, 0xc5, 0xbc, 0xe5, 0xc5, 0xdf, 0xe6, - 0x79, 0x9b, 0xe6, 0xbd, 0x86, 0xa0, 0x46, 0xc9, 0x0a, 0x26, 0x59, 0xec, 0xee, 0x79, 0xcb, 0xad, - 0xfd, 0xfb, 0x99, 0x79, 0x98, 0x7d, 0xb1, 0xfc, 0xc7, 0x46, 0xf2, 0x35, 0xbd, 0x2e, 0x53, 0x9d, - 0xd7, 0x28, 0x04, 0x2b, 0x8d, 0xad, 0x21, 0x1d, 0x60, 0xf2, 0x1e, 0x16, 0xa3, 0x47, 0x64, 0x07, - 0xbc, 0x6f, 0xb8, 0xb6, 0x03, 0xaa, 0xa3, 0x6a, 0xf7, 0x8a, 0x5d, 0xf6, 0xa8, 0x67, 0x0b, 0xa9, - 0x01, 0xef, 0xdc, 0xb7, 0x4e, 0xfa, 0x04, 0xb6, 0x4f, 0x39, 0xcb, 0x71, 0x30, 0x28, 0x02, 0xb7, - 0x2a, 0xec, 0x53, 0xb7, 0x2a, 0xd2, 0x97, 0xb0, 0xb0, 0xf7, 0x36, 0x15, 0x0f, 0xc1, 0x17, 0x1d, - 0x6b, 0x54, 0xd0, 0x54, 0xdf, 0x7e, 0x76, 0xd2, 0xb1, 0x86, 0x1a, 0x2e, 0xfd, 0xe1, 0xc2, 0x54, - 0x61, 0xf5, 0x83, 0x52, 0x3d, 0xb3, 0x4a, 0x06, 0x58, 0x71, 0x77, 0x10, 0x57, 0x9e, 0x77, 0x8c, - 0xa3, 0x35, 0x37, 0xa4, 0x16, 0x11, 0x02, 0xd3, 0x86, 0xd5, 0xc6, 0xdc, 0x90, 0xea, 0xf3, 0x66, - 0xde, 0xfc, 0x71, 0xde, 0x12, 0x08, 0x8a, 0x9e, 0x33, 0x59, 0xb5, 0x8d, 0xcd, 0xca, 0x35, 0x26, - 0xab, 0x0d, 0xa3, 0xe7, 0xba, 0xe1, 0xbb, 0xba, 0xe1, 0x7f, 0xda, 0xfc, 0x18, 0xa6, 0x72, 0xdd, - 0xa1, 0x0e, 0x51, 0xb4, 0x1f, 0xea, 0xe2, 0xd3, 0x75, 0x87, 0x54, 0xd3, 0xff, 0xe5, 0xf5, 0xf3, - 0xa7, 0x10, 0x0c, 0x72, 0x64, 0x0b, 0xe6, 0x9f, 0x8f, 0x3e, 0x1c, 0x9f, 0x1d, 0x1d, 0xec, 0x4c, - 0xc8, 0x36, 0x04, 0xc7, 0x67, 0xa7, 0x06, 0x39, 0xfb, 0x3f, 0x1d, 0xf0, 0x0f, 0xd4, 0x62, 0x20, - 0xbb, 0xe0, 0x1d, 0xb6, 0x25, 0xd9, 0xca, 0x6e, 0x12, 0x9c, 0xcc, 0x6d, 0x50, 0xd2, 0xc9, 0x2b, - 0x87, 0xbc, 0x80, 0x99, 0x59, 0x04, 0x24, 0xca, 0x46, 0xcb, 0x23, 0xb9, 0x93, 0x8d, 0x37, 0x44, - 0x3a, 0x21, 0x4b, 0xf0, 0xf5, 0x07, 0x4e, 0x16, 0xd9, 0xe6, 0x4e, 0x48, 0xa2, 0x6c, 0xf4, 0xdd, - 0x9b, 0x4a, 0xfd, 0xa7, 0x93, 0x45, 0xb6, 0x19, 0x8e, 0x24, 0xca, 0x46, 0x59, 0x48, 0x27, 0xe7, - 0x33, 0xbd, 0xbb, 0xde, 0xfc, 0x09, 0x00, 0x00, 0xff, 0xff, 0x6e, 0x42, 0x7f, 0x05, 0xde, 0x04, - 0x00, 0x00, + // 646 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0xdb, 0x6e, 0xd3, 0x4a, + 0x14, 0x8d, 0xed, 0x38, 0xb1, 0x77, 0x62, 0x9f, 0x6a, 0xce, 0x45, 0x96, 0x0f, 0xd0, 0xca, 0x12, + 0x52, 0xb8, 0x68, 0x02, 0xe1, 0x85, 0xcb, 0x1b, 0x14, 0x09, 0xa4, 0xd2, 0x4a, 0xd3, 0x96, 0xf7, + 0xa9, 0x3d, 0x4a, 0x03, 0xf5, 0x85, 0x99, 0x71, 0xa5, 0xbc, 0xf0, 0x23, 0xfc, 0x04, 0xff, 0x82, + 0xf8, 0x1f, 0x34, 0x17, 0xb7, 0xb6, 0x10, 0xaa, 0x10, 0x6f, 0x5e, 0x6b, 0xaf, 0xd9, 0xd9, 0x7b, + 0x69, 0x65, 0xc3, 0x6e, 0xc1, 0xce, 0xda, 0xf5, 0x52, 0x30, 0x7e, 0xb9, 0xc9, 0xd9, 0xb2, 0xe1, + 0xb5, 0xac, 0x97, 0x9a, 0xc3, 0xfa, 0x3b, 0xbb, 0x07, 0xd1, 0x1b, 0x46, 0x2f, 0xe4, 0x39, 0x61, + 0x9f, 0x5a, 0x26, 0x24, 0x4a, 0x60, 0x6a, 0xd5, 0x89, 0xb3, 0xe7, 0x2c, 0x42, 0xd2, 0xc1, 0x6c, + 0x01, 0x71, 0x27, 0x15, 0x4d, 0x5d, 0x09, 0x86, 0xfe, 0x83, 0x89, 0x90, 0x54, 0xb6, 0xc2, 0x4a, + 0x2d, 0xca, 0x16, 0x30, 0x3f, 0x96, 0x54, 0x8a, 0x9b, 0x7b, 0x7e, 0x77, 0x20, 0xb2, 0x52, 0xdb, + 0xf3, 0x16, 0x84, 0x72, 0x53, 0x32, 0x21, 0x69, 0xd9, 0x68, 0xf5, 0x98, 0x5c, 0x13, 0xba, 0x93, + 0xa4, 0x5c, 0xb2, 0x22, 0x71, 0x75, 0xad, 0x83, 0x6a, 0x96, 0xb6, 0x51, 0xc2, 0xc4, 0xd3, 0x05, + 0x8b, 0x14, 0x5f, 0xb2, 0xb2, 0xe6, 0xdb, 0x64, 0x6c, 0x78, 0x83, 0x54, 0x27, 0x79, 0xce, 0x19, + 0x2d, 0x44, 0xe2, 0x9b, 0x4e, 0x16, 0xa2, 0x18, 0xdc, 0x75, 0x9e, 0x4c, 0x34, 0xe9, 0xae, 0x73, + 0x94, 0x42, 0xc0, 0xcd, 0x22, 0x22, 0x99, 0x6a, 0xf6, 0x0a, 0xab, 0xee, 0x8c, 0xf3, 0x9a, 0x8b, + 0x24, 0x30, 0xdd, 0x0d, 0xca, 0x3e, 0x00, 0x1c, 0xd4, 0xeb, 0x1b, 0xf7, 0x37, 0x0e, 0x72, 0x46, + 0x4b, 0xbd, 0x4e, 0x40, 0x2c, 0x42, 0xff, 0x80, 0x9f, 0xd7, 0x6d, 0x25, 0xf5, 0x32, 0x1e, 0x31, + 0x40, 0xb1, 0x62, 0x53, 0xe5, 0x4c, 0xaf, 0xe2, 0x11, 0x03, 0xb2, 0xaf, 0x0e, 0x4c, 0x08, 0xcb, + 0x6b, 0x5e, 0xfc, 0x6c, 0x9e, 0xd7, 0x37, 0xef, 0x31, 0x04, 0x25, 0x93, 0xb4, 0xa0, 0x92, 0x26, + 0xee, 0x9e, 0xb7, 0x98, 0xad, 0xfe, 0xc5, 0xe6, 0x21, 0x7e, 0x67, 0xf9, 0xd7, 0x95, 0xe4, 0x5b, + 0x72, 0x25, 0x53, 0x93, 0x97, 0x4c, 0x08, 0xba, 0x36, 0xb6, 0x86, 0xa4, 0x83, 0xe9, 0x0b, 0x88, + 0x06, 0x8f, 0xd0, 0x0e, 0x78, 0x1f, 0xd9, 0xd6, 0x2e, 0xa8, 0x3e, 0xd5, 0xb8, 0x97, 0xf4, 0xa2, + 0x65, 0x7a, 0xb7, 0x90, 0x18, 0xf0, 0xdc, 0x7d, 0xea, 0x64, 0x77, 0x60, 0x7e, 0xc2, 0x69, 0xce, + 0x3a, 0x83, 0x62, 0x70, 0x37, 0x85, 0x7d, 0xea, 0x6e, 0x8a, 0xec, 0x21, 0x44, 0xb6, 0x6e, 0x53, + 0xf1, 0x3f, 0xf8, 0xa2, 0xa1, 0x95, 0x0a, 0x9a, 0x9a, 0xdb, 0xc7, 0xc7, 0x0d, 0xad, 0x88, 0xe1, + 0xb2, 0x2f, 0x2e, 0x8c, 0x15, 0x56, 0x3f, 0x28, 0xd5, 0x33, 0xdb, 0xc9, 0x00, 0xdb, 0xdc, 0xed, + 0x9a, 0x2b, 0xcf, 0x1b, 0xca, 0x99, 0x35, 0x37, 0x24, 0x16, 0x21, 0x04, 0xe3, 0x8a, 0x96, 0xc6, + 0xdc, 0x90, 0xe8, 0xef, 0x7e, 0xde, 0xfc, 0x61, 0xde, 0x52, 0x08, 0x8a, 0x96, 0x53, 0xb9, 0xa9, + 0x2b, 0x9b, 0x95, 0x2b, 0x8c, 0x96, 0x3d, 0xa3, 0xa7, 0x7a, 0xe0, 0xbf, 0xf5, 0xc0, 0xbf, 0xb4, + 0xf9, 0x36, 0x8c, 0xe5, 0xb6, 0x61, 0x3a, 0x44, 0xf1, 0x2a, 0xd4, 0xe2, 0x93, 0x6d, 0xc3, 0x88, + 0xa6, 0xff, 0xcc, 0xeb, 0x18, 0xe6, 0xaf, 0x68, 0x7e, 0xde, 0x79, 0x9d, 0x7d, 0x86, 0xc8, 0x62, + 0xeb, 0xed, 0x0a, 0x26, 0x5a, 0xdd, 0x99, 0x9b, 0xe2, 0x41, 0x1d, 0xbf, 0xd7, 0x45, 0x33, 0xb2, + 0x55, 0xa6, 0xcf, 0x60, 0xd6, 0xa3, 0x7f, 0x67, 0x9e, 0xfb, 0x77, 0x21, 0xe8, 0xd6, 0x43, 0x33, + 0x98, 0xbe, 0x3d, 0x7c, 0x79, 0x74, 0x7a, 0xb8, 0xbf, 0x33, 0x42, 0x73, 0x08, 0x8e, 0x4e, 0x4f, + 0x0c, 0x72, 0x56, 0xdf, 0x1c, 0xf0, 0xf7, 0xd5, 0xa1, 0x42, 0xbb, 0xe0, 0x1d, 0xd4, 0x6b, 0x34, + 0xc3, 0xd7, 0xff, 0xa8, 0x74, 0x6a, 0x83, 0x9b, 0x8d, 0x1e, 0x39, 0xe8, 0x01, 0x4c, 0xcc, 0x61, + 0x42, 0x31, 0x1e, 0x1c, 0xb3, 0xf4, 0x2f, 0x3c, 0xbc, 0x58, 0xd9, 0x08, 0x2d, 0xc0, 0xd7, 0x07, + 0x07, 0x45, 0xb8, 0x7f, 0xa3, 0xd2, 0x18, 0x0f, 0xee, 0x90, 0x51, 0xea, 0x10, 0xa2, 0x08, 0xf7, + 0xc3, 0x9a, 0xc6, 0x78, 0x90, 0x4d, 0xa3, 0xd4, 0x96, 0xa1, 0x08, 0xf7, 0xad, 0x4e, 0xe3, 0xa1, + 0x93, 0xd9, 0xe8, 0x6c, 0xa2, 0xaf, 0xee, 0x93, 0x1f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x22, 0x65, + 0x99, 0x10, 0x98, 0x05, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -657,6 +733,7 @@ type DebugClient interface { Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsResponse, error) Trace(ctx context.Context, in *TraceRequest, opts ...grpc.CallOption) (*TraceResponse, error) + Cache(ctx context.Context, in *CacheRequest, opts ...grpc.CallOption) (*CacheResponse, error) } type debugClient struct { @@ -726,12 +803,22 @@ func (c *debugClient) Trace(ctx context.Context, in *TraceRequest, opts ...grpc. return out, nil } +func (c *debugClient) Cache(ctx context.Context, in *CacheRequest, opts ...grpc.CallOption) (*CacheResponse, error) { + out := new(CacheResponse) + err := c.cc.Invoke(ctx, "/Debug/Cache", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DebugServer is the server API for Debug service. type DebugServer interface { Log(*LogRequest, Debug_LogServer) error Health(context.Context, *HealthRequest) (*HealthResponse, error) Stats(context.Context, *StatsRequest) (*StatsResponse, error) Trace(context.Context, *TraceRequest) (*TraceResponse, error) + Cache(context.Context, *CacheRequest) (*CacheResponse, error) } // UnimplementedDebugServer can be embedded to have forward compatible implementations. @@ -750,6 +837,9 @@ func (*UnimplementedDebugServer) Stats(ctx context.Context, req *StatsRequest) ( func (*UnimplementedDebugServer) Trace(ctx context.Context, req *TraceRequest) (*TraceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Trace not implemented") } +func (*UnimplementedDebugServer) Cache(ctx context.Context, req *CacheRequest) (*CacheResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Cache not implemented") +} func RegisterDebugServer(s *grpc.Server, srv DebugServer) { s.RegisterService(&_Debug_serviceDesc, srv) @@ -830,6 +920,24 @@ func _Debug_Trace_Handler(srv interface{}, ctx context.Context, dec func(interfa return interceptor(ctx, in, info, handler) } +func _Debug_Cache_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CacheRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DebugServer).Cache(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/Debug/Cache", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DebugServer).Cache(ctx, req.(*CacheRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Debug_serviceDesc = grpc.ServiceDesc{ ServiceName: "Debug", HandlerType: (*DebugServer)(nil), @@ -846,6 +954,10 @@ var _Debug_serviceDesc = grpc.ServiceDesc{ MethodName: "Trace", Handler: _Debug_Trace_Handler, }, + { + MethodName: "Cache", + Handler: _Debug_Cache_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/debug/service/proto/debug.pb.micro.go b/debug/service/proto/debug.pb.micro.go index 3d71b722..1e436e22 100644 --- a/debug/service/proto/debug.pb.micro.go +++ b/debug/service/proto/debug.pb.micro.go @@ -46,6 +46,7 @@ type DebugService interface { Health(ctx context.Context, in *HealthRequest, opts ...client.CallOption) (*HealthResponse, error) Stats(ctx context.Context, in *StatsRequest, opts ...client.CallOption) (*StatsResponse, error) Trace(ctx context.Context, in *TraceRequest, opts ...client.CallOption) (*TraceResponse, error) + Cache(ctx context.Context, in *CacheRequest, opts ...client.CallOption) (*CacheResponse, error) } type debugService struct { @@ -139,6 +140,16 @@ func (c *debugService) Trace(ctx context.Context, in *TraceRequest, opts ...clie return out, nil } +func (c *debugService) Cache(ctx context.Context, in *CacheRequest, opts ...client.CallOption) (*CacheResponse, error) { + req := c.c.NewRequest(c.name, "Debug.Cache", in) + out := new(CacheResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // Server API for Debug service type DebugHandler interface { @@ -146,6 +157,7 @@ type DebugHandler interface { Health(context.Context, *HealthRequest, *HealthResponse) error Stats(context.Context, *StatsRequest, *StatsResponse) error Trace(context.Context, *TraceRequest, *TraceResponse) error + Cache(context.Context, *CacheRequest, *CacheResponse) error } func RegisterDebugHandler(s server.Server, hdlr DebugHandler, opts ...server.HandlerOption) error { @@ -154,6 +166,7 @@ func RegisterDebugHandler(s server.Server, hdlr DebugHandler, opts ...server.Han Health(ctx context.Context, in *HealthRequest, out *HealthResponse) error Stats(ctx context.Context, in *StatsRequest, out *StatsResponse) error Trace(ctx context.Context, in *TraceRequest, out *TraceResponse) error + Cache(ctx context.Context, in *CacheRequest, out *CacheResponse) error } type Debug struct { debug @@ -217,3 +230,7 @@ func (h *debugHandler) Stats(ctx context.Context, in *StatsRequest, out *StatsRe func (h *debugHandler) Trace(ctx context.Context, in *TraceRequest, out *TraceResponse) error { return h.DebugHandler.Trace(ctx, in, out) } + +func (h *debugHandler) Cache(ctx context.Context, in *CacheRequest, out *CacheResponse) error { + return h.DebugHandler.Cache(ctx, in, out) +} diff --git a/debug/service/proto/debug.proto b/debug/service/proto/debug.proto index 7516651e..6646cb65 100644 --- a/debug/service/proto/debug.proto +++ b/debug/service/proto/debug.proto @@ -1,10 +1,11 @@ syntax = "proto3"; service Debug { - rpc Log(LogRequest) returns (stream Record) {}; - rpc Health(HealthRequest) returns (HealthResponse) {}; - rpc Stats(StatsRequest) returns (StatsResponse) {}; + rpc Log(LogRequest) returns (stream Record) {}; + rpc Health(HealthRequest) returns (HealthResponse) {}; + rpc Stats(StatsRequest) returns (StatsResponse) {}; rpc Trace(TraceRequest) returns (TraceResponse) {}; + rpc Cache(CacheRequest) returns (CacheResponse) {}; } message HealthRequest { @@ -97,3 +98,9 @@ message Span { map metadata = 7; SpanType type = 8; } + +message CacheRequest {} + +message CacheResponse { + map values = 1; +} \ No newline at end of file diff --git a/service.go b/service.go index 9c414654..4d793920 100644 --- a/service.go +++ b/service.go @@ -35,13 +35,14 @@ func newService(opts ...Option) Service { // service name serviceName := options.Server.Options().Name - // authFn returns the auth, we pass as a function since auth - // has not yet been set at this point. + // we pass functions to the wrappers since the values can change during initialisation authFn := func() auth.Auth { return options.Server.Options().Auth } + cacheFn := func() *client.Cache { return options.Client.Options().Cache } // wrap client to inject From-Service header on any calls options.Client = wrapper.FromService(serviceName, options.Client) options.Client = wrapper.TraceCall(serviceName, trace.DefaultTracer, options.Client) + options.Client = wrapper.CacheClient(cacheFn, options.Client) options.Client = wrapper.AuthClient(authFn, options.Client) // wrap the server to provide handler stats @@ -183,7 +184,7 @@ func (s *service) Run() error { // register the debug handler s.opts.Server.Handle( s.opts.Server.NewHandler( - handler.NewHandler(), + handler.NewHandler(s.opts.Client), server.InternalHandler(true), ), ) diff --git a/util/mux/mux.go b/util/mux/mux.go index 0f03298f..7ad7731f 100644 --- a/util/mux/mux.go +++ b/util/mux/mux.go @@ -5,6 +5,7 @@ import ( "context" "sync" + "github.com/micro/go-micro/v2/client" "github.com/micro/go-micro/v2/debug/service/handler" "github.com/micro/go-micro/v2/proxy" "github.com/micro/go-micro/v2/server" @@ -42,7 +43,7 @@ func New(name string, p proxy.Proxy) *Server { server.DefaultRouter.Handle( // inject the debug handler server.DefaultRouter.NewHandler( - handler.NewHandler(), + handler.NewHandler(client.DefaultClient), server.InternalHandler(true), ), ) diff --git a/util/wrapper/wrapper.go b/util/wrapper/wrapper.go index ffa3d61b..5149299b 100644 --- a/util/wrapper/wrapper.go +++ b/util/wrapper/wrapper.go @@ -2,6 +2,7 @@ package wrapper import ( "context" + "reflect" "strings" "github.com/micro/go-micro/v2/auth" @@ -221,7 +222,7 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper { } // Verify the caller has access to the resource - err := a.Verify(account, res, auth.VerifyNamespace(ns)) + err := a.Verify(account, res, auth.VerifyContext(ctx)) if err != nil && account != nil { return errors.Forbidden(req.Service(), "Forbidden call made to %v:%v by %v", req.Service(), req.Endpoint(), account.ID) } else if err != nil { @@ -238,3 +239,55 @@ func AuthHandler(fn func() auth.Auth) server.HandlerWrapper { } } } + +type cacheWrapper struct { + cacheFn func() *client.Cache + client.Client +} + +// Call executes the request. If the CacheExpiry option was set, the response will be cached using +// a hash of the metadata and request as the key. +func (c *cacheWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + // parse the options + var options client.CallOptions + for _, o := range opts { + o(&options) + } + + // if the client doesn't have a cacbe setup don't continue + cache := c.cacheFn() + if cache == nil { + return c.Client.Call(ctx, req, rsp, opts...) + } + + // if the cache expiry is not set, execute the call without the cache + if options.CacheExpiry == 0 { + return c.Client.Call(ctx, req, rsp, opts...) + } + + // if the response is nil don't call the cache since we can't assign the response + if rsp == nil { + return c.Client.Call(ctx, req, rsp, opts...) + } + + // check to see if there is a response cached, if there is assign it + if r, ok := cache.Get(ctx, &req); ok { + val := reflect.ValueOf(rsp).Elem() + val.Set(reflect.ValueOf(r).Elem()) + return nil + } + + // don't cache the result if there was an error + if err := c.Client.Call(ctx, req, rsp, opts...); err != nil { + return err + } + + // set the result in the cache + cache.Set(ctx, &req, rsp, options.CacheExpiry) + return nil +} + +// CacheClient wraps requests with the cache wrapper +func CacheClient(cacheFn func() *client.Cache, c client.Client) client.Client { + return &cacheWrapper{cacheFn, c} +} diff --git a/util/wrapper/wrapper_test.go b/util/wrapper/wrapper_test.go index 0905fac0..c6696f35 100644 --- a/util/wrapper/wrapper_test.go +++ b/util/wrapper/wrapper_test.go @@ -3,10 +3,13 @@ package wrapper import ( "context" "net/http" + "reflect" "testing" + "time" "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/errors" + "github.com/micro/go-micro/v2/client" "github.com/micro/go-micro/v2/metadata" "github.com/micro/go-micro/v2/server" ) @@ -358,6 +361,98 @@ func TestAuthHandler(t *testing.T) { } if !handlerCalled { t.Errorf("Expected the handler be called") +type testClient struct { + callCount int + callRsp interface{} + client.Client +} + +func (c *testClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + c.callCount++ + + if c.callRsp != nil { + val := reflect.ValueOf(rsp).Elem() + val.Set(reflect.ValueOf(c.callRsp).Elem()) + } + + return nil +} + +type testRsp struct { + value string +} + +func TestCacheWrapper(t *testing.T) { + req := client.NewRequest("go.micro.service.foo", "Foo.Bar", nil) + + t.Run("NilCache", func(t *testing.T) { + cli := new(testClient) + + w := CacheClient(func() *client.Cache { + return nil + }, cli) + + // perfroming two requests should increment the call count by two indicating the cache wasn't + // used even though the WithCache option was passed. + w.Call(context.TODO(), req, nil, client.WithCache(time.Minute)) + w.Call(context.TODO(), req, nil, client.WithCache(time.Minute)) + + if cli.callCount != 2 { + t.Errorf("Expected the client to have been called twice") + } + }) + + t.Run("OptionNotSet", func(t *testing.T) { + cli := new(testClient) + cache := client.NewCache() + + w := CacheClient(func() *client.Cache { + return cache + }, cli) + + // perfroming two requests should increment the call count by two since we didn't pass the WithCache + // option to Call. + w.Call(context.TODO(), req, nil) + w.Call(context.TODO(), req, nil) + + if cli.callCount != 2 { + t.Errorf("Expected the client to have been called twice") + } + }) + + t.Run("OptionSet", func(t *testing.T) { + val := "foo" + cli := &testClient{callRsp: &testRsp{value: val}} + cache := client.NewCache() + + w := CacheClient(func() *client.Cache { + return cache + }, cli) + + // perfroming two requests should increment the call count by once since the second request should + // have used the cache. The correct value should be set on both responses and no errors should + // be returned. + rsp1 := &testRsp{} + rsp2 := &testRsp{} + err1 := w.Call(context.TODO(), req, rsp1, client.WithCache(time.Minute)) + err2 := w.Call(context.TODO(), req, rsp2, client.WithCache(time.Minute)) + + if err1 != nil { + t.Errorf("Expected nil error, got %v", err1) + } + if err2 != nil { + t.Errorf("Expected nil error, got %v", err2) + } + + if rsp1.value != val { + t.Errorf("Expected %v to be assigned to the value, got %v", val, rsp1.value) + } + if rsp2.value != val { + t.Errorf("Expected %v to be assigned to the value, got %v", val, rsp2.value) + } + + if cli.callCount != 1 { + t.Errorf("Expected the client to be called 1 time, was actually called %v time(s)", cli.callCount) } }) }