Skip to content

Commit

Permalink
internal/cache: factor out cache into its own package
Browse files Browse the repository at this point in the history
Put the Redis-based page cache logic into its own
package.

Mostly for the non-trivial DeletePrefix method.

For golang/go#44217

Change-Id: I7248f60b5d71402a38ebd4c96143a70e21ae3c63
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/291249
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
  • Loading branch information
jba committed Feb 11, 2021
1 parent d223212 commit d66a759
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
89 changes: 89 additions & 0 deletions internal/cache/cache.go
@@ -0,0 +1,89 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package cache implements a redis-based page cache
// for pkgsite.
package cache

import (
"context"
"time"

"github.com/go-redis/redis/v8"
"golang.org/x/pkgsite/internal/derrors"
)

// Cache is a Redis-based cache.
type Cache struct {
client *redis.Client
}

// New creates a new Cache using the given Redis client.
func New(client *redis.Client) *Cache {
return &Cache{client: client}
}

// Get returns the value for key, or nil if the key does not exist.
func (c *Cache) Get(ctx context.Context, key string) (value []byte, err error) {
defer derrors.Wrap(&err, "Get(%q)", key)
val, err := c.client.Get(ctx, key).Bytes()
if err == redis.Nil { // not found
return nil, nil
}
if err != nil {
return nil, err
}
return val, nil
}

// Put inserts the key with the given data and time-to-live.
func (c *Cache) Put(ctx context.Context, key string, data []byte, ttl time.Duration) (err error) {
defer derrors.Wrap(&err, "Put(%q, data, %s)", key, ttl)
_, err = c.client.Set(ctx, key, data, ttl).Result()
return err
}

// Clear deletes all entries from the cache.
func (c *Cache) Clear(ctx context.Context) (err error) {
defer derrors.Wrap(&err, "Clear()")
status := c.client.FlushAll(ctx)
return status.Err()
}

// Delete deletes the given keys. It does not return an error if a key does not
// exist.
func (c *Cache) Delete(ctx context.Context, keys ...string) (err error) {
defer derrors.Wrap(&err, "Delete(%q)", keys)
cmd := c.client.Unlink(ctx, keys...) // faster, asynchronous delete
return cmd.Err()
}

// DeletePrefix deletes all keys beginning with prefix.
func (c *Cache) DeletePrefix(ctx context.Context, prefix string) (err error) {
defer derrors.Wrap(&err, "DeletePrefix(%q)", prefix)
iter := c.client.Scan(ctx, 0, prefix+"*", int64(scanCount)).Iterator()
var keys []string
for iter.Next(ctx) {
keys = append(keys, iter.Val())
if len(keys) > scanCount {
if err := c.Delete(ctx, keys...); err != nil {
return err
}
keys = keys[:0]
}
}
if iter.Err() != nil {
return iter.Err()
}
if len(keys) > 0 {
return c.Delete(ctx, keys...)
}
return nil
}

// The "count" argument to the Redis SCAN command, which is a hint for how much
// work to perform.
// Also used as the batch size for Delete calls in DeletePrefix.
// var for testing.
var scanCount = 100
87 changes: 87 additions & 0 deletions internal/cache/cache_test.go
@@ -0,0 +1,87 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cache

import (
"context"
"sort"
"testing"

"github.com/alicebob/miniredis/v2"
"github.com/go-redis/redis/v8"
"github.com/google/go-cmp/cmp"
)

func TestBasics(t *testing.T) {
ctx := context.Background()
s, err := miniredis.Run()
if err != nil {
t.Fatal(err)
}
defer s.Close()
c := New(redis.NewClient(&redis.Options{Addr: s.Addr()}))

val := []byte("value")
must(t, c.Put(ctx, "key", val, 0))
got, err := c.Get(ctx, "key")
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(got, val) {
t.Fatalf("got %v, want %v", got, val)
}

must(t, c.Delete(ctx, "key"))
got, err = c.Get(ctx, "key")
if err != nil {
t.Fatal(err)
}
if got != nil {
t.Fatalf("got %v, want nil", got)
}
}

func TestDeletePrefix(t *testing.T) {
ctx := context.Background()
s, err := miniredis.Run()
if err != nil {
t.Fatal(err)
}
defer s.Close()
c := New(redis.NewClient(&redis.Options{Addr: s.Addr()}))

check := func(want []string) {
t.Helper()
got, err := c.client.Keys(ctx, "*").Result()
if err != nil {
t.Fatal(err)
}
sort.Strings(want)
sort.Strings(got)
if !cmp.Equal(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}

all := []string{"a", "b", "c", "a@x", "a/x"}
for _, k := range all {
must(t, c.Put(ctx, k, []byte("value"), 0))
}
check(all)

scanCount = 1
must(t, c.DeletePrefix(ctx, "a"))
check([]string{"b", "c"})

must(t, c.Clear(ctx))
check([]string{})
}

func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}

0 comments on commit d66a759

Please sign in to comment.