Skip to content

Commit 18410c1

Browse files
author
abushwang
committed
Add optional TTL-based cleanup for httpcache to reduce disk and memory usage
Signed-off-by: abushwang <abushwang@tencent.com>
1 parent 0ef3e42 commit 18410c1

File tree

5 files changed

+348
-7
lines changed

5 files changed

+348
-7
lines changed

cache/cache.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"os"
2525
"path/filepath"
2626
"sync"
27+
"time"
2728

2829
"github.com/containerd/stargz-snapshotter/util/cacheutil"
2930
"github.com/containerd/stargz-snapshotter/util/namedmutex"
@@ -65,6 +66,10 @@ type DirectoryCacheConfig struct {
6566

6667
// FadvDontNeed forcefully clean fscache pagecache for saving memory.
6768
FadvDontNeed bool
69+
70+
// EntryTTL enables TTL-based cleanup for cached blob files.
71+
// When zero or negative, TTL-based cleanup is disabled.
72+
EntryTTL time.Duration
6873
}
6974

7075
// TODO: contents validation.
@@ -178,8 +183,12 @@ func NewDirectoryCache(directory string, config DirectoryCacheConfig) (BlobCache
178183
bufPool: bufPool,
179184
direct: config.Direct,
180185
fadvDontNeed: config.FadvDontNeed,
186+
entryTTL: config.EntryTTL,
181187
}
182188
dc.syncAdd = config.SyncAdd
189+
190+
dc.startCleanupIfNeeded()
191+
183192
return dc, nil
184193
}
185194

@@ -197,8 +206,11 @@ type directoryCache struct {
197206
direct bool
198207
fadvDontNeed bool
199208

200-
closed bool
201-
closedMu sync.Mutex
209+
closed bool
210+
closedMu sync.Mutex
211+
entryTTL time.Duration
212+
cleanupStopCh chan struct{}
213+
cleanupWg sync.WaitGroup
202214
}
203215

