1
0
mirror of https://github.com/redis/go-redis.git synced 2025-12-02 06:22:31 +03:00

optimize push notif

This commit is contained in:
Nedyalko Dyakov
2025-10-28 23:32:27 +02:00
parent 0752aecdfb
commit 54281d687c
5 changed files with 107 additions and 17 deletions

View File

@@ -18,9 +18,9 @@ import (
var noDeadline = time.Time{} var noDeadline = time.Time{}
// Global time cache updated every 50ms by background goroutine. // Global time cache updated every 100ms by background goroutine.
// This avoids expensive time.Now() syscalls in hot paths like getEffectiveReadTimeout. // This avoids expensive time.Now() syscalls in hot paths like getEffectiveReadTimeout.
// Max staleness: 50ms, which is acceptable for timeout deadline checks (timeouts are typically 3-30 seconds). // Max staleness: 100ms, which is acceptable for timeout deadline checks (timeouts are typically 3-30 seconds).
var globalTimeCache struct { var globalTimeCache struct {
nowNs atomic.Int64 nowNs atomic.Int64
} }
@@ -31,7 +31,7 @@ func init() {
// Start background updater // Start background updater
go func() { go func() {
ticker := time.NewTicker(50 * time.Millisecond) ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
@@ -41,12 +41,20 @@ func init() {
} }
// getCachedTimeNs returns the current time in nanoseconds from the global cache. // getCachedTimeNs returns the current time in nanoseconds from the global cache.
// This is updated every 50ms by a background goroutine, avoiding expensive syscalls. // This is updated every 100ms by a background goroutine, avoiding expensive syscalls.
// Max staleness: 50ms. // Max staleness: 100ms.
func getCachedTimeNs() int64 { func getCachedTimeNs() int64 {
return globalTimeCache.nowNs.Load() return globalTimeCache.nowNs.Load()
} }
// GetCachedTimeNs returns the current time in nanoseconds from the global cache.
// This is updated every 100ms by a background goroutine, avoiding expensive syscalls.
// Max staleness: 100ms.
// Exported for use by other packages that need fast time access.
func GetCachedTimeNs() int64 {
return getCachedTimeNs()
}
// Global atomic counter for connection IDs // Global atomic counter for connection IDs
var connIDCounter uint64 var connIDCounter uint64
@@ -170,6 +178,9 @@ func (cn *Conn) UsedAt() time.Time {
unixNano := atomic.LoadInt64(&cn.usedAt) unixNano := atomic.LoadInt64(&cn.usedAt)
return time.Unix(0, unixNano) return time.Unix(0, unixNano)
} }
func (cn *Conn) UsedAtNs() int64 {
return atomic.LoadInt64(&cn.usedAt)
}
func (cn *Conn) SetUsedAt(tm time.Time) { func (cn *Conn) SetUsedAt(tm time.Time) {
atomic.StoreInt64(&cn.usedAt, tm.UnixNano()) atomic.StoreInt64(&cn.usedAt, tm.UnixNano())
@@ -488,7 +499,7 @@ func (cn *Conn) getEffectiveReadTimeout(normalTimeout time.Duration) time.Durati
return time.Duration(readTimeoutNs) return time.Duration(readTimeoutNs)
} }
// Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks) // Use cached time to avoid expensive syscall (max 100ms staleness is acceptable for timeout checks)
nowNs := getCachedTimeNs() nowNs := getCachedTimeNs()
// Check if deadline has passed // Check if deadline has passed
if nowNs < deadlineNs { if nowNs < deadlineNs {
@@ -522,7 +533,7 @@ func (cn *Conn) getEffectiveWriteTimeout(normalTimeout time.Duration) time.Durat
return time.Duration(writeTimeoutNs) return time.Duration(writeTimeoutNs)
} }
// Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks) // Use cached time to avoid expensive syscall (max 100ms staleness is acceptable for timeout checks)
nowNs := getCachedTimeNs() nowNs := getCachedTimeNs()
// Check if deadline has passed // Check if deadline has passed
if nowNs < deadlineNs { if nowNs < deadlineNs {
@@ -879,7 +890,7 @@ func (cn *Conn) MaybeHasData() bool {
// deadline computes the effective deadline time based on context and timeout. // deadline computes the effective deadline time based on context and timeout.
// It updates the usedAt timestamp to now. // It updates the usedAt timestamp to now.
// Uses cached time to avoid expensive syscall (max 50ms staleness is acceptable for deadline calculation). // Uses cached time to avoid expensive syscall (max 100ms staleness is acceptable for deadline calculation).
func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time { func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time {
// Use cached time for deadline calculation (called 2x per command: read + write) // Use cached time for deadline calculation (called 2x per command: read + write)
tm := time.Unix(0, getCachedTimeNs()) tm := time.Unix(0, getCachedTimeNs())

View File

@@ -30,7 +30,7 @@ func connCheck(conn net.Conn) error {
var sysErr error var sysErr error
if err := rawConn.Read(func(fd uintptr) bool { if err := rawConn.Control(func(fd uintptr) {
var buf [1]byte var buf [1]byte
// Use MSG_PEEK to peek at data without consuming it // Use MSG_PEEK to peek at data without consuming it
n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT) n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT)
@@ -45,7 +45,6 @@ func connCheck(conn net.Conn) error {
default: default:
sysErr = err sysErr = err
} }
return true
}); err != nil { }); err != nil {
return err return err
} }

View File

@@ -155,10 +155,18 @@ type ConnPool struct {
var _ Pooler = (*ConnPool)(nil) var _ Pooler = (*ConnPool)(nil)
func NewConnPool(opt *Options) *ConnPool { func NewConnPool(opt *Options) *ConnPool {
p := &ConnPool{ semSize := opt.PoolSize
cfg: opt, if opt.MaxActiveConns > 0 && opt.MaxActiveConns < opt.PoolSize {
if opt.MaxActiveConns < opt.PoolSize {
opt.MaxActiveConns = opt.PoolSize
}
semSize = opt.MaxActiveConns
}
//semSize = opt.PoolSize
semaphore: internal.NewFastSemaphore(opt.PoolSize), p := &ConnPool{
cfg: opt,
semaphore: internal.NewFastSemaphore(semSize),
conns: make(map[uint64]*Conn), conns: make(map[uint64]*Conn),
idleConns: make([]*Conn, 0, opt.PoolSize), idleConns: make([]*Conn, 0, opt.PoolSize),
} }

View File

@@ -1351,13 +1351,39 @@ func (c *Conn) TxPipeline() Pipeliner {
// processPushNotifications processes all pending push notifications on a connection // processPushNotifications processes all pending push notifications on a connection
// This ensures that cluster topology changes are handled immediately before the connection is used // This ensures that cluster topology changes are handled immediately before the connection is used
// This method should be called by the client before using WithReader for command execution // This method should be called by the client before using WithWriter for command execution
//
// Performance optimization: Skip the expensive MaybeHasData() syscall if a health check
// was performed recently (within 5 seconds). The health check already verified the connection
// is healthy and checked for unexpected data (push notifications).
func (c *baseClient) processPushNotifications(ctx context.Context, cn *pool.Conn) error { func (c *baseClient) processPushNotifications(ctx context.Context, cn *pool.Conn) error {
// Only process push notifications for RESP3 connections with a processor // Only process push notifications for RESP3 connections with a processor
// Also check if there is any data to read before processing if c.opt.Protocol != 3 || c.pushProcessor == nil {
// Which is an optimization on UNIX systems where MaybeHasData is a syscall return nil
}
// Performance optimization: Skip MaybeHasData() syscall if health check was recent
// If the connection was health-checked within the last 5 seconds, we can skip the
// expensive syscall since the health check already verified no unexpected data.
// This is safe because:
// 1. Health check (connCheck) uses the same syscall (Recvfrom with MSG_PEEK)
// 2. If push notifications arrived, they would have been detected by health check
// 3. 5 seconds is short enough that connection state is still fresh
// 4. Push notifications will be processed by the next WithReader call
lastHealthCheckNs := cn.UsedAtNs()
if lastHealthCheckNs > 0 {
// Use pool's cached time to avoid expensive time.Now() syscall
nowNs := pool.GetCachedTimeNs()
if nowNs-lastHealthCheckNs < int64(5*time.Second) {
// Recent health check confirmed no unexpected data, skip the syscall
return nil
}
}
// Check if there is any data to read before processing
// This is an optimization on UNIX systems where MaybeHasData is a syscall
// On Windows, MaybeHasData always returns true, so this check is a no-op // On Windows, MaybeHasData always returns true, so this check is a no-op
if c.opt.Protocol != 3 || c.pushProcessor == nil || !cn.MaybeHasData() { if !cn.MaybeHasData() {
return nil return nil
} }

View File

@@ -245,6 +245,52 @@ var _ = Describe("Client", func() {
Expect(val).Should(HaveKeyWithValue("proto", int64(3))) Expect(val).Should(HaveKeyWithValue("proto", int64(3)))
}) })
It("should initialize idle connections created by MinIdleConns", func() {
opt := redisOptions()
opt.MinIdleConns = 5
opt.Password = "asdf" // Set password to require AUTH
opt.DB = 1 // Set DB to require SELECT
db := redis.NewClient(opt)
defer func() {
Expect(db.Close()).NotTo(HaveOccurred())
}()
// Wait for minIdle connections to be created
time.Sleep(100 * time.Millisecond)
// Verify that idle connections were created
stats := db.PoolStats()
Expect(stats.IdleConns).To(BeNumerically(">=", 5))
// Now use these connections - they should be properly initialized
// If they're not initialized, we'll get NOAUTH or WRONGDB errors
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Each goroutine performs multiple operations
for j := 0; j < 5; j++ {
key := fmt.Sprintf("test_key_%d_%d", id, j)
err := db.Set(ctx, key, "value", 0).Err()
Expect(err).NotTo(HaveOccurred())
val, err := db.Get(ctx, key).Result()
Expect(err).NotTo(HaveOccurred())
Expect(val).To(Equal("value"))
err = db.Del(ctx, key).Err()
Expect(err).NotTo(HaveOccurred())
}
}(i)
}
wg.Wait()
// Verify no errors occurred
Expect(db.Ping(ctx).Err()).NotTo(HaveOccurred())
})
It("processes custom commands", func() { It("processes custom commands", func() {
cmd := redis.NewCmd(ctx, "PING") cmd := redis.NewCmd(ctx, "PING")
_ = client.Process(ctx, cmd) _ = client.Process(ctx, cmd)