solver: simplify cache backend interface

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
docker-18.09
Tonis Tiigi 2018-03-16 21:07:18 -07:00
parent 5593bb9f48
commit 4df3b2e000
5 changed files with 98 additions and 258 deletions

View File

@ -10,7 +10,6 @@ import (
)
const (
mainBucket = "_main"
resultBucket = "_result"
linksBucket = "_links"
byResultBucket = "_byresult"
@ -27,7 +26,7 @@ func NewStore(dbPath string) (*Store, error) {
return nil, errors.Wrapf(err, "failed to open database file %s", dbPath)
}
if err := db.Update(func(tx *bolt.Tx) error {
for _, b := range []string{mainBucket, resultBucket, linksBucket, byResultBucket, backlinksBucket} {
for _, b := range []string{resultBucket, linksBucket, byResultBucket, backlinksBucket} {
if _, err := tx.CreateBucketIfNotExists([]byte(b)); err != nil {
return err
}
@ -39,47 +38,30 @@ func NewStore(dbPath string) (*Store, error) {
return &Store{db: db}, nil
}
func (s *Store) Get(id string) (solver.CacheKeyInfo, error) {
var cki solver.CacheKeyInfo
func (s *Store) Exists(id string) bool {
exists := false
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(mainBucket))
if b == nil {
return errors.WithStack(solver.ErrNotFound)
}
v := b.Get([]byte(id))
if v == nil {
return errors.WithStack(solver.ErrNotFound)
}
return json.Unmarshal(v, &cki)
b := tx.Bucket([]byte(linksBucket)).Bucket([]byte(id))
exists = b != nil
return nil
})
if err != nil {
return solver.CacheKeyInfo{}, err
return false
}
return cki, nil
}
func (s *Store) Set(info solver.CacheKeyInfo) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(mainBucket))
if b == nil {
return errors.WithStack(solver.ErrNotFound)
}
dt, err := json.Marshal(info)
if err != nil {
return err
}
return b.Put([]byte(info.ID), dt)
})
return exists
}
func (s *Store) Walk(fn func(id string) error) error {
ids := make([]string, 0)
if err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(mainBucket))
return b.ForEach(func(k, v []byte) error {
b := tx.Bucket([]byte(linksBucket))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if v == nil {
ids = append(ids, string(k))
}
}
return nil
})
}); err != nil {
return err
}
@ -272,7 +254,7 @@ func (s *Store) emptyBranchWithParents(tx *bolt.Tx, id []byte) error {
return err
}
}
return tx.Bucket([]byte(mainBucket)).Delete(id)
return nil
}
func (s *Store) AddLink(id string, link solver.CacheInfoLink, target string) error {

View File

@ -43,10 +43,12 @@ func NewCacheManager(id string, storage CacheKeyStorage, results CacheResultStor
}
type inMemoryCacheKey struct {
CacheKeyInfo CacheKeyInfo
manager *inMemoryCacheManager
cacheResult CacheResult
deps []CacheKeyWithSelector // only []*inMemoryCacheKey
digest digest.Digest
output Index
id string
CacheKey
}
@ -55,10 +57,10 @@ func (ck *inMemoryCacheKey) Deps() []CacheKeyWithSelector {
}
func (ck *inMemoryCacheKey) Digest() digest.Digest {
return ck.CacheKeyInfo.Base
return ck.digest
}
func (ck *inMemoryCacheKey) Output() Index {
return Index(ck.CacheKeyInfo.Output)
return ck.output
}
func withExporter(ck *inMemoryCacheKey, cacheResult *CacheResult, deps []CacheKeyWithSelector) ExportableCacheKey {
@ -78,7 +80,7 @@ type cacheExporter struct {
func (ce *cacheExporter) Export(ctx context.Context, m map[digest.Digest]*ExportRecord, converter func(context.Context, Result) (*Remote, error)) (*ExportRecord, error) {
var res Result
if ce.cacheResult == nil {
cr, err := ce.inMemoryCacheKey.manager.getBestResult(ce.inMemoryCacheKey.CacheKeyInfo)
cr, err := ce.inMemoryCacheKey.manager.getBestResult(ce.inMemoryCacheKey.id)
if err != nil {
return nil, err
}
@ -109,7 +111,7 @@ func (ce *cacheExporter) Export(ctx context.Context, m map[digest.Digest]*Export
}
}
cacheID := digest.FromBytes([]byte(ce.inMemoryCacheKey.CacheKeyInfo.ID))
cacheID := digest.FromBytes([]byte(ce.inMemoryCacheKey.id))
if remote != nil && len(remote.Descriptors) > 0 && remote.Descriptors[0].Digest != "" {
cacheID = remote.Descriptors[0].Digest
}
@ -164,20 +166,22 @@ func (c *inMemoryCacheManager) ID() string {
return c.id
}
func (c *inMemoryCacheManager) toInMemoryCacheKey(cki CacheKeyInfo, deps []CacheKeyWithSelector) *inMemoryCacheKey {
func (c *inMemoryCacheManager) toInMemoryCacheKey(id string, dgst digest.Digest, output Index, deps []CacheKeyWithSelector) *inMemoryCacheKey {
ck := &inMemoryCacheKey{
CacheKeyInfo: cki,
id: id,
output: output,
digest: dgst,
manager: c,
CacheKey: NewCacheKey("", 0, nil),
deps: deps,
}
ck.SetValue(internalMemoryKey, cki.ID)
ck.SetValue(internalMemoryKey, id)
return ck
}
func (c *inMemoryCacheManager) getBestResult(cki CacheKeyInfo) (*CacheResult, error) {
func (c *inMemoryCacheManager) getBestResult(id string) (*CacheResult, error) {
var results []*CacheResult
if err := c.backend.WalkResults(cki.ID, func(res CacheResult) error {
if err := c.backend.WalkResults(id, func(res CacheResult) error {
results = append(results, &res)
return nil
}); err != nil {
@ -241,7 +245,7 @@ func (c *inMemoryCacheManager) Query(deps []CacheKeyWithSelector, input Index, d
allRes := map[string]struct{}{}
for _, d := range allDeps {
if d.internalKey != nil {
if err := c.backend.WalkLinks(d.internalKey.CacheKeyInfo.ID, CacheInfoLink{input, output, dgst, d.key.Selector}, func(id string) error {
if err := c.backend.WalkLinks(d.internalKey.id, CacheInfoLink{input, output, dgst, d.key.Selector}, func(id string) error {
d.results[id] = struct{}{}
allRes[id] = struct{}{}
return nil
@ -267,7 +271,7 @@ func (c *inMemoryCacheManager) Query(deps []CacheKeyWithSelector, input Index, d
d.internalKey = internalKey
}
if _, ok := d.results[res]; !ok {
if err := c.backend.AddLink(d.internalKey.CacheKeyInfo.ID, CacheInfoLink{
if err := c.backend.AddLink(d.internalKey.id, CacheInfoLink{
Input: input,
Output: output,
Digest: dgst,
@ -279,21 +283,14 @@ func (c *inMemoryCacheManager) Query(deps []CacheKeyWithSelector, input Index, d
}
hadResults := false
cki, err := c.backend.Get(res)
if err != nil {
if errors.Cause(err) == ErrNotFound {
continue
}
return nil, errors.Wrapf(err, "failed lookup by internal ID %s", res)
}
deps := formatDeps(allDeps, int(input))
k := c.toInMemoryCacheKey(cki, deps)
fdeps := formatDeps(allDeps, int(input))
k := c.toInMemoryCacheKey(res, dgst, output, fdeps)
// TODO: invoke this only once per input
if err := c.backend.WalkResults(res, func(r CacheResult) error {
if c.results.Exists(r.ID) {
outs = append(outs, &CacheRecord{
ID: res + "@" + r.ID,
CacheKey: withExporter(k, &r, deps),
CacheKey: withExporter(k, &r, fdeps),
CacheManager: c,
Loadable: true,
CreatedAt: r.CreatedAt,
@ -308,18 +305,14 @@ func (c *inMemoryCacheManager) Query(deps []CacheKeyWithSelector, input Index, d
}
if !hadResults {
cki, err := c.backend.Get(res)
if err != nil {
if errors.Cause(err) == ErrNotFound {
if len(deps) == 0 {
if !c.backend.Exists(res) {
continue
}
return nil, errors.Wrapf(err, "failed lookup by internal ID %s", res)
}
deps := formatDeps(allDeps, int(input))
k := c.toInMemoryCacheKey(cki, deps)
outs = append(outs, &CacheRecord{
ID: res,
CacheKey: withExporter(k, nil, deps),
CacheKey: withExporter(k, nil, fdeps),
CacheManager: c,
Loadable: false,
})
@ -342,7 +335,7 @@ func (c *inMemoryCacheManager) Load(ctx context.Context, rec *CacheRecord) (Resu
return nil, err
}
res, err := c.backend.Load(ck.CacheKeyInfo.ID, keyParts[1])
res, err := c.backend.Load(ck.id, keyParts[1])
if err != nil {
return nil, err
}
@ -366,7 +359,7 @@ func (c *inMemoryCacheManager) Save(k CacheKey, r Result) (ExportableCacheKey, e
return empty, err
}
if err := c.backend.AddResult(ck.CacheKeyInfo.ID, res); err != nil {
if err := c.backend.AddResult(ck.id, res); err != nil {
return empty, err
}
@ -417,11 +410,7 @@ func (c *inMemoryCacheManager) getInternalKey(k CacheKey, createIfNotExist bool)
}
internalV := k.GetValue(internalMemoryKey)
if internalV != nil {
cki, err := c.backend.Get(internalV.(string))
if err != nil {
return nil, errors.Wrapf(err, "failed lookup by internal ID %s", internalV.(string))
}
return c.toInMemoryCacheKey(cki, k.Deps()), nil
return c.toInMemoryCacheKey(internalV.(string), k.Digest(), k.Output(), k.Deps()), nil
}
matches := make(map[string]struct{})
@ -444,7 +433,7 @@ func (c *inMemoryCacheManager) getInternalKey(k CacheKey, createIfNotExist bool)
for _, ck := range cks {
internalCk := ck.CacheKey.CacheKey.(*inMemoryCacheKey)
m2 := make(map[string]struct{})
if err := c.backend.WalkLinks(internalCk.CacheKeyInfo.ID, CacheInfoLink{
if err := c.backend.WalkLinks(internalCk.id, CacheInfoLink{
Input: Index(i),
Output: Index(k.Output()),
Digest: k.Digest(),
@ -486,46 +475,25 @@ func (c *inMemoryCacheManager) getInternalKey(k CacheKey, createIfNotExist bool)
if len(k.Deps()) == 0 {
internalKey = digest.FromBytes([]byte(fmt.Sprintf("%s@%d", k.Digest(), k.Output()))).String()
}
cki, err := c.backend.Get(internalKey)
if err != nil {
if errors.Cause(err) == ErrNotFound {
if !createIfNotExist {
return nil, err
}
} else {
return nil, err
}
} else {
return c.toInMemoryCacheKey(cki, k.Deps()), nil
}
}
cki := CacheKeyInfo{
ID: internalKey,
Base: k.Digest(),
Output: int(k.Output()),
}
if err := c.backend.Set(cki); err != nil {
return nil, err
return c.toInMemoryCacheKey(internalKey, k.Digest(), k.Output(), k.Deps()), nil
}
for i, dep := range deps {
for _, ck := range dep {
internalCk := ck.CacheKey.CacheKey.(*inMemoryCacheKey)
err := c.backend.AddLink(internalCk.CacheKeyInfo.ID, CacheInfoLink{
err := c.backend.AddLink(internalCk.id, CacheInfoLink{
Input: Index(i),
Output: Index(cki.Output),
Digest: cki.Base,
Output: k.Output(),
Digest: k.Digest(),
Selector: ck.Selector,
}, cki.ID)
}, internalKey)
if err != nil {
return nil, err
}
}
}
return c.toInMemoryCacheKey(cki, k.Deps()), nil
return c.toInMemoryCacheKey(internalKey, k.Digest(), k.Output(), k.Deps()), nil
}
func newCombinedCacheManager(cms []CacheManager, main CacheManager) CacheManager {

View File

@ -12,9 +12,8 @@ var ErrNotFound = errors.Errorf("not found")
// CacheKeyStorage is interface for persisting cache metadata
type CacheKeyStorage interface {
Get(id string) (CacheKeyInfo, error)
Exists(id string) bool
Walk(fn func(id string) error) error
Set(info CacheKeyInfo) error
WalkResults(id string, fn func(CacheResult) error) error
Load(id string, resultID string) (CacheResult, error)
@ -25,20 +24,6 @@ type CacheKeyStorage interface {
WalkLinks(id string, link CacheInfoLink, fn func(id string) error) error
}
// CacheKeyInfo is storable metadata about single cache key
type CacheKeyInfo struct {
ID string
Base digest.Digest
Output int
// Deps []CacheKeyInfoWithSelector
}
// CacheKeyInfoWithSelector is CacheKeyInfo combined with a selector
type CacheKeyInfoWithSelector struct {
ID string
Selector digest.Digest
}
// CacheResult is a record for a single solve result
type CacheResult struct {
// Payload []byte

View File

@ -14,7 +14,6 @@ import (
func RunCacheStorageTests(t *testing.T, st func() (CacheKeyStorage, func())) {
for _, tc := range []func(*testing.T, CacheKeyStorage){
testGetSet,
testResults,
testLinks,
testResultReleaseSingleLevel,
@ -32,53 +31,21 @@ func runStorageTest(t *testing.T, fn func(t *testing.T, st CacheKeyStorage), st
}))
}
func testGetSet(t *testing.T, st CacheKeyStorage) {
t.Parallel()
cki := CacheKeyInfo{
ID: "foo",
Base: digest.FromBytes([]byte("foo")),
}
err := st.Set(cki)
require.NoError(t, err)
cki2, err := st.Get(cki.ID)
require.NoError(t, err)
require.Equal(t, cki, cki2)
_, err = st.Get("bar")
require.Error(t, err)
require.Equal(t, errors.Cause(err), ErrNotFound)
}
func testResults(t *testing.T, st CacheKeyStorage) {
t.Parallel()
cki := CacheKeyInfo{
ID: "foo",
Base: digest.FromBytes([]byte("foo")),
}
err := st.Set(cki)
require.NoError(t, err)
cki2 := CacheKeyInfo{
ID: "bar",
Base: digest.FromBytes([]byte("bar")),
}
err = st.Set(cki2)
require.NoError(t, err)
err = st.AddResult(cki.ID, CacheResult{
err := st.AddResult("foo", CacheResult{
ID: "foo0",
CreatedAt: time.Now(),
})
require.NoError(t, err)
err = st.AddResult(cki.ID, CacheResult{
err = st.AddResult("foo", CacheResult{
ID: "foo1",
CreatedAt: time.Now(),
})
require.NoError(t, err)
err = st.AddResult(cki2.ID, CacheResult{
err = st.AddResult("bar", CacheResult{
ID: "bar0",
CreatedAt: time.Now(),
})
@ -132,36 +99,18 @@ func testResults(t *testing.T, st CacheKeyStorage) {
func testLinks(t *testing.T, st CacheKeyStorage) {
t.Parallel()
cki := CacheKeyInfo{
ID: "foo",
Base: digest.FromBytes([]byte("foo")),
}
err := st.Set(cki)
require.NoError(t, err)
cki2 := CacheKeyInfo{
ID: "bar",
Base: digest.FromBytes([]byte("bar")),
}
err = st.Set(cki2)
require.NoError(t, err)
require.NoError(t, st.Set(CacheKeyInfo{ID: "target0"}))
require.NoError(t, st.Set(CacheKeyInfo{ID: "target0-second"}))
require.NoError(t, st.Set(CacheKeyInfo{ID: "target1"}))
require.NoError(t, st.Set(CacheKeyInfo{ID: "target1-second"}))
l0 := CacheInfoLink{
Input: 0, Output: 1, Digest: digest.FromBytes([]byte(">target0")),
}
err = st.AddLink(cki.ID, l0, "target0")
err := st.AddLink("foo", l0, "target0")
require.NoError(t, err)
err = st.AddLink(cki2.ID, l0, "target0-second")
err = st.AddLink("bar", l0, "target0-second")
require.NoError(t, err)
m := map[string]struct{}{}
err = st.WalkLinks(cki.ID, l0, func(id string) error {
err = st.WalkLinks("foo", l0, func(id string) error {
m[id] = struct{}{}
return nil
})
@ -175,18 +124,18 @@ func testLinks(t *testing.T, st CacheKeyStorage) {
Input: 0, Output: 1, Digest: digest.FromBytes([]byte(">target1")),
}
m = map[string]struct{}{}
err = st.WalkLinks(cki.ID, l1, func(id string) error {
err = st.WalkLinks("foo", l1, func(id string) error {
m[id] = struct{}{}
return nil
})
require.NoError(t, err)
require.Equal(t, len(m), 0)
err = st.AddLink(cki.ID, l1, "target1")
err = st.AddLink("foo", l1, "target1")
require.NoError(t, err)
m = map[string]struct{}{}
err = st.WalkLinks(cki.ID, l1, func(id string) error {
err = st.WalkLinks("foo", l1, func(id string) error {
m[id] = struct{}{}
return nil
})
@ -196,11 +145,11 @@ func testLinks(t *testing.T, st CacheKeyStorage) {
_, ok = m["target1"]
require.True(t, ok)
err = st.AddLink(cki.ID, l1, "target1-second")
err = st.AddLink("foo", l1, "target1-second")
require.NoError(t, err)
m = map[string]struct{}{}
err = st.WalkLinks(cki.ID, l1, func(id string) error {
err = st.WalkLinks("foo", l1, func(id string) error {
m[id] = struct{}{}
return nil
})
@ -214,20 +163,14 @@ func testLinks(t *testing.T, st CacheKeyStorage) {
func testResultReleaseSingleLevel(t *testing.T, st CacheKeyStorage) {
t.Parallel()
cki := CacheKeyInfo{
ID: "foo",
Base: digest.FromBytes([]byte("foo")),
}
err := st.Set(cki)
require.NoError(t, err)
err = st.AddResult(cki.ID, CacheResult{
err := st.AddResult("foo", CacheResult{
ID: "foo0",
CreatedAt: time.Now(),
})
require.NoError(t, err)
err = st.AddResult(cki.ID, CacheResult{
err = st.AddResult("foo", CacheResult{
ID: "foo1",
CreatedAt: time.Now(),
})
@ -256,35 +199,18 @@ func testResultReleaseSingleLevel(t *testing.T, st CacheKeyStorage) {
})
require.Equal(t, len(m), 0)
_, err = st.Get("foo")
require.Error(t, err)
require.Error(t, errors.Cause(err), ErrNotFound)
}
func testResultReleaseMultiLevel(t *testing.T, st CacheKeyStorage) {
t.Parallel()
cki := CacheKeyInfo{
ID: "foo",
Base: digest.FromBytes([]byte("foo")),
}
err := st.Set(cki)
require.NoError(t, err)
err = st.AddResult(cki.ID, CacheResult{
err := st.AddResult("foo", CacheResult{
ID: "foo-result",
CreatedAt: time.Now(),
})
require.NoError(t, err)
sub0 := CacheKeyInfo{
ID: "sub0",
Base: digest.FromBytes([]byte("sub0")),
}
err = st.Set(sub0)
require.NoError(t, err)
err = st.AddResult(sub0.ID, CacheResult{
err = st.AddResult("sub0", CacheResult{
ID: "sub0-result",
CreatedAt: time.Now(),
})
@ -293,23 +219,16 @@ func testResultReleaseMultiLevel(t *testing.T, st CacheKeyStorage) {
l0 := CacheInfoLink{
Input: 0, Output: 1, Digest: digest.FromBytes([]byte("to-sub0")),
}
err = st.AddLink(cki.ID, l0, "sub0")
err = st.AddLink("foo", l0, "sub0")
require.NoError(t, err)
sub1 := CacheKeyInfo{
ID: "sub1",
Base: digest.FromBytes([]byte("sub1")),
}
err = st.Set(sub1)
require.NoError(t, err)
err = st.AddResult(sub1.ID, CacheResult{
err = st.AddResult("sub1", CacheResult{
ID: "sub1-result",
CreatedAt: time.Now(),
})
require.NoError(t, err)
err = st.AddLink(cki.ID, l0, "sub1")
err = st.AddLink("foo", l0, "sub1")
require.NoError(t, err)
// delete one sub doesn't delete parent
@ -328,9 +247,7 @@ func testResultReleaseMultiLevel(t *testing.T, st CacheKeyStorage) {
_, ok := m["foo-result"]
require.True(t, ok)
_, err = st.Get("sub0")
require.Error(t, err)
require.Equal(t, errors.Cause(err), ErrNotFound)
require.False(t, st.Exists("sub0"))
m = map[string]struct{}{}
err = st.WalkLinks("foo", l0, func(id string) error {
@ -348,8 +265,7 @@ func testResultReleaseMultiLevel(t *testing.T, st CacheKeyStorage) {
err = st.Release("foo-result")
require.NoError(t, err)
_, err = st.Get("foo")
require.NoError(t, err)
require.True(t, st.Exists("foo"))
m = map[string]struct{}{}
err = st.WalkResults("foo", func(res CacheResult) error {
@ -373,13 +289,8 @@ func testResultReleaseMultiLevel(t *testing.T, st CacheKeyStorage) {
err = st.Release("sub1-result")
require.NoError(t, err)
_, err = st.Get("sub1")
require.Error(t, err)
require.Equal(t, errors.Cause(err), ErrNotFound)
_, err = st.Get("foo")
require.Error(t, err)
require.Equal(t, errors.Cause(err), ErrNotFound)
require.False(t, st.Exists("sub1"))
require.False(t, st.Exists("foo"))
}
func getFunctionName(i interface{}) string {

View File

@ -22,37 +22,28 @@ type inMemoryStore struct {
}
type inMemoryKey struct {
CacheKeyInfo
id string
results map[string]CacheResult
links map[CacheInfoLink]map[string]struct{}
backlinks map[string]struct{}
}
func (s *inMemoryStore) Get(id string) (CacheKeyInfo, error) {
func (s *inMemoryStore) Exists(id string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
k, ok := s.byID[id]
if !ok {
return CacheKeyInfo{}, errors.WithStack(ErrNotFound)
if k, ok := s.byID[id]; ok {
return len(k.links) > 0
}
return k.CacheKeyInfo, nil
return false
}
func (s *inMemoryStore) Set(info CacheKeyInfo) error {
s.mu.Lock()
defer s.mu.Unlock()
k, ok := s.byID[info.ID]
if !ok {
k = &inMemoryKey{
func newKey(id string) *inMemoryKey {
return &inMemoryKey{
results: map[string]CacheResult{},
links: map[CacheInfoLink]map[string]struct{}{},
backlinks: map[string]struct{}{},
id: id,
}
s.byID[info.ID] = k
}
k.CacheKeyInfo = info
return nil
}
func (s *inMemoryStore) Walk(fn func(string) error) error {
@ -112,7 +103,8 @@ func (s *inMemoryStore) AddResult(id string, res CacheResult) error {
defer s.mu.Unlock()
k, ok := s.byID[id]
if !ok {
return errors.Wrapf(ErrNotFound, "no such key %s", id)
k = newKey(id)
s.byID[id] = k
}
k.results[res.ID] = res
m, ok := s.byResult[res.ID]
@ -161,7 +153,7 @@ func (s *inMemoryStore) emptyBranchWithParents(k *inMemoryKey) {
continue
}
for l := range p.links {
delete(p.links[l], k.ID)
delete(p.links[l], k.id)
if len(p.links[l]) == 0 {
delete(p.links, l)
}
@ -169,7 +161,7 @@ func (s *inMemoryStore) emptyBranchWithParents(k *inMemoryKey) {
s.emptyBranchWithParents(p)
}
delete(s.byID, k.ID)
delete(s.byID, k.id)
}
func (s *inMemoryStore) AddLink(id string, link CacheInfoLink, target string) error {
@ -177,11 +169,13 @@ func (s *inMemoryStore) AddLink(id string, link CacheInfoLink, target string) er
defer s.mu.Unlock()
k, ok := s.byID[id]
if !ok {
return errors.Wrapf(ErrNotFound, "no such key %s", id)
k = newKey(id)
s.byID[id] = k
}
k2, ok := s.byID[target]
if !ok {
return errors.Wrapf(ErrNotFound, "no such key %s", target)
k2 = newKey(target)
s.byID[target] = k2
}
m, ok := k.links[link]
if !ok {
@ -199,7 +193,7 @@ func (s *inMemoryStore) WalkLinks(id string, link CacheInfoLink, fn func(id stri
k, ok := s.byID[id]
if !ok {
s.mu.RUnlock()
return errors.Wrapf(ErrNotFound, "no such key %s", id)
return nil
}
var links []string
for target := range k.links[link] {