diff --git a/cmd/pipecd/server.go b/cmd/pipecd/server.go index 30bb39f8ef..4c4d4e7bef 100644 --- a/cmd/pipecd/server.go +++ b/cmd/pipecd/server.go @@ -63,6 +63,10 @@ var ( defaultSigningMethod = jwtgo.SigningMethodHS256 ) +const ( + defaultPipedStatHashKey = "HASHKEY:PIPED:STATS" +) + type httpHandler interface { Register(func(pattern string, handler func(http.ResponseWriter, *http.Request))) } @@ -185,6 +189,7 @@ func (s *server) run(ctx context.Context, t cli.Telemetry) error { cmds := commandstore.NewStore(ds, cache, t.Logger) is := insightstore.NewStore(fs) cmdOutputStore := commandoutputstore.NewStore(fs, t.Logger) + statCache := rediscache.NewTTLHashCache(rd, cfg.Cache.TTLDuration(), defaultPipedStatHashKey) // Start a gRPC server for handling PipedAPI requests. { @@ -196,7 +201,7 @@ func (s *server) run(ctx context.Context, t cli.Telemetry) error { datastore.NewPipedStore(ds), t.Logger, ) - service = grpcapi.NewPipedAPI(ctx, ds, sls, alss, cmds, cmdOutputStore, t.Logger) + service = grpcapi.NewPipedAPI(ctx, ds, sls, alss, cmds, statCache, cmdOutputStore, t.Logger) opts = []rpc.Option{ rpc.WithPort(s.pipedAPIPort), rpc.WithGracePeriod(s.gracePeriod), diff --git a/pkg/app/api/grpcapi/piped_api.go b/pkg/app/api/grpcapi/piped_api.go index 247032dce8..fd4097fd18 100644 --- a/pkg/app/api/grpcapi/piped_api.go +++ b/pkg/app/api/grpcapi/piped_api.go @@ -40,7 +40,6 @@ type PipedAPI struct { applicationStore datastore.ApplicationStore deploymentStore datastore.DeploymentStore environmentStore datastore.EnvironmentStore - pipedStatsStore datastore.PipedStatsStore pipedStore datastore.PipedStore projectStore datastore.ProjectStore eventStore datastore.EventStore @@ -52,17 +51,17 @@ type PipedAPI struct { appPipedCache cache.Cache deploymentPipedCache cache.Cache envProjectCache cache.Cache + pipedStatCache cache.Cache logger *zap.Logger } // NewPipedAPI creates a new PipedAPI instance. -func NewPipedAPI(ctx context.Context, ds datastore.DataStore, sls stagelogstore.Store, alss applicationlivestatestore.Store, cs commandstore.Store, cop commandOutputPutter, logger *zap.Logger) *PipedAPI { +func NewPipedAPI(ctx context.Context, ds datastore.DataStore, sls stagelogstore.Store, alss applicationlivestatestore.Store, cs commandstore.Store, hc cache.Cache, cop commandOutputPutter, logger *zap.Logger) *PipedAPI { a := &PipedAPI{ applicationStore: datastore.NewApplicationStore(ds), deploymentStore: datastore.NewDeploymentStore(ds), environmentStore: datastore.NewEnvironmentStore(ds), - pipedStatsStore: datastore.NewPipedStatsStore(ds), pipedStore: datastore.NewPipedStore(ds), projectStore: datastore.NewProjectStore(ds), eventStore: datastore.NewEventStore(ds), @@ -73,6 +72,7 @@ func NewPipedAPI(ctx context.Context, ds datastore.DataStore, sls stagelogstore. appPipedCache: memorycache.NewTTLCache(ctx, 24*time.Hour, 3*time.Hour), deploymentPipedCache: memorycache.NewTTLCache(ctx, 24*time.Hour, 3*time.Hour), envProjectCache: memorycache.NewTTLCache(ctx, 24*time.Hour, 3*time.Hour), + pipedStatCache: hc, logger: logger.Named("piped-api"), } return a @@ -94,8 +94,18 @@ func (a *PipedAPI) Ping(ctx context.Context, req *pipedservice.PingRequest) (*pi // ReportStat is periodically sent to report its realtime status/stats to control-plane. // The received stats will be pushed to the metrics collector. func (a *PipedAPI) ReportStat(ctx context.Context, req *pipedservice.ReportStatRequest) (*pipedservice.ReportStatResponse, error) { + _, pipedID, _, err := rpcauth.ExtractPipedToken(ctx) + if err != nil { + return nil, err + } + if err := a.pipedStatCache.Put(pipedID, req.PipedStats); err != nil { + a.logger.Error("failed to store the reported piped stat", + zap.String("piped-id", pipedID), + zap.Error(err), + ) + return nil, status.Error(codes.Internal, "failed to store the reported piped stat") + } return &pipedservice.ReportStatResponse{}, nil - // return nil, status.Error(codes.Unimplemented, "") } // ReportPipedMeta is sent by piped while starting up to report its metadata diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 904d511270..b854593cf8 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -19,12 +19,14 @@ import ( ) var ( - ErrNotFound = errors.New("not found") + ErrNotFound = errors.New("not found") + ErrUnimplemented = errors.New("unimplemented") ) // Getter wraps a method to read from cache. type Getter interface { Get(key interface{}) (interface{}, error) + GetAll() (map[interface{}]interface{}, error) } // Putter wraps a method to write to cache. @@ -82,3 +84,7 @@ func (mg *multiGetter) Get(key interface{}) (interface{}, error) { } return nil, firstErr } + +func (mg *multiGetter) GetAll() (map[interface{}]interface{}, error) { + return nil, ErrUnimplemented +} diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 237b79e6cc..240754509c 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -27,6 +27,10 @@ func (f getterFunc) Get(key interface{}) (interface{}, error) { return f(key) } +func (f getterFunc) GetAll() (map[interface{}]interface{}, error) { + return nil, ErrUnimplemented +} + func TestMultiGetter(t *testing.T) { value := "ok" err := errors.New("err") diff --git a/pkg/cache/memorycache/cache.go b/pkg/cache/memorycache/cache.go index d133d9379c..4b2474ffd2 100644 --- a/pkg/cache/memorycache/cache.go +++ b/pkg/cache/memorycache/cache.go @@ -54,3 +54,7 @@ func (c *Cache) Delete(key interface{}) error { c.values.Delete(key) return nil } + +func (c *Cache) GetAll() (map[interface{}]interface{}, error) { + return nil, cache.ErrUnimplemented +} diff --git a/pkg/cache/memorycache/lru_cache.go b/pkg/cache/memorycache/lru_cache.go index 4b6c45fcd0..ae8e558dd0 100644 --- a/pkg/cache/memorycache/lru_cache.go +++ b/pkg/cache/memorycache/lru_cache.go @@ -60,3 +60,7 @@ func (c *LRUCache) Delete(key interface{}) error { c.cache.Remove(key) return nil } + +func (c *LRUCache) GetAll() (map[interface{}]interface{}, error) { + return nil, cache.ErrUnimplemented +} diff --git a/pkg/cache/memorycache/ttl_cache.go b/pkg/cache/memorycache/ttl_cache.go index 8568201017..d37d602f10 100644 --- a/pkg/cache/memorycache/ttl_cache.go +++ b/pkg/cache/memorycache/ttl_cache.go @@ -97,3 +97,7 @@ func (c *TTLCache) Delete(key interface{}) error { c.entries.Delete(key) return nil } + +func (c *TTLCache) GetAll() (map[interface{}]interface{}, error) { + return nil, cache.ErrUnimplemented +} diff --git a/pkg/cache/rediscache/BUILD.bazel b/pkg/cache/rediscache/BUILD.bazel index 6081e5dc1c..e18894929a 100644 --- a/pkg/cache/rediscache/BUILD.bazel +++ b/pkg/cache/rediscache/BUILD.bazel @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["cache.go"], + srcs = [ + "cache.go", + "hashcache.go", + ], importpath = "github.com/pipe-cd/pipe/pkg/cache/rediscache", visibility = ["//visibility:public"], deps = [ diff --git a/pkg/cache/rediscache/cache.go b/pkg/cache/rediscache/cache.go index 41f0e92b2c..8d74a946be 100644 --- a/pkg/cache/rediscache/cache.go +++ b/pkg/cache/rediscache/cache.go @@ -95,3 +95,7 @@ func (c *RedisCache) Delete(k interface{}) error { _, err := conn.Do("DEL", k) return err } + +func (c *RedisCache) GetAll() (map[interface{}]interface{}, error) { + return nil, cache.ErrUnimplemented +} diff --git a/pkg/cache/rediscache/hashcache.go b/pkg/cache/rediscache/hashcache.go new file mode 100644 index 0000000000..d19a7b3bbc --- /dev/null +++ b/pkg/cache/rediscache/hashcache.go @@ -0,0 +1,111 @@ +// Copyright 2021 The PipeCD Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rediscache + +import ( + "errors" + "time" + + redigo "github.com/gomodule/redigo/redis" + + "github.com/pipe-cd/pipe/pkg/cache" + "github.com/pipe-cd/pipe/pkg/redis" +) + +type RedisHashCache struct { + redis redis.Redis + ttl uint + key string +} + +func NewHashCache(redis redis.Redis, key string) *RedisHashCache { + return &RedisHashCache{ + redis: redis, + key: key, + } +} + +func NewTTLHashCache(redis redis.Redis, ttl time.Duration, key string) *RedisHashCache { + return &RedisHashCache{ + redis: redis, + ttl: uint(ttl.Seconds()), + key: key, + } +} + +func (r *RedisHashCache) Get(k interface{}) (interface{}, error) { + conn := r.redis.Get() + defer conn.Close() + reply, err := conn.Do("HGET", r.key, k) + if err != nil { + if err == redigo.ErrNil { + return nil, cache.ErrNotFound + } + return nil, err + } + if reply == nil { + return nil, cache.ErrNotFound + } + if err, ok := reply.(redigo.Error); ok { + return nil, err + } + return reply, nil +} + +func (r *RedisHashCache) Put(k interface{}, v interface{}) error { + conn := r.redis.Get() + defer conn.Close() + _, err := conn.Do("HSET", r.key, k, v) + if r.ttl != 0 { + _, err = conn.Do("EXPIRE", r.key, r.ttl) + } + return err +} + +func (r *RedisHashCache) Delete(k interface{}) error { + conn := r.redis.Get() + defer conn.Close() + _, err := conn.Do("HDEL", r.key, k) + return err +} + +func (r *RedisHashCache) GetAll() (map[interface{}]interface{}, error) { + conn := r.redis.Get() + defer conn.Close() + reply, err := redigo.Values(conn.Do("HGETALL", r.key)) + if err != nil { + if err == redigo.ErrNil { + return nil, cache.ErrNotFound + } + return nil, err + } + if len(reply) == 0 { + return nil, cache.ErrNotFound + } + if len(reply)%2 != 0 { + return nil, errors.New("invalid key-value pair contained") + } + + out := make(map[interface{}]interface{}, len(reply)/2) + for i := 0; i < len(reply); i += 2 { + key, okKey := reply[i].([]byte) + if !okKey { + return nil, errors.New("error key not a bulk string value") + } + out[string(key)] = reply[i+1] + } + + return out, nil +}