From 2f9a8da00f69ffad8c1333e0daf0760abeda9290 Mon Sep 17 00:00:00 2001 From: Hunk Date: Sat, 25 Oct 2025 08:51:19 +0800 Subject: [PATCH 1/3] add t pool --- container/gpool/gpool.go | 138 +++------------------------ container/gpool/gpool_t.go | 185 +++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 127 deletions(-) create mode 100644 container/gpool/gpool_t.go diff --git a/container/gpool/gpool.go b/container/gpool/gpool.go index f6b5845480b..85bc157e322 100644 --- a/container/gpool/gpool.go +++ b/container/gpool/gpool.go @@ -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, - } - if len(expireFunc) > 0 { - r.ExpireFunc = expireFunc[0] + return &Pool{ + TPool: NewTPool(ttl, newFunc, expireFunc...), } - 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() } diff --git a/container/gpool/gpool_t.go b/container/gpool/gpool_t.go new file mode 100644 index 00000000000..7c183dcfde7 --- /dev/null +++ b/container/gpool/gpool_t.go @@ -0,0 +1,185 @@ +// 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.List // 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.New(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.(*tPoolItem[T]).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 r := p.list.PopFront(); r != nil { + f := r.(*tPoolItem[T]) + 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.(*tPoolItem[T]).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.(*tPoolItem[T]) + 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 + } + } +} From 14f017bdcf43dbb701eac4898bff5de3ef22dbd8 Mon Sep 17 00:00:00 2001 From: Hunk <54zhua@gmail.com> Date: Fri, 21 Nov 2025 17:52:16 +0800 Subject: [PATCH 2/3] improve base on TList --- container/gpool/gpool_t.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/container/gpool/gpool_t.go b/container/gpool/gpool_t.go index 7c183dcfde7..083e2214efa 100644 --- a/container/gpool/gpool_t.go +++ b/container/gpool/gpool_t.go @@ -20,10 +20,10 @@ import ( // TPool is an Object-Reusable Pool. type TPool[T any] 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() (T, error) // Callback function to create pool item. + 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. @@ -52,7 +52,7 @@ type TPoolExpireFunc[T any] func(T) // ttl > 0 : timeout expired; func NewTPool[T any](ttl time.Duration, newFunc TPoolNewFunc[T], expireFunc ...TPoolExpireFunc[T]) *TPool[T] { r := &TPool[T]{ - list: glist.New(true), + list: glist.NewT[*tPoolItem[T]](true), closed: gtype.NewBool(), TTL: ttl, NewFunc: newFunc, @@ -95,7 +95,7 @@ func (p *TPool[T]) Clear() { if p.ExpireFunc != nil { for { if r := p.list.PopFront(); r != nil { - p.ExpireFunc(r.(*tPoolItem[T]).value) + p.ExpireFunc(r.value) } else { break } @@ -109,8 +109,7 @@ func (p *TPool[T]) Clear() { // it creates and returns one from NewFunc. func (p *TPool[T]) Get() (value T, err error) { for !p.closed.Val() { - if r := p.list.PopFront(); r != nil { - f := r.(*tPoolItem[T]) + if f := p.list.PopFront(); f != nil { if f.expireAt == 0 || f.expireAt > gtime.TimestampMilli() { return f.value, nil } else if p.ExpireFunc != nil { @@ -148,7 +147,7 @@ func (p *TPool[T]) checkExpireItems(ctx context.Context) { if p.ExpireFunc != nil { for { if r := p.list.PopFront(); r != nil { - p.ExpireFunc(r.(*tPoolItem[T]).value) + p.ExpireFunc(r.value) } else { break } @@ -167,8 +166,7 @@ func (p *TPool[T]) checkExpireItems(ctx context.Context) { // every item expired, but high performance. var timestampMilli = gtime.TimestampMilli() for latestExpire <= timestampMilli { - if r := p.list.PopFront(); r != nil { - item := r.(*tPoolItem[T]) + if item := p.list.PopFront(); item != nil { latestExpire = item.expireAt // TODO improve the auto-expiration mechanism of the pool. if item.expireAt > timestampMilli { From 3aeca498a57a39124b30f3c1f3f76f1ea09cc238 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Thu, 27 Nov 2025 09:58:52 +0000 Subject: [PATCH 3/3] test(container/gpool): update Test_Gpool to use range in loop and add generic tests for TPool --- container/gpool/gpool_z_unit_generic_test.go | 112 +++++++++++++++++++ container/gpool/gpool_z_unit_test.go | 2 +- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 container/gpool/gpool_z_unit_generic_test.go diff --git a/container/gpool/gpool_z_unit_generic_test.go b/container/gpool/gpool_z_unit_generic_test.go new file mode 100644 index 00000000000..bff79ae242b --- /dev/null +++ b/container/gpool/gpool_z_unit_generic_test.go @@ -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 + }) +} diff --git a/container/gpool/gpool_z_unit_test.go b/container/gpool/gpool_z_unit_test.go index 9ffe94c9d43..7e7d5ce8297 100644 --- a/container/gpool/gpool_z_unit_test.go +++ b/container/gpool/gpool_z_unit_test.go @@ -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)