diff --git a/cmd/yorkie/server.go b/cmd/yorkie/server.go index 3fdb18a97..3e54fdf62 100644 --- a/cmd/yorkie/server.go +++ b/cmd/yorkie/server.go @@ -50,6 +50,7 @@ var ( authWebhookMaxWaitInterval time.Duration authWebhookCacheAuthTTL time.Duration authWebhookCacheUnauthTTL time.Duration + projectInfoCacheTTL time.Duration conf = server.NewConfig() ) @@ -66,6 +67,7 @@ func newServerCmd() *cobra.Command { conf.Backend.AuthWebhookMaxWaitInterval = authWebhookMaxWaitInterval.String() conf.Backend.AuthWebhookCacheAuthTTL = authWebhookCacheAuthTTL.String() conf.Backend.AuthWebhookCacheUnauthTTL = authWebhookCacheUnauthTTL.String() + conf.Backend.ProjectInfoCacheTTL = projectInfoCacheTTL.String() conf.Housekeeping.Interval = housekeepingInterval.String() @@ -331,6 +333,18 @@ func init() { server.DefaultAuthWebhookCacheUnauthTTL, "TTL value to set when caching unauthorized webhook response.", ) + cmd.Flags().IntVar( + &conf.Backend.ProjectInfoCacheSize, + "project-info-cache-size", + server.DefaultProjectInfoCacheSize, + "The cache size of the project info.", + ) + cmd.Flags().DurationVar( + &projectInfoCacheTTL, + "project-info-cache-ttl", + server.DefaultProjectInfoCacheTTL, + "TTL value to set when caching project info.", + ) cmd.Flags().StringVar( &conf.Backend.Hostname, "hostname", diff --git a/server/backend/config.go b/server/backend/config.go index 038b861c9..1201b4faf 100644 --- a/server/backend/config.go +++ b/server/backend/config.go @@ -70,6 +70,12 @@ type Config struct { // AuthWebhookCacheUnauthTTL is the TTL value to set when caching the unauthorized result. AuthWebhookCacheUnauthTTL string `yaml:"AuthWebhookCacheUnauthTTL"` + // ProjectInfoCacheSize is the cache size of the project info. + ProjectInfoCacheSize int `yaml:"ProjectInfoCacheSize"` + + // ProjectInfoCacheTTL is the TTL value to set when caching the project info. + ProjectInfoCacheTTL string `yaml:"ProjectInfoCacheTTL"` + // Hostname is yorkie server hostname. hostname is used by metrics. Hostname string `yaml:"Hostname"` } @@ -108,6 +114,14 @@ func (c *Config) Validate() error { ) } + if _, err := time.ParseDuration(c.ProjectInfoCacheTTL); err != nil { + return fmt.Errorf( + `invalid argument "%s" for "--project-info-cache-ttl" flag: %w`, + c.ProjectInfoCacheTTL, + err, + ) + } + return nil } @@ -154,3 +168,14 @@ func (c *Config) ParseAuthWebhookCacheUnauthTTL() time.Duration { return result } + +// ParseProjectInfoCacheTTL returns TTL for project info cache. +func (c *Config) ParseProjectInfoCacheTTL() time.Duration { + result, err := time.ParseDuration(c.ProjectInfoCacheTTL) + if err != nil { + fmt.Fprintln(os.Stderr, "parse project info cache ttl: %w", err) + os.Exit(1) + } + + return result +} diff --git a/server/backend/config_test.go b/server/backend/config_test.go index cbe447458..0a334212c 100644 --- a/server/backend/config_test.go +++ b/server/backend/config_test.go @@ -31,6 +31,7 @@ func TestConfig(t *testing.T) { AuthWebhookMaxWaitInterval: "0ms", AuthWebhookCacheAuthTTL: "10s", AuthWebhookCacheUnauthTTL: "10s", + ProjectInfoCacheTTL: "10m", } assert.NoError(t, validConf.Validate()) @@ -49,5 +50,9 @@ func TestConfig(t *testing.T) { conf4 := validConf conf4.AuthWebhookCacheUnauthTTL = "s" assert.Error(t, conf4.Validate()) + + conf5 := validConf + conf5.ProjectInfoCacheTTL = "10 minutes" + assert.Error(t, conf5.Validate()) }) } diff --git a/server/config.go b/server/config.go index c875102af..e0b1b9dd7 100644 --- a/server/config.go +++ b/server/config.go @@ -63,6 +63,8 @@ const ( DefaultAuthWebhookCacheSize = 5000 DefaultAuthWebhookCacheAuthTTL = 10 * time.Second DefaultAuthWebhookCacheUnauthTTL = 10 * time.Second + DefaultProjectInfoCacheSize = 256 + DefaultProjectInfoCacheTTL = 10 * time.Minute DefaultHostname = "" ) @@ -201,6 +203,14 @@ func (c *Config) ensureDefaultValue() { c.Backend.AuthWebhookCacheUnauthTTL = DefaultAuthWebhookCacheUnauthTTL.String() } + if c.Backend.ProjectInfoCacheSize == 0 { + c.Backend.ProjectInfoCacheSize = DefaultProjectInfoCacheSize + } + + if c.Backend.ProjectInfoCacheTTL == "" { + c.Backend.ProjectInfoCacheTTL = DefaultProjectInfoCacheTTL.String() + } + if c.Mongo != nil { if c.Mongo.ConnectionURI == "" { c.Mongo.ConnectionURI = DefaultMongoConnectionURI diff --git a/server/config.sample.yml b/server/config.sample.yml index 4c48b7128..22fc97d78 100644 --- a/server/config.sample.yml +++ b/server/config.sample.yml @@ -74,6 +74,12 @@ Backend: # AuthWebhookCacheUnauthTTL is the TTL value to set when caching the unauthorized result. AuthWebhookCacheUnauthTTL: "10s" + # ProjectInfoCacheSize is the size of the project info cache. + ProjectInfoCacheSize: 256 + + # ProjectInfoCacheTTL is the TTL value to set when caching the project info. + ProjectInfoCacheTTL: "10m" + # Hostname is the hostname of the server. If not provided, the hostname will be # determined automatically by the OS (Optional, default: os.Hostname()). Hostname: "" diff --git a/server/config_test.go b/server/config_test.go index eb0cb9661..43d2103a7 100644 --- a/server/config_test.go +++ b/server/config_test.go @@ -77,5 +77,9 @@ func TestNewConfigFromFile(t *testing.T) { authWebhookCacheUnauthTTL, err := time.ParseDuration(conf.Backend.AuthWebhookCacheUnauthTTL) assert.NoError(t, err) assert.Equal(t, authWebhookCacheUnauthTTL, server.DefaultAuthWebhookCacheUnauthTTL) + + projectInfoCacheTTL, err := time.ParseDuration(conf.Backend.ProjectInfoCacheTTL) + assert.NoError(t, err) + assert.Equal(t, projectInfoCacheTTL, server.DefaultProjectInfoCacheTTL) }) } diff --git a/server/rpc/interceptors/context.go b/server/rpc/interceptors/context.go index 62cb393c0..8f75d9fdf 100644 --- a/server/rpc/interceptors/context.go +++ b/server/rpc/interceptors/context.go @@ -28,7 +28,9 @@ import ( grpcstatus "google.golang.org/grpc/status" "github.com/yorkie-team/yorkie/api/types" + "github.com/yorkie-team/yorkie/pkg/cache" "github.com/yorkie-team/yorkie/server/backend" + "github.com/yorkie-team/yorkie/server/logging" "github.com/yorkie-team/yorkie/server/projects" "github.com/yorkie-team/yorkie/server/rpc/grpchelper" "github.com/yorkie-team/yorkie/server/rpc/metadata" @@ -36,13 +38,19 @@ import ( // ContextInterceptor is an interceptor for building additional context. type ContextInterceptor struct { - backend *backend.Backend + backend *backend.Backend + projectInfoCache *cache.LRUExpireCache[string, *types.Project] } // NewContextInterceptor creates a new instance of ContextInterceptor. func NewContextInterceptor(be *backend.Backend) *ContextInterceptor { + projectInfoCache, err := cache.NewLRUExpireCache[string, *types.Project](be.Config.ProjectInfoCacheSize) + if err != nil { + logging.DefaultLogger().Fatal("Failed to create project info cache: %v", err) + } return &ContextInterceptor{ - backend: be, + backend: be, + projectInfoCache: projectInfoCache, } } @@ -146,15 +154,19 @@ func (i *ContextInterceptor) buildContext(ctx context.Context) (context.Context, md.Authorization = authorization[0] } ctx = metadata.With(ctx, md) + cacheKey := md.APIKey // 02. building project - // TODO(hackerwins): Improve the performance of this function. - // Consider using a cache to store the info. - project, err := projects.GetProjectFromAPIKey(ctx, i.backend, md.APIKey) - if err != nil { - return nil, grpchelper.ToStatusError(err) + if cachedProjectInfo, ok := i.projectInfoCache.Get(cacheKey); ok { + ctx = projects.With(ctx, cachedProjectInfo) + } else { + project, err := projects.GetProjectFromAPIKey(ctx, i.backend, md.APIKey) + if err != nil { + return nil, grpchelper.ToStatusError(err) + } + i.projectInfoCache.Add(cacheKey, project, i.backend.Config.ParseProjectInfoCacheTTL()) + ctx = projects.With(ctx, project) } - ctx = projects.With(ctx, project) return ctx, nil } diff --git a/server/rpc/server_test.go b/server/rpc/server_test.go index e883d0e91..ae41aed59 100644 --- a/server/rpc/server_test.go +++ b/server/rpc/server_test.go @@ -76,6 +76,8 @@ func TestMain(m *testing.M) { ClientDeactivateThreshold: helper.ClientDeactivateThreshold, SnapshotThreshold: helper.SnapshotThreshold, AuthWebhookCacheSize: helper.AuthWebhookSize, + ProjectInfoCacheSize: helper.ProjectInfoCacheSize, + ProjectInfoCacheTTL: helper.ProjectInfoCacheTTL.String(), AdminTokenDuration: helper.AdminTokenDuration, }, &mongo.Config{ ConnectionURI: helper.MongoConnectionURI, diff --git a/test/helper/helper.go b/test/helper/helper.go index ba6c2da1f..eec425fd5 100644 --- a/test/helper/helper.go +++ b/test/helper/helper.go @@ -69,6 +69,8 @@ var ( AuthWebhookSize = 100 AuthWebhookCacheAuthTTL = 10 * gotime.Second AuthWebhookCacheUnauthTTL = 10 * gotime.Second + ProjectInfoCacheSize = 256 + ProjectInfoCacheTTL = 5 * gotime.Second MongoConnectionURI = "mongodb://localhost:27017" MongoConnectionTimeout = "5s" @@ -238,6 +240,8 @@ func TestConfig() *server.Config { AuthWebhookCacheSize: AuthWebhookSize, AuthWebhookCacheAuthTTL: AuthWebhookCacheAuthTTL.String(), AuthWebhookCacheUnauthTTL: AuthWebhookCacheUnauthTTL.String(), + ProjectInfoCacheSize: ProjectInfoCacheSize, + ProjectInfoCacheTTL: ProjectInfoCacheTTL.String(), }, Mongo: &mongo.Config{ ConnectionURI: MongoConnectionURI, diff --git a/test/integration/auth_webhook_test.go b/test/integration/auth_webhook_test.go index f5bb1aa5b..73b05ec38 100644 --- a/test/integration/auth_webhook_test.go +++ b/test/integration/auth_webhook_test.go @@ -161,6 +161,8 @@ func TestProjectAuthWebhook(t *testing.T) { ) assert.NoError(t, err) + projectInfoCacheTTL := 5 * time.Second + time.Sleep(projectInfoCacheTTL) cli, err := client.Dial( svr.RPCAddr(), client.WithAPIKey(project.PublicKey),