1717package cache
1818
1919import (
20+ "context"
2021 "encoding/json"
2122 "fmt"
2223 "os"
@@ -52,6 +53,13 @@ type HardlinkManager struct {
5253 mu sync.RWMutex
5354 links map [string ]* linkInfo
5455 cleanupInterval time.Duration
56+ // For batched persistence
57+ dirty bool
58+ lastPersist time.Time
59+ persistTicker * time.Ticker
60+ persistDone chan struct {}
61+ cleanupDone chan struct {} // Channel to signal cleanup goroutine to stop
62+ cleanupTicker * time.Ticker // Ticker for cleanup
5563}
5664
5765// NewHardlinkManager creates a new hardlink manager
@@ -67,15 +75,20 @@ func NewHardlinkManager(root string) (*HardlinkManager, error) {
6775 hlDir : hlDir ,
6876 links : make (map [string ]* linkInfo ),
6977 cleanupInterval : 24 * time .Hour ,
78+ persistTicker : time .NewTicker (5 * time .Second ), // Batch writes every 5 seconds
79+ persistDone : make (chan struct {}),
80+ cleanupDone : make (chan struct {}),
81+ cleanupTicker : time .NewTicker (24 * time .Hour ),
7082 }
7183
7284 // Restore persisted hardlink information
7385 if err := hm .restore (); err != nil {
7486 return nil , err
7587 }
7688
77- // Start periodic cleanup
89+ // Start periodic cleanup and persistence
7890 go hm .periodicCleanup ()
91+ go hm .persistWorker ()
7992
8093 return hm , nil
8194}
@@ -168,9 +181,9 @@ func (hm *HardlinkManager) CreateLink(key string, sourcePath string) error {
168181 CreatedAt : time .Now (),
169182 LastUsed : time .Now (),
170183 }
171-
172- // Persist link info
173- return hm . persist ()
184+ // Mark as dirty for async persistence
185+ hm . dirty = true
186+ return nil
174187}
175188
176189// GetLink gets the hardlink path if it exists
@@ -194,8 +207,21 @@ func (hm *HardlinkManager) GetLink(key string) (string, bool) {
194207
195208// cleanup cleans up expired hardlinks
196209func (hm * HardlinkManager ) cleanup () error {
197- hm .mu .Lock ()
198- defer hm .mu .Unlock ()
210+ // Use a timeout context
211+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
212+ defer cancel ()
213+ // Try to acquire lock with timeout
214+ lockChan := make (chan struct {})
215+ go func () {
216+ hm .mu .Lock ()
217+ close (lockChan )
218+ }()
219+ select {
220+ case <- lockChan :
221+ defer hm .mu .Unlock ()
222+ case <- ctx .Done ():
223+ return fmt .Errorf ("timeout waiting for lock" )
224+ }
199225
200226 now := time .Now ()
201227 expiredKeys := make ([]string , 0 )
@@ -218,47 +244,37 @@ func (hm *HardlinkManager) cleanup() error {
218244 }
219245
220246 if len (expiredKeys ) > 0 {
221- return hm .persist ()
247+ return hm .persistLocked () // Use persistLocked since we already have the lock
222248 }
223249 return nil
224250}
225251
226- // persist persists link information to root directory
227- func (hm * HardlinkManager ) persist () error {
252+ // persistLocked persists link information while holding the lock
253+ func (hm * HardlinkManager ) persistLocked () error {
228254 if len (hm .links ) == 0 {
229255 log .L .Debugf ("No links to persist" )
230256 return nil
231257 }
232258
233259 linksFile := filepath .Join (hm .root , linksFileName )
234-
235- // Create temporary file for atomic write
236260 tmpFile := linksFile + ".tmp"
261+
237262 f , err := os .OpenFile (tmpFile , os .O_WRONLY | os .O_CREATE | os .O_TRUNC , 0600 )
238263 if err != nil {
239264 return fmt .Errorf ("failed to create temporary links file: %w" , err )
240265 }
241266
242- // Use a closure to handle file close and cleanup
243- if err := func () error {
244- defer f .Close ()
245-
246- // Pretty print JSON for better readability
247- encoder := json .NewEncoder (f )
248- encoder .SetIndent ("" , " " )
249- if err := encoder .Encode (hm .links ); err != nil {
250- return fmt .Errorf ("failed to encode links data: %w" , err )
251- }
267+ defer f .Close ()
252268
253- // Ensure data is written to disk
254- if err := f .Sync (); err != nil {
255- return fmt .Errorf ("failed to sync links file: %w" , err )
256- }
269+ // Use more compact JSON encoding
270+ if err := json .NewEncoder (f ).Encode (hm .links ); err != nil {
271+ os .Remove (tmpFile )
272+ return fmt .Errorf ("failed to encode links data: %w" , err )
273+ }
257274
258- return nil
259- }(); err != nil {
275+ if err := f .Sync (); err != nil {
260276 os .Remove (tmpFile )
261- return err
277+ return fmt . Errorf ( "failed to sync links file: %w" , err )
262278 }
263279
264280 // Atomic rename
@@ -267,10 +283,17 @@ func (hm *HardlinkManager) persist() error {
267283 return fmt .Errorf ("failed to rename links file: %w" , err )
268284 }
269285
270- log .L .Debugf ("Successfully persisted %d links to %s " , len (hm .links ), linksFile )
286+ log .L .Debugf ("Persisted %d links" , len (hm .links ))
271287 return nil
272288}
273289
290+ // persist acquires lock and persists link information
291+ func (hm * HardlinkManager ) persist () error {
292+ hm .mu .Lock ()
293+ defer hm .mu .Unlock ()
294+ return hm .persistLocked ()
295+ }
296+
274297// restore restores link information from root directory
275298func (hm * HardlinkManager ) restore () error {
276299 linksFile := filepath .Join (hm .root , linksFileName )
@@ -315,12 +338,44 @@ func (hm *HardlinkManager) restore() error {
315338
316339// periodicCleanup performs periodic cleanup
317340func (hm * HardlinkManager ) periodicCleanup () {
318- ticker := time .NewTicker (hm .cleanupInterval )
319- defer ticker .Stop ()
341+ for {
342+ select {
343+ case <- hm .cleanupTicker .C :
344+ if err := hm .cleanup (); err != nil {
345+ log .L .Warnf ("Failed to cleanup hardlinks: %v" , err )
346+ }
347+ case <- hm .cleanupDone :
348+ return
349+ }
350+ }
351+ }
320352
321- for range ticker .C {
322- if err := hm .cleanup (); err != nil {
323- log .L .Warnf ("Failed to cleanup hardlinks: %v" , err )
353+ // persistWorker handles periodic persistence of link information
354+ func (hm * HardlinkManager ) persistWorker () {
355+ for {
356+ select {
357+ case <- hm .persistTicker .C :
358+ hm .mu .Lock ()
359+ if hm .dirty && time .Since (hm .lastPersist ) > 5 * time .Second {
360+ if err := hm .persistLocked (); err != nil {
361+ log .L .Warnf ("Failed to persist hardlink info: %v" , err )
362+ }
363+ hm .dirty = false
364+ hm .lastPersist = time .Now ()
365+ }
366+ hm .mu .Unlock ()
367+ case <- hm .persistDone :
368+ return
324369 }
325370 }
326371}
372+
373+ func (hm * HardlinkManager ) Close () error {
374+ // Stop all background goroutines
375+ hm .persistTicker .Stop ()
376+ hm .cleanupTicker .Stop ()
377+ close (hm .persistDone )
378+ close (hm .cleanupDone )
379+ // Final persist of any remaining changes
380+ return hm .persist ()
381+ }
0 commit comments