|
| 1 | +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. |
| 2 | +// |
| 3 | +// This Source Code Form is subject to the terms of the MIT License. |
| 4 | +// If a copy of the MIT was not distributed with this file, |
| 5 | +// You can obtain one at https://github.com/gogf/gf. |
| 6 | + |
| 7 | +package gpool |
| 8 | + |
| 9 | +import ( |
| 10 | + "context" |
| 11 | + "time" |
| 12 | + |
| 13 | + "github.com/gogf/gf/v2/container/glist" |
| 14 | + "github.com/gogf/gf/v2/container/gtype" |
| 15 | + "github.com/gogf/gf/v2/errors/gcode" |
| 16 | + "github.com/gogf/gf/v2/errors/gerror" |
| 17 | + "github.com/gogf/gf/v2/os/gtime" |
| 18 | + "github.com/gogf/gf/v2/os/gtimer" |
| 19 | +) |
| 20 | + |
| 21 | +// TPool is an Object-Reusable Pool. |
| 22 | +type TPool[T any] struct { |
| 23 | + list *glist.TList[*tPoolItem[T]] // Available/idle items list. |
| 24 | + closed *gtype.Bool // Whether the pool is closed. |
| 25 | + TTL time.Duration // Time To Live for pool items. |
| 26 | + NewFunc func() (T, error) // Callback function to create pool item. |
| 27 | + // ExpireFunc is the function for expired items destruction. |
| 28 | + // This function needs to be defined when the pool items |
| 29 | + // need to perform additional destruction operations. |
| 30 | + // Eg: net.Conn, os.File, etc. |
| 31 | + ExpireFunc func(T) |
| 32 | +} |
| 33 | + |
| 34 | +// TPool item. |
| 35 | +type tPoolItem[T any] struct { |
| 36 | + value T // Item value. |
| 37 | + expireAt int64 // Expire timestamp in milliseconds. |
| 38 | +} |
| 39 | + |
| 40 | +// TPoolNewFunc Creation function for object. |
| 41 | +type TPoolNewFunc[T any] func() (T, error) |
| 42 | + |
| 43 | +// TPoolExpireFunc Destruction function for object. |
| 44 | +type TPoolExpireFunc[T any] func(T) |
| 45 | + |
| 46 | +// NewTPool creates and returns a new object pool. |
| 47 | +// To ensure execution efficiency, the expiration time cannot be modified once it is set. |
| 48 | +// |
| 49 | +// Note the expiration logic: |
| 50 | +// ttl = 0 : not expired; |
| 51 | +// ttl < 0 : immediate expired after use; |
| 52 | +// ttl > 0 : timeout expired; |
| 53 | +func NewTPool[T any](ttl time.Duration, newFunc TPoolNewFunc[T], expireFunc ...TPoolExpireFunc[T]) *TPool[T] { |
| 54 | + r := &TPool[T]{ |
| 55 | + list: glist.NewT[*tPoolItem[T]](true), |
| 56 | + closed: gtype.NewBool(), |
| 57 | + TTL: ttl, |
| 58 | + NewFunc: newFunc, |
| 59 | + } |
| 60 | + if len(expireFunc) > 0 { |
| 61 | + r.ExpireFunc = expireFunc[0] |
| 62 | + } |
| 63 | + gtimer.AddSingleton(context.Background(), time.Second, r.checkExpireItems) |
| 64 | + return r |
| 65 | +} |
| 66 | + |
| 67 | +// Put puts an item to pool. |
| 68 | +func (p *TPool[T]) Put(value T) error { |
| 69 | + if p.closed.Val() { |
| 70 | + return gerror.NewCode(gcode.CodeInvalidOperation, "pool is closed") |
| 71 | + } |
| 72 | + item := &tPoolItem[T]{ |
| 73 | + value: value, |
| 74 | + } |
| 75 | + if p.TTL == 0 { |
| 76 | + item.expireAt = 0 |
| 77 | + } else { |
| 78 | + // As for Golang version < 1.13, there's no method Milliseconds for time.Duration. |
| 79 | + // So we need calculate the milliseconds using its nanoseconds value. |
| 80 | + item.expireAt = gtime.TimestampMilli() + p.TTL.Nanoseconds()/1000000 |
| 81 | + } |
| 82 | + p.list.PushBack(item) |
| 83 | + return nil |
| 84 | +} |
| 85 | + |
| 86 | +// MustPut puts an item to pool, it panics if any error occurs. |
| 87 | +func (p *TPool[T]) MustPut(value T) { |
| 88 | + if err := p.Put(value); err != nil { |
| 89 | + panic(err) |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +// Clear clears pool, which means it will remove all items from pool. |
| 94 | +func (p *TPool[T]) Clear() { |
| 95 | + if p.ExpireFunc != nil { |
| 96 | + for { |
| 97 | + if r := p.list.PopFront(); r != nil { |
| 98 | + p.ExpireFunc(r.value) |
| 99 | + } else { |
| 100 | + break |
| 101 | + } |
| 102 | + } |
| 103 | + } else { |
| 104 | + p.list.RemoveAll() |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +// Get picks and returns an item from pool. If the pool is empty and NewFunc is defined, |
| 109 | +// it creates and returns one from NewFunc. |
| 110 | +func (p *TPool[T]) Get() (value T, err error) { |
| 111 | + for !p.closed.Val() { |
| 112 | + if f := p.list.PopFront(); f != nil { |
| 113 | + if f.expireAt == 0 || f.expireAt > gtime.TimestampMilli() { |
| 114 | + return f.value, nil |
| 115 | + } else if p.ExpireFunc != nil { |
| 116 | + // TODO: move expire function calling asynchronously out from `Get` operation. |
| 117 | + p.ExpireFunc(f.value) |
| 118 | + } |
| 119 | + } else { |
| 120 | + break |
| 121 | + } |
| 122 | + } |
| 123 | + if p.NewFunc != nil { |
| 124 | + return p.NewFunc() |
| 125 | + } |
| 126 | + err = gerror.NewCode(gcode.CodeInvalidOperation, "pool is empty") |
| 127 | + return |
| 128 | +} |
| 129 | + |
| 130 | +// Size returns the count of available items of pool. |
| 131 | +func (p *TPool[T]) Size() int { |
| 132 | + return p.list.Len() |
| 133 | +} |
| 134 | + |
| 135 | +// Close closes the pool. If `p` has ExpireFunc, |
| 136 | +// then it automatically closes all items using this function before it's closed. |
| 137 | +// Commonly you do not need to call this function manually. |
| 138 | +func (p *TPool[T]) Close() { |
| 139 | + p.closed.Set(true) |
| 140 | +} |
| 141 | + |
| 142 | +// checkExpire removes expired items from pool in every second. |
| 143 | +func (p *TPool[T]) checkExpireItems(ctx context.Context) { |
| 144 | + if p.closed.Val() { |
| 145 | + // If p has ExpireFunc, |
| 146 | + // then it must close all items using this function. |
| 147 | + if p.ExpireFunc != nil { |
| 148 | + for { |
| 149 | + if r := p.list.PopFront(); r != nil { |
| 150 | + p.ExpireFunc(r.value) |
| 151 | + } else { |
| 152 | + break |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + gtimer.Exit() |
| 157 | + } |
| 158 | + // All items do not expire. |
| 159 | + if p.TTL == 0 { |
| 160 | + return |
| 161 | + } |
| 162 | + // The latest item expire timestamp in milliseconds. |
| 163 | + var latestExpire int64 = -1 |
| 164 | + // Retrieve the current timestamp in milliseconds, it expires the items |
| 165 | + // by comparing with this timestamp. It is not accurate comparison for |
| 166 | + // every item expired, but high performance. |
| 167 | + var timestampMilli = gtime.TimestampMilli() |
| 168 | + for latestExpire <= timestampMilli { |
| 169 | + if item := p.list.PopFront(); item != nil { |
| 170 | + latestExpire = item.expireAt |
| 171 | + // TODO improve the auto-expiration mechanism of the pool. |
| 172 | + if item.expireAt > timestampMilli { |
| 173 | + p.list.PushFront(item) |
| 174 | + break |
| 175 | + } |
| 176 | + if p.ExpireFunc != nil { |
| 177 | + p.ExpireFunc(item.value) |
| 178 | + } |
| 179 | + } else { |
| 180 | + break |
| 181 | + } |
| 182 | + } |
| 183 | +} |
0 commit comments