Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

1323 строки
41 KiB

  1. package redis
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net"
  7. "sync"
  8. "sync/atomic"
  9. "time"
  10. "github.com/redis/go-redis/v9/auth"
  11. "github.com/redis/go-redis/v9/internal"
  12. "github.com/redis/go-redis/v9/internal/auth/streaming"
  13. "github.com/redis/go-redis/v9/internal/hscan"
  14. "github.com/redis/go-redis/v9/internal/pool"
  15. "github.com/redis/go-redis/v9/internal/proto"
  16. "github.com/redis/go-redis/v9/maintnotifications"
  17. "github.com/redis/go-redis/v9/push"
  18. )
  19. // Scanner internal/hscan.Scanner exposed interface.
  20. type Scanner = hscan.Scanner
  21. // Nil reply returned by Redis when key does not exist.
  22. const Nil = proto.Nil
  23. // SetLogger set custom log
  24. // Use with VoidLogger to disable logging.
  25. func SetLogger(logger internal.Logging) {
  26. internal.Logger = logger
  27. }
  28. // SetLogLevel sets the log level for the library.
  29. func SetLogLevel(logLevel internal.LogLevelT) {
  30. internal.LogLevel = logLevel
  31. }
  32. //------------------------------------------------------------------------------
  33. type Hook interface {
  34. DialHook(next DialHook) DialHook
  35. ProcessHook(next ProcessHook) ProcessHook
  36. ProcessPipelineHook(next ProcessPipelineHook) ProcessPipelineHook
  37. }
  38. type (
  39. DialHook func(ctx context.Context, network, addr string) (net.Conn, error)
  40. ProcessHook func(ctx context.Context, cmd Cmder) error
  41. ProcessPipelineHook func(ctx context.Context, cmds []Cmder) error
  42. )
  43. type hooksMixin struct {
  44. hooksMu *sync.RWMutex
  45. slice []Hook
  46. initial hooks
  47. current hooks
  48. }
  49. func (hs *hooksMixin) initHooks(hooks hooks) {
  50. hs.hooksMu = new(sync.RWMutex)
  51. hs.initial = hooks
  52. hs.chain()
  53. }
  54. type hooks struct {
  55. dial DialHook
  56. process ProcessHook
  57. pipeline ProcessPipelineHook
  58. txPipeline ProcessPipelineHook
  59. }
  60. func (h *hooks) setDefaults() {
  61. if h.dial == nil {
  62. h.dial = func(ctx context.Context, network, addr string) (net.Conn, error) { return nil, nil }
  63. }
  64. if h.process == nil {
  65. h.process = func(ctx context.Context, cmd Cmder) error { return nil }
  66. }
  67. if h.pipeline == nil {
  68. h.pipeline = func(ctx context.Context, cmds []Cmder) error { return nil }
  69. }
  70. if h.txPipeline == nil {
  71. h.txPipeline = func(ctx context.Context, cmds []Cmder) error { return nil }
  72. }
  73. }
  74. // AddHook is to add a hook to the queue.
  75. // Hook is a function executed during network connection, command execution, and pipeline,
  76. // it is a first-in-first-out stack queue (FIFO).
  77. // You need to execute the next hook in each hook, unless you want to terminate the execution of the command.
  78. // For example, you added hook-1, hook-2:
  79. //
  80. // client.AddHook(hook-1, hook-2)
  81. //
  82. // hook-1:
  83. //
  84. // func (Hook1) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
  85. // return func(ctx context.Context, cmd Cmder) error {
  86. // print("hook-1 start")
  87. // next(ctx, cmd)
  88. // print("hook-1 end")
  89. // return nil
  90. // }
  91. // }
  92. //
  93. // hook-2:
  94. //
  95. // func (Hook2) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
  96. // return func(ctx context.Context, cmd redis.Cmder) error {
  97. // print("hook-2 start")
  98. // next(ctx, cmd)
  99. // print("hook-2 end")
  100. // return nil
  101. // }
  102. // }
  103. //
  104. // The execution sequence is:
  105. //
  106. // hook-1 start -> hook-2 start -> exec redis cmd -> hook-2 end -> hook-1 end
  107. //
  108. // Please note: "next(ctx, cmd)" is very important, it will call the next hook,
  109. // if "next(ctx, cmd)" is not executed, the redis command will not be executed.
  110. func (hs *hooksMixin) AddHook(hook Hook) {
  111. hs.slice = append(hs.slice, hook)
  112. hs.chain()
  113. }
  114. func (hs *hooksMixin) chain() {
  115. hs.initial.setDefaults()
  116. hs.hooksMu.Lock()
  117. defer hs.hooksMu.Unlock()
  118. hs.current.dial = hs.initial.dial
  119. hs.current.process = hs.initial.process
  120. hs.current.pipeline = hs.initial.pipeline
  121. hs.current.txPipeline = hs.initial.txPipeline
  122. for i := len(hs.slice) - 1; i >= 0; i-- {
  123. if wrapped := hs.slice[i].DialHook(hs.current.dial); wrapped != nil {
  124. hs.current.dial = wrapped
  125. }
  126. if wrapped := hs.slice[i].ProcessHook(hs.current.process); wrapped != nil {
  127. hs.current.process = wrapped
  128. }
  129. if wrapped := hs.slice[i].ProcessPipelineHook(hs.current.pipeline); wrapped != nil {
  130. hs.current.pipeline = wrapped
  131. }
  132. if wrapped := hs.slice[i].ProcessPipelineHook(hs.current.txPipeline); wrapped != nil {
  133. hs.current.txPipeline = wrapped
  134. }
  135. }
  136. }
  137. func (hs *hooksMixin) clone() hooksMixin {
  138. hs.hooksMu.Lock()
  139. defer hs.hooksMu.Unlock()
  140. clone := *hs
  141. l := len(clone.slice)
  142. clone.slice = clone.slice[:l:l]
  143. clone.hooksMu = new(sync.RWMutex)
  144. return clone
  145. }
  146. func (hs *hooksMixin) withProcessHook(ctx context.Context, cmd Cmder, hook ProcessHook) error {
  147. for i := len(hs.slice) - 1; i >= 0; i-- {
  148. if wrapped := hs.slice[i].ProcessHook(hook); wrapped != nil {
  149. hook = wrapped
  150. }
  151. }
  152. return hook(ctx, cmd)
  153. }
  154. func (hs *hooksMixin) withProcessPipelineHook(
  155. ctx context.Context, cmds []Cmder, hook ProcessPipelineHook,
  156. ) error {
  157. for i := len(hs.slice) - 1; i >= 0; i-- {
  158. if wrapped := hs.slice[i].ProcessPipelineHook(hook); wrapped != nil {
  159. hook = wrapped
  160. }
  161. }
  162. return hook(ctx, cmds)
  163. }
  164. func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) {
  165. // Access to hs.current is guarded by a read-only lock since it may be mutated by AddHook(...)
  166. // while this dialer is concurrently accessed by the background connection pool population
  167. // routine when MinIdleConns > 0.
  168. hs.hooksMu.RLock()
  169. current := hs.current
  170. hs.hooksMu.RUnlock()
  171. return current.dial(ctx, network, addr)
  172. }
  173. func (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error {
  174. return hs.current.process(ctx, cmd)
  175. }
  176. func (hs *hooksMixin) processPipelineHook(ctx context.Context, cmds []Cmder) error {
  177. return hs.current.pipeline(ctx, cmds)
  178. }
  179. func (hs *hooksMixin) processTxPipelineHook(ctx context.Context, cmds []Cmder) error {
  180. return hs.current.txPipeline(ctx, cmds)
  181. }
  182. //------------------------------------------------------------------------------
  183. type baseClient struct {
  184. opt *Options
  185. optLock sync.RWMutex
  186. connPool pool.Pooler
  187. pubSubPool *pool.PubSubPool
  188. hooksMixin
  189. onClose func() error // hook called when client is closed
  190. // Push notification processing
  191. pushProcessor push.NotificationProcessor
  192. // Maintenance notifications manager
  193. maintNotificationsManager *maintnotifications.Manager
  194. maintNotificationsManagerLock sync.RWMutex
  195. // streamingCredentialsManager is used to manage streaming credentials
  196. streamingCredentialsManager *streaming.Manager
  197. }
  198. func (c *baseClient) clone() *baseClient {
  199. c.maintNotificationsManagerLock.RLock()
  200. maintNotificationsManager := c.maintNotificationsManager
  201. c.maintNotificationsManagerLock.RUnlock()
  202. clone := &baseClient{
  203. opt: c.opt,
  204. connPool: c.connPool,
  205. onClose: c.onClose,
  206. pushProcessor: c.pushProcessor,
  207. maintNotificationsManager: maintNotificationsManager,
  208. streamingCredentialsManager: c.streamingCredentialsManager,
  209. }
  210. return clone
  211. }
  212. func (c *baseClient) withTimeout(timeout time.Duration) *baseClient {
  213. opt := c.opt.clone()
  214. opt.ReadTimeout = timeout
  215. opt.WriteTimeout = timeout
  216. clone := c.clone()
  217. clone.opt = opt
  218. return clone
  219. }
  220. func (c *baseClient) String() string {
  221. return fmt.Sprintf("Redis<%s db:%d>", c.getAddr(), c.opt.DB)
  222. }
  223. func (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) {
  224. if c.opt.Limiter != nil {
  225. err := c.opt.Limiter.Allow()
  226. if err != nil {
  227. return nil, err
  228. }
  229. }
  230. cn, err := c._getConn(ctx)
  231. if err != nil {
  232. if c.opt.Limiter != nil {
  233. c.opt.Limiter.ReportResult(err)
  234. }
  235. return nil, err
  236. }
  237. return cn, nil
  238. }
  239. func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {
  240. cn, err := c.connPool.Get(ctx)
  241. if err != nil {
  242. return nil, err
  243. }
  244. if cn.IsInited() {
  245. return cn, nil
  246. }
  247. if err := c.initConn(ctx, cn); err != nil {
  248. c.connPool.Remove(ctx, cn, err)
  249. if err := errors.Unwrap(err); err != nil {
  250. return nil, err
  251. }
  252. return nil, err
  253. }
  254. return cn, nil
  255. }
  256. func (c *baseClient) reAuthConnection() func(poolCn *pool.Conn, credentials auth.Credentials) error {
  257. return func(poolCn *pool.Conn, credentials auth.Credentials) error {
  258. var err error
  259. username, password := credentials.BasicAuth()
  260. // Use background context - timeout is handled by ReadTimeout in WithReader/WithWriter
  261. ctx := context.Background()
  262. connPool := pool.NewSingleConnPool(c.connPool, poolCn)
  263. // Pass hooks so that reauth commands are recorded/traced
  264. cn := newConn(c.opt, connPool, &c.hooksMixin)
  265. if username != "" {
  266. err = cn.AuthACL(ctx, username, password).Err()
  267. } else {
  268. err = cn.Auth(ctx, password).Err()
  269. }
  270. return err
  271. }
  272. }
  273. func (c *baseClient) onAuthenticationErr() func(poolCn *pool.Conn, err error) {
  274. return func(poolCn *pool.Conn, err error) {
  275. if err != nil {
  276. if isBadConn(err, false, c.opt.Addr) {
  277. // Close the connection to force a reconnection.
  278. err := c.connPool.CloseConn(poolCn)
  279. if err != nil {
  280. internal.Logger.Printf(context.Background(), "redis: failed to close connection: %v", err)
  281. // try to close the network connection directly
  282. // so that no resource is leaked
  283. err := poolCn.Close()
  284. if err != nil {
  285. internal.Logger.Printf(context.Background(), "redis: failed to close network connection: %v", err)
  286. }
  287. }
  288. }
  289. internal.Logger.Printf(context.Background(), "redis: re-authentication failed: %v", err)
  290. }
  291. }
  292. }
  293. func (c *baseClient) wrappedOnClose(newOnClose func() error) func() error {
  294. onClose := c.onClose
  295. return func() error {
  296. var firstErr error
  297. err := newOnClose()
  298. // Even if we have an error we would like to execute the onClose hook
  299. // if it exists. We will return the first error that occurred.
  300. // This is to keep error handling consistent with the rest of the code.
  301. if err != nil {
  302. firstErr = err
  303. }
  304. if onClose != nil {
  305. err = onClose()
  306. if err != nil && firstErr == nil {
  307. firstErr = err
  308. }
  309. }
  310. return firstErr
  311. }
  312. }
  313. func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
  314. if !cn.Inited.CompareAndSwap(false, true) {
  315. return nil
  316. }
  317. var err error
  318. connPool := pool.NewSingleConnPool(c.connPool, cn)
  319. conn := newConn(c.opt, connPool, &c.hooksMixin)
  320. username, password := "", ""
  321. if c.opt.StreamingCredentialsProvider != nil {
  322. credListener, err := c.streamingCredentialsManager.Listener(
  323. cn,
  324. c.reAuthConnection(),
  325. c.onAuthenticationErr(),
  326. )
  327. if err != nil {
  328. return fmt.Errorf("failed to create credentials listener: %w", err)
  329. }
  330. credentials, unsubscribeFromCredentialsProvider, err := c.opt.StreamingCredentialsProvider.
  331. Subscribe(credListener)
  332. if err != nil {
  333. return fmt.Errorf("failed to subscribe to streaming credentials: %w", err)
  334. }
  335. c.onClose = c.wrappedOnClose(unsubscribeFromCredentialsProvider)
  336. cn.SetOnClose(unsubscribeFromCredentialsProvider)
  337. username, password = credentials.BasicAuth()
  338. } else if c.opt.CredentialsProviderContext != nil {
  339. username, password, err = c.opt.CredentialsProviderContext(ctx)
  340. if err != nil {
  341. return fmt.Errorf("failed to get credentials from context provider: %w", err)
  342. }
  343. } else if c.opt.CredentialsProvider != nil {
  344. username, password = c.opt.CredentialsProvider()
  345. } else if c.opt.Username != "" || c.opt.Password != "" {
  346. username, password = c.opt.Username, c.opt.Password
  347. }
  348. // for redis-server versions that do not support the HELLO command,
  349. // RESP2 will continue to be used.
  350. if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil {
  351. // Authentication successful with HELLO command
  352. } else if !isRedisError(err) {
  353. // When the server responds with the RESP protocol and the result is not a normal
  354. // execution result of the HELLO command, we consider it to be an indication that
  355. // the server does not support the HELLO command.
  356. // The server may be a redis-server that does not support the HELLO command,
  357. // or it could be DragonflyDB or a third-party redis-proxy. They all respond
  358. // with different error string results for unsupported commands, making it
  359. // difficult to rely on error strings to determine all results.
  360. return err
  361. } else if password != "" {
  362. // Try legacy AUTH command if HELLO failed
  363. if username != "" {
  364. err = conn.AuthACL(ctx, username, password).Err()
  365. } else {
  366. err = conn.Auth(ctx, password).Err()
  367. }
  368. if err != nil {
  369. return fmt.Errorf("failed to authenticate: %w", err)
  370. }
  371. }
  372. _, err = conn.Pipelined(ctx, func(pipe Pipeliner) error {
  373. if c.opt.DB > 0 {
  374. pipe.Select(ctx, c.opt.DB)
  375. }
  376. if c.opt.readOnly {
  377. pipe.ReadOnly(ctx)
  378. }
  379. if c.opt.ClientName != "" {
  380. pipe.ClientSetName(ctx, c.opt.ClientName)
  381. }
  382. return nil
  383. })
  384. if err != nil {
  385. return fmt.Errorf("failed to initialize connection options: %w", err)
  386. }
  387. // Enable maintnotifications if maintnotifications are configured
  388. c.optLock.RLock()
  389. maintNotifEnabled := c.opt.MaintNotificationsConfig != nil && c.opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled
  390. protocol := c.opt.Protocol
  391. endpointType := c.opt.MaintNotificationsConfig.EndpointType
  392. c.optLock.RUnlock()
  393. var maintNotifHandshakeErr error
  394. if maintNotifEnabled && protocol == 3 {
  395. maintNotifHandshakeErr = conn.ClientMaintNotifications(
  396. ctx,
  397. true,
  398. endpointType.String(),
  399. ).Err()
  400. if maintNotifHandshakeErr != nil {
  401. if !isRedisError(maintNotifHandshakeErr) {
  402. // if not redis error, fail the connection
  403. return maintNotifHandshakeErr
  404. }
  405. c.optLock.Lock()
  406. // handshake failed - check and modify config atomically
  407. switch c.opt.MaintNotificationsConfig.Mode {
  408. case maintnotifications.ModeEnabled:
  409. // enabled mode, fail the connection
  410. c.optLock.Unlock()
  411. return fmt.Errorf("failed to enable maintnotifications: %w", maintNotifHandshakeErr)
  412. default: // will handle auto and any other
  413. internal.Logger.Printf(ctx, "auto mode fallback: maintnotifications disabled due to handshake error: %v", maintNotifHandshakeErr)
  414. c.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeDisabled
  415. c.optLock.Unlock()
  416. // auto mode, disable maintnotifications and continue
  417. if err := c.disableMaintNotificationsUpgrades(); err != nil {
  418. // Log error but continue - auto mode should be resilient
  419. internal.Logger.Printf(ctx, "failed to disable maintnotifications in auto mode: %v", err)
  420. }
  421. }
  422. } else {
  423. // handshake was executed successfully
  424. // to make sure that the handshake will be executed on other connections as well if it was successfully
  425. // executed on this connection, we will force the handshake to be executed on all connections
  426. c.optLock.Lock()
  427. c.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeEnabled
  428. c.optLock.Unlock()
  429. }
  430. }
  431. if !c.opt.DisableIdentity && !c.opt.DisableIndentity {
  432. libName := ""
  433. libVer := Version()
  434. if c.opt.IdentitySuffix != "" {
  435. libName = c.opt.IdentitySuffix
  436. }
  437. p := conn.Pipeline()
  438. p.ClientSetInfo(ctx, WithLibraryName(libName))
  439. p.ClientSetInfo(ctx, WithLibraryVersion(libVer))
  440. // Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid
  441. // out of order responses later on.
  442. if _, err = p.Exec(ctx); err != nil && !isRedisError(err) {
  443. return err
  444. }
  445. }
  446. // mark the connection as usable and inited
  447. // once returned to the pool as idle, this connection can be used by other clients
  448. cn.SetUsable(true)
  449. cn.SetUsed(false)
  450. cn.Inited.Store(true)
  451. // Set the connection initialization function for potential reconnections
  452. cn.SetInitConnFunc(c.createInitConnFunc())
  453. if c.opt.OnConnect != nil {
  454. return c.opt.OnConnect(ctx, conn)
  455. }
  456. return nil
  457. }
  458. func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {
  459. if c.opt.Limiter != nil {
  460. c.opt.Limiter.ReportResult(err)
  461. }
  462. if isBadConn(err, false, c.opt.Addr) {
  463. c.connPool.Remove(ctx, cn, err)
  464. } else {
  465. // process any pending push notifications before returning the connection to the pool
  466. if err := c.processPushNotifications(ctx, cn); err != nil {
  467. internal.Logger.Printf(ctx, "push: error processing pending notifications before releasing connection: %v", err)
  468. }
  469. c.connPool.Put(ctx, cn)
  470. }
  471. }
  472. func (c *baseClient) withConn(
  473. ctx context.Context, fn func(context.Context, *pool.Conn) error,
  474. ) error {
  475. cn, err := c.getConn(ctx)
  476. if err != nil {
  477. return err
  478. }
  479. var fnErr error
  480. defer func() {
  481. c.releaseConn(ctx, cn, fnErr)
  482. }()
  483. fnErr = fn(ctx, cn)
  484. return fnErr
  485. }
  486. func (c *baseClient) dial(ctx context.Context, network, addr string) (net.Conn, error) {
  487. return c.opt.Dialer(ctx, network, addr)
  488. }
  489. func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
  490. var lastErr error
  491. for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
  492. attempt := attempt
  493. retry, err := c._process(ctx, cmd, attempt)
  494. if err == nil || !retry {
  495. return err
  496. }
  497. lastErr = err
  498. }
  499. return lastErr
  500. }
  501. func (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) {
  502. switch cmd.(type) {
  503. case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd:
  504. if c.opt.UnstableResp3 {
  505. return true, nil
  506. } else {
  507. return false, fmt.Errorf("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3. See the README and the release notes for guidance")
  508. }
  509. default:
  510. return false, nil
  511. }
  512. }
  513. func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
  514. if attempt > 0 {
  515. if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
  516. return false, err
  517. }
  518. }
  519. retryTimeout := uint32(0)
  520. if err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
  521. // Process any pending push notifications before executing the command
  522. if err := c.processPushNotifications(ctx, cn); err != nil {
  523. internal.Logger.Printf(ctx, "push: error processing pending notifications before command: %v", err)
  524. }
  525. if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
  526. return writeCmd(wr, cmd)
  527. }); err != nil {
  528. atomic.StoreUint32(&retryTimeout, 1)
  529. return err
  530. }
  531. readReplyFunc := cmd.readReply
  532. // Apply unstable RESP3 search module.
  533. if c.opt.Protocol != 2 {
  534. useRawReply, err := c.assertUnstableCommand(cmd)
  535. if err != nil {
  536. return err
  537. }
  538. if useRawReply {
  539. readReplyFunc = cmd.readRawReply
  540. }
  541. }
  542. if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), func(rd *proto.Reader) error {
  543. // To be sure there are no buffered push notifications, we process them before reading the reply
  544. if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
  545. internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
  546. }
  547. return readReplyFunc(rd)
  548. }); err != nil {
  549. if cmd.readTimeout() == nil {
  550. atomic.StoreUint32(&retryTimeout, 1)
  551. } else {
  552. atomic.StoreUint32(&retryTimeout, 0)
  553. }
  554. return err
  555. }
  556. return nil
  557. }); err != nil {
  558. retry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)
  559. return retry, err
  560. }
  561. return false, nil
  562. }
  563. func (c *baseClient) retryBackoff(attempt int) time.Duration {
  564. return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
  565. }
  566. func (c *baseClient) cmdTimeout(cmd Cmder) time.Duration {
  567. if timeout := cmd.readTimeout(); timeout != nil {
  568. t := *timeout
  569. if t == 0 {
  570. return 0
  571. }
  572. return t + 10*time.Second
  573. }
  574. return c.opt.ReadTimeout
  575. }
  576. // context returns the context for the current connection.
  577. // If the context timeout is enabled, it returns the original context.
  578. // Otherwise, it returns a new background context.
  579. func (c *baseClient) context(ctx context.Context) context.Context {
  580. if c.opt.ContextTimeoutEnabled {
  581. return ctx
  582. }
  583. return context.Background()
  584. }
  585. // createInitConnFunc creates a connection initialization function that can be used for reconnections.
  586. func (c *baseClient) createInitConnFunc() func(context.Context, *pool.Conn) error {
  587. return func(ctx context.Context, cn *pool.Conn) error {
  588. return c.initConn(ctx, cn)
  589. }
  590. }
  591. // enableMaintNotificationsUpgrades initializes the maintnotifications upgrade manager and pool hook.
  592. // This function is called during client initialization.
  593. // will register push notification handlers for all maintenance upgrade events.
  594. // will start background workers for handoff processing in the pool hook.
  595. func (c *baseClient) enableMaintNotificationsUpgrades() error {
  596. // Create client adapter
  597. clientAdapterInstance := newClientAdapter(c)
  598. // Create maintnotifications manager directly
  599. manager, err := maintnotifications.NewManager(clientAdapterInstance, c.connPool, c.opt.MaintNotificationsConfig)
  600. if err != nil {
  601. return err
  602. }
  603. // Set the manager reference and initialize pool hook
  604. c.maintNotificationsManagerLock.Lock()
  605. c.maintNotificationsManager = manager
  606. c.maintNotificationsManagerLock.Unlock()
  607. // Initialize pool hook (safe to call without lock since manager is now set)
  608. manager.InitPoolHook(c.dialHook)
  609. return nil
  610. }
  611. func (c *baseClient) disableMaintNotificationsUpgrades() error {
  612. c.maintNotificationsManagerLock.Lock()
  613. defer c.maintNotificationsManagerLock.Unlock()
  614. // Close the maintnotifications manager
  615. if c.maintNotificationsManager != nil {
  616. // Closing the manager will also shutdown the pool hook
  617. // and remove it from the pool
  618. c.maintNotificationsManager.Close()
  619. c.maintNotificationsManager = nil
  620. }
  621. return nil
  622. }
  623. // Close closes the client, releasing any open resources.
  624. //
  625. // It is rare to Close a Client, as the Client is meant to be
  626. // long-lived and shared between many goroutines.
  627. func (c *baseClient) Close() error {
  628. var firstErr error
  629. // Close maintnotifications manager first
  630. if err := c.disableMaintNotificationsUpgrades(); err != nil {
  631. firstErr = err
  632. }
  633. if c.onClose != nil {
  634. if err := c.onClose(); err != nil && firstErr == nil {
  635. firstErr = err
  636. }
  637. }
  638. if c.connPool != nil {
  639. if err := c.connPool.Close(); err != nil && firstErr == nil {
  640. firstErr = err
  641. }
  642. }
  643. if c.pubSubPool != nil {
  644. if err := c.pubSubPool.Close(); err != nil && firstErr == nil {
  645. firstErr = err
  646. }
  647. }
  648. return firstErr
  649. }
  650. func (c *baseClient) getAddr() string {
  651. return c.opt.Addr
  652. }
  653. func (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error {
  654. if err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds); err != nil {
  655. return err
  656. }
  657. return cmdsFirstErr(cmds)
  658. }
  659. func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
  660. if err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds); err != nil {
  661. return err
  662. }
  663. return cmdsFirstErr(cmds)
  664. }
  665. type pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error)
  666. func (c *baseClient) generalProcessPipeline(
  667. ctx context.Context, cmds []Cmder, p pipelineProcessor,
  668. ) error {
  669. var lastErr error
  670. for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
  671. if attempt > 0 {
  672. if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
  673. setCmdsErr(cmds, err)
  674. return err
  675. }
  676. }
  677. // Enable retries by default to retry dial errors returned by withConn.
  678. canRetry := true
  679. lastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
  680. // Process any pending push notifications before executing the pipeline
  681. if err := c.processPushNotifications(ctx, cn); err != nil {
  682. internal.Logger.Printf(ctx, "push: error processing pending notifications before processing pipeline: %v", err)
  683. }
  684. var err error
  685. canRetry, err = p(ctx, cn, cmds)
  686. return err
  687. })
  688. if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) {
  689. // The error should be set here only when failing to obtain the conn.
  690. if !isRedisError(lastErr) {
  691. setCmdsErr(cmds, lastErr)
  692. }
  693. return lastErr
  694. }
  695. }
  696. return lastErr
  697. }
  698. func (c *baseClient) pipelineProcessCmds(
  699. ctx context.Context, cn *pool.Conn, cmds []Cmder,
  700. ) (bool, error) {
  701. // Process any pending push notifications before executing the pipeline
  702. if err := c.processPushNotifications(ctx, cn); err != nil {
  703. internal.Logger.Printf(ctx, "push: error processing pending notifications before writing pipeline: %v", err)
  704. }
  705. if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
  706. return writeCmds(wr, cmds)
  707. }); err != nil {
  708. setCmdsErr(cmds, err)
  709. return true, err
  710. }
  711. if err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
  712. // read all replies
  713. return c.pipelineReadCmds(ctx, cn, rd, cmds)
  714. }); err != nil {
  715. return true, err
  716. }
  717. return false, nil
  718. }
  719. func (c *baseClient) pipelineReadCmds(ctx context.Context, cn *pool.Conn, rd *proto.Reader, cmds []Cmder) error {
  720. for i, cmd := range cmds {
  721. // To be sure there are no buffered push notifications, we process them before reading the reply
  722. if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
  723. internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
  724. }
  725. err := cmd.readReply(rd)
  726. cmd.SetErr(err)
  727. if err != nil && !isRedisError(err) {
  728. setCmdsErr(cmds[i+1:], err)
  729. return err
  730. }
  731. }
  732. // Retry errors like "LOADING redis is loading the dataset in memory".
  733. return cmds[0].Err()
  734. }
  735. func (c *baseClient) txPipelineProcessCmds(
  736. ctx context.Context, cn *pool.Conn, cmds []Cmder,
  737. ) (bool, error) {
  738. // Process any pending push notifications before executing the transaction pipeline
  739. if err := c.processPushNotifications(ctx, cn); err != nil {
  740. internal.Logger.Printf(ctx, "push: error processing pending notifications before transaction: %v", err)
  741. }
  742. if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
  743. return writeCmds(wr, cmds)
  744. }); err != nil {
  745. setCmdsErr(cmds, err)
  746. return true, err
  747. }
  748. if err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
  749. statusCmd := cmds[0].(*StatusCmd)
  750. // Trim multi and exec.
  751. trimmedCmds := cmds[1 : len(cmds)-1]
  752. if err := c.txPipelineReadQueued(ctx, cn, rd, statusCmd, trimmedCmds); err != nil {
  753. setCmdsErr(cmds, err)
  754. return err
  755. }
  756. // Read replies.
  757. return c.pipelineReadCmds(ctx, cn, rd, trimmedCmds)
  758. }); err != nil {
  759. return false, err
  760. }
  761. return false, nil
  762. }
  763. // txPipelineReadQueued reads queued replies from the Redis server.
  764. // It returns an error if the server returns an error or if the number of replies does not match the number of commands.
  765. func (c *baseClient) txPipelineReadQueued(ctx context.Context, cn *pool.Conn, rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error {
  766. // To be sure there are no buffered push notifications, we process them before reading the reply
  767. if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
  768. internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
  769. }
  770. // Parse +OK.
  771. if err := statusCmd.readReply(rd); err != nil {
  772. return err
  773. }
  774. // Parse +QUEUED.
  775. for _, cmd := range cmds {
  776. // To be sure there are no buffered push notifications, we process them before reading the reply
  777. if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
  778. internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
  779. }
  780. if err := statusCmd.readReply(rd); err != nil {
  781. cmd.SetErr(err)
  782. if !isRedisError(err) {
  783. return err
  784. }
  785. }
  786. }
  787. // To be sure there are no buffered push notifications, we process them before reading the reply
  788. if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
  789. internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
  790. }
  791. // Parse number of replies.
  792. line, err := rd.ReadLine()
  793. if err != nil {
  794. if err == Nil {
  795. err = TxFailedErr
  796. }
  797. return err
  798. }
  799. if line[0] != proto.RespArray {
  800. return fmt.Errorf("redis: expected '*', but got line %q", line)
  801. }
  802. return nil
  803. }
  804. //------------------------------------------------------------------------------
  805. // Client is a Redis client representing a pool of zero or more underlying connections.
  806. // It's safe for concurrent use by multiple goroutines.
  807. //
  808. // Client creates and frees connections automatically; it also maintains a free pool
  809. // of idle connections. You can control the pool size with Config.PoolSize option.
  810. type Client struct {
  811. *baseClient
  812. cmdable
  813. }
  814. // NewClient returns a client to the Redis Server specified by Options.
  815. func NewClient(opt *Options) *Client {
  816. if opt == nil {
  817. panic("redis: NewClient nil options")
  818. }
  819. // clone to not share options with the caller
  820. opt = opt.clone()
  821. opt.init()
  822. // Push notifications are always enabled for RESP3 (cannot be disabled)
  823. c := Client{
  824. baseClient: &baseClient{
  825. opt: opt,
  826. },
  827. }
  828. c.init()
  829. // Initialize push notification processor using shared helper
  830. // Use void processor for RESP2 connections (push notifications not available)
  831. c.pushProcessor = initializePushProcessor(opt)
  832. // set opt push processor for child clients
  833. c.opt.PushNotificationProcessor = c.pushProcessor
  834. // Create connection pools
  835. var err error
  836. c.connPool, err = newConnPool(opt, c.dialHook)
  837. if err != nil {
  838. panic(fmt.Errorf("redis: failed to create connection pool: %w", err))
  839. }
  840. c.pubSubPool, err = newPubSubPool(opt, c.dialHook)
  841. if err != nil {
  842. panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err))
  843. }
  844. if opt.StreamingCredentialsProvider != nil {
  845. c.streamingCredentialsManager = streaming.NewManager(c.connPool, c.opt.PoolTimeout)
  846. c.connPool.AddPoolHook(c.streamingCredentialsManager.PoolHook())
  847. }
  848. // Initialize maintnotifications first if enabled and protocol is RESP3
  849. if opt.MaintNotificationsConfig != nil && opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled && opt.Protocol == 3 {
  850. err := c.enableMaintNotificationsUpgrades()
  851. if err != nil {
  852. internal.Logger.Printf(context.Background(), "failed to initialize maintnotifications: %v", err)
  853. if opt.MaintNotificationsConfig.Mode == maintnotifications.ModeEnabled {
  854. /*
  855. Design decision: panic here to fail fast if maintnotifications cannot be enabled when explicitly requested.
  856. We choose to panic instead of returning an error to avoid breaking the existing client API, which does not expect
  857. an error from NewClient. This ensures that misconfiguration or critical initialization failures are surfaced
  858. immediately, rather than allowing the client to continue in a partially initialized or inconsistent state.
  859. Clients relying on maintnotifications should be aware that initialization errors will cause a panic, and should
  860. handle this accordingly (e.g., via recover or by validating configuration before calling NewClient).
  861. This approach is only used when MaintNotificationsConfig.Mode is MaintNotificationsEnabled, indicating that maintnotifications
  862. upgrades are required for correct operation. In other modes, initialization failures are logged but do not panic.
  863. */
  864. panic(fmt.Errorf("failed to enable maintnotifications: %w", err))
  865. }
  866. }
  867. }
  868. return &c
  869. }
  870. func (c *Client) init() {
  871. c.cmdable = c.Process
  872. c.initHooks(hooks{
  873. dial: c.baseClient.dial,
  874. process: c.baseClient.process,
  875. pipeline: c.baseClient.processPipeline,
  876. txPipeline: c.baseClient.processTxPipeline,
  877. })
  878. }
  879. func (c *Client) WithTimeout(timeout time.Duration) *Client {
  880. clone := *c
  881. clone.baseClient = c.baseClient.withTimeout(timeout)
  882. clone.init()
  883. return &clone
  884. }
  885. func (c *Client) Conn() *Conn {
  886. return newConn(c.opt, pool.NewStickyConnPool(c.connPool), &c.hooksMixin)
  887. }
  888. func (c *Client) Process(ctx context.Context, cmd Cmder) error {
  889. err := c.processHook(ctx, cmd)
  890. cmd.SetErr(err)
  891. return err
  892. }
  893. // Options returns read-only Options that were used to create the client.
  894. func (c *Client) Options() *Options {
  895. return c.opt
  896. }
  897. // GetMaintNotificationsManager returns the maintnotifications manager instance for monitoring and control.
  898. // Returns nil if maintnotifications are not enabled.
  899. func (c *Client) GetMaintNotificationsManager() *maintnotifications.Manager {
  900. c.maintNotificationsManagerLock.RLock()
  901. defer c.maintNotificationsManagerLock.RUnlock()
  902. return c.maintNotificationsManager
  903. }
  904. // initializePushProcessor initializes the push notification processor for any client type.
  905. // This is a shared helper to avoid duplication across NewClient, NewFailoverClient, and NewSentinelClient.
  906. func initializePushProcessor(opt *Options) push.NotificationProcessor {
  907. // Always use custom processor if provided
  908. if opt.PushNotificationProcessor != nil {
  909. return opt.PushNotificationProcessor
  910. }
  911. // Push notifications are always enabled for RESP3, disabled for RESP2
  912. if opt.Protocol == 3 {
  913. // Create default processor for RESP3 connections
  914. return NewPushNotificationProcessor()
  915. }
  916. // Create void processor for RESP2 connections (push notifications not available)
  917. return NewVoidPushNotificationProcessor()
  918. }
  919. // RegisterPushNotificationHandler registers a handler for a specific push notification name.
  920. // Returns an error if a handler is already registered for this push notification name.
  921. // If protected is true, the handler cannot be unregistered.
  922. func (c *Client) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error {
  923. return c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected)
  924. }
  925. // GetPushNotificationHandler returns the handler for a specific push notification name.
  926. // Returns nil if no handler is registered for the given name.
  927. func (c *Client) GetPushNotificationHandler(pushNotificationName string) push.NotificationHandler {
  928. return c.pushProcessor.GetHandler(pushNotificationName)
  929. }
  930. type PoolStats pool.Stats
  931. // PoolStats returns connection pool stats.
  932. func (c *Client) PoolStats() *PoolStats {
  933. stats := c.connPool.Stats()
  934. stats.PubSubStats = *(c.pubSubPool.Stats())
  935. return (*PoolStats)(stats)
  936. }
  937. func (c *Client) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
  938. return c.Pipeline().Pipelined(ctx, fn)
  939. }
  940. func (c *Client) Pipeline() Pipeliner {
  941. pipe := Pipeline{
  942. exec: pipelineExecer(c.processPipelineHook),
  943. }
  944. pipe.init()
  945. return &pipe
  946. }
  947. func (c *Client) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
  948. return c.TxPipeline().Pipelined(ctx, fn)
  949. }
  950. // TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
  951. func (c *Client) TxPipeline() Pipeliner {
  952. pipe := Pipeline{
  953. exec: func(ctx context.Context, cmds []Cmder) error {
  954. cmds = wrapMultiExec(ctx, cmds)
  955. return c.processTxPipelineHook(ctx, cmds)
  956. },
  957. }
  958. pipe.init()
  959. return &pipe
  960. }
  961. func (c *Client) pubSub() *PubSub {
  962. pubsub := &PubSub{
  963. opt: c.opt,
  964. newConn: func(ctx context.Context, addr string, channels []string) (*pool.Conn, error) {
  965. cn, err := c.pubSubPool.NewConn(ctx, c.opt.Network, addr, channels)
  966. if err != nil {
  967. return nil, err
  968. }
  969. // will return nil if already initialized
  970. err = c.initConn(ctx, cn)
  971. if err != nil {
  972. _ = cn.Close()
  973. return nil, err
  974. }
  975. // Track connection in PubSubPool
  976. c.pubSubPool.TrackConn(cn)
  977. return cn, nil
  978. },
  979. closeConn: func(cn *pool.Conn) error {
  980. // Untrack connection from PubSubPool
  981. c.pubSubPool.UntrackConn(cn)
  982. _ = cn.Close()
  983. return nil
  984. },
  985. pushProcessor: c.pushProcessor,
  986. }
  987. pubsub.init()
  988. return pubsub
  989. }
  990. // Subscribe subscribes the client to the specified channels.
  991. // Channels can be omitted to create empty subscription.
  992. // Note that this method does not wait on a response from Redis, so the
  993. // subscription may not be active immediately. To force the connection to wait,
  994. // you may call the Receive() method on the returned *PubSub like so:
  995. //
  996. // sub := client.Subscribe(queryResp)
  997. // iface, err := sub.Receive()
  998. // if err != nil {
  999. // // handle error
  1000. // }
  1001. //
  1002. // // Should be *Subscription, but others are possible if other actions have been
  1003. // // taken on sub since it was created.
  1004. // switch iface.(type) {
  1005. // case *Subscription:
  1006. // // subscribe succeeded
  1007. // case *Message:
  1008. // // received first message
  1009. // case *Pong:
  1010. // // pong received
  1011. // default:
  1012. // // handle error
  1013. // }
  1014. //
  1015. // ch := sub.Channel()
  1016. func (c *Client) Subscribe(ctx context.Context, channels ...string) *PubSub {
  1017. pubsub := c.pubSub()
  1018. if len(channels) > 0 {
  1019. _ = pubsub.Subscribe(ctx, channels...)
  1020. }
  1021. return pubsub
  1022. }
  1023. // PSubscribe subscribes the client to the given patterns.
  1024. // Patterns can be omitted to create empty subscription.
  1025. func (c *Client) PSubscribe(ctx context.Context, channels ...string) *PubSub {
  1026. pubsub := c.pubSub()
  1027. if len(channels) > 0 {
  1028. _ = pubsub.PSubscribe(ctx, channels...)
  1029. }
  1030. return pubsub
  1031. }
  1032. // SSubscribe Subscribes the client to the specified shard channels.
  1033. // Channels can be omitted to create empty subscription.
  1034. func (c *Client) SSubscribe(ctx context.Context, channels ...string) *PubSub {
  1035. pubsub := c.pubSub()
  1036. if len(channels) > 0 {
  1037. _ = pubsub.SSubscribe(ctx, channels...)
  1038. }
  1039. return pubsub
  1040. }
  1041. //------------------------------------------------------------------------------
  1042. // Conn represents a single Redis connection rather than a pool of connections.
  1043. // Prefer running commands from Client unless there is a specific need
  1044. // for a continuous single Redis connection.
  1045. type Conn struct {
  1046. baseClient
  1047. cmdable
  1048. statefulCmdable
  1049. }
  1050. // newConn is a helper func to create a new Conn instance.
  1051. // the Conn instance is not thread-safe and should not be shared between goroutines.
  1052. // the parentHooks will be cloned, no need to clone before passing it.
  1053. func newConn(opt *Options, connPool pool.Pooler, parentHooks *hooksMixin) *Conn {
  1054. c := Conn{
  1055. baseClient: baseClient{
  1056. opt: opt,
  1057. connPool: connPool,
  1058. },
  1059. }
  1060. if parentHooks != nil {
  1061. c.hooksMixin = parentHooks.clone()
  1062. }
  1063. // Initialize push notification processor using shared helper
  1064. // Use void processor for RESP2 connections (push notifications not available)
  1065. c.pushProcessor = initializePushProcessor(opt)
  1066. c.cmdable = c.Process
  1067. c.statefulCmdable = c.Process
  1068. c.initHooks(hooks{
  1069. dial: c.baseClient.dial,
  1070. process: c.baseClient.process,
  1071. pipeline: c.baseClient.processPipeline,
  1072. txPipeline: c.baseClient.processTxPipeline,
  1073. })
  1074. return &c
  1075. }
  1076. func (c *Conn) Process(ctx context.Context, cmd Cmder) error {
  1077. err := c.processHook(ctx, cmd)
  1078. cmd.SetErr(err)
  1079. return err
  1080. }
  1081. // RegisterPushNotificationHandler registers a handler for a specific push notification name.
  1082. // Returns an error if a handler is already registered for this push notification name.
  1083. // If protected is true, the handler cannot be unregistered.
  1084. func (c *Conn) RegisterPushNotificationHandler(pushNotificationName string, handler push.NotificationHandler, protected bool) error {
  1085. return c.pushProcessor.RegisterHandler(pushNotificationName, handler, protected)
  1086. }
  1087. func (c *Conn) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
  1088. return c.Pipeline().Pipelined(ctx, fn)
  1089. }
  1090. func (c *Conn) Pipeline() Pipeliner {
  1091. pipe := Pipeline{
  1092. exec: c.processPipelineHook,
  1093. }
  1094. pipe.init()
  1095. return &pipe
  1096. }
  1097. func (c *Conn) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
  1098. return c.TxPipeline().Pipelined(ctx, fn)
  1099. }
  1100. // TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
  1101. func (c *Conn) TxPipeline() Pipeliner {
  1102. pipe := Pipeline{
  1103. exec: func(ctx context.Context, cmds []Cmder) error {
  1104. cmds = wrapMultiExec(ctx, cmds)
  1105. return c.processTxPipelineHook(ctx, cmds)
  1106. },
  1107. }
  1108. pipe.init()
  1109. return &pipe
  1110. }
  1111. // processPushNotifications processes all pending push notifications on a connection
  1112. // This ensures that cluster topology changes are handled immediately before the connection is used
  1113. // This method should be called by the client before using WithReader for command execution
  1114. func (c *baseClient) processPushNotifications(ctx context.Context, cn *pool.Conn) error {
  1115. // Only process push notifications for RESP3 connections with a processor
  1116. // Also check if there is any data to read before processing
  1117. // Which is an optimization on UNIX systems where MaybeHasData is a syscall
  1118. // On Windows, MaybeHasData always returns true, so this check is a no-op
  1119. if c.opt.Protocol != 3 || c.pushProcessor == nil || !cn.MaybeHasData() {
  1120. return nil
  1121. }
  1122. // Use WithReader to access the reader and process push notifications
  1123. // This is critical for maintnotifications to work properly
  1124. // NOTE: almost no timeouts are set for this read, so it should not block
  1125. // longer than necessary, 10us should be plenty of time to read if there are any push notifications
  1126. // on the socket.
  1127. return cn.WithReader(ctx, 10*time.Microsecond, func(rd *proto.Reader) error {
  1128. // Create handler context with client, connection pool, and connection information
  1129. handlerCtx := c.pushNotificationHandlerContext(cn)
  1130. return c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd)
  1131. })
  1132. }
  1133. // processPendingPushNotificationWithReader processes all pending push notifications on a connection
  1134. // This method should be called by the client in WithReader before reading the reply
  1135. func (c *baseClient) processPendingPushNotificationWithReader(ctx context.Context, cn *pool.Conn, rd *proto.Reader) error {
  1136. // if we have the reader, we don't need to check for data on the socket, we are waiting
  1137. // for either a reply or a push notification, so we can block until we get a reply or reach the timeout
  1138. if c.opt.Protocol != 3 || c.pushProcessor == nil {
  1139. return nil
  1140. }
  1141. // Create handler context with client, connection pool, and connection information
  1142. handlerCtx := c.pushNotificationHandlerContext(cn)
  1143. return c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd)
  1144. }
  1145. // pushNotificationHandlerContext creates a handler context for push notification processing
  1146. func (c *baseClient) pushNotificationHandlerContext(cn *pool.Conn) push.NotificationHandlerContext {
  1147. return push.NotificationHandlerContext{
  1148. Client: c,
  1149. ConnPool: c.connPool,
  1150. Conn: cn, // Wrap in adapter for easier interface access
  1151. }
  1152. }