Skip to content

Commit 9a17fff

Browse files
Remove GetBlock/PutBlock, GetN/PutN; split pool into themed files
- Remove GetBlock, PutBlock and blocking support (Cond/Mutex, blockedShards) - Remove GetN, PutN; callers use Get/Put in a loop - Update README: drop GetBlock/PutBlock from docs - Split pool.go into themed files: - config.go: errors, GcLevel, CleanupPolicy, Config, GrowthPolicy, validation - poolable.go: Poolable, Fields, Allocator, Cleaner - cleanup.go: startCleaner, cleanup, filterUsableObjects, reinsertKeptObjects - pool.go: Shard, ShardedPool, NewPool, Get, Put, clear, Close, runtime linknames - Adjust tests for removed APIs (initShards, TestClear, TestClose, TestClearRaceCondition)
1 parent d2f79d3 commit 9a17fff

6 files changed

Lines changed: 367 additions & 622 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ type GrowthPolicy struct {
170170
}
171171
```
172172

173-
> If the pool reaches its limit, it returns nil. To avoid this behavior, you can use GetBlock() or PutBlock(), which block until resources become available.
173+
> If the pool reaches its limit, Get() returns nil.
174174
175175
## Cleanup Policy
176176

pool/cleanup.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package pool
2+
3+
import (
4+
"time"
5+
)
6+
7+
// startCleaner starts the background cleanup goroutine.
8+
func (p *ShardedPool[T, P]) startCleaner() {
9+
p.cleanWg.Add(1)
10+
go func() {
11+
defer p.cleanWg.Done()
12+
ticker := time.NewTicker(p.cfg.Cleanup.Interval)
13+
defer ticker.Stop()
14+
15+
for {
16+
select {
17+
case <-ticker.C:
18+
p.cleanup()
19+
case <-p.stopClean:
20+
return
21+
}
22+
}
23+
}()
24+
}
25+
26+
// cleanup removes idle objects based on the [CleanupPolicy].
27+
func (p *ShardedPool[T, P]) cleanup() {
28+
if !p.cfg.Cleanup.Enabled {
29+
return
30+
}
31+
32+
for _, shard := range p.Shards {
33+
p.cleanupShard(shard)
34+
}
35+
}
36+
37+
func (p *ShardedPool[T, P]) cleanupShard(shard *Shard[T, P]) {
38+
oldHead := p.tryTakeOwnership(shard)
39+
if oldHead == nil {
40+
return
41+
}
42+
43+
keptHead, keptTail, evictedCount := p.filterUsableObjects(oldHead)
44+
45+
if evictedCount > 0 {
46+
p.CurrentPoolLength.Add(-int64(evictedCount))
47+
}
48+
49+
if keptHead != nil {
50+
p.reinsertKeptObjects(shard, keptHead, keptTail)
51+
}
52+
}
53+
54+
func (p *ShardedPool[T, P]) tryTakeOwnership(shard *Shard[T, P]) P {
55+
head := P(shard.Head.Load())
56+
if head == nil {
57+
return nil
58+
}
59+
if !shard.Head.CompareAndSwap(head, nil) {
60+
return nil
61+
}
62+
return head
63+
}
64+
65+
// filterUsableObjects filters objects based on usage count and returns the kept head, kept tail, and number of evicted objects.
66+
func (p *ShardedPool[T, P]) filterUsableObjects(head P) (keptHead, keptTail P, evictedCount int) {
67+
current := head
68+
69+
for current != nil {
70+
next := current.GetNext()
71+
usageCount := current.GetUsageCount()
72+
73+
if usageCount >= p.cfg.Cleanup.MinUsageCount {
74+
current.ResetUsage()
75+
if keptHead == nil {
76+
keptHead = current
77+
} else {
78+
keptTail.SetNext(current)
79+
}
80+
keptTail = current
81+
} else {
82+
current.SetNext(nil)
83+
evictedCount++
84+
}
85+
current = next
86+
}
87+
88+
if keptHead == nil {
89+
return nil, nil, evictedCount
90+
}
91+
92+
keptTail.SetNext(nil)
93+
return keptHead, keptTail, evictedCount
94+
}
95+
96+
func (p *ShardedPool[T, P]) reinsertKeptObjects(shard *Shard[T, P], keptHead, keptTail P) {
97+
// Find the shard index for this shard
98+
var shardID int
99+
for i, s := range p.Shards {
100+
if s == shard {
101+
shardID = i
102+
break
103+
}
104+
}
105+
106+
// Set the correct shard index for all kept objects
107+
current := keptHead
108+
for current != nil {
109+
current.SetShardIndex(shardID)
110+
current = current.GetNext()
111+
}
112+
113+
for {
114+
currentHead := P(shard.Head.Load())
115+
if currentHead != nil {
116+
keptTail.SetNext(currentHead)
117+
}
118+
if shard.Head.CompareAndSwap(currentHead, keptHead) {
119+
break
120+
}
121+
}
122+
}

pool/config.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Config, cleanup policy, growth policy, and validation for the pool.
2+
package pool
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"runtime"
8+
"time"
9+
)
10+
11+
// Common errors that may be returned by the pool.
12+
var (
13+
// ErrNoAllocator is returned when attempting to get an object but no allocator is configured.
14+
ErrNoAllocator = errors.New("no allocator configured")
15+
16+
// ErrNoCleaner is returned when attempting to create a pool but no cleaner is configured.
17+
ErrNoCleaner = errors.New("no cleaner configured")
18+
)
19+
20+
// GcLevel offers different levels for clean up configuration.
21+
// These presets control how aggressively GenPool reclaims memory.
22+
// Note: Go's GC may still run unless you explicitly suppress it via debug.SetGCPercent(-1)
23+
type GcLevel string
24+
25+
var (
26+
// GcDisable disables GenPool's cleanup completely.
27+
// Objects will stay in the pool indefinitely unless manually cleared.
28+
GcDisable GcLevel = "disable"
29+
30+
// GcLow performs cleanup at long intervals with minimal aggression.
31+
// Good for low-latency, high-reuse scenarios.
32+
GcLow GcLevel = "low"
33+
34+
// GcModerate performs cleanup at regular intervals and evicts objects
35+
// that are lightly used. Balances reuse and memory usage.
36+
GcModerate GcLevel = "moderate"
37+
38+
// GcAggressive enables frequent cleanup and removes objects
39+
// that are not reused often. Best for memory-constrained environments.
40+
GcAggressive GcLevel = "aggressive"
41+
)
42+
43+
// The number of shards is tied to GOMAXPROCS (max OS threads running Go code in parallel).
44+
// To reduce sharding, adjust GOMAXPROCS via runtime.GOMAXPROCS(n) before creating the pool.
45+
var (
46+
numShards = runtime.GOMAXPROCS(0)
47+
)
48+
49+
// CleanupPolicy defines how the pool should clean up unused objects.
50+
type CleanupPolicy struct {
51+
// Enabled determines if automatic cleanup is enabled.
52+
Enabled bool
53+
// Interval is how often the cleanup should run.
54+
Interval time.Duration
55+
// MinUsageCount is the number of usage BELOW which an object will be evicted.
56+
MinUsageCount int64
57+
}
58+
59+
// DefaultCleanupPolicy returns a default cleanup configuration based on specified level.
60+
func DefaultCleanupPolicy(level GcLevel) CleanupPolicy {
61+
switch level {
62+
case GcDisable:
63+
return CleanupPolicy{}
64+
case GcLow:
65+
return CleanupPolicy{
66+
Enabled: true,
67+
Interval: 10 * time.Minute,
68+
MinUsageCount: 1,
69+
}
70+
case GcModerate:
71+
return CleanupPolicy{
72+
Enabled: true,
73+
Interval: 2 * time.Minute,
74+
MinUsageCount: 2,
75+
}
76+
case GcAggressive:
77+
return CleanupPolicy{
78+
Enabled: true,
79+
Interval: 30 * time.Second,
80+
MinUsageCount: 3,
81+
}
82+
default:
83+
// Fallback to moderate if unrecognized
84+
return CleanupPolicy{
85+
Enabled: true,
86+
Interval: 2 * time.Minute,
87+
MinUsageCount: 2,
88+
}
89+
}
90+
}
91+
92+
// Config holds configuration options for the pool.
93+
type Config[T any, P Poolable[T]] struct {
94+
// Cleanup defines the cleanup policy for the pool
95+
Cleanup CleanupPolicy
96+
97+
// Growth defined the growth policy for the pool
98+
Growth GrowthPolicy
99+
100+
// Allocator is the function to create new objects
101+
Allocator Allocator[T]
102+
103+
// Cleaner is the function to clean objects before returning them to the pool
104+
Cleaner Cleaner[T]
105+
}
106+
107+
// GrowthPolicy controls how the pool is allowed to grow.
108+
// If unset, the pool will grow indefinitely, and any cleanup will rely solely on the CleanupPolicy.
109+
type GrowthPolicy struct {
110+
// MaxPoolSize defines the maximum number of objects the pool is allowed to grow to.
111+
MaxPoolSize int64
112+
113+
// Enable activates growth control. If disabled, the pool will grow and shrink freely based on your configuration.
114+
Enable bool
115+
}
116+
117+
// DefaultConfig returns a default pool configuration for type T.
118+
func DefaultConfig[T any, P Poolable[T]](allocator Allocator[T], cleaner Cleaner[T]) Config[T, P] {
119+
return Config[T, P]{
120+
Cleanup: DefaultCleanupPolicy(GcModerate),
121+
Allocator: allocator,
122+
Cleaner: cleaner,
123+
}
124+
}
125+
126+
func validateConfig[T any, P Poolable[T]](cfg Config[T, P]) error {
127+
if cfg.Allocator == nil {
128+
return fmt.Errorf("%w: allocator is required", ErrNoAllocator)
129+
}
130+
if cfg.Cleaner == nil {
131+
return fmt.Errorf("%w: cleaner is required", ErrNoCleaner)
132+
}
133+
134+
return nil
135+
}
136+
137+
func validateCleanupConfig[T any, P Poolable[T]](cfg Config[T, P]) error {
138+
if cfg.Cleanup.Interval <= 0 {
139+
return errors.New("cleanup interval must be greater than 0")
140+
}
141+
if cfg.Cleanup.MinUsageCount <= 0 {
142+
return errors.New("minimum usage count must be greater than 0")
143+
}
144+
return nil
145+
}

0 commit comments

Comments
 (0)