blob: bc3062848323a55a4966320e76a65f737bd780ca [file] [log] [blame]
// Copyright 2015 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 dscache
import (
"context"
"crypto/sha1"
"encoding/base64"
"fmt"
ds "go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/common/data/rand/mathrand"
"go.chromium.org/luci/common/logging"
)
type supportContext struct {
ds.KeyContext
c context.Context
impl Cache
shardsForKey []ShardFunction
}
func (s *supportContext) numShards(k *ds.Key) int {
ret := DefaultShards
for _, fn := range s.shardsForKey {
if amt, ok := fn(k); ok {
ret = amt
}
}
if ret < 1 {
return 0 // disable caching entirely
}
if ret > MaxShards {
ret = MaxShards
}
return ret
}
func (s *supportContext) mkRandKeys(keys []*ds.Key, metas ds.MultiMetaGetter, rnd mathrand.Rand) []string {
ret := []string(nil)
for i, key := range keys {
mg := metas.GetSingle(i)
if !ds.GetMetaDefault(mg, CacheEnableMeta, true).(bool) {
continue
}
shards := s.numShards(key)
if shards == 0 {
continue
}
shard := 0
if shards > 1 {
shard = rnd.Intn(shards)
}
if ret == nil {
ret = make([]string, len(keys))
}
ret[i] = makeMemcacheKey(shard, key)
}
return ret
}
func (s *supportContext) mkAllKeys(keys []*ds.Key) []string {
size := 0
nums := make([]int, len(keys))
for i, key := range keys {
if !key.IsIncomplete() {
shards := s.numShards(key)
nums[i] = shards
size += shards
}
}
if size == 0 {
return nil
}
ret := make([]string, 0, size)
for i, key := range keys {
if !key.IsIncomplete() {
keySuffix := hashKey(key)
for shard := 0; shard < nums[i]; shard++ {
ret = append(ret, fmt.Sprintf(KeyFormat, shard, keySuffix))
}
}
}
return ret
}
func (s *supportContext) mutation(keys []*ds.Key, f func() error) error {
itemKeys := s.mkAllKeys(keys)
if len(itemKeys) == 0 {
return f()
}
if err := s.impl.PutLocks(s.c, itemKeys, MutationLockTimeout); err != nil {
// this is a hard failure. No mutation can occur if we're unable to set
// locks out. See "DANGER ZONE" in the docs.
logging.WithError(err).Errorf(s.c, "dscache: HARD FAILURE: supportContext.mutation(): PutLocks")
return err
}
err := f()
// Note: the mutation can *eventually* succeed even if `err` is non-nil
// here. So on errors we pessimistically keep the locks until they expire.
if err == nil {
if err := s.impl.DropLocks(s.c, itemKeys); err != nil {
logging.WithError(err).Debugf(s.c, "dscache: DropLocks")
}
}
return err
}
// generateNonce creates a pseudo-random sequence of bytes for use as a nonce
// using the non-cryptographic PRNG in "math/rand".
//
// The random values here are controlled entirely by the application, will never
// be shown to, or provided by, the user, so this should be fine.
func generateNonce(rnd mathrand.Rand) []byte {
nonce := make([]byte, NonceBytes)
_, _ = rnd.Read(nonce) // This Read will always return len(nonce), nil.
return nonce
}
// makeMemcacheKey generates a memcache key for the given datastore Key.
func makeMemcacheKey(shard int, k *ds.Key) string {
return fmt.Sprintf(KeyFormat, shard, hashKey(k))
}
// hashKey generates just the hashed portion of the memcache key.
func hashKey(k *ds.Key) string {
dgst := sha1.Sum(ds.Serialize.ToBytes(k))
return base64.RawStdEncoding.EncodeToString(dgst[:])
}