From b89ce028158f2c0efd469a5f8275a7fd203e1b29 Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 19 Sep 2023 11:12:55 +0200 Subject: [PATCH] feat: cache OPL when loading from HTTP(S) (#1429) --- internal/driver/config/opl_cache_test.go | 52 +++++++++++++++++++ .../config/opl_config_namespace_watcher.go | 30 ++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 internal/driver/config/opl_cache_test.go diff --git a/internal/driver/config/opl_cache_test.go b/internal/driver/config/opl_cache_test.go new file mode 100644 index 000000000..61dd4d8ac --- /dev/null +++ b/internal/driver/config/opl_cache_test.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ory/x/configx" + "github.com/ory/x/logrusx" + "github.com/stretchr/testify/require" +) + +func TestNewOPLConfigWatcher(t *testing.T) { + hits := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits++ + io.WriteString(w, testOPL) + })) + t.Cleanup(ts.Close) + ctx := context.Background() + cfg, err := NewDefault(ctx, nil, logrusx.New("", ""), configx.SkipValidation()) + require.NoError(t, err) + cw, err := newOPLConfigWatcher(ctx, cfg, ts.URL) + require.NoError(t, err) + require.Equal(t, 1, hits, "HTTP request made") + _, err = cw.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + _, err = cw.GetNamespaceByName(ctx, "Document") + require.NoError(t, err) + + cache.Wait() + + cw, err = newOPLConfigWatcher(ctx, cfg, ts.URL) + require.NoError(t, err) + require.Equal(t, 1, hits, "content was cached") + _, err = cw.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + _, err = cw.GetNamespaceByName(ctx, "Document") + require.NoError(t, err) +} + +var testOPL = ` +import { Namespace } from "@ory/keto-namespace-types" + +class User implements Namespace {} +class Document implements Namespace {} +` diff --git a/internal/driver/config/opl_config_namespace_watcher.go b/internal/driver/config/opl_config_namespace_watcher.go index f07e21344..bca9086a7 100644 --- a/internal/driver/config/opl_config_namespace_watcher.go +++ b/internal/driver/config/opl_config_namespace_watcher.go @@ -4,11 +4,14 @@ package config import ( + "bytes" "context" "fmt" "io" "sync" + "time" + "github.com/dgraph-io/ristretto" "github.com/ory/x/logrusx" "github.com/ory/x/urlx" "github.com/ory/x/watcherx" @@ -33,7 +36,14 @@ type ( } ) -var _ namespace.Manager = (*oplConfigWatcher)(nil) +var ( + _ namespace.Manager = (*oplConfigWatcher)(nil) + cache, _ = ristretto.NewCache(&ristretto.Config{ + MaxCost: 20_000_000, // 20 MB max size, each item ca. 10 KB => max 2000 items + NumCounters: 20_000, // max 2000 items => 20000 counters + BufferItems: 64, + }) +) func newOPLConfigWatcher(ctx context.Context, c *Config, target string) (*oplConfigWatcher, error) { nw := &oplConfigWatcher{ @@ -51,7 +61,7 @@ func newOPLConfigWatcher(ctx context.Context, c *Config, target string) (*oplCon switch targetUrl.Scheme { case "file", "": return nw, watchTarget(ctx, target, nw, c.l) - case "http", "https", "base64": + case "base64": file, err := c.Fetcher().FetchContext(ctx, target) if err != nil { return nil, err @@ -59,6 +69,22 @@ func newOPLConfigWatcher(ctx context.Context, c *Config, target string) (*oplCon nw.files.byPath[targetUrl.String()] = file nw.parseFiles() return nw, err + case "http", "https": + var file io.Reader + if item, ok := cache.Get(target); ok { + file = bytes.NewReader(item.([]byte)) + } else { + buf, err := c.Fetcher().FetchContext(ctx, target) + if err != nil { + return nil, err + } + b := buf.Bytes() + cache.SetWithTTL(target, b, int64(cap(b)), 30*time.Minute) + file = bytes.NewReader(b) + } + nw.files.byPath[targetUrl.String()] = file + nw.parseFiles() + return nw, err default: return nil, fmt.Errorf("unexpected url scheme: %q", targetUrl.Scheme) }