Skip to content

Commit de43b99

Browse files
committed
feat(core): make backend construction type-safe and refactor New; fix WorkerPool shutdown/resizing
factory: introduce typed IBackendConstructor[T] returning backend.IBackend[T]; store constructors opaquely in manager; update default registrations and RegisterBackend signature hypercache: split New into small helpers (resolveBackend, newHyperCacheBase, configureEvictionSettings, initEvictionAlgorithm, configureStats, checkCapacity, initExpirationTrigger) without changing behavior; use typed constructors via registry pool: fix Shutdown to close jobs first and wait, then close quit/errorChan; handle closed jobs channel in worker; correct Resize decrease loop to send -diff quit signals tests: add WorkerPool tests covering enqueue/shutdown, error handling, resize up/down, zero->up, negative resize, and enqueue-after-shutdown panic Context: resolves test hang in WorkerPool_ResizeDecrease; improves maintainability and type safety of backend creation; no public API changes to New signature.
1 parent 75a7033 commit de43b99

4 files changed

Lines changed: 318 additions & 93 deletions

File tree

factory.go

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,41 @@ package hypercache
22

33
import (
44
"github.com/hyp3rd/hypercache/internal/constants"
5-
"github.com/hyp3rd/hypercache/internal/sentinel"
65
"github.com/hyp3rd/hypercache/pkg/backend"
76
)
87

