blob: 20bd15088cc768eeaaac84506e5b666586034b71 [file] [log] [blame]
// Copyright 2025 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gaememcache
import (
"context"
"time"
"google.golang.org/protobuf/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/server/caching"
"go.chromium.org/luci/server/internal/gae"
"go.chromium.org/luci/server/internal/gae/memcache"
)
// gaeBlobCache implements caching.BlobCache using GAE memcache.
type gaeBlobCache struct {
namespace string
}
var _ caching.BlobCache = (*gaeBlobCache)(nil)
// Get returns a cached item or ErrCacheMiss if it's not in the cache.
func (rc *gaeBlobCache) Get(ctx context.Context, key string) (blob []byte, err error) {
req := &memcache.MemcacheGetRequest{
Key: [][]byte{[]byte(key)},
NameSpace: &rc.namespace,
}
res := &memcache.MemcacheGetResponse{}
if err := gae.Call(ctx, "memcache", "Get", req, res); err != nil {
return nil, err
}
switch len(res.Item) {
case 0:
return nil, caching.ErrCacheMiss
case 1:
item := res.Item[0]
if string(item.Key) != key {
return nil, errors.Reason("the API unexpectedly returned wrong item").Err()
}
if item.GetIsDeleteLocked() {
return nil, caching.ErrCacheMiss
}
return item.Value, nil
default:
return nil, errors.Reason("the API unexpectedly returned %d items", len(res.Item)).Err()
}
}
// Set unconditionally overwrites an item in the cache.
//
// If 'exp' is zero, the item will have no expiration time.
func (rc *gaeBlobCache) Set(ctx context.Context, key string, value []byte, exp time.Duration) (err error) {
var expirationTime *uint32
if exp != 0 {
if exp < time.Second {
// Expiration duration has seconds precision. If the item expires less
// than a second from now (or we somehow got negative expiration here),
// use an absolute timestamp in the past to make the item expire ASAP. If
// we just use 0, the item will live forever.
//
// Note that memcache API recognizes "large" values of ExpirationTime as
// absolute unix timestamps.
expirationTime = proto.Uint32(uint32(time.Now().Unix()) - 5)
} else {
// Cap expiration by at most 1y. That way memcache API will always treat
// this as a relative duration (not an absolute timestamp).
expirationTime = proto.Uint32(uint32(min(exp, time.Hour*24*365) / time.Second))
}
}
req := &memcache.MemcacheSetRequest{
Item: []*memcache.MemcacheSetRequest_Item{
{
Key: []byte(key),
Value: value,
SetPolicy: memcache.MemcacheSetRequest_SET.Enum(),
ExpirationTime: expirationTime,
},
},
NameSpace: &rc.namespace,
}
res := &memcache.MemcacheSetResponse{}
if err := gae.Call(ctx, "memcache", "Set", req, res); err != nil {
return err
}
switch {
case len(res.SetStatus) != 1:
return errors.Reason("the API unexpectedly returned wrong number of statuses %d", len(res.SetStatus)).Err()
case res.SetStatus[0] != memcache.MemcacheSetResponse_STORED:
return errors.Reason("failed to store the item: %s", res.SetStatus[0]).Err()
default:
return nil
}
}