Skip to content

Commit abecb77

Browse files
committed
optimize the hardlink persistence
1. Added batched persistence with a 5-second interval 2. Made persistence asynchronous using a background worker 3. Removed pretty-printing of JSON to reduce file size 4. Added proper cleanup on manager close 5. Reduced logging verbosity 6. Added dirty flag to track changes needing persistence Signed-off-by: ChengyuZhu6 <hudson@cyzhu.com>
1 parent 370814f commit abecb77

1 file changed

Lines changed: 89 additions & 34 deletions

File tree

cache/hardlink.go

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package cache
1818

1919
import (
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
196209
func (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
275298
func (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
317340
func (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

Comments
 (0)