mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Add redis support for distributed caching (#83)
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
5fcbd54955
commit
e64fc7f730
+337
@@ -0,0 +1,337 @@
|
||||
package backends
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConnectionPool manages a pool of Redis connections
|
||||
// Pure-Go implementation compatible with Yaegi
|
||||
type ConnectionPool struct {
|
||||
config *PoolConfig
|
||||
|
||||
connections chan *RedisConn
|
||||
mu sync.Mutex
|
||||
closed atomic.Bool
|
||||
|
||||
// Metrics
|
||||
activeConns atomic.Int32
|
||||
totalConns atomic.Int32
|
||||
gets atomic.Int64
|
||||
puts atomic.Int64
|
||||
timeouts atomic.Int64
|
||||
}
|
||||
|
||||
// PoolConfig holds connection pool configuration
|
||||
type PoolConfig struct {
|
||||
Address string
|
||||
Password string
|
||||
DB int
|
||||
MaxConnections int
|
||||
ConnectTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
EnableHealthCheck bool // Enable connection health validation
|
||||
MaxRetries int // Max retries for failed operations
|
||||
RetryDelay time.Duration // Initial delay between retries
|
||||
}
|
||||
|
||||
// NewConnectionPool creates a new connection pool
|
||||
func NewConnectionPool(config *PoolConfig) (*ConnectionPool, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is required")
|
||||
}
|
||||
|
||||
if config.MaxConnections <= 0 {
|
||||
config.MaxConnections = 10
|
||||
}
|
||||
|
||||
if config.ConnectTimeout == 0 {
|
||||
config.ConnectTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
pool := &ConnectionPool{
|
||||
config: config,
|
||||
connections: make(chan *RedisConn, config.MaxConnections),
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// Get retrieves a connection from the pool or creates a new one
|
||||
func (p *ConnectionPool) Get(ctx context.Context) (*RedisConn, error) {
|
||||
if p.closed.Load() {
|
||||
return nil, ErrBackendClosed
|
||||
}
|
||||
|
||||
p.gets.Add(1)
|
||||
|
||||
// Try to get a connection with validation
|
||||
maxAttempts := 3
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
var conn *RedisConn
|
||||
var err error
|
||||
|
||||
select {
|
||||
case conn = <-p.connections:
|
||||
// Reuse existing connection - validate if health check enabled
|
||||
if p.config.EnableHealthCheck && !p.isConnectionHealthy(conn) {
|
||||
// Connection is stale, close it and try again
|
||||
conn.Close()
|
||||
p.totalConns.Add(-1)
|
||||
continue
|
||||
}
|
||||
p.activeConns.Add(1)
|
||||
return conn, nil
|
||||
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
|
||||
default:
|
||||
// No available connection, create new one if under limit
|
||||
if p.totalConns.Load() < int32(p.config.MaxConnections) {
|
||||
conn, err = p.createConnection()
|
||||
if err != nil {
|
||||
// If this is the last attempt, return error
|
||||
if attempt == maxAttempts-1 {
|
||||
return nil, err
|
||||
}
|
||||
// Wait before retry with exponential backoff
|
||||
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
p.activeConns.Add(1)
|
||||
p.totalConns.Add(1)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Pool exhausted, wait for a connection with timeout
|
||||
select {
|
||||
case conn = <-p.connections:
|
||||
// Validate connection if health check enabled
|
||||
if p.config.EnableHealthCheck && !p.isConnectionHealthy(conn) {
|
||||
conn.Close()
|
||||
p.totalConns.Add(-1)
|
||||
continue
|
||||
}
|
||||
p.activeConns.Add(1)
|
||||
return conn, nil
|
||||
case <-ctx.Done():
|
||||
p.timeouts.Add(1)
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(p.config.ConnectTimeout):
|
||||
p.timeouts.Add(1)
|
||||
return nil, ErrPoolExhausted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("failed to get healthy connection after retries")
|
||||
}
|
||||
|
||||
// Put returns a connection to the pool
|
||||
func (p *ConnectionPool) Put(conn *RedisConn) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.puts.Add(1)
|
||||
p.activeConns.Add(-1)
|
||||
|
||||
if p.closed.Load() || conn.closed.Load() {
|
||||
conn.Close()
|
||||
p.totalConns.Add(-1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return to pool (non-blocking)
|
||||
select {
|
||||
case p.connections <- conn:
|
||||
// Successfully returned to pool
|
||||
default:
|
||||
// Pool full, close connection
|
||||
conn.Close()
|
||||
p.totalConns.Add(-1)
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all connections in the pool
|
||||
func (p *ConnectionPool) Close() error {
|
||||
if p.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
close(p.connections)
|
||||
|
||||
// Close all pooled connections
|
||||
for conn := range p.connections {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats returns pool statistics
|
||||
func (p *ConnectionPool) Stats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"active_connections": p.activeConns.Load(),
|
||||
"total_connections": p.totalConns.Load(),
|
||||
"max_connections": p.config.MaxConnections,
|
||||
"gets": p.gets.Load(),
|
||||
"puts": p.puts.Load(),
|
||||
"timeouts": p.timeouts.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
// createConnection creates a new Redis connection
|
||||
func (p *ConnectionPool) createConnection() (*RedisConn, error) {
|
||||
// Connect with timeout
|
||||
dialer := &net.Dialer{
|
||||
Timeout: p.config.ConnectTimeout,
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", p.config.Address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
}
|
||||
|
||||
redisConn := &RedisConn{
|
||||
conn: conn,
|
||||
readTimeout: p.config.ReadTimeout,
|
||||
writeTimeout: p.config.WriteTimeout,
|
||||
}
|
||||
|
||||
// Authenticate if password is provided
|
||||
if p.config.Password != "" {
|
||||
if _, err := redisConn.Do("AUTH", p.config.Password); err != nil {
|
||||
redisConn.Close()
|
||||
return nil, fmt.Errorf("authentication failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Select database
|
||||
if p.config.DB != 0 {
|
||||
if _, err := redisConn.Do("SELECT", fmt.Sprintf("%d", p.config.DB)); err != nil {
|
||||
redisConn.Close()
|
||||
return nil, fmt.Errorf("failed to select database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return redisConn, nil
|
||||
}
|
||||
|
||||
// RedisConn represents a single Redis connection
|
||||
type RedisConn struct {
|
||||
conn net.Conn
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
closed atomic.Bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Do executes a Redis command and returns the response
|
||||
func (c *RedisConn) Do(command string, args ...string) (interface{}, error) {
|
||||
if c.closed.Load() {
|
||||
return nil, ErrBackendClosed
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Build command arguments
|
||||
// Check for overflow: ensure len(args)+1 doesn't cause allocation overflow
|
||||
// Limit to a safe value that prevents integer overflow in allocation size calculation
|
||||
// (capacity * sizeof(string) must fit in int/size_t)
|
||||
argsLen := len(args)
|
||||
const maxSafeArgs = (1 << 20) - 1 // 1M args is already absurdly large for Redis commands
|
||||
if argsLen < 0 || argsLen > maxSafeArgs {
|
||||
return nil, errors.New("too many arguments")
|
||||
}
|
||||
const maxTotalArgBytes = 64 << 20 // 64 MiB max total size
|
||||
totalBytes := len(command)
|
||||
for _, s := range args {
|
||||
// Protect against possible overflow
|
||||
if len(s) > maxTotalArgBytes-totalBytes {
|
||||
return nil, errors.New("arguments too large (would overflow maximum allowed total size)")
|
||||
}
|
||||
totalBytes += len(s)
|
||||
if totalBytes > maxTotalArgBytes {
|
||||
return nil, errors.New("total argument size exceeds maximum allowed")
|
||||
}
|
||||
}
|
||||
cmdArgs := make([]string, 0, argsLen+1)
|
||||
cmdArgs = append(cmdArgs, command)
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
|
||||
// Set write timeout
|
||||
if c.writeTimeout > 0 {
|
||||
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||
}
|
||||
|
||||
// Write command (using pooled writer for memory efficiency)
|
||||
writer := NewRESPWriter(c.conn)
|
||||
err := writer.WriteCommand(cmdArgs...)
|
||||
writer.Release() // Return to pool immediately after use
|
||||
if err != nil {
|
||||
c.closed.Store(true)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set read timeout
|
||||
if c.readTimeout > 0 {
|
||||
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||
}
|
||||
|
||||
// Read response (using pooled reader for memory efficiency)
|
||||
reader := NewRESPReader(c.conn)
|
||||
resp, err := reader.ReadResponse()
|
||||
reader.Release() // Return to pool immediately after use
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrNilResponse) {
|
||||
c.closed.Store(true)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (c *RedisConn) Close() error {
|
||||
if c.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isConnectionHealthy validates a connection is still working
|
||||
func (p *ConnectionPool) isConnectionHealthy(conn *RedisConn) bool {
|
||||
if conn == nil || conn.closed.Load() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Set a read deadline for the ping
|
||||
if conn.conn != nil {
|
||||
conn.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||
defer conn.conn.SetReadDeadline(time.Time{}) // Clear deadline
|
||||
}
|
||||
|
||||
_, err := conn.Do("PING")
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user