Skip to content

Commit e4884b4

Browse files
committed
feat: implement RequiresPurge interface method
- Add RequiresPurge() to Store interface with detailed docs - Implements method in MemoryStore and FileStore - Enables StoreEngine to detect stores needing manual cleanup - Adds storeEngineOptions type for future configuration - Refactors StoreEngine initialization structure Refs: #25 Refs: #23
1 parent 3865310 commit e4884b4

3 files changed

Lines changed: 80 additions & 14 deletions

File tree

session/file.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ func (s *FileStore) Purge(ctx context.Context, maxAge time.Duration) error {
177177
return nil
178178
}
179179

180+
func (s *FileStore) RequiresPurge() bool {
181+
return true
182+
}
183+
180184
// Touch updates the LastTouched timestamp for the session entry identified by
181185
// id, effectively renewing its expiration for sliding expiration policies.
182186
// Only the session's last access time is updated; the session value itself is

session/memory.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,14 @@ func (s *MemoryStore) Purge(ctx context.Context, maxAge time.Duration) error {
103103
return nil
104104
}
105105

106+
func (s *MemoryStore) RequiresPurge() bool {
107+
return true
108+
}
109+
106110
// Touch updates the LastTouched timestamp for the session entry identified by
107111
// id, effectively renewing its expiration for sliding expiration policies.
108112
// Only the session's last access time is updated; the session value itself is
109-
// not changed.
110-
// Returns an error if the entry does not exist.
113+
// not changed. Returns an error if the entry does not exist.
111114
func (s *MemoryStore) Touch(ctx context.Context, id string) error {
112115
s.mu.Lock()
113116
defer s.mu.Unlock()

session/store.go

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"time"
77

8+
"github.com/candango/httpok/logger"
89
"github.com/candango/httpok/security"
910
)
1011

@@ -45,32 +46,43 @@ type Store interface {
4546
// Stop tears down resources.
4647
Stop(ctx context.Context) error
4748

49+
// RequiresPurge returns whether this Store implementation requires manual
50+
// session expiration cleanup. Stores with automatic TTL/expiration (e.g.,
51+
// Redis) return false. Stores tracking expiration manually (e.g.,
52+
// MemoryStore, FileStore) return true, signaling that StoreEngine must
53+
// periodically call Purge to remove expired sessions.
54+
//
55+
// Example implementations:
56+
// - MemoryStore.RequiresPurge() -> true (manual LastTouched tracking)
57+
// - FileStore.RequiresPurge() -> true (file mtime tracking)
58+
// - RedisStore.RequiresPurge() -> false (Redis TTL automatic)
59+
RequiresPurge() bool
60+
4861
// Touch updates the session's ttl, typically to implement sliding
4962
// expiration. It does not modify the session data.
5063
// Returns an error if the id does not exist.
5164
Touch(ctx context.Context, id string) error
5265
}
5366

67+
type storeEngineOptions func(*StoreEngine)
68+
5469
// StoreEngine implements the Engine interface by delegating session operations
5570
// to a pluggable Store backend. It holds engine properties and a Store
5671
// instance, allowing flexible session storage strategies (e.g., in-memory,
5772
// file, etc.).
5873
type StoreEngine struct {
5974
properties *EngineProperties
6075
Store
76+
logger logger.Logger
77+
purgeDone chan struct{}
78+
started bool
6179
}
6280

6381
// NewStoreEngine creates and returns a new StoreEngine.
6482
// If custom properties are provided, they are used; otherwise, default
6583
// settings are applied.
66-
func NewStoreEngine(store Store, props ...*EngineProperties) *StoreEngine {
67-
if len(props) > 0 && props[0] != nil {
68-
return &StoreEngine{
69-
properties: props[0],
70-
Store: store,
71-
}
72-
}
73-
return &StoreEngine{
84+
func NewStoreEngine(store Store, opts ...storeEngineOptions) *StoreEngine {
85+
e := &StoreEngine{
7486
properties: &EngineProperties{
7587
AgeLimit: 30 * time.Minute,
7688
Enabled: true,
@@ -79,7 +91,24 @@ func NewStoreEngine(store Store, props ...*EngineProperties) *StoreEngine {
7991
Prefix: DefaultPrefix,
8092
PurgeDuration: 2 * time.Minute,
8193
},
82-
Store: store,
94+
Store: store,
95+
logger: &logger.StandardLogger{},
96+
}
97+
for _, opt := range opts {
98+
opt(e)
99+
}
100+
return e
101+
}
102+
103+
func WithLogger(l logger.Logger) storeEngineOptions {
104+
return func(e *StoreEngine) {
105+
e.logger = l
106+
}
107+
}
108+
109+
func WithProperties(p *EngineProperties) storeEngineOptions {
110+
return func(e *StoreEngine) {
111+
e.properties = p
83112
}
84113
}
85114

@@ -91,6 +120,15 @@ func (e *StoreEngine) NewId(ctx context.Context) string {
91120

92121
// Start initializes the engine with the given context.
93122
func (e *StoreEngine) Start(ctx context.Context) error {
123+
if e.started {
124+
return errors.New("store engine already started")
125+
}
126+
e.started = true
127+
if e.RequiresPurge() {
128+
e.purgeDone = make(chan struct{})
129+
go e.periodicPurge(ctx)
130+
}
131+
94132
return e.Store.Start(ctx)
95133
}
96134

@@ -105,6 +143,27 @@ func (e *StoreEngine) Properties() *EngineProperties {
105143
return e.properties
106144
}
107145

146+
func (e *StoreEngine) periodicPurge(ctx context.Context) error {
147+
ticker := time.NewTicker(e.properties.PurgeDuration)
148+
defer ticker.Stop()
149+
150+
for {
151+
select {
152+
case <-ticker.C:
153+
ticker.Stop()
154+
155+
if err := e.Purge(ctx); err != nil {
156+
e.logger.Errorf("periodic purge failed: %v", err)
157+
}
158+
ticker = time.NewTicker(e.properties.PurgeDuration)
159+
case <-e.purgeDone:
160+
return nil
161+
case <-ctx.Done():
162+
return nil
163+
}
164+
}
165+
}
166+
108167
// Purge removes expired or invalid sessions.
109168
func (e *StoreEngine) Purge(ctx context.Context) error {
110169
if !e.properties.Enabled {
@@ -120,7 +179,7 @@ func (e *StoreEngine) GetSession(ctx context.Context, id string) (Session, error
120179
return s, errors.New("engine is disabled")
121180
}
122181
if id == "" {
123-
return s, errors.New("engine is disabled")
182+
return s, errors.New("session id is empty")
124183
}
125184
var v map[string]any
126185
ok, err := e.Store.Exists(ctx, id)
@@ -138,7 +197,7 @@ func (e *StoreEngine) GetSession(ctx context.Context, id string) (Session, error
138197
if err != nil {
139198
return s, err
140199
}
141-
e.Store.Touch(ctx, id)
200+
err = e.Store.Touch(ctx, id)
142201
if err != nil {
143202
return s, err
144203
}
@@ -170,7 +229,7 @@ func (e *StoreEngine) SaveSession(ctx context.Context, id string, session Sessio
170229
return errors.New("engine is disabled")
171230
}
172231
if id == "" {
173-
return errors.New("engine is disabled")
232+
return errors.New("session id is empty")
174233
}
175234

176235
data, err := e.properties.Encoder.Encode(session.Data)

0 commit comments

Comments
 (0)