Skip to content

Commit 81515d6

Browse files
author
Pascal Minder
committed
doc
1 parent d6d5dbb commit 81515d6

File tree

3 files changed

+94
-26
lines changed

3 files changed

+94
-26
lines changed

cachePersistence.go

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

2157
var ipDB *CachePersist // stays nil if disabled
22-
if path, err := ValidatePersistencePath(cachePath); len(path) > 0 {
58+
if path, err := ValidatePersistencePath(opts.CachePath); len(path) > 0 {
2359
// load existing cache
2460
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)
61+
opts.Logger.Printf("%s: could not load IP DB snapshot (%s): %v", opts.Name, path, err)
2662
}
2763

2864
ipDB = &CachePersist{
2965
path: path,
30-
persistTicker: time.NewTicker(15 * time.Second),
66+
persistTicker: time.NewTicker(opts.PersistInterval),
3167
persistChannel: make(chan struct{}, 1),
3268
cache: cache,
33-
log: logger,
34-
name: name,
69+
log: opts.Logger,
70+
name: opts.Name,
3571
}
3672

3773
go func(ctx context.Context, p *CachePersist) {
@@ -49,16 +85,17 @@ func InitializeCache(ctx context.Context, logger *log.Logger, name string, cache
4985
}
5086
}(ctx, ipDB)
5187

52-
logger.Printf("%s: IP database persistence enabled -> %s", name, path)
88+
opts.Logger.Printf("%s: IP database persistence enabled -> %s", opts.Name, path)
5389
} else if err != nil {
54-
logger.Printf("%s: IP database persistence disabled: %v", name, err)
90+
opts.Logger.Printf("%s: IP database persistence disabled: %v", opts.Name, err)
5591
} else {
56-
logger.Printf("%s: IP database persistence disabled (no path)", name)
92+
opts.Logger.Printf("%s: IP database persistence disabled (no path)", opts.Name)
5793
}
5894

59-
return cache, ipDB
95+
return cache, ipDB, nil
6096
}
6197

98+
// CachePersist periodically snapshots a cache to disk.
6299
type CachePersist struct {
63100
path string
64101
persistTicker *time.Ticker
@@ -70,6 +107,7 @@ type CachePersist struct {
70107
name string
71108
}
72109

110+
// MarkDirty marks the cache as modified and schedules a snapshot.
73111
func (p *CachePersist) MarkDirty() {
74112
if p == nil { // feature OFF
75113
return
@@ -81,6 +119,7 @@ func (p *CachePersist) MarkDirty() {
81119
}
82120
}
83121

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

geoblock.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"fmt"
77
"io"
8-
"io/fs"
98
"log"
109
"net"
1110
"net/http"
@@ -24,7 +23,7 @@ const (
2423
unknownCountryCode = "AA"
2524
countryCodeLength = 2
2625
defaultDeniedRequestHTTPStatusCode = 403
27-
filePermissions = fs.FileMode(0666)
26+
defaultCacheWriteCycle = 15
2827
)
2928

3029
// Config the plugin configuration.
@@ -142,7 +141,17 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
142141
}
143142

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

147156
return &GeoBlock{
148157
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)