diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fd1479..24ea8c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Dev: Improve Livestreamfails tests. (#297, #301) - Dev: Improve default resolver tests. (#300) - Dev: Resolve imgur.io links. (#365) +- Dev: Don't use `stampede` for link resolver links. (#394) ## 1.2.3 diff --git a/internal/resolvers/default/initialize.go b/internal/resolvers/default/initialize.go index 5511dd1f..9d3a7f82 100644 --- a/internal/resolvers/default/initialize.go +++ b/internal/resolvers/default/initialize.go @@ -28,11 +28,10 @@ var defaultTooltip = template.Must(template.New("default_tooltip").Parse(default func Initialize(ctx context.Context, cfg config.APIConfig, pool db.Pool, router *chi.Mux, helixClient *helix.Client) { defaultLinkResolver := New(ctx, cfg, pool, helixClient) - cached := stampede.Handler(512, 10*time.Second) imageCached := stampede.Handler(256, 2*time.Second) generatedValuesCached := stampede.Handler(256, 2*time.Second) - router.With(cached).Get("/link_resolver/{url}", defaultLinkResolver.HandleRequest) + router.Get("/link_resolver/{url}", defaultLinkResolver.HandleRequest) router.With(imageCached).Get("/thumbnail/{url}", defaultLinkResolver.HandleThumbnailRequest) router.With(generatedValuesCached).Get("/generated/{url}", defaultLinkResolver.HandleGeneratedValueRequest) } diff --git a/pkg/cache/db.go b/pkg/cache/db.go index b07d98a5..ab037636 100644 --- a/pkg/cache/db.go +++ b/pkg/cache/db.go @@ -3,6 +3,7 @@ package cache import ( "context" "net/http" + "sync" "time" "github.com/Chatterino/api/internal/db" @@ -33,6 +34,11 @@ var ( ) ) +type wrappedResponse struct { + response *Response + err error +} + func init() { prometheus.MustRegister(cacheHits) prometheus.MustRegister(cacheMisses) @@ -49,6 +55,9 @@ type PostgreSQLCache struct { pool db.Pool dependentCaches []DependentCache + + requestsMutex sync.Mutex + requests map[string][]chan wrappedResponse } // TODO: Make the "internal error" tooltip an actual tooltip @@ -187,9 +196,39 @@ func (c *PostgreSQLCache) Get(ctx context.Context, key string, r *http.Request) return cacheResponse, nil } + // If key is not in cache, sign up as a listener and ensure loader is only called once cacheMisses.Inc() log.Debugw("DB Get cache miss", "cacheKey", cacheKey) - return c.load(ctx, key, r) + responseChannel := make(chan wrappedResponse) + + c.requestsMutex.Lock() + + c.requests[key] = append(c.requests[key], responseChannel) + + first := len(c.requests[key]) == 1 + + c.requestsMutex.Unlock() + + if first { + go func() { + response, err := c.load(ctx, key, r) + + r := wrappedResponse{ + response, + err, + } + c.requestsMutex.Lock() + for _, ch := range c.requests[key] { + ch <- r + } + delete(c.requests, key) + c.requestsMutex.Unlock() + }() + } + + // Wait for loader to complete, then return value from loader + response := <-responseChannel + return response.response, response.err } func (c *PostgreSQLCache) GetOnly(ctx context.Context, key string) *Response { @@ -280,6 +319,7 @@ func NewPostgreSQLCache(ctx context.Context, cfg config.APIConfig, pool db.Pool, loader: loader, cacheDuration: cacheDuration, pool: pool, + requests: make(map[string][]chan wrappedResponse), } }