diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go index cdacb5f58c..34bd56a2e4 100644 --- a/go/apps/api/routes/register.go +++ b/go/apps/api/routes/register.go @@ -73,13 +73,13 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitLimit.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - ClickHouse: svc.ClickHouse, - Ratelimit: svc.Ratelimit, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, - TestMode: srv.Flags().TestMode, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + ClickHouse: svc.ClickHouse, + Ratelimit: svc.Ratelimit, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, + TestMode: srv.Flags().TestMode, }, ) @@ -87,11 +87,11 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitSetOverride.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - Auditlogs: svc.Auditlogs, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + Auditlogs: svc.Auditlogs, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, }, ) @@ -99,10 +99,10 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitGetOverride.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, }, ) @@ -110,11 +110,11 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitDeleteOverride.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - Auditlogs: svc.Auditlogs, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + Auditlogs: svc.Auditlogs, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, }, ) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go index da40cea0f5..a6358e9806 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go @@ -44,11 +44,11 @@ func TestDeleteOverrideSuccessfully(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go index 163d590ef3..8587200d45 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go @@ -18,10 +18,10 @@ func TestBadRequests(t *testing.T) { rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go index 31b74d6c1a..c34f054203 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go @@ -14,11 +14,11 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go index d3b2a56256..4e0ab744b6 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go @@ -45,11 +45,11 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go index 396614c050..efe7a1d5ff 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go @@ -31,11 +31,11 @@ func TestNotFound(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go index 4384b3e7f9..dce3de6c48 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go @@ -25,11 +25,11 @@ type Response = openapi.V2RatelimitDeleteOverrideResponseBody // Handler implements zen.Route interface for the v2 ratelimit delete override endpoint type Handler struct { - Logger logging.Logger - DB db.Database - Keys keys.KeyService - Auditlogs auditlogs.AuditLogService - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] + Logger logging.Logger + DB db.Database + Keys keys.KeyService + Auditlogs auditlogs.AuditLogService + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] } // Method returns the HTTP method this route responds to @@ -156,7 +156,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - h.RatelimitNamespaceByNameCache.Remove(ctx, namespace.Name) + h.RatelimitNamespaceCache.Remove(ctx, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.ID, + }, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.Name, + }, + ) return nil }) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go index 8e884e62dd..12dec85e4e 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go @@ -47,10 +47,10 @@ func TestGetOverrideSuccessfully(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/400_test.go b/go/apps/api/routes/v2_ratelimit_get_override/400_test.go index f813c4fbf4..f0c3d25d97 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/400_test.go @@ -19,10 +19,10 @@ func TestBadRequests(t *testing.T) { rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go index 59d304c7b7..e4f3b62abf 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go @@ -14,10 +14,10 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go index aa60db1006..c076d5bc28 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go @@ -45,10 +45,10 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/404_test.go b/go/apps/api/routes/v2_ratelimit_get_override/404_test.go index ac7f1a9fae..b47f697a98 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/404_test.go @@ -31,10 +31,10 @@ func TestOverrideNotFound(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/handler.go b/go/apps/api/routes/v2_ratelimit_get_override/handler.go index dd5362c8cf..b4ae042245 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/handler.go @@ -26,10 +26,10 @@ type Response = openapi.V2RatelimitGetOverrideResponseBody // Handler implements zen.Route interface for the v2 ratelimit get override endpoint type Handler struct { // Services as public fields - Logger logging.Logger - DB db.Database - Keys keys.KeyService - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] + Logger logging.Logger + DB db.Database + Keys keys.KeyService + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] } // Method returns the HTTP method this route responds to diff --git a/go/apps/api/routes/v2_ratelimit_limit/200_test.go b/go/apps/api/routes/v2_ratelimit_limit/200_test.go index 6d0113dd61..4e14cb9c9a 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/200_test.go @@ -22,12 +22,12 @@ func TestLimitSuccessfully(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - ClickHouse: h.ClickHouse, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + ClickHouse: h.ClickHouse, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/400_test.go b/go/apps/api/routes/v2_ratelimit_limit/400_test.go index a77099c626..0817b08a98 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/400_test.go @@ -20,11 +20,11 @@ func TestBadRequests(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/401_test.go b/go/apps/api/routes/v2_ratelimit_limit/401_test.go index 239898523f..e50ea8ac25 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/401_test.go @@ -13,11 +13,11 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/403_test.go b/go/apps/api/routes/v2_ratelimit_limit/403_test.go index 176ad9fa93..5386b683ae 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/403_test.go @@ -31,11 +31,11 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/404_test.go b/go/apps/api/routes/v2_ratelimit_limit/404_test.go index 5b91d0c61f..a2f7a5446b 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/404_test.go @@ -20,11 +20,11 @@ func TestNamespaceNotFound(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go b/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go index 44255c9fb9..d00c456eb5 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go @@ -57,12 +57,12 @@ func TestRateLimitAccuracy(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - ClickHouse: h.ClickHouse, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + ClickHouse: h.ClickHouse, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) ctx := context.Background() diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index e042d1f00c..03b1471787 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -32,13 +32,13 @@ type Response = openapi.V2RatelimitLimitResponseBody // Handler implements zen.Route interface for the v2 ratelimit limit endpoint type Handler struct { // Services as public fields - Logger logging.Logger - Keys keys.KeyService - DB db.Database - ClickHouse clickhouse.Bufferer - Ratelimit ratelimit.Service - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] - TestMode bool + Logger logging.Logger + Keys keys.KeyService + DB db.Database + ClickHouse clickhouse.Bufferer + Ratelimit ratelimit.Service + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] + TestMode bool } // Method returns the HTTP method this route responds to @@ -70,43 +70,45 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } ctx, span := tracing.Start(ctx, "FindRatelimitNamespace") - namespace, err := h.RatelimitNamespaceByNameCache.SWR(ctx, req.Namespace, func(ctx context.Context) (db.FindRatelimitNamespace, error) { - response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ - WorkspaceID: auth.AuthorizedWorkspaceID, - Name: sql.NullString{String: req.Namespace, Valid: true}, - ID: sql.NullString{String: "", Valid: false}, - }) - result := db.FindRatelimitNamespace{} // nolint:exhaustruct - if err != nil { - return result, err - } + namespace, err := h.RatelimitNamespaceCache.SWR(ctx, + cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: req.Namespace}, + func(ctx context.Context) (db.FindRatelimitNamespace, error) { + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Name: sql.NullString{String: req.Namespace, Valid: true}, + ID: sql.NullString{String: "", Valid: false}, + }) + result := db.FindRatelimitNamespace{} // nolint:exhaustruct + if err != nil { + return result, err + } - result = db.FindRatelimitNamespace{ - ID: response.ID, - WorkspaceID: response.WorkspaceID, - Name: response.Name, - CreatedAtM: response.CreatedAtM, - UpdatedAtM: response.UpdatedAtM, - DeletedAtM: response.DeletedAtM, - DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride), - WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0), - } + result = db.FindRatelimitNamespace{ + ID: response.ID, + WorkspaceID: response.WorkspaceID, + Name: response.Name, + CreatedAtM: response.CreatedAtM, + UpdatedAtM: response.UpdatedAtM, + DeletedAtM: response.DeletedAtM, + DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride), + WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0), + } - overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0) - err = json.Unmarshal(response.Overrides.([]byte), &overrides) - if err != nil { - return result, err - } + overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0) + err = json.Unmarshal(response.Overrides.([]byte), &overrides) + if err != nil { + return result, err + } - for _, override := range overrides { - result.DirectOverrides[override.Identifier] = override - if strings.Contains(override.Identifier, "*") { - result.WildcardOverrides = append(result.WildcardOverrides, override) + for _, override := range overrides { + result.DirectOverrides[override.Identifier] = override + if strings.Contains(override.Identifier, "*") { + result.WildcardOverrides = append(result.WildcardOverrides, override) + } } - } - return result, nil - }, caches.DefaultFindFirstOp) + return result, nil + }, caches.DefaultFindFirstOp) span.End() if err != nil { diff --git a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go index 2b3553a657..7e5e671f9c 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go @@ -30,11 +30,11 @@ func TestSetOverrideSuccessfully(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go index 30b6f13c8a..1023b3dc24 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go @@ -18,10 +18,10 @@ func TestBadRequests(t *testing.T) { rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.set_override") route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go index 32109244eb..ab6e131c8c 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go @@ -14,11 +14,11 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go index 8018a6126a..9c02ccc4b3 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go @@ -31,11 +31,11 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/404_test.go b/go/apps/api/routes/v2_ratelimit_set_override/404_test.go index 3eac1a6715..2458fe82d0 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/404_test.go @@ -15,11 +15,11 @@ func TestNamespaceNotFound(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/handler.go b/go/apps/api/routes/v2_ratelimit_set_override/handler.go index 860c696b1c..e8185a4cf0 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/handler.go @@ -28,11 +28,11 @@ type Response = openapi.V2RatelimitSetOverrideResponseBody // Handler implements zen.Route interface for the v2 ratelimit set override endpoint type Handler struct { // Services as public fields - Logger logging.Logger - DB db.Database - Keys keys.KeyService - Auditlogs auditlogs.AuditLogService - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] + Logger logging.Logger + DB db.Database + Keys keys.KeyService + Auditlogs auditlogs.AuditLogService + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] } // Method returns the HTTP method this route responds to @@ -137,7 +137,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return "", err } - h.RatelimitNamespaceByNameCache.Remove(ctx, namespace.Name) + h.RatelimitNamespaceCache.Remove(ctx, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.ID, + }, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.Name, + }, + ) return overrideID, nil }) diff --git a/go/internal/services/caches/caches.go b/go/internal/services/caches/caches.go index eb2c96447d..1930fad62b 100644 --- a/go/internal/services/caches/caches.go +++ b/go/internal/services/caches/caches.go @@ -13,9 +13,9 @@ import ( // Caches holds all cache instances used throughout the application. // Each field represents a specialized cache for a specific data entity. type Caches struct { - // RatelimitNamespaceByName caches ratelimit namespace lookups by name. - // Keys are db.FindRatelimitNamespaceByNameParams and values are db.RatelimitNamespace. - RatelimitNamespaceByName cache.Cache[string, db.FindRatelimitNamespace] + // RatelimitNamespace caches ratelimit namespace lookups by name or ID. + // Keys are cache.ScopedKey and values are db.FindRatelimitNamespace. + RatelimitNamespace cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] // VerificationKeyByHash caches verification key lookups by their hash. // Keys are string (hash) and values are db.VerificationKey. @@ -65,7 +65,7 @@ type Config struct { // // Use the caches // key, err := caches.KeyByHash.Get(ctx, "some-hash") func New(config Config) (Caches, error) { - ratelimitNamespace, err := cache.New(cache.Config[string, db.FindRatelimitNamespace]{ + ratelimitNamespace, err := cache.New(cache.Config[cache.ScopedKey, db.FindRatelimitNamespace]{ Fresh: time.Minute, Stale: 24 * time.Hour, Logger: config.Logger, @@ -103,8 +103,8 @@ func New(config Config) (Caches, error) { } return Caches{ - RatelimitNamespaceByName: middleware.WithTracing(ratelimitNamespace), - ApiByID: middleware.WithTracing(apiById), - VerificationKeyByHash: middleware.WithTracing(verificationKeyByHash), + RatelimitNamespace: middleware.WithTracing(ratelimitNamespace), + ApiByID: middleware.WithTracing(apiById), + VerificationKeyByHash: middleware.WithTracing(verificationKeyByHash), }, nil } diff --git a/go/pkg/cache/cache.go b/go/pkg/cache/cache.go index 013fa30ea2..572edf1102 100644 --- a/go/pkg/cache/cache.go +++ b/go/pkg/cache/cache.go @@ -153,10 +153,10 @@ func (c *cache[K, V]) get(_ context.Context, key K) (swrEntry[V], bool) { return v, ok } -func (c *cache[K, V]) Remove(ctx context.Context, key K) { - - c.otter.Delete(key) - +func (c *cache[K, V]) Remove(ctx context.Context, keys ...K) { + for _, key := range keys { + c.otter.Delete(key) + } } func (c *cache[K, V]) Dump(ctx context.Context) ([]byte, error) { diff --git a/go/pkg/cache/interface.go b/go/pkg/cache/interface.go index 0978698e06..b8272bf206 100644 --- a/go/pkg/cache/interface.go +++ b/go/pkg/cache/interface.go @@ -15,8 +15,9 @@ type Cache[K comparable, V any] interface { // Sets the given key to null, indicating that the value does not exist in the origin. SetNull(ctx context.Context, key K) - // Removes the key from the cache. - Remove(ctx context.Context, key K) + // Remove removes one or more keys from the cache. + // Multiple keys can be provided for efficient bulk removal. + Remove(ctx context.Context, keys ...K) SWR(ctx context.Context, key K, refreshFromOrigin func(ctx context.Context) (V, error), op func(error) Op) (value V, err error) diff --git a/go/pkg/cache/middleware/tracing.go b/go/pkg/cache/middleware/tracing.go index e96360ea5d..27179e4d5f 100644 --- a/go/pkg/cache/middleware/tracing.go +++ b/go/pkg/cache/middleware/tracing.go @@ -44,13 +44,15 @@ func (mw *tracingMiddleware[K, V]) SetNull(ctx context.Context, key K) { mw.next.SetNull(ctx, key) } -func (mw *tracingMiddleware[K, V]) Remove(ctx context.Context, key K) { +func (mw *tracingMiddleware[K, V]) Remove(ctx context.Context, keys ...K) { ctx, span := tracing.Start(ctx, "cache.Remove") defer span.End() - span.SetAttributes(attribute.String("key", fmt.Sprintf("%+v", key))) - - mw.next.Remove(ctx, key) + span.SetAttributes( + attribute.String("keys", fmt.Sprintf("%+v", keys)), + attribute.Int("count", len(keys)), + ) + mw.next.Remove(ctx, keys...) } func (mw *tracingMiddleware[K, V]) Dump(ctx context.Context) ([]byte, error) { diff --git a/go/pkg/cache/noop.go b/go/pkg/cache/noop.go index e81fb6db2f..7e46d2a5e9 100644 --- a/go/pkg/cache/noop.go +++ b/go/pkg/cache/noop.go @@ -13,7 +13,7 @@ func (c *noopCache[K, V]) Get(ctx context.Context, key K) (value V, hit CacheHit func (c *noopCache[K, V]) Set(ctx context.Context, key K, value V) {} func (c *noopCache[K, V]) SetNull(ctx context.Context, key K) {} -func (c *noopCache[K, V]) Remove(ctx context.Context, key K) {} +func (c *noopCache[K, V]) Remove(ctx context.Context, keys ...K) {} func (c *noopCache[K, V]) Dump(ctx context.Context) ([]byte, error) { return []byte{}, nil diff --git a/go/pkg/cache/scoped_key.go b/go/pkg/cache/scoped_key.go new file mode 100644 index 0000000000..a560dd95dd --- /dev/null +++ b/go/pkg/cache/scoped_key.go @@ -0,0 +1,57 @@ +package cache + +// ScopedKey represents a cache key that is scoped to a specific workspace. +// +// This type is designed for caching data where keys are only unique within +// a workspace context, rather than being globally unique. For example, a user +// might create a ratelimit namespace called "api-calls" which is unique within +// their workspace but could exist in multiple workspaces. +// +// The ScopedKey ensures cache isolation between workspaces by combining the +// workspace ID with the resource key, preventing cache collisions and data +// leakage between different workspaces. +// +// # Usage +// +// Use ScopedKey when caching data that is workspace-specific: +// +// // Cache a ratelimit namespace by name +// key := cache.ScopedKey{ +// WorkspaceID: "ws_123", +// Key: "api-calls", +// } +// +// // Cache by ID (still workspace-scoped for consistency) +// key := cache.ScopedKey{ +// WorkspaceID: "ws_123", +// Key: "ns_456", +// } +// +// // Cache any workspace-scoped resource +// key := cache.ScopedKey{ +// WorkspaceID: "ws_123", +// Key: "some-resource-identifier", +// } +// +// # Design Rationale +// +// We chose this approach over concatenating strings because it provides type +// safety and makes the workspace scoping explicit in the API. It also allows +// for future extension if additional scoping dimensions are needed. +// +// The generic Key field can hold any string identifier (names, IDs, slugs, etc.) +// while maintaining consistent workspace isolation across all cache usage patterns. +type ScopedKey struct { + // WorkspaceID is the unique identifier for the workspace that owns this resource. + // This ensures that cache keys are isolated between different workspaces, + // preventing accidental data leakage or cache collisions. + WorkspaceID string + + // Key is the identifier for the resource within the workspace. + // This can be a user-provided name, system-generated ID, slug, or any other + // string identifier that uniquely identifies the resource within the workspace. + // + // The key is only guaranteed to be unique within the workspace context. + // Different workspaces may have resources with the same key value. + Key string +}