Skip to content

Commit 1c39288

Browse files
committed
pool: bound New connect by context, add WaitConnected
pool.New / pool.NewWithOpts no longer wait up to Opts.CheckTimeout for slow instances during the initial connect: they return once the first instance connects, cancel the remaining dials, and let the background controllers reconnect them. The connect wait is bounded only by the supplied context; Opts.CheckTimeout now only drives the reconnect and role-relocate timer. Add Pool.WaitConnected(ctx, mode) (also on the Pooler interface) to block until a connection in the given mode is ready, since New may return a pool with no live connections when instances are unreachable.
1 parent 64dacc6 commit 1c39288

5 files changed

Lines changed: 234 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
3636
* New `MockRequestNamed` type for verifying specific requests in tests.
3737
* New `test_helpers.ExecuteOnAll` function to execute operations on all
3838
instances in parallel with context support.
39+
* New `pool.Pool.WaitConnected(ctx, mode)` method (also added to the
40+
`pool.Pooler` interface) that blocks until the pool holds a connection
41+
satisfying the mode, or `ctx` is done, or the pool is closed. `pool.New`
42+
still returns a pool even when no instance is reachable, so callers that
43+
require a usable connection before proceeding should use `WaitConnected`
44+
instead of the racy `ConnectedNow` snapshot.
3945

4046
### Changed
4147

