@@ -18,13 +18,13 @@ package cache
1818
1919import (
2020 "bytes"
21- "errors"
2221 "fmt"
2322 "io"
2423 "os"
2524 "path/filepath"
2625 "sync"
2726
27+ "github.com/containerd/log"
2828 "github.com/containerd/stargz-snapshotter/util/cacheutil"
2929 "github.com/containerd/stargz-snapshotter/util/namedmutex"
3030)
@@ -61,6 +61,9 @@ type DirectoryCacheConfig struct {
6161 // Direct forcefully enables direct mode for all operation in cache.
6262 // Thus operation won't use on-memory caches.
6363 Direct bool
64+
65+ // EnableHardlink enables hardlinking of cache files to reduce memory usage
66+ EnableHardlink bool
6467}
6568
6669// TODO: contents validation.
@@ -99,6 +102,7 @@ type Writer interface {
99102type cacheOpt struct {
100103 direct bool
101104 passThrough bool
105+ chunkDigest string
102106}
103107
104108type Option func (o * cacheOpt ) * cacheOpt
@@ -123,6 +127,14 @@ func PassThrough() Option {
123127 }
124128}
125129
130+ // ChunkDigest option allows specifying a chunk digest for the cache
131+ func ChunkDigest (digest string ) Option {
132+ return func (o * cacheOpt ) * cacheOpt {
133+ o .chunkDigest = digest
134+ return o
135+ }
136+ }
137+
126138func NewDirectoryCache (directory string , config DirectoryCacheConfig ) (BlobCache , error ) {
127139 if ! filepath .IsAbs (directory ) {
128140 return nil , fmt .Errorf ("dir cache path must be an absolute path; got %q" , directory )
@@ -166,15 +178,24 @@ func NewDirectoryCache(directory string, config DirectoryCacheConfig) (BlobCache
166178 return nil , err
167179 }
168180 dc := & directoryCache {
169- cache : dataCache ,
170- fileCache : fdCache ,
171- wipLock : new (namedmutex.NamedMutex ),
172- directory : directory ,
173- wipDirectory : wipdir ,
174- bufPool : bufPool ,
175- direct : config .Direct ,
181+ cache : dataCache ,
182+ fileCache : fdCache ,
183+ wipLock : new (namedmutex.NamedMutex ),
184+ directory : directory ,
185+ wipDirectory : wipdir ,
186+ bufPool : bufPool ,
187+ direct : config .Direct ,
188+ enableHardlink : config .EnableHardlink ,
189+ syncAdd : config .SyncAdd ,
190+ }
191+
192+ // Initialize hardlink manager if enabled
193+ if config .EnableHardlink {
194+ hlManager , enabled := InitializeHardlinkManager (filepath .Dir (filepath .Dir (directory )), config .EnableHardlink )
195+ dc .hlManager = hlManager
196+ dc .enableHardlink = enabled
176197 }
177- dc . syncAdd = config . SyncAdd
198+
178199 return dc , nil
179200}
180201
@@ -193,6 +214,9 @@ type directoryCache struct {
193214
194215 closed bool
195216 closedMu sync.Mutex
217+
218+ enableHardlink bool
219+ hlManager * HardlinkManager
196220}
197221
198222func (dc * directoryCache ) Get (key string , opts ... Option ) (Reader , error ) {
@@ -205,9 +229,15 @@ func (dc *directoryCache) Get(key string, opts ...Option) (Reader, error) {
205229 opt = o (opt )
206230 }
207231
232+ // Try to get from memory cache
208233 if ! dc .direct && ! opt .direct {
209- // Get data from memory
210- if b , done , ok := dc .cache .Get (key ); ok {
234+ // Try memory cache for digest or key
235+ cacheKey := key
236+ if dc .hlManager != nil && dc .hlManager .IsEnabled () && opt .chunkDigest != "" {
237+ cacheKey = opt .chunkDigest
238+ }
239+
240+ if b , done , ok := dc .cache .Get (cacheKey ); ok {
211241 return & reader {
212242 ReaderAt : bytes .NewReader (b .(* bytes.Buffer ).Bytes ()),
213243 closeFunc : func () error {
@@ -217,8 +247,8 @@ func (dc *directoryCache) Get(key string, opts ...Option) (Reader, error) {
217247 }, nil
218248 }
219249
220- // Get data from disk. If the file is already opened, use it.
221- if f , done , ok := dc .fileCache .Get (key ); ok {
250+ // Get data from file cache for digest or key
251+ if f , done , ok := dc .fileCache .Get (cacheKey ); ok {
222252 return & reader {
223253 ReaderAt : f .(* os.File ),
224254 closeFunc : func () error {
@@ -229,10 +259,21 @@ func (dc *directoryCache) Get(key string, opts ...Option) (Reader, error) {
229259 }
230260 }
231261
262+ // First try regular file path
263+ filepath := BuildCachePath (dc .directory , key )
264+
265+ // Check hardlink manager for existing digest file
266+ if dc .hlManager != nil && opt .chunkDigest != "" {
267+ if digestPath , exists := dc .hlManager .ProcessCacheGet (key , opt .chunkDigest , opt .direct ); exists {
268+ log .L .Debugf ("Using existing file for digest %q instead of key %q" , opt .chunkDigest , key )
269+ filepath = digestPath
270+ }
271+ }
272+
232273 // Open the cache file and read the target region
233274 // TODO: If the target cache is write-in-progress, should we wait for the completion
234275 // or simply report the cache miss?
235- file , err := os .Open (dc . cachePath ( key ) )
276+ file , err := os .Open (filepath )
236277 if err != nil {
237278 return nil , fmt .Errorf ("failed to open blob file for %q: %w" , key , err )
238279 }
@@ -261,7 +302,12 @@ func (dc *directoryCache) Get(key string, opts ...Option) (Reader, error) {
261302 return & reader {
262303 ReaderAt : file ,
263304 closeFunc : func () error {
264- _ , done , added := dc .fileCache .Add (key , file )
305+ cacheKey := key
306+ if dc .hlManager != nil && dc .hlManager .IsEnabled () && opt .chunkDigest != "" {
307+ cacheKey = opt .chunkDigest
308+ }
309+
310+ _ , done , added := dc .fileCache .Add (cacheKey , file )
265311 defer done () // Release it immediately. Cleaned up on eviction.
266312 if ! added {
267313 return file .Close () // file already exists in the cache. close it.
@@ -281,81 +327,76 @@ func (dc *directoryCache) Add(key string, opts ...Option) (Writer, error) {
281327 opt = o (opt )
282328 }
283329
284- wip , err := dc .wipFile (key )
330+ // If hardlink manager exists and digest is provided, check if a hardlink can be created
331+ if dc .hlManager != nil && opt .chunkDigest != "" {
332+ keyPath := BuildCachePath (dc .directory , key )
333+
334+ // Try to create a hardlink from existing digest file
335+ if dc .hlManager .ProcessCacheAdd (key , opt .chunkDigest , keyPath ) {
336+ // Return a no-op writer since the file already exists
337+ return & writer {
338+ WriteCloser : nopWriteCloser (io .Discard ),
339+ commitFunc : func () error { return nil },
340+ abortFunc : func () error { return nil },
341+ }, nil
342+ }
343+ }
344+
345+ // Create temporary file
346+ w , err := WipFile (dc .wipDirectory , key )
285347 if err != nil {
286348 return nil , err
287349 }
288- w := & writer {
289- WriteCloser : wip ,
350+
351+ // Create writer
352+ writer := & writer {
353+ WriteCloser : w ,
290354 commitFunc : func () error {
291355 if dc .isClosed () {
292356 return fmt .Errorf ("cache is already closed" )
293357 }
294- // Commit the cache contents
295- c := dc .cachePath (key )
296- if err := os .MkdirAll (filepath .Dir (c ), os .ModePerm ); err != nil {
297- var errs []error
298- if err := os .Remove (wip .Name ()); err != nil {
299- errs = append (errs , err )
358+
359+ // Commit file
360+ targetPath := BuildCachePath (dc .directory , key )
361+ if err := os .MkdirAll (filepath .Dir (targetPath ), 0700 ); err != nil {
362+ return fmt .Errorf ("failed to create cache directory: %w" , err )
363+ }
364+
365+ if err := os .Rename (w .Name (), targetPath ); err != nil {
366+ return fmt .Errorf ("failed to commit cache file: %w" , err )
367+ }
368+
369+ // If hardlink manager exists and digest is provided, register the file
370+ if dc .hlManager != nil && dc .hlManager .IsEnabled () && opt .chunkDigest != "" {
371+ // Register this file as the primary source for this digest
372+ if err := dc .hlManager .RegisterDigestFile (opt .chunkDigest , targetPath ); err != nil {
373+ log .L .Debugf ("Failed to register digest file: %v" , err )
374+ }
375+
376+ // Map key to digest
377+ internalKey := dc .hlManager .GenerateInternalKey (dc .directory , key )
378+ if err := dc .hlManager .MapKeyToDigest (internalKey , opt .chunkDigest ); err != nil {
379+ log .L .Debugf ("Failed to map key to digest: %v" , err )
300380 }
301- errs = append (errs , fmt .Errorf ("failed to create cache directory %q: %w" , c , err ))
302- return errors .Join (errs ... )
303381 }
304- return os .Rename (wip .Name (), c )
382+
383+ return nil
305384 },
306385 abortFunc : func () error {
307- return os .Remove (wip .Name ())
386+ return os .Remove (w .Name ())
308387 },
309388 }
310389
311390 // If "direct" option is specified, do not cache the passed data on memory.
312391 // This option is useful for preventing memory cache from being polluted by data
313392 // that won't be accessed immediately.
314393 if dc .direct || opt .direct {
315- return w , nil
394+ return writer , nil
316395 }
317396
397+ // Create memory cache
318398 b := dc .bufPool .Get ().(* bytes.Buffer )
319- memW := & writer {
320- WriteCloser : nopWriteCloser (io .Writer (b )),
321- commitFunc : func () error {
322- if dc .isClosed () {
323- w .Close ()
324- return fmt .Errorf ("cache is already closed" )
325- }
326- cached , done , added := dc .cache .Add (key , b )
327- if ! added {
328- dc .putBuffer (b ) // already exists in the cache. abort it.
329- }
330- commit := func () error {
331- defer done ()
332- defer w .Close ()
333- n , err := w .Write (cached .(* bytes.Buffer ).Bytes ())
334- if err != nil || n != cached .(* bytes.Buffer ).Len () {
335- w .Abort ()
336- return err
337- }
338- return w .Commit ()
339- }
340- if dc .syncAdd {
341- return commit ()
342- }
343- go func () {
344- if err := commit (); err != nil {
345- fmt .Println ("failed to commit to file:" , err )
346- }
347- }()
348- return nil
349- },
350- abortFunc : func () error {
351- defer w .Close ()
352- defer w .Abort ()
353- dc .putBuffer (b ) // abort it.
354- return nil
355- },
356- }
357-
358- return memW , nil
399+ return dc .wrapMemoryWriter (b , writer , key )
359400}
360401
361402func (dc * directoryCache ) putBuffer (b * bytes.Buffer ) {
@@ -380,14 +421,6 @@ func (dc *directoryCache) isClosed() bool {
380421 return closed
381422}
382423
383- func (dc * directoryCache ) cachePath (key string ) string {
384- return filepath .Join (dc .directory , key [:2 ], key )
385- }
386-
387- func (dc * directoryCache ) wipFile (key string ) (* os.File , error ) {
388- return os .CreateTemp (dc .wipDirectory , key + "-*" )
389- }
390-
391424func NewMemoryCache () BlobCache {
392425 return & MemoryCache {
393426 Membuf : map [string ]* bytes.Buffer {},
@@ -463,3 +496,50 @@ func (w *writeCloser) Close() error { return w.closeFunc() }
463496func nopWriteCloser (w io.Writer ) io.WriteCloser {
464497 return & writeCloser {w , func () error { return nil }}
465498}
499+
500+ // wrapMemoryWriter wraps a writer with memory caching
501+ func (dc * directoryCache ) wrapMemoryWriter (b * bytes.Buffer , w * writer , key string ) (Writer , error ) {
502+ return & writer {
503+ WriteCloser : nopWriteCloser (b ),
504+ commitFunc : func () error {
505+ if dc .isClosed () {
506+ w .Close ()
507+ return fmt .Errorf ("cache is already closed" )
508+ }
509+
510+ cached , done , added := dc .cache .Add (key , b )
511+ if ! added {
512+ dc .putBuffer (b )
513+ }
514+
515+ commit := func () error {
516+ defer done ()
517+ defer w .Close ()
518+
519+ n , err := w .Write (cached .(* bytes.Buffer ).Bytes ())
520+ if err != nil || n != cached .(* bytes.Buffer ).Len () {
521+ w .Abort ()
522+ return err
523+ }
524+ return w .Commit ()
525+ }
526+
527+ if dc .syncAdd {
528+ return commit ()
529+ }
530+
531+ go func () {
532+ if err := commit (); err != nil {
533+ log .L .Infof ("failed to commit to file: %v" , err )
534+ }
535+ }()
536+ return nil
537+ },
538+ abortFunc : func () error {
539+ defer w .Close ()
540+ defer w .Abort ()
541+ dc .putBuffer (b )
542+ return nil
543+ },
544+ }, nil
545+ }
0 commit comments