@@ -2,6 +2,7 @@ package pool
2
2
3
3
import (
4
4
"context"
5
+ "sync"
5
6
"time"
6
7
7
8
"golang.org/x/sync/errgroup"
@@ -38,12 +39,32 @@ type (
38
39
createTimeout time.Duration
39
40
closeTimeout time.Duration
40
41
41
- mu xsync.Mutex
42
- idle []PT
43
- index map [PT ]struct {}
44
- done chan struct {}
42
+ // queue is a buffered channel that holds ready-to-use items.
43
+ // Newly created items are sent to this channel by spawner goroutine.
44
+ // getItem reads from this channel to get items for usage.
45
+ // putItems sends item to this channel when it's no longer needed.
46
+ // Len of the buffered channel should be equal to configured pool size
47
+ // (MUST NOT be less).
48
+ // If item is in this queue, then it's considered idle (not in use).
49
+ queue chan PT
50
+
51
+ // itemTokens similarly to 'queue' is a buffered channel, and it holds 'tokens'.
52
+ // Presence of token in this channel indicates that there's requests to create item.
53
+ // Every token will eventually result in creation of new item (spawnItems makes sure of that).
54
+ //
55
+ // itemTokens must have same size as queue.
56
+ // Sum of every existing token plus sum of every existing item in any time MUST be equal
57
+ // to pool size. New token MUST be added by getItem/putItem if they discovered item in use to be
58
+ // no good and discarded it.
59
+ itemTokens chan struct {}
60
+
61
+ done chan struct {}
45
62
46
63
stats * safeStats
64
+
65
+ spawnCancel context.CancelFunc
66
+
67
+ wg * sync.WaitGroup
47
68
}
48
69
option [PT Item [T ], T any ] func (p * Pool [PT , T ])
49
70
)
@@ -159,6 +180,15 @@ func New[PT Item[T], T any](
159
180
}
160
181
}
161
182
183
+ p .queue = make (chan PT , p .limit )
184
+ p .itemTokens = make (chan struct {}, p .limit )
185
+ go func () {
186
+ // fill tokens
187
+ for i := 0 ; i < p .limit ; i ++ {
188
+ p .itemTokens <- struct {}{}
189
+ }
190
+ }()
191
+
162
192
onDone := p .trace .OnNew (& NewStartInfo {
163
193
Context : & ctx ,
164
194
Call : stack .FunctionID ("github.com/ydb-platform/ydb-go-sdk/3/internal/pool.New" ),
@@ -172,16 +202,73 @@ func New[PT Item[T], T any](
172
202
173
203
p .createItem = createItemWithTimeoutHandling (p .createItem , p )
174
204
175
- p .idle = make ([]PT , 0 , p .limit )
176
- p .index = make (map [PT ]struct {}, p .limit )
177
205
p .stats = & safeStats {
178
206
v : stats.Stats {Limit : p .limit },
179
207
onChange : p .trace .OnChange ,
180
208
}
181
209
210
+ var spawnCtx context.Context
211
+ p .wg = & sync.WaitGroup {}
212
+ spawnCtx , p .spawnCancel = xcontext .WithCancel (xcontext .ValueOnly (ctx ))
213
+ p .wg .Add (1 )
214
+ go p .spawnItems (spawnCtx )
215
+
182
216
return p
183
217
}
184
218
219
+ // spawnItems creates one item per each available itemToken and sends new item to internal item queue.
220
+ // It ensures that pool would always have amount of connections equal to configured limit.
221
+ // If item creation ended with error it will be retried infinity with configured interval until success.
222
+ func (p * Pool [PT , T ]) spawnItems (ctx context.Context ) {
223
+ defer p .wg .Done ()
224
+ for {
225
+ select {
226
+ case <- ctx .Done ():
227
+ return
228
+ case <- p .done :
229
+ return
230
+ case <- p .itemTokens :
231
+ // got token, must create item
232
+ createLoop:
233
+ for {
234
+ select {
235
+ case <- ctx .Done ():
236
+ return
237
+ case <- p .done :
238
+ return
239
+ default :
240
+ p .wg .Add (1 )
241
+ err := p .trySpawn (ctx )
242
+ if err == nil {
243
+ break createLoop
244
+ }
245
+ }
246
+ // spawn was unsuccessful, need to try again.
247
+ // token must always result in new item and not be lost.
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ func (p * Pool [PT , T ]) trySpawn (ctx context.Context ) error {
254
+ defer p .wg .Done ()
255
+ item , err := p .createItem (ctx )
256
+ if err != nil {
257
+ return err
258
+ }
259
+ // item was created successfully, put it in queue
260
+ select {
261
+ case <- ctx .Done ():
262
+ return nil
263
+ case <- p .done :
264
+ return nil
265
+ case p .queue <- item :
266
+ p .stats .Idle ().Inc ()
267
+ }
268
+
269
+ return nil
270
+ }
271
+
185
272
// defaultCreateItem returns a new item
186
273
func defaultCreateItem [T any , PT Item [T ]](ctx context.Context ) (PT , error ) {
187
274
var item T
@@ -247,31 +334,12 @@ func createItemWithContext[PT Item[T], T any](
247
334
return xerrors .WithStackTrace (err )
248
335
}
249
336
250
- needCloseItem := true
251
- defer func () {
252
- if needCloseItem {
253
- _ = p .closeItem (ctx , newItem )
254
- }
255
- }()
256
-
257
337
select {
258
338
case <- p .done :
259
339
return xerrors .WithStackTrace (errClosedPool )
260
340
case <- ctx .Done ():
261
- p .mu .Lock ()
262
- defer p .mu .Unlock ()
263
-
264
- if len (p .index ) < p .limit {
265
- p .idle = append (p .idle , newItem )
266
- p .index [newItem ] = struct {}{}
267
- p .stats .Index ().Inc ()
268
- needCloseItem = false
269
- }
270
-
271
341
return xerrors .WithStackTrace (ctx .Err ())
272
342
case ch <- newItem :
273
- needCloseItem = false
274
-
275
343
return nil
276
344
}
277
345
}
@@ -280,6 +348,10 @@ func (p *Pool[PT, T]) Stats() stats.Stats {
280
348
return p .stats .Get ()
281
349
}
282
350
351
+ // getItem retrieves item from the queue.
352
+ // If retrieved item happens to be not alive, then it's destroyed
353
+ // and tokens queue is filled to +1 so new item can be created by spawner goroutine.
354
+ // After, the process will be repeated until alive item is retrieved.
283
355
func (p * Pool [PT , T ]) getItem (ctx context.Context ) (_ PT , finalErr error ) {
284
356
onDone := p .trace .OnGet (& GetStartInfo {
285
357
Context : & ctx ,
@@ -295,48 +367,30 @@ func (p *Pool[PT, T]) getItem(ctx context.Context) (_ PT, finalErr error) {
295
367
return nil , xerrors .WithStackTrace (err )
296
368
}
297
369
298
- select {
299
- case <- p .done :
300
- return nil , xerrors .WithStackTrace (errClosedPool )
301
- case <- ctx .Done ():
302
- return nil , xerrors .WithStackTrace (ctx .Err ())
303
- default :
304
- var item PT
305
- p .mu .WithLock (func () {
306
- if len (p .idle ) > 0 {
307
- item , p .idle = p .idle [0 ], p .idle [1 :]
308
- p .stats .Idle ().Dec ()
309
- }
310
- })
311
-
312
- if item != nil {
313
- if item .IsAlive () {
314
- return item , nil
315
- }
316
- _ = p .closeItem (ctx , item )
317
- p .mu .WithLock (func () {
318
- delete (p .index , item )
319
- })
320
- p .stats .Index ().Dec ()
321
- }
322
-
323
- item , err := p .createItem (ctx )
324
- if err != nil {
325
- return nil , xerrors .WithStackTrace (err )
326
- }
370
+ // get item and ensure it's alive.
371
+ // Infinite loop here guarantees that we either return alive item
372
+ // or block infinitely until we have one.
373
+ // It is assumed that calling code should use context if it wishes to time out the call.
374
+ for {
375
+ select {
376
+ case <- p .done :
377
+ return nil , xerrors .WithStackTrace (errClosedPool )
378
+ case <- ctx .Done ():
379
+ return nil , xerrors .WithStackTrace (ctx .Err ())
380
+ case item := <- p .queue : // get or wait for item
381
+ p .stats .Idle ().Dec ()
382
+ if item != nil {
383
+ if item .IsAlive () {
384
+ // item is alive, return it
327
385
328
- addedToIndex := false
329
- p .mu .WithLock (func () {
330
- if len (p .index ) < p .limit {
331
- p .index [item ] = struct {}{}
332
- addedToIndex = true
386
+ return item , nil
387
+ }
388
+ // item is not alive
389
+ _ = p .closeItem (ctx , item ) // clean up dead item
333
390
}
334
- })
335
- if addedToIndex {
336
- p .stats .Index ().Inc ()
391
+ p .itemTokens <- struct {}{} // signal spawn goroutine to create a new item
392
+ // and try again
337
393
}
338
-
339
- return item , nil
340
394
}
341
395
}
342
396
@@ -358,25 +412,28 @@ func (p *Pool[PT, T]) putItem(ctx context.Context, item PT) (finalErr error) {
358
412
select {
359
413
case <- p .done :
360
414
return xerrors .WithStackTrace (errClosedPool )
415
+ case <- ctx .Done ():
416
+ return xerrors .WithStackTrace (ctx .Err ())
361
417
default :
362
- if ! item .IsAlive () {
418
+ if item .IsAlive () {
419
+ // put back in the queue
420
+ select {
421
+ case <- p .done :
422
+ return xerrors .WithStackTrace (errClosedPool )
423
+ case <- ctx .Done ():
424
+ return xerrors .WithStackTrace (ctx .Err ())
425
+ case p .queue <- item :
426
+ p .stats .Idle ().Inc ()
427
+ }
428
+ } else {
429
+ // item is not alive
430
+ // add token and close
431
+ p .itemTokens <- struct {}{}
363
432
_ = p .closeItem (ctx , item )
364
-
365
- p .mu .WithLock (func () {
366
- delete (p .index , item )
367
- })
368
- p .stats .Index ().Dec ()
369
-
370
- return xerrors .WithStackTrace (errItemIsNotAlive )
371
433
}
372
-
373
- p .mu .WithLock (func () {
374
- p .idle = append (p .idle , item )
375
- })
376
- p .stats .Idle ().Inc ()
377
-
378
- return nil
379
434
}
435
+
436
+ return nil
380
437
}
381
438
382
439
func (p * Pool [PT , T ]) closeItem (ctx context.Context , item PT ) error {
@@ -412,14 +469,13 @@ func (p *Pool[PT, T]) try(ctx context.Context, f func(ctx context.Context, item
412
469
413
470
return xerrors .WithStackTrace (err )
414
471
}
472
+ p .stats .InUse ().Inc ()
415
473
416
474
defer func () {
417
475
_ = p .putItem (ctx , item )
476
+ p .stats .InUse ().Dec ()
418
477
}()
419
478
420
- p .stats .InUse ().Inc ()
421
- defer p .stats .InUse ().Dec ()
422
-
423
479
err = f (ctx , item )
424
480
if err != nil {
425
481
return xerrors .WithStackTrace (err )
@@ -479,17 +535,27 @@ func (p *Pool[PT, T]) Close(ctx context.Context) (finalErr error) {
479
535
})
480
536
}()
481
537
538
+ // canceling spawner (and any underlying createItem calls)
539
+ p .spawnCancel ()
540
+
541
+ // Only closing done channel.
542
+ // Due to multiple senders queue is not closed here,
543
+ // we're just making sure to drain it fully to close any existing item.
482
544
close (p .done )
483
545
484
- p .mu .Lock ()
485
- defer p .mu .Unlock ()
546
+ p .wg .Wait ()
486
547
487
548
var g errgroup.Group
488
- for item := range p .index {
489
- item := item
490
- g .Go (func () error {
491
- return item .Close (ctx )
492
- })
549
+ shutdownLoop:
550
+ for {
551
+ select {
552
+ case item := <- p .queue :
553
+ g .Go (func () error {
554
+ return item .Close (ctx )
555
+ })
556
+ default :
557
+ break shutdownLoop
558
+ }
493
559
}
494
560
if err := g .Wait (); err != nil {
495
561
return xerrors .WithStackTrace (err )
0 commit comments