204216
func (dc *directoryCache) Get(key string, opts ...Option) (Reader, error) {
@@ -384,11 +396,19 @@ func (dc *directoryCache) putBuffer(b *bytes.Buffer) {
384396

385397
func (dc *directoryCache) Close() error {
386398
dc.closedMu.Lock()
387-
defer dc.closedMu.Unlock()
388399
if dc.closed {
400+
dc.closedMu.Unlock()
389401
return nil
390402
}
391403
dc.closed = true
404+
stopCh := dc.cleanupStopCh
405+
dc.cleanupStopCh = nil
406+
dc.closedMu.Unlock()
407+
408+
if stopCh != nil {
409+
close(stopCh)
410+
dc.cleanupWg.Wait()
411+
}
392412
return os.RemoveAll(dc.directory)
393413
}
394414

cache/httpcache_ttl.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"io"
23+
"io/fs"
24+
"os"
25+
"path/filepath"
26+
"sync"
27+
"time"
28+
29+
"github.com/containerd/log"
30+
"github.com/containerd/stargz-snapshotter/util/cacheutil"
31+
)
32+
33+
func (dc *directoryCache) startCleanupIfNeeded() {
34+
if dc.entryTTL <= 0 {
35+
return
36+
}
37+
dc.closedMu.Lock()
38+
if dc.closed || dc.cleanupStopCh != nil {
39+
dc.closedMu.Unlock()
40+
return
41+
}
42+
stopCh := make(chan struct{})
43+
dc.cleanupStopCh = stopCh
44+
dc.cleanupWg.Add(1)
45+
dc.closedMu.Unlock()
46+
47+
interval := dc.entryTTL
48+
go func() {
49+
defer dc.cleanupWg.Done()
50+
51+
ticker := time.NewTicker(interval)
52+
defer ticker.Stop()
53+
54+
dc.cleanupOnce()
55+
for {
56+
select {
57+
case <-ticker.C:
58+
dc.cleanupOnce()
59+
case <-stopCh:
60+
return
61+
}
62+
}
63+
}()
64+
}
65+
66+
func (dc *directoryCache) cleanupOnce() {
67+
if dc.entryTTL <= 0 {
68+
return
69+
}
70+
if dc.isClosed() {
71+
return
72+
}
73+
74+
cutoff := time.Now().Add(-dc.entryTTL)
75+
wipBase := dc.wipDirectory
76+
77+
_ = filepath.WalkDir(dc.directory, func(path string, d fs.DirEntry, walkErr error) error {
78+
if walkErr != nil {
79+
return nil
80+
}
81+
if dc.isClosed() {
82+
return fs.SkipAll
83+
}
84+
if d.IsDir() {
85+
if path == wipBase {
86+
return fs.SkipDir
87+
}
88+
return nil
89+
}
90+
91+
info, err := d.Info()
92+
if err != nil {
93+
return nil
94+
}
95+
if info.ModTime().After(cutoff) {
96+
return nil
97+
}
98+
99+
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
100+
log.L.WithError(err).Debugf("failed to remove expired cache entry %q", path)
101+
}
102+
return nil
103+
})
104+
105+
_ = filepath.WalkDir(wipBase, func(path string, d fs.DirEntry, walkErr error) error {
106+
if walkErr != nil {
107+
return nil
108+
}
109+
if dc.isClosed() {
110+
return fs.SkipAll
111+
}
112+
if d.IsDir() {
113+
return nil
114+
}
115+
116+
info, err := d.Info()
117+
if err != nil {
118+
return nil
119+
}
120+
if info.ModTime().After(cutoff) {
121+
return nil
122+
}
123+
_ = os.Remove(path)
124+
return nil
125+
})
126+
}
127+
128+
func NewMemoryCacheWithTTL(ttl time.Duration) BlobCache {
129+
if ttl <= 0 {
130+
return NewMemoryCache()
131+
}
132+
bufPool := &sync.Pool{
133+
New: func() interface{} {
134+
return new(bytes.Buffer)
135+
},
136+
}
137+
c := cacheutil.NewTTLCache(ttl)
138+
c.OnEvicted = func(key string, value interface{}) {
139+
b := value.(*bytes.Buffer)
140+
b.Reset()
141+
bufPool.Put(b)
142+
}
143+
return &ttlMemoryCache{
144+
c: c,
145+
bufPool: bufPool,
146+
}
147+
}
148+
149+
type ttlMemoryCache struct {
150+
c *cacheutil.TTLCache
151+
bufPool *sync.Pool
152+
}
153+
154+
func (mc *ttlMemoryCache) Get(key string, opts ...Option) (Reader, error) {
155+
v, done, ok := mc.c.Get(key)
156+
if !ok {
157+
return nil, fmt.Errorf("missed cache: %q", key)
158+
}
159+
b := v.(*bytes.Buffer)
160+
return &reader{
161+
ReaderAt: bytes.NewReader(b.Bytes()),
162+
closeFunc: func() error {
163+
done(false)
164+
return nil
165+
},
166+
}, nil
167+
}
168+
169+
func (mc *ttlMemoryCache) Add(key string, opts ...Option) (Writer, error) {
170+
b := mc.bufPool.Get().(*bytes.Buffer)
171+
b.Reset()
172+
return &writer{
173+
WriteCloser: nopWriteCloser(io.Writer(b)),
174+
commitFunc: func() error {
175+
mc.c.Remove(key)
176+
_, done, _ := mc.c.Add(key, b)
177+
done(false)
178+
return nil
179+
},
180+
abortFunc: func() error {
181+
b.Reset()
182+
mc.bufPool.Put(b)
183+
return nil
184+
},
185+
}, nil
186+
}
187+
188+
func (mc *ttlMemoryCache) Close() error {
189+
return nil
190+
}