@@ -49,6 +55,13 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
4955
* `box.New` returns an error instead of panic (#448).
5056
* Now cases of `<-ctx.Done()` returns wrapped error provided by `ctx.Cause()`.
5157
Allows you compare it using `errors.Is/As` (#457).
58+
* `pool.NewWithOpts`/`pool.New` no longer wait up to `Opts.CheckTimeout` for
59+
slow instances during the initial connect: they return as soon as the first
60+
instance connects (cancelling the remaining dials, which the background
61+
controllers retry). The connect wait is now bounded only by the supplied
62+
`ctx`. `Opts.CheckTimeout` is unchanged for the reconnect/role-relocate
63+
timer. Use `pool.Pool.WaitConnected` if you need a connection in a
64+
specific mode to be ready before proceeding.
5265
* Removed deprecated `pool` methods, related interfaces and tests are
5366
updated (#478).
5467
* Removed deprecated `box.session.push()` support: Future.AppendPush()

MIGRATION.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,76 @@ TODO
7575
* `pool.Pool.DoInstance()` renamed to `pool.Pool.DoOn()`.
7676
* `pool.Connect()` renamed to `pool.New()`, `pool.ConnectWithOpts()` renamed to
7777
`pool.NewWithOpts()`.
78+
* `pool.New()`/`pool.NewWithOpts()` no longer wait up to `pool.Opts.CheckTimeout`
79+
for slow instances during the initial connect. They now return as soon as the
80+
first instance connects, cancel the remaining dials, and let the background
81+
controllers reconnect the rest (within `CheckTimeout`). The connect wait is
82+
bounded only by the supplied `ctx`. `pool.Opts.CheckTimeout` is unchanged for
83+
the reconnect/role-relocate timer. Consequences: the returned pool may have
84+
fewer instances connected at return time; and if `ctx` has no deadline and
85+
every instance is unreachable, a pool with no live connections is returned
86+
(as before, it keeps reconnecting). Use the new
87+
`pool.Pool.WaitConnected(ctx, mode)` to block until a connection in the
88+
required mode is ready — it is the readiness counterpart to the racy
89+
`ConnectedNow` snapshot and is also part of the `pool.Pooler` interface.
90+
91+
Before:
92+
```Go
93+
connPool, err := pool.ConnectWithOpts(ctx, instances, poolOpts)
94+
// a working connection was likely (but not guaranteed) available here
95+
```
96+
97+
After:
98+
```Go
99+
connPool, err := pool.NewWithOpts(ctx, instances, poolOpts)
100+
if err != nil {
101+
// ...
102+
}
103+
// ctx should carry a deadline; WaitConnected returns ctx.Err() on timeout.
104+
if err := connPool.WaitConnected(ctx, pool.ModeRW); err != nil {
105+
// ...
106+
}
107+
```
108+
109+
If you relied on the old behaviour where the constructor effectively
110+
blocked until a connection was (likely) available, reproduce it explicitly
111+
by giving the call a deadline via `context.WithTimeout` and waiting for the
112+
pool to become ready:
113+
```Go
114+
ctx, cancel := context.WithTimeout(context.Background(), connectTimeout)
115+
defer cancel()
116+
117+
connPool, err := pool.NewWithOpts(ctx, instances, poolOpts)
118+
if err != nil {
119+
// ctx expired before any instance connected, or an invalid argument
120+
return err
121+
}
122+
if err := connPool.WaitConnected(ctx, pool.ModeRW); err != nil {
123+
// no usable connection within connectTimeout (returns ctx.Err()),
124+
// or the pool was closed (pool.ErrClosed)
125+
_ = connPool.Close()
126+
return err
127+
}
128+
// here a master connection is guaranteed to be available
129+
```
130+
Note `pool.NewWithOpts` no longer takes any timeout of its own — pass a
131+
bounded `context.Context` instead. `pool.Opts.CheckTimeout` only controls
132+
the background reconnect/role-relocate timer now.
133+
* New `pool.Pool.WaitConnected(ctx context.Context, mode pool.Mode) error`
134+
method (also added to the `pool.Pooler` interface). It blocks until the pool
135+
holds a connection satisfying `mode`, returning `nil`; it returns `ctx.Err()`
136+
if `ctx` is done first and `pool.ErrClosed` if the pool is (or becomes)
137+
closed. Because `pool.New`/`pool.NewWithOpts` may return a pool with no live
138+
connections (they do not fail when instances are unreachable), prefer
139+
`WaitConnected` over the racy `ConnectedNow` snapshot when you need a usable
140+
connection before proceeding. Typical usage:
141+
```Go
142+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
143+
defer cancel()
144+
if err := connPool.WaitConnected(ctx, pool.ModeRW); err != nil {
145+
// no master available within the timeout (or pool closed)
146+
}
147+
```
78148
* `pool` enum constants renamed to use prefix: `ANY``ModeAny`, `RW`
79149
`ModeRW`, `RO``ModeRO`, `PreferRW``ModePreferRW`, `PreferRO`
80150
`ModePreferRO`, `UnknownRole``RoleUnknown`, `MasterRole``RoleMaster`,

pool/pool.go

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,16 @@ type Pool struct {
147147
// WithGroup("tarantool") namespace.
148148
rawLogger *slog.Logger
149149

150-
state state
151-
done chan struct{}
152-
roPool *roundRobinStrategy
153-
rwPool *roundRobinStrategy
154-
anyPool *roundRobinStrategy
155-
poolsMutex sync.RWMutex
150+
state state
151+
done chan struct{}
152+
roPool *roundRobinStrategy
153+
rwPool *roundRobinStrategy
154+
anyPool *roundRobinStrategy
155+
poolsMutex sync.RWMutex
156+
// connEvent is closed and replaced with a fresh channel whenever the set
157+
// of active connections changes. It lets WaitConnected block until the
158+
// pool becomes usable. Guarded by poolsMutex.
159+
connEvent chan struct{}
156160
watcherContainer watcherContainer
157161
}
158162

@@ -204,6 +208,13 @@ func newEndpoint(name string, dialer tarantool.Dialer, opts tarantool.Opts) *end
204208

205209
// NewWithOpts creates pool for instances with specified instances and
206210
// opts. Instances must have unique names.
211+
//
212+
// It dials the instances concurrently and returns once the first connection
213+
// is established; ctx bounds that wait. If ctx is done before any instance
214+
// connects, an error is returned. If ctx has no deadline and every instance
215+
// is unreachable, a pool with no live connections is returned (the pool keeps
216+
// reconnecting in the background) — use Pool.WaitConnected if you need a
217+
// usable connection before proceeding.
207218
func NewWithOpts(ctx context.Context, instances []Instance,
208219
opts Opts) (*Pool, error) {
209220
unique := make(map[string]bool)
@@ -232,13 +243,14 @@ func NewWithOpts(ctx context.Context, instances []Instance,
232243
anyPool := newRoundRobinStrategy(size)
233244

234245
p := &Pool{
235-
ends: make(map[string]*endpoint),
236-
opts: opts,
237-
state: connectedState,
238-
done: make(chan struct{}),
239-
rwPool: rwPool,
240-
roPool: roPool,
241-
anyPool: anyPool,
246+
ends: make(map[string]*endpoint),
247+
opts: opts,
248+
state: connectedState,
249+
done: make(chan struct{}),
250+
rwPool: rwPool,
251+
roPool: roPool,
252+
anyPool: anyPool,
253+
connEvent: make(chan struct{}),
242254
}
243255

244256
if opts.Logger == nil {
@@ -248,33 +260,26 @@ func NewWithOpts(ctx context.Context, instances []Instance,
248260
p.rawLogger = opts.Logger
249261
}
250262

263+
// Dial all instances concurrently. Block until the first one connects
264+
// (so the returned pool is immediately usable), then cancel the slower
265+
// dials and let the background controllers reconnect them. The only
266+
// deadline is ctx: if it expires before anything connects, fail; if it
267+
// has no deadline and every dial fails fast, return a pool with no live
268+
// connections (the controllers keep retrying) — callers that require a
269+
// connection should follow up with Pool.WaitConnected.
251270
fillCtx, fillCancel := context.WithCancel(ctx)
252271
defer fillCancel()
253272

254-
var timeout <-chan time.Time
255-
256-
timeout = make(chan time.Time)
257273
filled := p.fillPools(fillCtx, instances)
258-
done := 0
259-
success := len(instances) == 0
260-
261-
for done < len(instances) {
262-
select {
263-
case <-timeout:
274+
connected := len(instances) == 0
275+
for range instances {
276+
if err := <-filled; err == nil && !connected {
277+
connected = true
264278
fillCancel()
265-
// To be sure that the branch is called only once.
266-
timeout = make(chan time.Time)
267-
case err := <-filled:
268-
done++
269-
270-
if err == nil && !success {
271-
timeout = time.After(opts.CheckTimeout)
272-
success = true
273-
}
274279
}
275280
}
276281

277-
if !success && ctx.Err() != nil {
282+
if !connected && ctx.Err() != nil {
278283
p.state.set(closedState)
279284
return nil, ctx.Err()
280285
}
@@ -297,30 +302,68 @@ func New(ctx context.Context, instances []Instance) (*Pool, error) {
297302
return NewWithOpts(ctx, instances, opts)
298303
}
299304

300-
// ConnectedNow gets connected status of pool.
301-
func (p *Pool) ConnectedNow(mode Mode) (bool, error) {
302-
p.poolsMutex.RLock()
303-
defer p.poolsMutex.RUnlock()
304-
305-
if p.state.get() != connectedState {
306-
return false, nil
307-
}
305+
// hasConnection reports whether the pool currently holds a connection
306+
// satisfying mode. The caller must hold poolsMutex (read or write).
307+
func (p *Pool) hasConnection(mode Mode) (bool, error) {
308308
switch mode {
309309
case ModeAny:
310310
return !p.anyPool.IsEmpty(), nil
311311
case ModeRW:
312312
return !p.rwPool.IsEmpty(), nil
313313
case ModeRO:
314314
return !p.roPool.IsEmpty(), nil
315-
case ModePreferRW:
316-
fallthrough
317-
case ModePreferRO:
315+
case ModePreferRW, ModePreferRO:
318316
return !p.rwPool.IsEmpty() || !p.roPool.IsEmpty(), nil
319317
default:
320318
return false, ErrNoHealthyInstance
321319
}
322320
}
323321

322+
// ConnectedNow gets connected status of pool.
323+
func (p *Pool) ConnectedNow(mode Mode) (bool, error) {
324+
p.poolsMutex.RLock()
325+
defer p.poolsMutex.RUnlock()
326+
327+
if p.state.get() != connectedState {
328+
return false, nil
329+
}
330+
return p.hasConnection(mode)
331+
}
332+
333+
// WaitConnected blocks until the pool holds at least one connection
334+
// satisfying mode, returning nil. It returns ctx.Err() if ctx is done first
335+
// and ErrClosed if the pool is (or becomes) closed.
336+
//
337+
// NewWithOpts does not fail when instances are unreachable: it returns a pool
338+
// that may have no live connections yet and keeps reconnecting in the
339+
// background. Callers that need a usable connection before proceeding should
340+
// use WaitConnected (typically with a deadline on ctx) instead of relying on
341+
// the racy ConnectedNow snapshot.
342+
func (p *Pool) WaitConnected(ctx context.Context, mode Mode) error {
343+
for {
344+
p.poolsMutex.RLock()
345+
closed := p.state.get() != connectedState
346+
ok, err := p.hasConnection(mode)
347+
event := p.connEvent
348+
p.poolsMutex.RUnlock()
349+
350+
switch {
351+
case closed:
352+
return ErrClosed
353+
case err != nil:
354+
return err
355+
case ok:
356+
return nil
357+
}
358+
359+
select {
360+
case <-ctx.Done():
361+
return ctx.Err()
362+
case <-event:
363+
}
364+
}
365+
}
366+
324367
// ConfiguredTimeout gets timeout of current connection.
325368
func (p *Pool) ConfiguredTimeout(mode Mode) (time.Duration, error) {
326369
conn, err := p.getNextConnection(mode)
@@ -457,6 +500,10 @@ func (p *Pool) Close() error {
457500
close(s.close)
458501
}
459502
p.endsMutex.RUnlock()
503+
504+
p.poolsMutex.Lock()
505+
p.notifyConnEvent()
506+
p.poolsMutex.Unlock()
460507
}
461508

462509
return p.waitClose()
@@ -472,6 +519,10 @@ func (p *Pool) CloseGraceful() error {
472519
close(s.shutdown)
473520
}
474521
p.endsMutex.RUnlock()
522+
523+
p.poolsMutex.Lock()
524+
p.notifyConnEvent()
525+
p.poolsMutex.Unlock()
475526
}
476527

477528
return p.waitClose()
@@ -692,11 +743,20 @@ func (p *Pool) getConnectionFromPool(name string) (*tarantool.Connection, Role)
692743
return p.anyPool.GetConnection(name), RoleUnknown
693744
}
694745

746+
// notifyConnEvent wakes WaitConnected callers by closing the current
747+
// connEvent channel and installing a fresh one. The caller must hold
748+
// poolsMutex for writing.
749+
func (p *Pool) notifyConnEvent() {
750+
close(p.connEvent)
751+
p.connEvent = make(chan struct{})
752+
}
753+
695754
func (p *Pool) deleteConnection(name string) {
696755
if conn := p.anyPool.DeleteConnection(name); conn != nil {
697756
if conn := p.rwPool.DeleteConnection(name); conn == nil {
698757
p.roPool.DeleteConnection(name)
699758
}
759+
p.notifyConnEvent()
700760
// The internal connection deinitialization.
701761
p.watcherContainer.mutex.RLock()
702762
defer p.watcherContainer.mutex.RUnlock()
@@ -754,6 +814,7 @@ func (p *Pool) addConnection(name string,
754814
case RoleReplica:
755815
p.roPool.AddConnection(name, conn)
756816
}
817+
p.notifyConnEvent()
757818
return nil
758819
}
759820

pool/pool_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,50 @@ func TestNewWithOpts_error_no_timeout(t *testing.T) {
196196
require.ErrorIs(t, err, pool.ErrWrongCheckTimeout)
197197
}
198198

199+
func TestPool_WaitConnected(t *testing.T) {
200+
ctx, cancel := test_helpers.GetPoolConnectContext()
201+
defer cancel()
202+
connPool, err := pool.New(ctx, makeInstances(servers, connOpts))
203+
require.NoError(t, err)
204+
require.NotNil(t, connPool)
205+
defer func() { _ = connPool.Close() }()
206+
207+
wctx, wcancel := context.WithTimeout(context.Background(), 10*time.Second)
208+
defer wcancel()
209+
210+
require.NoError(t, connPool.WaitConnected(wctx, pool.ModeAny))
211+
require.NoError(t, connPool.WaitConnected(wctx, pool.ModeRW))
212+
require.NoError(t, connPool.WaitConnected(wctx, pool.ModePreferRO))
213+
}
214+
215+
func TestPool_WaitConnected_context_canceled(t *testing.T) {
216+
ctx, cancel := test_helpers.GetPoolConnectContext()
217+
defer cancel()
218+
// "err" does not resolve, so the pool stays without connections.
219+
connPool, err := pool.New(ctx, makeInstances([]string{"err"}, connOpts))
220+
require.NoError(t, err)
221+
require.NotNil(t, connPool)
222+
defer func() { _ = connPool.Close() }()
223+
224+
wctx, wcancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
225+
defer wcancel()
226+
227+
err = connPool.WaitConnected(wctx, pool.ModeAny)
228+
require.ErrorIs(t, err, context.DeadlineExceeded)
229+
}
230+
231+
func TestPool_WaitConnected_closed(t *testing.T) {
232+
ctx, cancel := test_helpers.GetPoolConnectContext()
233+
defer cancel()
234+
connPool, err := pool.New(ctx, makeInstances([]string{"err"}, connOpts))
235+
require.NoError(t, err)
236+
require.NotNil(t, connPool)
237+
require.NoError(t, connPool.Close())
238+
239+
err = connPool.WaitConnected(context.Background(), pool.ModeAny)
240+
require.ErrorIs(t, err, pool.ErrClosed)
241+
}
242+
199243
func TestConnectWithOpts_error_reconnect(t *testing.T) {
200244
ctx, cancel := test_helpers.GetPoolConnectContext()
201245
defer cancel()

0 commit comments

Comments
 (0)