9-
// IBackendConstructor is an interface for backend constructors.
10-
type IBackendConstructor interface {
11-
Create(config any) (any, error)
8+
// IBackendConstructor is an interface for backend constructors with type safety.
9+
// It returns a typed backend.IBackend[T] instead of any.
10+
type IBackendConstructor[T backend.IBackendConstrain] interface {
11+
Create(cfg *Config[T]) (backend.IBackend[T], error)
1212
}
1313

14-
// InMemoryBackendConstructor is a backend constructor for InMemory.
14+
// InMemoryBackendConstructor constructs InMemory backends.
1515
type InMemoryBackendConstructor struct{}
1616

1717
// Create creates a new InMemory backend.
18-
func (ibc InMemoryBackendConstructor) Create(config any) (any, error) {
19-
inMemoryConfig, ok := config.(*Config[backend.InMemory])
20-
if !ok {
21-
return nil, sentinel.ErrInvalidBackendType
22-
}
23-
24-
return backend.NewInMemory(inMemoryConfig.InMemoryOptions...)
18+
func (InMemoryBackendConstructor) Create(cfg *Config[backend.InMemory]) (backend.IBackend[backend.InMemory], error) {
19+
return backend.NewInMemory(cfg.InMemoryOptions...)
2520
}
2621

27-
// RedisBackendConstructor is a backend constructor for Redis.
22+
// RedisBackendConstructor constructs Redis backends.
2823
type RedisBackendConstructor struct{}
2924

3025
// Create creates a new Redis backend.
31-
func (rbc RedisBackendConstructor) Create(config any) (any, error) {
32-
redisConfig, ok := config.(*Config[backend.Redis])
33-
if !ok {
34-
return nil, sentinel.ErrInvalidBackendType
35-
}
36-
37-
return backend.NewRedis(redisConfig.RedisOptions...)
26+
func (RedisBackendConstructor) Create(cfg *Config[backend.Redis]) (backend.IBackend[backend.Redis], error) {
27+
return backend.NewRedis(cfg.RedisOptions...)
3828
}
3929

4030
// BackendManager is a factory for creating HyperCache backend instances.
31+
// It maintains a registry of backend constructors. We store them as any internally,
32+
// and cast to the typed constructor at use site based on T.
4133
type BackendManager struct {
42-
backendRegistry map[string]IBackendConstructor
34+
backendRegistry map[string]any
4335
}
4436

4537
// getDefaultBackends returns the default set of backend constructors.
46-
func getDefaultBackends() map[string]IBackendConstructor {
47-
return map[string]IBackendConstructor{
38+
func getDefaultBackends() map[string]any {
39+
return map[string]any{
4840
constants.InMemoryBackend: InMemoryBackendConstructor{},
4941
constants.RedisBackend: RedisBackendConstructor{},
5042
}
@@ -53,7 +45,7 @@ func getDefaultBackends() map[string]IBackendConstructor {
5345
// NewBackendManager creates a new BackendManager with default backends pre-registered.
5446
func NewBackendManager() *BackendManager {
5547
manager := &BackendManager{
56-
backendRegistry: make(map[string]IBackendConstructor),
48+
backendRegistry: make(map[string]any),
5749
}
5850
// Register the default backends
5951
for name, constructor := range getDefaultBackends() {
@@ -67,17 +59,16 @@ func NewBackendManager() *BackendManager {
6759
// This is useful for testing or when you want to register only specific backends.
6860
func NewEmptyBackendManager() *BackendManager {
6961
return &BackendManager{
70-
backendRegistry: make(map[string]IBackendConstructor),
62+
backendRegistry: make(map[string]any),
7163
}
7264
}
7365

74-
// RegisterBackend registers a new backend constructor.
75-
func (hcm *BackendManager) RegisterBackend(name string, constructor IBackendConstructor) {
66+
// RegisterBackend registers a new backend constructor. The constructor should be
67+
// a value implementing IBackendConstructor[T] for some T; stored as any.
68+
func (hcm *BackendManager) RegisterBackend(name string, constructor any) {
7669
hcm.backendRegistry[name] = constructor
7770
}
7871

7972
// GetDefaultManager returns a new BackendManager with default backends pre-registered.
8073
// This replaces the previous global instance with a factory function.
81-
func GetDefaultManager() *BackendManager {
82-
return NewBackendManager()
83-
}
74+
func GetDefaultManager() *BackendManager { return NewBackendManager() }

hypercache.go

Lines changed: 136 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -104,102 +104,181 @@ func NewInMemoryWithDefaults(capacity int) (*HyperCache[backend.InMemory], error
104104
// - The eviction algorithm is set to LRU.
105105
// - The expiration interval is set to 30 minutes.
106106
// - The stats collector is set to the HistogramStatsCollector stats collector.
107-
//
108-
//nolint:cyclop
109107
func New[T backend.IBackendConstrain](bm *BackendManager, config *Config[T]) (*HyperCache[T], error) {
110-
// Get the backend constructor from the registry
111-
constructor, exists := bm.backendRegistry[config.BackendType]
112-
if !exists {
113-
return nil, ewrap.Newf("unknown backend type: %s", config.BackendType)
108+
// Resolve typed backend from registry
109+
backendTyped, err := resolveBackend[T](bm, config)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
// Initialize base cache struct
115+
hyperCache := newHyperCacheBase[T](backendTyped)
116+
117+
// Initialize the cache backend type checker
118+
hyperCache.cacheBackendChecker = introspect.CacheBackendChecker[T]{
119+
Backend: hyperCache.backend,
120+
BackendType: config.BackendType,
121+
}
122+
123+
// Apply options and configure eviction-related settings
124+
ApplyHyperCacheOptions(hyperCache, config.HyperCacheOptions...)
125+
configureEvictionSettings(hyperCache)
126+
127+
// Initialize eviction algorithm
128+
err = initEvictionAlgorithm(hyperCache)
129+
if err != nil {
130+
return hyperCache, err
131+
}
132+
133+
// Stats collector
134+
err = configureStats(hyperCache)
135+
if err != nil {
136+
return hyperCache, err
114137
}
115138

116-
// Create the backend
117-
backendInstance, err := constructor.Create(config)
139+
// Capacity check (fatal)
140+
err = checkCapacity(hyperCache)
118141
if err != nil {
119142
return nil, err
120143
}
121144

122-
// Check if the backend implements the IBackend interface
123-
backend, ok := backendInstance.(backend.IBackend[T])
124-
if !ok {
145+
// Initialize expiration trigger channel and start background jobs
146+
initExpirationTrigger(hyperCache)
147+
hyperCache.startBackgroundJobs()
148+
149+
return hyperCache, nil
150+
}
151+
152+
// resolveBackend constructs a typed backend instance based on the config.BackendType.
153+
func resolveBackend[T backend.IBackendConstrain](bm *BackendManager, config *Config[T]) (backend.IBackend[T], error) {
154+
constructor, exists := bm.backendRegistry[config.BackendType]
155+
if !exists {
156+
return nil, ewrap.Newf("unknown backend type: %s", config.BackendType)
157+
}
158+
159+
switch config.BackendType {
160+
case constants.InMemoryBackend:
161+
inMemoryConstructor, ok := constructor.(InMemoryBackendConstructor)
162+
if !ok {
163+
return nil, sentinel.ErrInvalidBackendType
164+
}
165+
166+
cfg, ok := any(config).(*Config[backend.InMemory])
167+
if !ok {
168+
return nil, sentinel.ErrInvalidBackendType
169+
}
170+
171+
bi, err := inMemoryConstructor.Create(cfg)
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
if b, ok := any(bi).(backend.IBackend[T]); ok {
177+
return b, nil
178+
}
179+
180+
return nil, sentinel.ErrInvalidBackendType
181+
182+
case constants.RedisBackend:
183+
redisConstructor, ok := constructor.(RedisBackendConstructor)
184+
if !ok {
185+
return nil, sentinel.ErrInvalidBackendType
186+
}
187+
188+
cfg, ok := any(config).(*Config[backend.Redis])
189+
if !ok {
190+
return nil, sentinel.ErrInvalidBackendType
191+
}
192+
193+
bi, err := redisConstructor.Create(cfg)
194+
if err != nil {
195+
return nil, err
196+
}
197+
198+
if b, ok := any(bi).(backend.IBackend[T]); ok {
199+
return b, nil
200+
}
201+
125202
return nil, sentinel.ErrInvalidBackendType
126203
}
127204

128-
// Initialize the cache
129-
hyperCache := &HyperCache[T]{
130-
backend: backend,
205+
return nil, ewrap.Newf("unknown backend type: %s", config.BackendType)
206+
}
207+
208+
// newHyperCacheBase builds the base HyperCache instance with default timings and internals.
209+
func newHyperCacheBase[T backend.IBackendConstrain](b backend.IBackend[T]) *HyperCache[T] {
210+
return &HyperCache[T]{
211+
backend: b,
131212
itemPoolManager: cache.NewItemPoolManager(),
132213
workerPool: NewWorkerPool(runtime.NumCPU()),
133214
stop: make(chan bool, 2),
134215
expirationInterval: constants.DefaultExpirationInterval,
135216
evictionInterval: constants.DefaultEvictionInterval,
136217
}
218+
}
137219

138-
// Initialize the cache backend type checker
139-
hyperCache.cacheBackendChecker = introspect.CacheBackendChecker[T]{
140-
Backend: hyperCache.backend,
141-
BackendType: config.BackendType,
142-
}
143-
144-
// Apply options
145-
ApplyHyperCacheOptions(hyperCache, config.HyperCacheOptions...)
146-
147-
// evaluate if the cache should evict items proactively
148-
hyperCache.shouldEvict.Store(hyperCache.evictionInterval == 0 && hyperCache.backend.Capacity() > 0)
220+
// configureEvictionSettings computes derived eviction settings like shouldEvict and default maxEvictionCount.
221+
func configureEvictionSettings[T backend.IBackendConstrain](hc *HyperCache[T]) {
222+
hc.shouldEvict.Store(hc.evictionInterval == 0 && hc.backend.Capacity() > 0)
149223

150-
// Set the max eviction count to the capacity if it is not set or is zero
151-
if hyperCache.maxEvictionCount == 0 {
224+
if hc.maxEvictionCount == 0 {
152225
//nolint:gosec
153-
hyperCache.maxEvictionCount = uint(hyperCache.backend.Capacity())
226+
hc.maxEvictionCount = uint(hc.backend.Capacity())
154227
}
228+
}
155229

156-
// Initialize the eviction algorithm
157-
if hyperCache.evictionAlgorithmName == "" {
230+
// initEvictionAlgorithm initializes the eviction algorithm for the cache.
231+
func initEvictionAlgorithm[T backend.IBackendConstrain](hc *HyperCache[T]) error {
232+
var err error
233+
if hc.evictionAlgorithmName == "" {
158234
// Use the default eviction algorithm if none is specified
159235
//nolint:gosec
160-
hyperCache.evictionAlgorithm, err = eviction.NewLRUAlgorithm(int(hyperCache.maxEvictionCount))
236+
hc.evictionAlgorithm, err = eviction.NewLRUAlgorithm(int(hc.maxEvictionCount))
161237
} else {
162238
// Use the specified eviction algorithm
163239
//nolint:gosec
164-
hyperCache.evictionAlgorithm, err = eviction.NewEvictionAlgorithm(hyperCache.evictionAlgorithmName, int(hyperCache.maxEvictionCount))
240+
hc.evictionAlgorithm, err = eviction.NewEvictionAlgorithm(hc.evictionAlgorithmName, int(hc.maxEvictionCount))
165241
}
166242

167-
if err != nil {
168-
return hyperCache, err
169-
}
243+
return err
244+
}
170245

171-
// Initialize the stats collector
172-
if hyperCache.statsCollectorName == "" {
173-
// Use the default stats collector if none is specified
174-
hyperCache.StatsCollector = stats.NewHistogramStatsCollector()
175-
} else {
176-
// Use the specified stats collector
177-
hyperCache.StatsCollector, err = stats.NewCollector(hyperCache.statsCollectorName)
178-
if err != nil {
179-
return hyperCache, err
180-
}
246+
// configureStats sets the stats collector, using default if none specified.
247+
func configureStats[T backend.IBackendConstrain](hc *HyperCache[T]) error {
248+
if hc.statsCollectorName == "" {
249+
hc.StatsCollector = stats.NewHistogramStatsCollector()
250+
251+
return nil
181252
}
182253

183-
// If the capacity is less than zero, we return
184-
if hyperCache.backend.Capacity() < 0 {
185-
return nil, sentinel.ErrInvalidCapacity
254+
var err error
255+
256+
hc.StatsCollector, err = stats.NewCollector(hc.statsCollectorName)
257+
258+
return err
259+
}
260+
261+
// checkCapacity validates the backend capacity and returns an error if invalid.
262+
func checkCapacity[T backend.IBackendConstrain](hc *HyperCache[T]) error {
263+
if hc.backend.Capacity() < 0 {
264+
return sentinel.ErrInvalidCapacity
186265
}
187266

188-
// Initialize the expiration trigger channel with configurable buffer size (default: half capacity)
189-
buf := hyperCache.backend.Capacity() / 2
190-
if hyperCache.expirationTriggerBufSize > 0 {
191-
buf = hyperCache.expirationTriggerBufSize
267+
return nil
268+
}
269+
270+
// initExpirationTrigger initializes the expiration trigger channel with optional buffer override.
271+
func initExpirationTrigger[T backend.IBackendConstrain](hc *HyperCache[T]) {
272+
buf := hc.backend.Capacity() / 2
273+
if hc.expirationTriggerBufSize > 0 {
274+
buf = hc.expirationTriggerBufSize
192275
}
193276

194277
if buf < 1 {
195278
buf = 1
196279
}
197280

198-
hyperCache.expirationTriggerCh = make(chan bool, buf)
199-
200-
hyperCache.startBackgroundJobs()
201-
202-
return hyperCache, err
281+
hc.expirationTriggerCh = make(chan bool, buf)
203282
}
204283

205284
// startBackgroundJobs starts the background jobs for the hyper cache.

pool.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,13 @@ func (pool *WorkerPool) Enqueue(job JobFunc) {
3939

4040
// Shutdown shuts down the worker pool. It waits for all jobs to finish.
4141
func (pool *WorkerPool) Shutdown() {
42-
close(pool.quit)
43-
pool.wg.Wait()
42+
// Stop accepting new jobs and let workers drain the queue
4443
close(pool.jobs)
44+
// Wait for all enqueued jobs to complete
45+
pool.wg.Wait()
46+
// Now signal any lingering workers to exit select loop
47+
close(pool.quit)
48+
// It's now safe to close the error channel (no more sends after wg completes)
4549
close(pool.errorChan)
4650
}
4751

@@ -71,7 +75,7 @@ func (pool *WorkerPool) Resize(newSize int) {
7175
} else {
7276
// Decrease the number of workers
7377
// Send only the number of quit signals needed to remove workers
74-
for range diff {
78+
for range -diff {
7579
pool.quit <- struct{}{}
7680
}
7781
}
@@ -88,9 +92,9 @@ func (pool *WorkerPool) start() {
8892
func (pool *WorkerPool) worker() {
8993
for {
9094
select {
91-
case job := <-pool.jobs:
92-
if job == nil {
93-
// jobs channel closed
95+
case job, ok := <-pool.jobs:
96+
if !ok {
97+
// jobs channel closed and drained
9498
return
9599
}
96100

0 commit comments

Comments
 (0)