Skip to content

Commit cac5fc6

Browse files
author
Pascal Minder
committed
doc
1 parent d6d5dbb commit cac5fc6

File tree

3 files changed

+90
-23
lines changed

3 files changed

+90
-23
lines changed

cachePersistence.go

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package geoblock
33
import (
44
"bytes"
55
"context"
6+
"fmt"
67
"log"
78
"os"
89
"path/filepath"
@@ -12,26 +13,59 @@ import (
1213
lru "github.com/PascalMinder/geoblock/lrucache"
1314
)
1415

15-
func InitializeCache(ctx context.Context, logger *log.Logger, name string, cacheSize int, cachePath string) (*lru.LRUCache, *CachePersist) {
16-
cache, err := lru.NewLRUCache(cacheSize)
16+
// Options configures cache initialization and persistence behavior.
17+
type Options struct {
18+
// CacheSize specifies the number of entries to store in memory.
19+
CacheSize int
20+
21+
// CachePath is the file path used for on-disk persistence.
22+
// Leave empty to disable persistence.
23+
CachePath string
24+
25+
// PersistInterval defines how often the cache is automatically
26+
// flushed to disk. Defaults to 15 seconds if zero.
27+
PersistInterval time.Duration
28+
29+
// Logger is used for diagnostic messages.
30+
Logger *log.Logger
31+
32+
// Name is included in log messages to identify the cache instance.
33+
Name string
34+
}
35+
36+
// InitializeCache creates a new LRU cache and, if a valid persistence
37+
// path is provided, starts a background goroutine to periodically
38+
// save snapshots to disk.
39+
//
40+
// The returned `CachePersist` can be used to mark the cache as dirty when
41+
// data changes. If persistence is disabled, it is a no-op.
42+
//
43+
// Callers should cancel the provided context to stop persistence
44+
// and ensure a final snapshot is written.
45+
func InitializeCache(ctx context.Context, opts Options) (*lru.LRUCache, *CachePersist, error) {
46+
if opts.PersistInterval <= 0 {
47+
opts.PersistInterval = 15 * time.Second
48+
}
49+
50+
cache, err := lru.NewLRUCache(opts.CacheSize)
1751
if err != nil {
18-
logger.Fatal(err)
52+
return nil, nil, fmt.Errorf("create LRU cache: %w", err)
1953
}
2054

2155
var ipDB *CachePersist // stays nil if disabled
22-
if path, err := ValidatePersistencePath(cachePath); len(path) > 0 {
56+
if path, err := ValidatePersistencePath(opts.CachePath); len(path) > 0 {
2357
// load existing cache
2458
if err := cache.ImportFromFile(path); err != nil && !os.IsNotExist(err) {
25-
logger.Printf("%s: could not load IP DB snapshot (%s): %v", name, path, err)
59+
opts.Logger.Printf("%s: could not load IP DB snapshot (%s): %v", opts.Name, path, err)
2660
}
2761

2862
ipDB = &CachePersist{
2963
path: path,
3064
persistTicker: time.NewTicker(15 * time.Second),
3165
persistChannel: make(chan struct{}, 1),
3266
cache: cache,
33-
log: logger,
34-
name: name,
67+
log: opts.Logger,
68+
name: opts.Name,
3569
}
3670

3771
go func(ctx context.Context, p *CachePersist) {
@@ -49,16 +83,17 @@ func InitializeCache(ctx context.Context, logger *log.Logger, name string, cache
4983
}
5084
}(ctx, ipDB)
5185

52-
logger.Printf("%s: IP database persistence enabled -> %s", name, path)
86+
opts.Logger.Printf("%s: IP database persistence enabled -> %s", opts.Name, path)
5387
} else if err != nil {
54-
logger.Printf("%s: IP database persistence disabled: %v", name, err)
88+
opts.Logger.Printf("%s: IP database persistence disabled: %v", opts.Name, err)
5589
} else {
56-
logger.Printf("%s: IP database persistence disabled (no path)", name)
90+
opts.Logger.Printf("%s: IP database persistence disabled (no path)", opts.Name)
5791
}
5892

59-
return cache, ipDB
93+
return cache, ipDB, nil
6094
}
6195

96+
// CachePersist periodically snapshots a cache to disk.
6297
type CachePersist struct {
6398
path string
6499
persistTicker *time.Ticker
@@ -70,6 +105,7 @@ type CachePersist struct {
70105
name string
71106
}
72107

108+
// MarkDirty marks the cache as modified and schedules a snapshot.
73109
func (p *CachePersist) MarkDirty() {
74110
if p == nil { // feature OFF
75111
return
@@ -81,6 +117,7 @@ func (p *CachePersist) MarkDirty() {
81117
}
82118
}
83119

120+
// Snapshot writes the cache to disk if it has been marked dirty.
84121
func (p *CachePersist) snapshotToDisk() {
85122
if p == nil || atomic.LoadUint32(&p.ipDirty) == 0 {
86123
return

geoblock.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,17 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
142142
}
143143

144144
// initialize local IP lookup cache
145-
cache, ipDB := InitializeCache(ctx, infoLogger, name, config.CacheSize, config.IPDatabaseCachePath)
145+
cacheOptions := Options{
146+
CacheSize: config.CacheSize,
147+
CachePath: config.IPDatabaseCachePath,
148+
PersistInterval: 15 * time.Second,
149+
Logger: infoLogger,
150+
Name: name,
151+
}
152+
cache, ipDB, err := InitializeCache(ctx, cacheOptions)
153+
if err != nil {
154+
infoLogger.Fatal(err)
155+
}
146156

147157
return &GeoBlock{
148158
next: next,

logPersistence.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,61 @@ import (
77
"os"
88
)
99

10+
// CreateCustomLogTarget configures the provided logger to write to a custom
11+
// log file and automatically restores standard output when the given context
12+
// is canceled.
13+
//
14+
// It returns the opened *os.File handle, which is also closed automatically
15+
// when the context is done.
16+
//
17+
// If logFilePath is empty or invalid, the logger remains unchanged and no
18+
// file is created.
19+
//
20+
// The logger output will be reset to stdout when the context is canceled.
1021
func CreateCustomLogTarget(ctx context.Context, logger *log.Logger, name string, logFilePath string) (*os.File, error) {
11-
logFile, err := initializeLogFile(logFilePath, logger)
22+
logFile, err := openLogFile(logFilePath, logger)
1223
if err != nil {
13-
return nil, fmt.Errorf("error initializing log file: %v", err)
24+
return nil, fmt.Errorf("initialize log file: %w", err)
1425
}
1526

16-
// Set up a goroutine to close the file when the context is done
1727
if logFile != nil {
1828
go func(logger *log.Logger) {
19-
<-ctx.Done() // Wait for context cancellation
29+
<-ctx.Done()
2030
logger.SetOutput(os.Stdout)
21-
logFile.Close()
22-
logger.Printf("%s: Log file closed for middleware\n", name)
31+
if cerr := logFile.Close(); cerr != nil {
32+
logger.Printf("%s: error closing log file: %v", name, cerr)
33+
}
34+
logger.Printf("%s: log file closed for middleware", name)
2335
}(logger)
2436
}
2537

26-
logger.Printf("%s: Log file opened for middleware\n", name)
38+
logger.Printf("%s: log file opened for middleware", name)
2739
return logFile, nil
2840
}
2941

30-
func initializeLogFile(logFilePath string, logger *log.Logger) (*os.File, error) {
42+
// openLogFile validates the provided path and opens the target log file.
43+
//
44+
// If the path is empty, it returns (nil, nil).
45+
// If the folder is not writable or validation fails, an error is returned.
46+
//
47+
// The logger’s output is redirected to the newly opened file.
48+
// The caller is responsible for closing the returned file when done.
49+
func openLogFile(logFilePath string, logger *log.Logger) (*os.File, error) {
3150
if len(logFilePath) == 0 {
3251
return nil, nil
3352
}
3453

3554
path, err := ValidatePersistencePath(logFilePath)
3655
if err != nil {
37-
return nil, err
56+
return nil, fmt.Errorf("validate persistence path: %w", err)
3857
} else if len(path) == 0 {
39-
return nil, fmt.Errorf("folder is not writeable (%s)", path)
58+
return nil, fmt.Errorf("folder is not writable: %s", logFilePath)
4059
}
4160

61+
const filePermissions = 0o600
4262
logFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, filePermissions)
4363
if err != nil {
44-
return nil, fmt.Errorf("failed to open log file (%s): %v", path, err)
64+
return nil, fmt.Errorf("open log file %q: %w", path, err)
4565
}
4666

4767
logger.SetOutput(logFile)

0 commit comments

Comments
 (0)