diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e4db94e..b3fc8136 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Test environment env: @@ -83,7 +83,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Run tests uses: ./.github/actions/run-tests diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 81853524..353581e5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index e645afb9..55d022ed 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -36,7 +36,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Test doc examples working-directory: ./doctests diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 458ae340..97e2b8d6 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,9 +20,9 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: golangci-lint - uses: golangci/golangci-lint-action@v9.0.0 + uses: golangci/golangci-lint-action@v9.1.0 with: verify: true diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index d2efad8d..05932d60 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Check Spelling uses: rojopolis/spellcheck-github-actions@0.54.0 with: diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index faf62902..a84e1f78 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -20,10 +20,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Clone Redis EE docker repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: RedisLabs/redis-ee-docker path: redis-ee diff --git a/command.go b/command.go index b99f1312..2dbc2ad8 100644 --- a/command.go +++ b/command.go @@ -64,6 +64,7 @@ var keylessCommands = map[string]struct{}{ "sync": {}, "unsubscribe": {}, "unwatch": {}, + "wait": {}, } type Cmder interface { diff --git a/internal/pool/conn.go b/internal/pool/conn.go index 7682aa87..95d83bfd 100644 --- a/internal/pool/conn.go +++ b/internal/pool/conn.go @@ -27,16 +27,17 @@ var ( errConnNotAvailableForWrite = errors.New("redis: connection not available for write operation") ) -// getCachedTimeNs returns the current time in nanoseconds from the global cache. -// This is updated every 50ms by a background goroutine, avoiding expensive syscalls. -// Max staleness: 50ms. +// getCachedTimeNs returns the current time in nanoseconds. +// This function previously used a global cache updated by a background goroutine, +// but that caused unnecessary CPU usage when the client was idle (ticker waking up +// the scheduler every 50ms). We now use time.Now() directly, which is fast enough +// on modern systems (vDSO on Linux) and only adds ~1-2% overhead in extreme +// high-concurrency benchmarks while eliminating idle CPU usage. func getCachedTimeNs() int64 { - return globalTimeCache.nowNs.Load() + return time.Now().UnixNano() } -// GetCachedTimeNs returns the current time in nanoseconds from the global cache. -// This is updated every 50ms by a background goroutine, avoiding expensive syscalls. -// Max staleness: 50ms. +// GetCachedTimeNs returns the current time in nanoseconds. // Exported for use by other packages that need fast time access. func GetCachedTimeNs() int64 { return getCachedTimeNs() diff --git a/internal/pool/global_time_cache.go b/internal/pool/global_time_cache.go deleted file mode 100644 index d7d21ea7..00000000 --- a/internal/pool/global_time_cache.go +++ /dev/null @@ -1,74 +0,0 @@ -package pool - -import ( - "sync" - "sync/atomic" - "time" -) - -// Global time cache updated every 50ms by background goroutine. -// 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). -var globalTimeCache struct { - nowNs atomic.Int64 - lock sync.Mutex - started bool - stop chan struct{} - subscribers int32 -} - -func subscribeToGlobalTimeCache() { - globalTimeCache.lock.Lock() - globalTimeCache.subscribers += 1 - globalTimeCache.lock.Unlock() -} - -func unsubscribeFromGlobalTimeCache() { - globalTimeCache.lock.Lock() - globalTimeCache.subscribers -= 1 - globalTimeCache.lock.Unlock() -} - -func startGlobalTimeCache() { - globalTimeCache.lock.Lock() - if globalTimeCache.started { - globalTimeCache.lock.Unlock() - return - } - - globalTimeCache.started = true - globalTimeCache.nowNs.Store(time.Now().UnixNano()) - globalTimeCache.stop = make(chan struct{}) - globalTimeCache.lock.Unlock() - // Start background updater - go func(stopChan chan struct{}) { - ticker := time.NewTicker(50 * time.Millisecond) - defer ticker.Stop() - - for range ticker.C { - select { - case <-stopChan: - return - default: - } - globalTimeCache.nowNs.Store(time.Now().UnixNano()) - } - }(globalTimeCache.stop) -} - -// stopGlobalTimeCache stops the global time cache if there are no subscribers. -// This should only be called when the last subscriber is removed. -func stopGlobalTimeCache() { - globalTimeCache.lock.Lock() - if !globalTimeCache.started || globalTimeCache.subscribers > 0 { - globalTimeCache.lock.Unlock() - return - } - globalTimeCache.started = false - close(globalTimeCache.stop) - globalTimeCache.lock.Unlock() -} - -func init() { - startGlobalTimeCache() -} diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 7b9ba049..48839717 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -182,9 +182,6 @@ func NewConnPool(opt *Options) *ConnPool { p.connsMu.Unlock() } - startGlobalTimeCache() - subscribeToGlobalTimeCache() - return p } @@ -985,9 +982,6 @@ func (p *ConnPool) Close() error { return ErrClosed } - unsubscribeFromGlobalTimeCache() - stopGlobalTimeCache() - var firstErr error p.connsMu.Lock() for _, cn := range p.conns { diff --git a/scripts/tag.sh b/scripts/tag.sh index 5b637637..62b28e99 100755 --- a/scripts/tag.sh +++ b/scripts/tag.sh @@ -4,9 +4,25 @@ set -e DRY_RUN=1 +helps() { + cat <<- EOF +Usage: $0 TAGVERSION [-t] + +Creates git tags for public Go packages. + +ARGUMENTS: + TAGVERSION Tag version to create, for example v1.0.0 + +OPTIONS: + -t Execute git commands (default: dry run) +EOF + exit 0 +} + + if [ $# -eq 0 ]; then echo "Error: Tag version is required" - help + helps fi TAG=$1 @@ -24,20 +40,6 @@ while getopts "t" opt; do esac done -help() { - cat <<- EOF -Usage: $0 TAGVERSION [-t] - -Creates git tags for public Go packages. - -ARGUMENTS: - TAGVERSION Tag version to create, for example v1.0.0 - -OPTIONS: - -t Execute git commands (default: dry run) -EOF - exit 0 -} if [ "$DRY_RUN" -eq 1 ]; then echo "Running in dry-run mode"