cache/httpcache_ttl_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
"time"
24+
)
25+
26+
func TestNewMemoryCacheWithTTL_Disabled(t *testing.T) {
27+
c := NewMemoryCacheWithTTL(0)
28+
if _, ok := c.(*MemoryCache); !ok {
29+
t.Fatalf("expected *MemoryCache when ttl is disabled; got %T", c)
30+
}
31+
}
32+
33+
func TestNewMemoryCacheWithTTL_Expires(t *testing.T) {
34+
ttl := 30 * time.Millisecond
35+
c := NewMemoryCacheWithTTL(ttl)
36+
37+
w, err := c.Add("k1")
38+
if err != nil {
39+
t.Fatalf("Add failed: %v", err)
40+
}
41+
if _, err := w.Write([]byte("abc")); err != nil {
42+
t.Fatalf("Write failed: %v", err)
43+
}
44+
if err := w.Commit(); err != nil {
45+
t.Fatalf("Commit failed: %v", err)
46+
}
47+
_ = w.Close()
48+
49+
r, err := c.Get("k1")
50+
if err != nil {
51+
t.Fatalf("Get failed: %v", err)
52+
}
53+
_ = r.Close()
54+
55+
deadline := time.Now().Add(2 * time.Second)
56+
for {
57+
if time.Now().After(deadline) {
58+
t.Fatalf("entry did not expire within deadline")
59+
}
60+
time.Sleep(10 * time.Millisecond)
61+
if _, err := c.Get("k1"); err == nil {
62+
continue
63+
}
64+
break
65+
}
66+
}
67+
68+
func TestDirectoryCacheCleanupOnce_RemovesExpiredFiles(t *testing.T) {
69+
base := t.TempDir()
70+
wip := filepath.Join(base, "wip")
71+
if err := os.MkdirAll(wip, 0700); err != nil {
72+
t.Fatalf("mkdir wip failed: %v", err)
73+
}
74+
75+
dc := &directoryCache{
76+
directory: base,
77+
wipDirectory: wip,
78+
entryTTL: 100 * time.Millisecond,
79+
}
80+
81+
expired := filepath.Join(base, "aa", "expired")
82+
if err := os.MkdirAll(filepath.Dir(expired), 0700); err != nil {
83+
t.Fatalf("mkdir expired dir failed: %v", err)
84+
}
85+
if err := os.WriteFile(expired, []byte("x"), 0600); err != nil {
86+
t.Fatalf("write expired failed: %v", err)
87+
}
88+
old := time.Now().Add(-2 * time.Second)
89+
if err := os.Chtimes(expired, old, old); err != nil {
90+
t.Fatalf("chtimes expired failed: %v", err)
91+
}
92+
93+
fresh := filepath.Join(base, "bb", "fresh")
94+
if err := os.MkdirAll(filepath.Dir(fresh), 0700); err != nil {
95+
t.Fatalf("mkdir fresh dir failed: %v", err)
96+
}
97+
if err := os.WriteFile(fresh, []byte("y"), 0600); err != nil {
98+
t.Fatalf("write fresh failed: %v", err)
99+
}
100+
101+
expiredWip := filepath.Join(wip, "tmp-expired")
102+
if err := os.WriteFile(expiredWip, []byte("z"), 0600); err != nil {
103+
t.Fatalf("write expired wip failed: %v", err)
104+
}
105+
if err := os.Chtimes(expiredWip, old, old); err != nil {
106+
t.Fatalf("chtimes expired wip failed: %v", err)
107+
}
108+
109+
dc.cleanupOnce()
110+
111+
if _, err := os.Stat(expired); err == nil {
112+
t.Fatalf("expected expired file to be removed")
113+
}
114+
if _, err := os.Stat(expiredWip); err == nil {
115+
t.Fatalf("expected expired wip file to be removed")
116+
}
117+
if _, err := os.Stat(fresh); err != nil {
118+
t.Fatalf("expected fresh file to remain; err=%v", err)
119+
}
120+
}

fs/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ type Config struct {
3939
// Other values default to cache them on disk.
4040
HTTPCacheType string `toml:"http_cache_type" json:"http_cache_type"`
4141

42+
// HTTPCacheChunkTTLSec specifies TTL (in sec) for each http cache chunk.
43+
// Default is 0, which disables TTL-based cleanup.
44+
// When zero or negative, TTL-based cleanup is disabled.
45+
HTTPCacheChunkTTLSec int `toml:"http_cache_chunk_ttl_sec" json:"http_cache_chunk_ttl_sec"`
46+
4247
// Type of cache for uncompressed files contents. "memory" stores them on memory. Other values
4348
// default to cache them on disk.
4449
FSCacheType string `toml:"filesystem_cache_type" json:"filesystem_cache_type"`

0 commit comments

Comments
 (0)