mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-15 02:12:55 +01:00
Winni Neessen
c8684886ed
Updated the Get method in connpool.go and its usage in tests to include a context argument for better cancellation and timeout handling. Removed the redundant dialContext field from the connection pool struct and added a new test to validate context timeout behavior.
215 lines
6.1 KiB
Go
215 lines
6.1 KiB
Go
// SPDX-FileCopyrightText: 2022-2024 The go-mail Authors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
)
|
|
|
|
// Parts of the connection pool code is forked/took inspiration from https://github.com/fatih/pool/
|
|
// Thanks to Fatih Arslan and the project contributors for providing this great concurrency template.
|
|
|
|
var (
|
|
// ErrClosed is returned when an operation is attempted on a closed connection pool.
|
|
ErrClosed = errors.New("connection pool is closed")
|
|
// ErrNilConn is returned when a nil connection is passed back to the connection pool.
|
|
ErrNilConn = errors.New("connection is nil")
|
|
// ErrPoolInvalidCap is returned when the connection pool's capacity settings are
|
|
// invalid (e.g., initial capacity is negative).
|
|
ErrPoolInvalidCap = errors.New("invalid connection pool capacity settings")
|
|
)
|
|
|
|
// Pool interface describes a connection pool implementation. A Pool is
|
|
// thread-/go-routine safe.
|
|
type Pool interface {
|
|
// Get returns a new connection from the pool. Closing the connections returns
|
|
// it back into the Pool. Closing a connection when the Pool is destroyed or
|
|
// full will be counted as an error.
|
|
Get(ctx context.Context) (net.Conn, error)
|
|
|
|
// Close closes the pool and all its connections. After Close() the pool is
|
|
// no longer usable.
|
|
Close()
|
|
|
|
// Size returns the current number of connections of the pool.
|
|
Size() int
|
|
}
|
|
|
|
// connPool implements the Pool interface
|
|
type connPool struct {
|
|
// mutex is used to synchronize access to the connection pool to ensure thread-safe operations.
|
|
mutex sync.RWMutex
|
|
// conns is a channel used to manage and distribute net.Conn objects within the connection pool.
|
|
conns chan net.Conn
|
|
|
|
// dialCtxFunc represents the actual net.Conn returned by the DialContextFunc.
|
|
dialCtxFunc DialContextFunc
|
|
// dialNetwork specifies the network type (e.g., "tcp", "udp") used to establish connections in
|
|
// the connection pool.
|
|
dialNetwork string
|
|
// dialAddress specifies the address used to establish network connections within the connection pool.
|
|
dialAddress string
|
|
}
|
|
|
|
// PoolConn is a wrapper around net.Conn to modify the the behavior of net.Conn's Close() method.
|
|
type PoolConn struct {
|
|
net.Conn
|
|
mutex sync.RWMutex
|
|
pool *connPool
|
|
unusable bool
|
|
}
|
|
|
|
// Close puts a given pool connection back to the pool instead of closing it.
|
|
func (c *PoolConn) Close() error {
|
|
c.mutex.RLock()
|
|
defer c.mutex.RUnlock()
|
|
|
|
if c.unusable {
|
|
if c.Conn != nil {
|
|
return c.Conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
return c.pool.put(c.Conn)
|
|
}
|
|
|
|
// MarkUnusable marks the connection not usable any more, to let the pool close it instead
|
|
// of returning it to pool.
|
|
func (c *PoolConn) MarkUnusable() {
|
|
c.mutex.Lock()
|
|
c.unusable = true
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
// NewConnPool returns a new pool based on buffered channels with an initial
|
|
// capacity and maximum capacity. The DialContextFunc is used when the initial
|
|
// capacity is greater than zero to fill the pool. A zero initialCap doesn't
|
|
// fill the Pool until a new Get() is called. During a Get(), if there is no
|
|
// new connection available in the pool, a new connection will be created via
|
|
// the corresponding DialContextFunc() method.
|
|
func NewConnPool(ctx context.Context, initialCap, maxCap int, dialCtxFunc DialContextFunc,
|
|
network, address string,
|
|
) (Pool, error) {
|
|
if initialCap < 0 || maxCap <= 0 || initialCap > maxCap {
|
|
return nil, ErrPoolInvalidCap
|
|
}
|
|
|
|
pool := &connPool{
|
|
conns: make(chan net.Conn, maxCap),
|
|
dialCtxFunc: dialCtxFunc,
|
|
dialAddress: address,
|
|
dialNetwork: network,
|
|
}
|
|
|
|
// Initial connections for the pool. Pool will be closed on connection error
|
|
for i := 0; i < initialCap; i++ {
|
|
conn, err := dialCtxFunc(ctx, network, address)
|
|
if err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("dialContextFunc is not able to fill the connection pool: %w", err)
|
|
}
|
|
pool.conns <- conn
|
|
}
|
|
|
|
return pool, nil
|
|
}
|
|
|
|
// Get satisfies the Get() method of the Pool inteface. If there is no new
|
|
// connection available in the Pool, a new connection will be created via the
|
|
// DialContextFunc() method.
|
|
func (p *connPool) Get(ctx context.Context) (net.Conn, error) {
|
|
conns, dialCtxFunc := p.getConnsAndDialContext()
|
|
if conns == nil {
|
|
return nil, ErrClosed
|
|
}
|
|
|
|
// wrap the connections into the custom net.Conn implementation that puts
|
|
// connections back to the pool
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("failed to get connection: %w", ctx.Err())
|
|
case conn := <-conns:
|
|
if conn == nil {
|
|
return nil, ErrClosed
|
|
}
|
|
return p.wrapConn(conn), nil
|
|
default:
|
|
conn, err := dialCtxFunc(ctx, p.dialNetwork, p.dialAddress)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dialContextFunc failed: %w", err)
|
|
}
|
|
return p.wrapConn(conn), nil
|
|
}
|
|
}
|
|
|
|
// Close terminates all connections in the pool and frees associated resources. Once closed,
|
|
// the pool is no longer usable.
|
|
func (p *connPool) Close() {
|
|
p.mutex.Lock()
|
|
conns := p.conns
|
|
p.conns = nil
|
|
p.dialCtxFunc = nil
|
|
p.dialAddress = ""
|
|
p.dialNetwork = ""
|
|
p.mutex.Unlock()
|
|
|
|
if conns == nil {
|
|
return
|
|
}
|
|
|
|
close(conns)
|
|
for conn := range conns {
|
|
_ = conn.Close()
|
|
}
|
|
}
|
|
|
|
// Size returns the current number of connections in the connection pool.
|
|
func (p *connPool) Size() int {
|
|
conns, _ := p.getConnsAndDialContext()
|
|
return len(conns)
|
|
}
|
|
|
|
// getConnsAndDialContext returns the connection channel and the DialContext function for the
|
|
// connection pool.
|
|
func (p *connPool) getConnsAndDialContext() (chan net.Conn, DialContextFunc) {
|
|
p.mutex.RLock()
|
|
conns := p.conns
|
|
dialCtxFunc := p.dialCtxFunc
|
|
p.mutex.RUnlock()
|
|
return conns, dialCtxFunc
|
|
}
|
|
|
|
// put puts a passed connection back into the pool. If the pool is full or closed,
|
|
// conn is simply closed. A nil conn will be rejected with an error.
|
|
func (p *connPool) put(conn net.Conn) error {
|
|
if conn == nil {
|
|
return ErrNilConn
|
|
}
|
|
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
|
|
if p.conns == nil {
|
|
return conn.Close()
|
|
}
|
|
|
|
select {
|
|
case p.conns <- conn:
|
|
return nil
|
|
default:
|
|
return conn.Close()
|
|
}
|
|
}
|
|
|
|
// wrapConn wraps a given net.Conn with a PoolConn, modifying the net.Conn's Close() method.
|
|
func (p *connPool) wrapConn(conn net.Conn) net.Conn {
|
|
poolconn := &PoolConn{pool: p}
|
|
poolconn.Conn = conn
|
|
return poolconn
|
|
}
|