mirror of
https://gitee.com/johng/gf
synced 2026-07-04 21:03:13 +08:00
feat(container/gpool): add generic pool feature (#4493)
add TPool[T] and let Pool base on it. --------- Co-authored-by: hailaz <739476267@qq.com>
This commit is contained in:
@ -8,41 +8,19 @@
|
||||
package gpool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/glist"
|
||||
"github.com/gogf/gf/v2/container/gtype"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/os/gtimer"
|
||||
)
|
||||
|
||||
// Pool is an Object-Reusable Pool.
|
||||
type Pool struct {
|
||||
list *glist.List // Available/idle items list.
|
||||
closed *gtype.Bool // Whether the pool is closed.
|
||||
TTL time.Duration // Time To Live for pool items.
|
||||
NewFunc func() (any, error) // Callback function to create pool item.
|
||||
// ExpireFunc is the function for expired items destruction.
|
||||
// This function needs to be defined when the pool items
|
||||
// need to perform additional destruction operations.
|
||||
// Eg: net.Conn, os.File, etc.
|
||||
ExpireFunc func(any)
|
||||
}
|
||||
|
||||
// Pool item.
|
||||
type poolItem struct {
|
||||
value any // Item value.
|
||||
expireAt int64 // Expire timestamp in milliseconds.
|
||||
*TPool[any]
|
||||
}
|
||||
|
||||
// NewFunc Creation function for object.
|
||||
type NewFunc func() (any, error)
|
||||
type NewFunc = TPoolNewFunc[any]
|
||||
|
||||
// ExpireFunc Destruction function for object.
|
||||
type ExpireFunc func(any)
|
||||
type ExpireFunc = TPoolExpireFunc[any]
|
||||
|
||||
// New creates and returns a new object pool.
|
||||
// To ensure execution efficiency, the expiration time cannot be modified once it is set.
|
||||
@ -52,134 +30,40 @@ type ExpireFunc func(any)
|
||||
// ttl < 0 : immediate expired after use;
|
||||
// ttl > 0 : timeout expired;
|
||||
func New(ttl time.Duration, newFunc NewFunc, expireFunc ...ExpireFunc) *Pool {
|
||||
r := &Pool{
|
||||
list: glist.New(true),
|
||||
closed: gtype.NewBool(),
|
||||
TTL: ttl,
|
||||
NewFunc: newFunc,
|
||||
return &Pool{
|
||||
TPool: NewTPool(ttl, newFunc, expireFunc...),
|
||||
}
|
||||
if len(expireFunc) > 0 {
|
||||
r.ExpireFunc = expireFunc[0]
|
||||
}
|
||||
gtimer.AddSingleton(context.Background(), time.Second, r.checkExpireItems)
|
||||
return r
|
||||
}
|
||||
|
||||
// Put puts an item to pool.
|
||||
func (p *Pool) Put(value any) error {
|
||||
if p.closed.Val() {
|
||||
return gerror.NewCode(gcode.CodeInvalidOperation, "pool is closed")
|
||||
}
|
||||
item := &poolItem{
|
||||
value: value,
|
||||
}
|
||||
if p.TTL == 0 {
|
||||
item.expireAt = 0
|
||||
} else {
|
||||
// As for Golang version < 1.13, there's no method Milliseconds for time.Duration.
|
||||
// So we need calculate the milliseconds using its nanoseconds value.
|
||||
item.expireAt = gtime.TimestampMilli() + p.TTL.Nanoseconds()/1000000
|
||||
}
|
||||
p.list.PushBack(item)
|
||||
return nil
|
||||
return p.TPool.Put(value)
|
||||
}
|
||||
|
||||
// MustPut puts an item to pool, it panics if any error occurs.
|
||||
func (p *Pool) MustPut(value any) {
|
||||
if err := p.Put(value); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p.TPool.MustPut(value)
|
||||
}
|
||||
|
||||
// Clear clears pool, which means it will remove all items from pool.
|
||||
func (p *Pool) Clear() {
|
||||
if p.ExpireFunc != nil {
|
||||
for {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
p.ExpireFunc(r.(*poolItem).value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.list.RemoveAll()
|
||||
}
|
||||
p.TPool.Clear()
|
||||
}
|
||||
|
||||
// Get picks and returns an item from pool. If the pool is empty and NewFunc is defined,
|
||||
// it creates and returns one from NewFunc.
|
||||
func (p *Pool) Get() (any, error) {
|
||||
for !p.closed.Val() {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
f := r.(*poolItem)
|
||||
if f.expireAt == 0 || f.expireAt > gtime.TimestampMilli() {
|
||||
return f.value, nil
|
||||
} else if p.ExpireFunc != nil {
|
||||
// TODO: move expire function calling asynchronously out from `Get` operation.
|
||||
p.ExpireFunc(f.value)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.NewFunc != nil {
|
||||
return p.NewFunc()
|
||||
}
|
||||
return nil, gerror.NewCode(gcode.CodeInvalidOperation, "pool is empty")
|
||||
return p.TPool.Get()
|
||||
}
|
||||
|
||||
// Size returns the count of available items of pool.
|
||||
func (p *Pool) Size() int {
|
||||
return p.list.Len()
|
||||
return p.TPool.Size()
|
||||
}
|
||||
|
||||
// Close closes the pool. If `p` has ExpireFunc,
|
||||
// then it automatically closes all items using this function before it's closed.
|
||||
// Commonly you do not need to call this function manually.
|
||||
func (p *Pool) Close() {
|
||||
p.closed.Set(true)
|
||||
}
|
||||
|
||||
// checkExpire removes expired items from pool in every second.
|
||||
func (p *Pool) checkExpireItems(ctx context.Context) {
|
||||
if p.closed.Val() {
|
||||
// If p has ExpireFunc,
|
||||
// then it must close all items using this function.
|
||||
if p.ExpireFunc != nil {
|
||||
for {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
p.ExpireFunc(r.(*poolItem).value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
gtimer.Exit()
|
||||
}
|
||||
// All items do not expire.
|
||||
if p.TTL == 0 {
|
||||
return
|
||||
}
|
||||
// The latest item expire timestamp in milliseconds.
|
||||
var latestExpire int64 = -1
|
||||
// Retrieve the current timestamp in milliseconds, it expires the items
|
||||
// by comparing with this timestamp. It is not accurate comparison for
|
||||
// every item expired, but high performance.
|
||||
var timestampMilli = gtime.TimestampMilli()
|
||||
for latestExpire <= timestampMilli {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
item := r.(*poolItem)
|
||||
latestExpire = item.expireAt
|
||||
// TODO improve the auto-expiration mechanism of the pool.
|
||||
if item.expireAt > timestampMilli {
|
||||
p.list.PushFront(item)
|
||||
break
|
||||
}
|
||||
if p.ExpireFunc != nil {
|
||||
p.ExpireFunc(item.value)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.TPool.Close()
|
||||
}
|
||||
|
||||
183
container/gpool/gpool_t.go
Normal file
183
container/gpool/gpool_t.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gpool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/glist"
|
||||
"github.com/gogf/gf/v2/container/gtype"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/os/gtimer"
|
||||
)
|
||||
|
||||
// TPool is an Object-Reusable Pool.
|
||||
type TPool[T any] struct {
|
||||
list *glist.TList[*tPoolItem[T]] // Available/idle items list.
|
||||
closed *gtype.Bool // Whether the pool is closed.
|
||||
TTL time.Duration // Time To Live for pool items.
|
||||
NewFunc func() (T, error) // Callback function to create pool item.
|
||||
// ExpireFunc is the function for expired items destruction.
|
||||
// This function needs to be defined when the pool items
|
||||
// need to perform additional destruction operations.
|
||||
// Eg: net.Conn, os.File, etc.
|
||||
ExpireFunc func(T)
|
||||
}
|
||||
|
||||
// TPool item.
|
||||
type tPoolItem[T any] struct {
|
||||
value T // Item value.
|
||||
expireAt int64 // Expire timestamp in milliseconds.
|
||||
}
|
||||
|
||||
// TPoolNewFunc Creation function for object.
|
||||
type TPoolNewFunc[T any] func() (T, error)
|
||||
|
||||
// TPoolExpireFunc Destruction function for object.
|
||||
type TPoolExpireFunc[T any] func(T)
|
||||
|
||||
// NewTPool creates and returns a new object pool.
|
||||
// To ensure execution efficiency, the expiration time cannot be modified once it is set.
|
||||
//
|
||||
// Note the expiration logic:
|
||||
// ttl = 0 : not expired;
|
||||
// ttl < 0 : immediate expired after use;
|
||||
// ttl > 0 : timeout expired;
|
||||
func NewTPool[T any](ttl time.Duration, newFunc TPoolNewFunc[T], expireFunc ...TPoolExpireFunc[T]) *TPool[T] {
|
||||
r := &TPool[T]{
|
||||
list: glist.NewT[*tPoolItem[T]](true),
|
||||
closed: gtype.NewBool(),
|
||||
TTL: ttl,
|
||||
NewFunc: newFunc,
|
||||
}
|
||||
if len(expireFunc) > 0 {
|
||||
r.ExpireFunc = expireFunc[0]
|
||||
}
|
||||
gtimer.AddSingleton(context.Background(), time.Second, r.checkExpireItems)
|
||||
return r
|
||||
}
|
||||
|
||||
// Put puts an item to pool.
|
||||
func (p *TPool[T]) Put(value T) error {
|
||||
if p.closed.Val() {
|
||||
return gerror.NewCode(gcode.CodeInvalidOperation, "pool is closed")
|
||||
}
|
||||
item := &tPoolItem[T]{
|
||||
value: value,
|
||||
}
|
||||
if p.TTL == 0 {
|
||||
item.expireAt = 0
|
||||
} else {
|
||||
// As for Golang version < 1.13, there's no method Milliseconds for time.Duration.
|
||||
// So we need calculate the milliseconds using its nanoseconds value.
|
||||
item.expireAt = gtime.TimestampMilli() + p.TTL.Nanoseconds()/1000000
|
||||
}
|
||||
p.list.PushBack(item)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustPut puts an item to pool, it panics if any error occurs.
|
||||
func (p *TPool[T]) MustPut(value T) {
|
||||
if err := p.Put(value); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears pool, which means it will remove all items from pool.
|
||||
func (p *TPool[T]) Clear() {
|
||||
if p.ExpireFunc != nil {
|
||||
for {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
p.ExpireFunc(r.value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.list.RemoveAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Get picks and returns an item from pool. If the pool is empty and NewFunc is defined,
|
||||
// it creates and returns one from NewFunc.
|
||||
func (p *TPool[T]) Get() (value T, err error) {
|
||||
for !p.closed.Val() {
|
||||
if f := p.list.PopFront(); f != nil {
|
||||
if f.expireAt == 0 || f.expireAt > gtime.TimestampMilli() {
|
||||
return f.value, nil
|
||||
} else if p.ExpireFunc != nil {
|
||||
// TODO: move expire function calling asynchronously out from `Get` operation.
|
||||
p.ExpireFunc(f.value)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.NewFunc != nil {
|
||||
return p.NewFunc()
|
||||
}
|
||||
err = gerror.NewCode(gcode.CodeInvalidOperation, "pool is empty")
|
||||
return
|
||||
}
|
||||
|
||||
// Size returns the count of available items of pool.
|
||||
func (p *TPool[T]) Size() int {
|
||||
return p.list.Len()
|
||||
}
|
||||
|
||||
// Close closes the pool. If `p` has ExpireFunc,
|
||||
// then it automatically closes all items using this function before it's closed.
|
||||
// Commonly you do not need to call this function manually.
|
||||
func (p *TPool[T]) Close() {
|
||||
p.closed.Set(true)
|
||||
}
|
||||
|
||||
// checkExpire removes expired items from pool in every second.
|
||||
func (p *TPool[T]) checkExpireItems(ctx context.Context) {
|
||||
if p.closed.Val() {
|
||||
// If p has ExpireFunc,
|
||||
// then it must close all items using this function.
|
||||
if p.ExpireFunc != nil {
|
||||
for {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
p.ExpireFunc(r.value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
gtimer.Exit()
|
||||
}
|
||||
// All items do not expire.
|
||||
if p.TTL == 0 {
|
||||
return
|
||||
}
|
||||
// The latest item expire timestamp in milliseconds.
|
||||
var latestExpire int64 = -1
|
||||
// Retrieve the current timestamp in milliseconds, it expires the items
|
||||
// by comparing with this timestamp. It is not accurate comparison for
|
||||
// every item expired, but high performance.
|
||||
var timestampMilli = gtime.TimestampMilli()
|
||||
for latestExpire <= timestampMilli {
|
||||
if item := p.list.PopFront(); item != nil {
|
||||
latestExpire = item.expireAt
|
||||
// TODO improve the auto-expiration mechanism of the pool.
|
||||
if item.expireAt > timestampMilli {
|
||||
p.list.PushFront(item)
|
||||
break
|
||||
}
|
||||
if p.ExpireFunc != nil {
|
||||
p.ExpireFunc(item.value)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
112
container/gpool/gpool_z_unit_generic_test.go
Normal file
112
container/gpool/gpool_z_unit_generic_test.go
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gpool_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gpool"
|
||||
"github.com/gogf/gf/v2/container/gtype"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_TPool_Int(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a pool for int
|
||||
var (
|
||||
newFunc = func() (int, error) {
|
||||
return 100, nil
|
||||
}
|
||||
expireVal = gtype.NewInt(0)
|
||||
expireFunc = func(i int) {
|
||||
expireVal.Set(i)
|
||||
}
|
||||
)
|
||||
|
||||
// TTL = 0, no expiration by time
|
||||
p := gpool.NewTPool(0, newFunc, expireFunc)
|
||||
|
||||
// Test Put and Get
|
||||
p.Put(1)
|
||||
p.Put(2)
|
||||
t.Assert(p.Size(), 2)
|
||||
|
||||
v, err := p.Get()
|
||||
t.AssertNil(err)
|
||||
t.AssertIN(v, g.Slice{1, 2})
|
||||
|
||||
v, err = p.Get()
|
||||
t.AssertNil(err)
|
||||
t.AssertIN(v, g.Slice{1, 2})
|
||||
|
||||
t.Assert(p.Size(), 0)
|
||||
|
||||
// Test NewFunc when empty
|
||||
v, err = p.Get()
|
||||
t.AssertNil(err)
|
||||
t.Assert(v, 100)
|
||||
|
||||
// Test Clear and ExpireFunc
|
||||
p.Put(50)
|
||||
t.Assert(p.Size(), 1)
|
||||
p.Clear()
|
||||
t.Assert(p.Size(), 0)
|
||||
t.Assert(expireVal.Val(), 50)
|
||||
|
||||
// Test Close
|
||||
p.Put(60)
|
||||
p.Close()
|
||||
// Close should trigger expire for existing items?
|
||||
// Looking at implementation: Close() sets closed=true.
|
||||
// It does NOT automatically clear items unless checkExpireItems runs or we call Clear?
|
||||
// Wait, checkExpireItems checks closed.Val(). If closed, it clears items.
|
||||
// But checkExpireItems runs in a separate goroutine every second.
|
||||
// So we might need to wait or trigger it.
|
||||
// Actually, let's check the implementation of Close again.
|
||||
/*
|
||||
func (p *TPool[T]) Close() {
|
||||
p.closed.Set(true)
|
||||
}
|
||||
*/
|
||||
// And checkExpireItems:
|
||||
/*
|
||||
func (p *TPool[T]) checkExpireItems(ctx context.Context) {
|
||||
if p.closed.Val() {
|
||||
// ... clears items ...
|
||||
gtimer.Exit()
|
||||
}
|
||||
// ...
|
||||
}
|
||||
*/
|
||||
// So it relies on the timer to clean up.
|
||||
})
|
||||
}
|
||||
|
||||
func Test_TPool_Struct(t *testing.T) {
|
||||
type User struct {
|
||||
Id int
|
||||
Name string
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
p := gpool.NewTPool[User](time.Hour, nil)
|
||||
u1 := User{Id: 1, Name: "john"}
|
||||
p.Put(u1)
|
||||
|
||||
v, err := p.Get()
|
||||
t.AssertNil(err)
|
||||
t.Assert(v, u1)
|
||||
|
||||
// Test empty with no NewFunc
|
||||
v, err = p.Get()
|
||||
t.AssertNE(err, nil)
|
||||
t.Assert(err.Error(), "pool is empty")
|
||||
t.Assert(v, User{}) // Zero value
|
||||
})
|
||||
}
|
||||
@ -77,7 +77,7 @@ func Test_Gpool(t *testing.T) {
|
||||
t.Assert(err2, errors.New("pool is empty"))
|
||||
t.Assert(v2, nil)
|
||||
// test close expireFunc
|
||||
for index := 0; index < 10; index++ {
|
||||
for index := range 10 {
|
||||
p2.Put(index)
|
||||
}
|
||||
t.Assert(p2.Size(), 10)
|
||||
|
||||
Reference in New Issue
Block a user