11package bdcache
22
33import (
4+ "bufio"
45 "context"
56 "encoding/gob"
67 "errors"
@@ -10,14 +11,32 @@ import (
1011 "path/filepath"
1112 "sort"
1213 "strings"
14+ "sync"
1315 "time"
1416)
1517
1618const maxKeyLength = 127 // Maximum key length to avoid filesystem constraints
1719
20+ var (
21+ // Pool for bufio.Writer to reduce allocations
22+ writerPool = sync.Pool {
23+ New : func () any {
24+ return bufio .NewWriterSize (nil , 4096 )
25+ },
26+ }
27+ // Pool for bufio.Reader to reduce allocations
28+ readerPool = sync.Pool {
29+ New : func () any {
30+ return bufio .NewReaderSize (nil , 4096 )
31+ },
32+ }
33+ )
34+
1835// filePersist implements PersistenceLayer using local files with gob encoding.
1936type filePersist [K comparable , V any ] struct {
20- dir string
37+ dir string
38+ subdirsMu sync.RWMutex
39+ subdirsMade map [string ]bool // Cache of created subdirectories
2140}
2241
2342// ValidateKey checks if a key is valid for file persistence.
@@ -69,7 +88,10 @@ func newFilePersist[K comparable, V any](cacheID string) (*filePersist[K, V], er
6988
7089 slog .Debug ("initialized file persistence" , "dir" , dir )
7190
72- return & filePersist [K , V ]{dir : dir }, nil
91+ return & filePersist [K , V ]{
92+ dir : dir ,
93+ subdirsMade : make (map [string ]bool ),
94+ }, nil
7395}
7496
7597// keyToFilename converts a cache key to a filename with squid-style directory layout.
@@ -105,8 +127,13 @@ func (f *filePersist[K, V]) Load(ctx context.Context, key K) (V, time.Time, bool
105127 }
106128 }()
107129
130+ // Get reader from pool and reset it for this file
131+ reader := readerPool .Get ().(* bufio.Reader )
132+ reader .Reset (file )
133+ defer readerPool .Put (reader )
134+
108135 var entry Entry [K , V ]
109- dec := gob .NewDecoder (file )
136+ dec := gob .NewDecoder (reader )
110137 if err := dec .Decode (& entry ); err != nil {
111138 // File corrupted, remove it
112139 if err := os .Remove (filename ); err != nil && ! os .IsNotExist (err ) {
@@ -129,10 +156,22 @@ func (f *filePersist[K, V]) Load(ctx context.Context, key K) (V, time.Time, bool
129156// Store saves a value to a file.
130157func (f * filePersist [K , V ]) Store (ctx context.Context , key K , value V , expiry time.Time ) error {
131158 filename := filepath .Join (f .dir , f .keyToFilename (key ))
159+ subdir := filepath .Dir (filename )
160+
161+ // Check if subdirectory already created (cache to avoid syscalls)
162+ f .subdirsMu .RLock ()
163+ exists := f .subdirsMade [subdir ]
164+ f .subdirsMu .RUnlock ()
132165
133- // Create subdirectory if it doesn't exist (for squid-style layout)
134- if err := os .MkdirAll (filepath .Dir (filename ), 0o750 ); err != nil {
135- return fmt .Errorf ("create subdirectory: %w" , err )
166+ if ! exists {
167+ // Create subdirectory if needed
168+ if err := os .MkdirAll (subdir , 0o750 ); err != nil {
169+ return fmt .Errorf ("create subdirectory: %w" , err )
170+ }
171+ // Cache that we created it
172+ f .subdirsMu .Lock ()
173+ f .subdirsMade [subdir ] = true
174+ f .subdirsMu .Unlock ()
136175 }
137176
138177 entry := Entry [K , V ]{
@@ -149,8 +188,19 @@ func (f *filePersist[K, V]) Store(ctx context.Context, key K, value V, expiry ti
149188 return fmt .Errorf ("create temp file: %w" , err )
150189 }
151190
152- enc := gob .NewEncoder (file )
191+ // Get writer from pool and reset it for this file
192+ writer := writerPool .Get ().(* bufio.Writer )
193+ writer .Reset (file )
194+
195+ enc := gob .NewEncoder (writer )
153196 encErr := enc .Encode (entry )
197+ if encErr == nil {
198+ encErr = writer .Flush () // Ensure buffered data is written
199+ }
200+
201+ // Return writer to pool
202+ writerPool .Put (writer )
203+
154204 closeErr := file .Close ()
155205
156206 if encErr != nil {
@@ -227,10 +277,15 @@ func (f *filePersist[K, V]) LoadRecent(ctx context.Context, limit int) (<-chan E
227277 return nil
228278 }
229279
280+ // Get reader from pool and reset it for this file
281+ reader := readerPool .Get ().(* bufio.Reader )
282+ reader .Reset (file )
283+
230284 var e Entry [K , V ]
231- dec := gob .NewDecoder (file )
285+ dec := gob .NewDecoder (reader )
232286 if err := dec .Decode (& e ); err != nil {
233287 slog .Warn ("failed to decode cache file" , "file" , path , "error" , err )
288+ readerPool .Put (reader )
234289 if err := file .Close (); err != nil {
235290 slog .Debug ("failed to close file after decode error" , "file" , path , "error" , err )
236291 }
@@ -239,6 +294,7 @@ func (f *filePersist[K, V]) LoadRecent(ctx context.Context, limit int) (<-chan E
239294 }
240295 return nil
241296 }
297+ readerPool .Put (reader )
242298 if err := file .Close (); err != nil {
243299 slog .Debug ("failed to close file" , "file" , path , "error" , err )
244300 }
0 commit comments