connection.py 133 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614
  1. import copy
  2. import os
  3. import socket
  4. import sys
  5. import threading
  6. import time
  7. import weakref
  8. from abc import ABC, abstractmethod
  9. from itertools import chain
  10. from queue import Empty, Full, LifoQueue
  11. from typing import (
  12. Any,
  13. Callable,
  14. Dict,
  15. Iterable,
  16. List,
  17. Literal,
  18. Optional,
  19. Type,
  20. TypeVar,
  21. Union,
  22. )
  23. from urllib.parse import parse_qs, unquote, urlparse
  24. from redis.cache import (
  25. CacheEntry,
  26. CacheEntryStatus,
  27. CacheFactory,
  28. CacheFactoryInterface,
  29. CacheInterface,
  30. CacheKey,
  31. CacheProxy,
  32. )
  33. from ._parsers import Encoder, _HiredisParser, _RESP2Parser, _RESP3Parser
  34. from .auth.token import TokenInterface
  35. from .backoff import NoBackoff
  36. from .credentials import CredentialProvider, UsernamePasswordCredentialProvider
  37. from .driver_info import DriverInfo, resolve_driver_info
  38. from .event import AfterConnectionReleasedEvent, EventDispatcher
  39. from .exceptions import (
  40. AuthenticationError,
  41. AuthenticationWrongNumberOfArgsError,
  42. ChildDeadlockedError,
  43. ConnectionError,
  44. DataError,
  45. MaxConnectionsError,
  46. RedisError,
  47. ResponseError,
  48. TimeoutError,
  49. )
  50. from .maint_notifications import (
  51. MaintenanceState,
  52. MaintNotificationsConfig,
  53. MaintNotificationsConnectionHandler,
  54. MaintNotificationsPoolHandler,
  55. OSSMaintNotificationsHandler,
  56. )
  57. from .observability.attributes import (
  58. DB_CLIENT_CONNECTION_POOL_NAME,
  59. DB_CLIENT_CONNECTION_STATE,
  60. AttributeBuilder,
  61. ConnectionState,
  62. CSCReason,
  63. CSCResult,
  64. get_pool_name,
  65. )
  66. from .observability.metrics import CloseReason
  67. from .observability.recorder import (
  68. init_csc_items,
  69. record_connection_closed,
  70. record_connection_count,
  71. record_connection_create_time,
  72. record_connection_wait_time,
  73. record_csc_eviction,
  74. record_csc_network_saved,
  75. record_csc_request,
  76. record_error_count,
  77. register_csc_items_callback,
  78. )
  79. from .retry import Retry
  80. from .utils import (
  81. CRYPTOGRAPHY_AVAILABLE,
  82. HIREDIS_AVAILABLE,
  83. SSL_AVAILABLE,
  84. check_protocol_version,
  85. compare_versions,
  86. deprecated_args,
  87. ensure_string,
  88. format_error_message,
  89. str_if_bytes,
  90. )
  91. if SSL_AVAILABLE:
  92. import ssl
  93. from ssl import VerifyFlags
  94. else:
  95. ssl = None
  96. VerifyFlags = None
  97. if HIREDIS_AVAILABLE:
  98. import hiredis
  99. SYM_STAR = b"*"
  100. SYM_DOLLAR = b"$"
  101. SYM_CRLF = b"\r\n"
  102. SYM_EMPTY = b""
  103. DEFAULT_RESP_VERSION = 2
  104. SENTINEL = object()
  105. DefaultParser: Type[Union[_RESP2Parser, _RESP3Parser, _HiredisParser]]
  106. if HIREDIS_AVAILABLE:
  107. DefaultParser = _HiredisParser
  108. else:
  109. DefaultParser = _RESP2Parser
  110. class HiredisRespSerializer:
  111. def pack(self, *args: List):
  112. """Pack a series of arguments into the Redis protocol"""
  113. output = []
  114. if isinstance(args[0], str):
  115. args = tuple(args[0].encode().split()) + args[1:]
  116. elif b" " in args[0]:
  117. args = tuple(args[0].split()) + args[1:]
  118. try:
  119. output.append(hiredis.pack_command(args))
  120. except TypeError:
  121. _, value, traceback = sys.exc_info()
  122. raise DataError(value).with_traceback(traceback)
  123. return output
  124. class PythonRespSerializer:
  125. def __init__(self, buffer_cutoff, encode) -> None:
  126. self._buffer_cutoff = buffer_cutoff
  127. self.encode = encode
  128. def pack(self, *args):
  129. """Pack a series of arguments into the Redis protocol"""
  130. output = []
  131. # the client might have included 1 or more literal arguments in
  132. # the command name, e.g., 'CONFIG GET'. The Redis server expects these
  133. # arguments to be sent separately, so split the first argument
  134. # manually. These arguments should be bytestrings so that they are
  135. # not encoded.
  136. if isinstance(args[0], str):
  137. args = tuple(args[0].encode().split()) + args[1:]
  138. elif b" " in args[0]:
  139. args = tuple(args[0].split()) + args[1:]
  140. buff = SYM_EMPTY.join((SYM_STAR, str(len(args)).encode(), SYM_CRLF))
  141. buffer_cutoff = self._buffer_cutoff
  142. for arg in map(self.encode, args):
  143. # to avoid large string mallocs, chunk the command into the
  144. # output list if we're sending large values or memoryviews
  145. arg_length = len(arg)
  146. if (
  147. len(buff) > buffer_cutoff
  148. or arg_length > buffer_cutoff
  149. or isinstance(arg, memoryview)
  150. ):
  151. buff = SYM_EMPTY.join(
  152. (buff, SYM_DOLLAR, str(arg_length).encode(), SYM_CRLF)
  153. )
  154. output.append(buff)
  155. output.append(arg)
  156. buff = SYM_CRLF
  157. else:
  158. buff = SYM_EMPTY.join(
  159. (
  160. buff,
  161. SYM_DOLLAR,
  162. str(arg_length).encode(),
  163. SYM_CRLF,
  164. arg,
  165. SYM_CRLF,
  166. )
  167. )
  168. output.append(buff)
  169. return output
  170. class ConnectionInterface:
  171. @abstractmethod
  172. def repr_pieces(self):
  173. pass
  174. @abstractmethod
  175. def register_connect_callback(self, callback):
  176. pass
  177. @abstractmethod
  178. def deregister_connect_callback(self, callback):
  179. pass
  180. @abstractmethod
  181. def set_parser(self, parser_class):
  182. pass
  183. @abstractmethod
  184. def get_protocol(self):
  185. pass
  186. @abstractmethod
  187. def connect(self):
  188. pass
  189. @abstractmethod
  190. def on_connect(self):
  191. pass
  192. @abstractmethod
  193. def disconnect(self, *args, **kwargs):
  194. pass
  195. @abstractmethod
  196. def check_health(self):
  197. pass
  198. @abstractmethod
  199. def send_packed_command(self, command, check_health=True):
  200. pass
  201. @abstractmethod
  202. def send_command(self, *args, **kwargs):
  203. pass
  204. @abstractmethod
  205. def can_read(self, timeout=0):
  206. pass
  207. @abstractmethod
  208. def read_response(
  209. self,
  210. disable_decoding=False,
  211. *,
  212. disconnect_on_error=True,
  213. push_request=False,
  214. ):
  215. pass
  216. @abstractmethod
  217. def pack_command(self, *args):
  218. pass
  219. @abstractmethod
  220. def pack_commands(self, commands):
  221. pass
  222. @property
  223. @abstractmethod
  224. def handshake_metadata(self) -> Union[Dict[bytes, bytes], Dict[str, str]]:
  225. pass
  226. @abstractmethod
  227. def set_re_auth_token(self, token: TokenInterface):
  228. pass
  229. @abstractmethod
  230. def re_auth(self):
  231. pass
  232. @abstractmethod
  233. def mark_for_reconnect(self):
  234. """
  235. Mark the connection to be reconnected on the next command.
  236. This is useful when a connection is moved to a different node.
  237. """
  238. pass
  239. @abstractmethod
  240. def should_reconnect(self):
  241. """
  242. Returns True if the connection should be reconnected.
  243. """
  244. pass
  245. @abstractmethod
  246. def reset_should_reconnect(self):
  247. """
  248. Reset the internal flag to False.
  249. """
  250. pass
  251. class MaintNotificationsAbstractConnection:
  252. """
  253. Abstract class for handling maintenance notifications logic.
  254. This class is expected to be used as base class together with ConnectionInterface.
  255. This class is intended to be used with multiple inheritance!
  256. All logic related to maintenance notifications is encapsulated in this class.
  257. """
  258. def __init__(
  259. self,
  260. maint_notifications_config: Optional[MaintNotificationsConfig],
  261. maint_notifications_pool_handler: Optional[
  262. MaintNotificationsPoolHandler
  263. ] = None,
  264. maintenance_state: "MaintenanceState" = MaintenanceState.NONE,
  265. maintenance_notification_hash: Optional[int] = None,
  266. orig_host_address: Optional[str] = None,
  267. orig_socket_timeout: Optional[float] = None,
  268. orig_socket_connect_timeout: Optional[float] = None,
  269. oss_cluster_maint_notifications_handler: Optional[
  270. OSSMaintNotificationsHandler
  271. ] = None,
  272. parser: Optional[Union[_HiredisParser, _RESP3Parser]] = None,
  273. event_dispatcher: Optional[EventDispatcher] = None,
  274. ):
  275. """
  276. Initialize the maintenance notifications for the connection.
  277. Args:
  278. maint_notifications_config (MaintNotificationsConfig): The configuration for maintenance notifications.
  279. maint_notifications_pool_handler (Optional[MaintNotificationsPoolHandler]): The pool handler for maintenance notifications.
  280. maintenance_state (MaintenanceState): The current maintenance state of the connection.
  281. maintenance_notification_hash (Optional[int]): The current maintenance notification hash of the connection.
  282. orig_host_address (Optional[str]): The original host address of the connection.
  283. orig_socket_timeout (Optional[float]): The original socket timeout of the connection.
  284. orig_socket_connect_timeout (Optional[float]): The original socket connect timeout of the connection.
  285. oss_cluster_maint_notifications_handler (Optional[OSSMaintNotificationsHandler]): The OSS cluster handler for maintenance notifications.
  286. parser (Optional[Union[_HiredisParser, _RESP3Parser]]): The parser to use for maintenance notifications.
  287. If not provided, the parser from the connection is used.
  288. This is useful when the parser is created after this object.
  289. """
  290. self.maint_notifications_config = maint_notifications_config
  291. self.maintenance_state = maintenance_state
  292. self.maintenance_notification_hash = maintenance_notification_hash
  293. if event_dispatcher is not None:
  294. self.event_dispatcher = event_dispatcher
  295. else:
  296. self.event_dispatcher = EventDispatcher()
  297. self._configure_maintenance_notifications(
  298. maint_notifications_pool_handler,
  299. orig_host_address,
  300. orig_socket_timeout,
  301. orig_socket_connect_timeout,
  302. oss_cluster_maint_notifications_handler,
  303. parser,
  304. )
  305. self._processed_start_maint_notifications = set()
  306. self._skipped_end_maint_notifications = set()
  307. @abstractmethod
  308. def _get_parser(self) -> Union[_HiredisParser, _RESP3Parser]:
  309. pass
  310. @abstractmethod
  311. def _get_socket(self) -> Optional[socket.socket]:
  312. pass
  313. @abstractmethod
  314. def get_protocol(self) -> Union[int, str]:
  315. """
  316. Returns:
  317. The RESP protocol version, or ``None`` if the protocol is not specified,
  318. in which case the server default will be used.
  319. """
  320. pass
  321. @property
  322. @abstractmethod
  323. def host(self) -> str:
  324. pass
  325. @host.setter
  326. @abstractmethod
  327. def host(self, value: str):
  328. pass
  329. @property
  330. @abstractmethod
  331. def socket_timeout(self) -> Optional[Union[float, int]]:
  332. pass
  333. @socket_timeout.setter
  334. @abstractmethod
  335. def socket_timeout(self, value: Optional[Union[float, int]]):
  336. pass
  337. @property
  338. @abstractmethod
  339. def socket_connect_timeout(self) -> Optional[Union[float, int]]:
  340. pass
  341. @socket_connect_timeout.setter
  342. @abstractmethod
  343. def socket_connect_timeout(self, value: Optional[Union[float, int]]):
  344. pass
  345. @abstractmethod
  346. def send_command(self, *args, **kwargs):
  347. pass
  348. @abstractmethod
  349. def read_response(
  350. self,
  351. disable_decoding=False,
  352. *,
  353. disconnect_on_error=True,
  354. push_request=False,
  355. ):
  356. pass
  357. @abstractmethod
  358. def disconnect(self, *args, **kwargs):
  359. pass
  360. def _configure_maintenance_notifications(
  361. self,
  362. maint_notifications_pool_handler: Optional[
  363. MaintNotificationsPoolHandler
  364. ] = None,
  365. orig_host_address=None,
  366. orig_socket_timeout=None,
  367. orig_socket_connect_timeout=None,
  368. oss_cluster_maint_notifications_handler: Optional[
  369. OSSMaintNotificationsHandler
  370. ] = None,
  371. parser: Optional[Union[_HiredisParser, _RESP3Parser]] = None,
  372. ):
  373. """
  374. Enable maintenance notifications by setting up
  375. handlers and storing original connection parameters.
  376. Should be used ONLY with parsers that support push notifications.
  377. """
  378. if (
  379. not self.maint_notifications_config
  380. or not self.maint_notifications_config.enabled
  381. ):
  382. self._maint_notifications_pool_handler = None
  383. self._maint_notifications_connection_handler = None
  384. self._oss_cluster_maint_notifications_handler = None
  385. return
  386. if not parser:
  387. raise RedisError(
  388. "To configure maintenance notifications, a parser must be provided!"
  389. )
  390. if not isinstance(parser, _HiredisParser) and not isinstance(
  391. parser, _RESP3Parser
  392. ):
  393. raise RedisError(
  394. "Maintenance notifications are only supported with hiredis and RESP3 parsers!"
  395. )
  396. if maint_notifications_pool_handler:
  397. # Extract a reference to a new pool handler that copies all properties
  398. # of the original one and has a different connection reference
  399. # This is needed because when we attach the handler to the parser
  400. # we need to make sure that the handler has a reference to the
  401. # connection that the parser is attached to.
  402. self._maint_notifications_pool_handler = (
  403. maint_notifications_pool_handler.get_handler_for_connection()
  404. )
  405. self._maint_notifications_pool_handler.set_connection(self)
  406. else:
  407. self._maint_notifications_pool_handler = None
  408. self._maint_notifications_connection_handler = (
  409. MaintNotificationsConnectionHandler(self, self.maint_notifications_config)
  410. )
  411. if oss_cluster_maint_notifications_handler:
  412. self._oss_cluster_maint_notifications_handler = (
  413. oss_cluster_maint_notifications_handler
  414. )
  415. else:
  416. self._oss_cluster_maint_notifications_handler = None
  417. # Set up OSS cluster handler to parser if available
  418. if self._oss_cluster_maint_notifications_handler:
  419. parser.set_oss_cluster_maint_push_handler(
  420. self._oss_cluster_maint_notifications_handler.handle_notification
  421. )
  422. # Set up pool handler to parser if available
  423. if self._maint_notifications_pool_handler:
  424. parser.set_node_moving_push_handler(
  425. self._maint_notifications_pool_handler.handle_notification
  426. )
  427. # Set up connection handler
  428. parser.set_maintenance_push_handler(
  429. self._maint_notifications_connection_handler.handle_notification
  430. )
  431. # Store original connection parameters
  432. self.orig_host_address = orig_host_address if orig_host_address else self.host
  433. self.orig_socket_timeout = (
  434. orig_socket_timeout if orig_socket_timeout else self.socket_timeout
  435. )
  436. self.orig_socket_connect_timeout = (
  437. orig_socket_connect_timeout
  438. if orig_socket_connect_timeout
  439. else self.socket_connect_timeout
  440. )
  441. def set_maint_notifications_pool_handler_for_connection(
  442. self, maint_notifications_pool_handler: MaintNotificationsPoolHandler
  443. ):
  444. # Deep copy the pool handler to avoid sharing the same pool handler
  445. # between multiple connections, because otherwise each connection will override
  446. # the connection reference and the pool handler will only hold a reference
  447. # to the last connection that was set.
  448. maint_notifications_pool_handler_copy = (
  449. maint_notifications_pool_handler.get_handler_for_connection()
  450. )
  451. maint_notifications_pool_handler_copy.set_connection(self)
  452. self._get_parser().set_node_moving_push_handler(
  453. maint_notifications_pool_handler_copy.handle_notification
  454. )
  455. self._maint_notifications_pool_handler = maint_notifications_pool_handler_copy
  456. # Update maintenance notification connection handler if it doesn't exist
  457. if not self._maint_notifications_connection_handler:
  458. self._maint_notifications_connection_handler = (
  459. MaintNotificationsConnectionHandler(
  460. self, maint_notifications_pool_handler.config
  461. )
  462. )
  463. self._get_parser().set_maintenance_push_handler(
  464. self._maint_notifications_connection_handler.handle_notification
  465. )
  466. else:
  467. self._maint_notifications_connection_handler.config = (
  468. maint_notifications_pool_handler.config
  469. )
  470. def set_maint_notifications_cluster_handler_for_connection(
  471. self, oss_cluster_maint_notifications_handler: OSSMaintNotificationsHandler
  472. ):
  473. self._get_parser().set_oss_cluster_maint_push_handler(
  474. oss_cluster_maint_notifications_handler.handle_notification
  475. )
  476. self._oss_cluster_maint_notifications_handler = (
  477. oss_cluster_maint_notifications_handler
  478. )
  479. # Update maintenance notification connection handler if it doesn't exist
  480. if not self._maint_notifications_connection_handler:
  481. self._maint_notifications_connection_handler = (
  482. MaintNotificationsConnectionHandler(
  483. self, oss_cluster_maint_notifications_handler.config
  484. )
  485. )
  486. self._get_parser().set_maintenance_push_handler(
  487. self._maint_notifications_connection_handler.handle_notification
  488. )
  489. else:
  490. self._maint_notifications_connection_handler.config = (
  491. oss_cluster_maint_notifications_handler.config
  492. )
  493. def activate_maint_notifications_handling_if_enabled(self, check_health=True):
  494. # Send maintenance notifications handshake if RESP3 is active
  495. # and maintenance notifications are enabled
  496. # and we have a host to determine the endpoint type from
  497. # When the maint_notifications_config enabled mode is "auto",
  498. # we just log a warning if the handshake fails
  499. # When the mode is enabled=True, we raise an exception in case of failure
  500. if (
  501. self.get_protocol() not in [2, "2"]
  502. and self.maint_notifications_config
  503. and self.maint_notifications_config.enabled
  504. and self._maint_notifications_connection_handler
  505. and hasattr(self, "host")
  506. ):
  507. self._enable_maintenance_notifications(
  508. maint_notifications_config=self.maint_notifications_config,
  509. check_health=check_health,
  510. )
  511. def _enable_maintenance_notifications(
  512. self, maint_notifications_config: MaintNotificationsConfig, check_health=True
  513. ):
  514. try:
  515. host = getattr(self, "host", None)
  516. if host is None:
  517. raise ValueError(
  518. "Cannot enable maintenance notifications for connection"
  519. " object that doesn't have a host attribute."
  520. )
  521. else:
  522. endpoint_type = maint_notifications_config.get_endpoint_type(host, self)
  523. self.send_command(
  524. "CLIENT",
  525. "MAINT_NOTIFICATIONS",
  526. "ON",
  527. "moving-endpoint-type",
  528. endpoint_type.value,
  529. check_health=check_health,
  530. )
  531. response = self.read_response()
  532. if not response or str_if_bytes(response) != "OK":
  533. raise ResponseError(
  534. "The server doesn't support maintenance notifications"
  535. )
  536. except Exception as e:
  537. if (
  538. isinstance(e, ResponseError)
  539. and maint_notifications_config.enabled == "auto"
  540. ):
  541. # Log warning but don't fail the connection
  542. import logging
  543. logger = logging.getLogger(__name__)
  544. logger.debug(f"Failed to enable maintenance notifications: {e}")
  545. else:
  546. raise
  547. def get_resolved_ip(self) -> Optional[str]:
  548. """
  549. Extract the resolved IP address from an
  550. established connection or resolve it from the host.
  551. First tries to get the actual IP from the socket (most accurate),
  552. then falls back to DNS resolution if needed.
  553. Args:
  554. connection: The connection object to extract the IP from
  555. Returns:
  556. str: The resolved IP address, or None if it cannot be determined
  557. """
  558. # Method 1: Try to get the actual IP from the established socket connection
  559. # This is most accurate as it shows the exact IP being used
  560. try:
  561. conn_socket = self._get_socket()
  562. if conn_socket is not None:
  563. peer_addr = conn_socket.getpeername()
  564. if peer_addr and len(peer_addr) >= 1:
  565. # For TCP sockets, peer_addr is typically (host, port) tuple
  566. # Return just the host part
  567. return peer_addr[0]
  568. except (AttributeError, OSError):
  569. # Socket might not be connected or getpeername() might fail
  570. pass
  571. # Method 2: Fallback to DNS resolution of the host
  572. # This is less accurate but works when socket is not available
  573. try:
  574. host = getattr(self, "host", "localhost")
  575. port = getattr(self, "port", 6379)
  576. if host:
  577. # Use getaddrinfo to resolve the hostname to IP
  578. # This mimics what the connection would do during _connect()
  579. addr_info = socket.getaddrinfo(
  580. host, port, socket.AF_UNSPEC, socket.SOCK_STREAM
  581. )
  582. if addr_info:
  583. # Return the IP from the first result
  584. # addr_info[0] is (family, socktype, proto, canonname, sockaddr)
  585. # sockaddr[0] is the IP address
  586. return str(addr_info[0][4][0])
  587. except (AttributeError, OSError, socket.gaierror):
  588. # DNS resolution might fail
  589. pass
  590. return None
  591. @property
  592. def maintenance_state(self) -> MaintenanceState:
  593. return self._maintenance_state
  594. @maintenance_state.setter
  595. def maintenance_state(self, state: "MaintenanceState"):
  596. self._maintenance_state = state
  597. def add_maint_start_notification(self, id: int):
  598. self._processed_start_maint_notifications.add(id)
  599. def get_processed_start_notifications(self) -> set:
  600. return self._processed_start_maint_notifications
  601. def add_skipped_end_notification(self, id: int):
  602. self._skipped_end_maint_notifications.add(id)
  603. def get_skipped_end_notifications(self) -> set:
  604. return self._skipped_end_maint_notifications
  605. def reset_received_notifications(self):
  606. self._processed_start_maint_notifications.clear()
  607. self._skipped_end_maint_notifications.clear()
  608. def getpeername(self):
  609. """
  610. Returns the peer name of the connection.
  611. """
  612. conn_socket = self._get_socket()
  613. if conn_socket:
  614. return conn_socket.getpeername()[0]
  615. return None
  616. def update_current_socket_timeout(self, relaxed_timeout: Optional[float] = None):
  617. conn_socket = self._get_socket()
  618. if conn_socket:
  619. timeout = relaxed_timeout if relaxed_timeout != -1 else self.socket_timeout
  620. # if the current timeout is 0 it means we are in the middle of a can_read call
  621. # in this case we don't want to change the timeout because the operation
  622. # is non-blocking and should return immediately
  623. # Changing the state from non-blocking to blocking in the middle of a read operation
  624. # will lead to a deadlock
  625. if conn_socket.gettimeout() != 0:
  626. conn_socket.settimeout(timeout)
  627. self.update_parser_timeout(timeout)
  628. def update_parser_timeout(self, timeout: Optional[float] = None):
  629. parser = self._get_parser()
  630. if parser and parser._buffer:
  631. if isinstance(parser, _RESP3Parser) and timeout:
  632. parser._buffer.socket_timeout = timeout
  633. elif isinstance(parser, _HiredisParser):
  634. parser._socket_timeout = timeout
  635. def set_tmp_settings(
  636. self,
  637. tmp_host_address: Optional[Union[str, object]] = SENTINEL,
  638. tmp_relaxed_timeout: Optional[float] = None,
  639. ):
  640. """
  641. The value of SENTINEL is used to indicate that the property should not be updated.
  642. """
  643. if tmp_host_address and tmp_host_address != SENTINEL:
  644. self.host = str(tmp_host_address)
  645. if tmp_relaxed_timeout != -1:
  646. self.socket_timeout = tmp_relaxed_timeout
  647. self.socket_connect_timeout = tmp_relaxed_timeout
  648. def reset_tmp_settings(
  649. self,
  650. reset_host_address: bool = False,
  651. reset_relaxed_timeout: bool = False,
  652. ):
  653. if reset_host_address:
  654. self.host = self.orig_host_address
  655. if reset_relaxed_timeout:
  656. self.socket_timeout = self.orig_socket_timeout
  657. self.socket_connect_timeout = self.orig_socket_connect_timeout
  658. class AbstractConnection(MaintNotificationsAbstractConnection, ConnectionInterface):
  659. "Manages communication to and from a Redis server"
  660. @deprecated_args(
  661. args_to_warn=["lib_name", "lib_version"],
  662. reason="Use 'driver_info' parameter instead. "
  663. "lib_name and lib_version will be removed in a future version.",
  664. )
  665. def __init__(
  666. self,
  667. db: int = 0,
  668. password: Optional[str] = None,
  669. socket_timeout: Optional[float] = None,
  670. socket_connect_timeout: Optional[float] = None,
  671. retry_on_timeout: bool = False,
  672. retry_on_error: Union[Iterable[Type[Exception]], object] = SENTINEL,
  673. encoding: str = "utf-8",
  674. encoding_errors: str = "strict",
  675. decode_responses: bool = False,
  676. parser_class=DefaultParser,
  677. socket_read_size: int = 65536,
  678. health_check_interval: int = 0,
  679. client_name: Optional[str] = None,
  680. lib_name: Optional[str] = None,
  681. lib_version: Optional[str] = None,
  682. driver_info: Optional[DriverInfo] = None,
  683. username: Optional[str] = None,
  684. retry: Union[Any, None] = None,
  685. redis_connect_func: Optional[Callable[[], None]] = None,
  686. credential_provider: Optional[CredentialProvider] = None,
  687. protocol: Optional[int] = 2,
  688. command_packer: Optional[Callable[[], None]] = None,
  689. event_dispatcher: Optional[EventDispatcher] = None,
  690. maint_notifications_config: Optional[MaintNotificationsConfig] = None,
  691. maint_notifications_pool_handler: Optional[
  692. MaintNotificationsPoolHandler
  693. ] = None,
  694. maintenance_state: "MaintenanceState" = MaintenanceState.NONE,
  695. maintenance_notification_hash: Optional[int] = None,
  696. orig_host_address: Optional[str] = None,
  697. orig_socket_timeout: Optional[float] = None,
  698. orig_socket_connect_timeout: Optional[float] = None,
  699. oss_cluster_maint_notifications_handler: Optional[
  700. OSSMaintNotificationsHandler
  701. ] = None,
  702. ):
  703. """
  704. Initialize a new Connection.
  705. To specify a retry policy for specific errors, first set
  706. `retry_on_error` to a list of the error/s to retry on, then set
  707. `retry` to a valid `Retry` object.
  708. To retry on TimeoutError, `retry_on_timeout` can also be set to `True`.
  709. Parameters
  710. ----------
  711. driver_info : DriverInfo, optional
  712. Driver metadata for CLIENT SETINFO. If provided, lib_name and lib_version
  713. are ignored. If not provided, a DriverInfo will be created from lib_name
  714. and lib_version (or defaults if those are also None).
  715. lib_name : str, optional
  716. **Deprecated.** Use driver_info instead. Library name for CLIENT SETINFO.
  717. lib_version : str, optional
  718. **Deprecated.** Use driver_info instead. Library version for CLIENT SETINFO.
  719. """
  720. if (username or password) and credential_provider is not None:
  721. raise DataError(
  722. "'username' and 'password' cannot be passed along with 'credential_"
  723. "provider'. Please provide only one of the following arguments: \n"
  724. "1. 'password' and (optional) 'username'\n"
  725. "2. 'credential_provider'"
  726. )
  727. if event_dispatcher is None:
  728. self._event_dispatcher = EventDispatcher()
  729. else:
  730. self._event_dispatcher = event_dispatcher
  731. self.pid = os.getpid()
  732. self.db = db
  733. self.client_name = client_name
  734. # Handle driver_info: if provided, use it; otherwise create from lib_name/lib_version
  735. self.driver_info = resolve_driver_info(driver_info, lib_name, lib_version)
  736. self.credential_provider = credential_provider
  737. self.password = password
  738. self.username = username
  739. self._socket_timeout = socket_timeout
  740. if socket_connect_timeout is None:
  741. socket_connect_timeout = socket_timeout
  742. self._socket_connect_timeout = socket_connect_timeout
  743. self.retry_on_timeout = retry_on_timeout
  744. if retry_on_error is SENTINEL:
  745. retry_on_errors_list = []
  746. else:
  747. retry_on_errors_list = list(retry_on_error)
  748. if retry_on_timeout:
  749. # Add TimeoutError to the errors list to retry on
  750. retry_on_errors_list.append(TimeoutError)
  751. self.retry_on_error = retry_on_errors_list
  752. if retry or self.retry_on_error:
  753. if retry is None:
  754. self.retry = Retry(NoBackoff(), 1)
  755. else:
  756. # deep-copy the Retry object as it is mutable
  757. self.retry = copy.deepcopy(retry)
  758. if self.retry_on_error:
  759. # Update the retry's supported errors with the specified errors
  760. self.retry.update_supported_errors(self.retry_on_error)
  761. else:
  762. self.retry = Retry(NoBackoff(), 0)
  763. self.health_check_interval = health_check_interval
  764. self.next_health_check = 0
  765. self.redis_connect_func = redis_connect_func
  766. self.encoder = Encoder(encoding, encoding_errors, decode_responses)
  767. self.handshake_metadata = None
  768. self._sock = None
  769. self._socket_read_size = socket_read_size
  770. self._connect_callbacks = []
  771. self._buffer_cutoff = 6000
  772. self._re_auth_token: Optional[TokenInterface] = None
  773. try:
  774. p = int(protocol)
  775. except TypeError:
  776. p = DEFAULT_RESP_VERSION
  777. except ValueError:
  778. raise ConnectionError("protocol must be an integer")
  779. else:
  780. if p < 2 or p > 3:
  781. raise ConnectionError("protocol must be either 2 or 3")
  782. self.protocol = p
  783. if self.protocol == 3 and parser_class == _RESP2Parser:
  784. # If the protocol is 3 but the parser is RESP2, change it to RESP3
  785. # This is needed because the parser might be set before the protocol
  786. # or might be provided as a kwarg to the constructor
  787. # We need to react on discrepancy only for RESP2 and RESP3
  788. # as hiredis supports both
  789. parser_class = _RESP3Parser
  790. self.set_parser(parser_class)
  791. self._command_packer = self._construct_command_packer(command_packer)
  792. self._should_reconnect = False
  793. # Set up maintenance notifications
  794. MaintNotificationsAbstractConnection.__init__(
  795. self,
  796. maint_notifications_config,
  797. maint_notifications_pool_handler,
  798. maintenance_state,
  799. maintenance_notification_hash,
  800. orig_host_address,
  801. orig_socket_timeout,
  802. orig_socket_connect_timeout,
  803. oss_cluster_maint_notifications_handler,
  804. self._parser,
  805. event_dispatcher=self._event_dispatcher,
  806. )
  807. def __repr__(self):
  808. repr_args = ",".join([f"{k}={v}" for k, v in self.repr_pieces()])
  809. return f"<{self.__class__.__module__}.{self.__class__.__name__}({repr_args})>"
  810. @abstractmethod
  811. def repr_pieces(self):
  812. pass
  813. def __del__(self):
  814. try:
  815. self.disconnect()
  816. except Exception:
  817. pass
  818. def _construct_command_packer(self, packer):
  819. if packer is not None:
  820. return packer
  821. elif HIREDIS_AVAILABLE:
  822. return HiredisRespSerializer()
  823. else:
  824. return PythonRespSerializer(self._buffer_cutoff, self.encoder.encode)
  825. def register_connect_callback(self, callback):
  826. """
  827. Register a callback to be called when the connection is established either
  828. initially or reconnected. This allows listeners to issue commands that
  829. are ephemeral to the connection, for example pub/sub subscription or
  830. key tracking. The callback must be a _method_ and will be kept as
  831. a weak reference.
  832. """
  833. wm = weakref.WeakMethod(callback)
  834. if wm not in self._connect_callbacks:
  835. self._connect_callbacks.append(wm)
  836. def deregister_connect_callback(self, callback):
  837. """
  838. De-register a previously registered callback. It will no-longer receive
  839. notifications on connection events. Calling this is not required when the
  840. listener goes away, since the callbacks are kept as weak methods.
  841. """
  842. try:
  843. self._connect_callbacks.remove(weakref.WeakMethod(callback))
  844. except ValueError:
  845. pass
  846. def set_parser(self, parser_class):
  847. """
  848. Creates a new instance of parser_class with socket size:
  849. _socket_read_size and assigns it to the parser for the connection
  850. :param parser_class: The required parser class
  851. """
  852. self._parser = parser_class(socket_read_size=self._socket_read_size)
  853. def _get_parser(self) -> Union[_HiredisParser, _RESP3Parser, _RESP2Parser]:
  854. return self._parser
  855. def connect(self):
  856. "Connects to the Redis server if not already connected"
  857. # try once the socket connect with the handshake, retry the whole
  858. # connect/handshake flow based on retry policy
  859. self.retry.call_with_retry(
  860. lambda: self.connect_check_health(
  861. check_health=True, retry_socket_connect=False
  862. ),
  863. lambda error: self.disconnect(error),
  864. )
  865. def connect_check_health(
  866. self, check_health: bool = True, retry_socket_connect: bool = True
  867. ):
  868. if self._sock:
  869. return
  870. # Track actual retry attempts for error reporting
  871. actual_retry_attempts = [0]
  872. def failure_callback(error, failure_count):
  873. actual_retry_attempts[0] = failure_count
  874. self.disconnect(error=error, failure_count=failure_count)
  875. try:
  876. if retry_socket_connect:
  877. sock = self.retry.call_with_retry(
  878. self._connect,
  879. failure_callback,
  880. with_failure_count=True,
  881. )
  882. else:
  883. sock = self._connect()
  884. except socket.timeout:
  885. e = TimeoutError("Timeout connecting to server")
  886. record_error_count(
  887. server_address=self.host,
  888. server_port=self.port,
  889. network_peer_address=self.host,
  890. network_peer_port=self.port,
  891. error_type=e,
  892. retry_attempts=actual_retry_attempts[0],
  893. )
  894. raise e
  895. except OSError as e:
  896. e = ConnectionError(self._error_message(e))
  897. record_error_count(
  898. server_address=getattr(self, "host", None),
  899. server_port=getattr(self, "port", None),
  900. network_peer_address=getattr(self, "host", None),
  901. network_peer_port=getattr(self, "port", None),
  902. error_type=e,
  903. retry_attempts=actual_retry_attempts[0],
  904. )
  905. raise e
  906. self._sock = sock
  907. try:
  908. if self.redis_connect_func is None:
  909. # Use the default on_connect function
  910. self.on_connect_check_health(check_health=check_health)
  911. else:
  912. # Use the passed function redis_connect_func
  913. self.redis_connect_func(self)
  914. except RedisError:
  915. # clean up after any error in on_connect
  916. self.disconnect()
  917. raise
  918. # run any user callbacks. right now the only internal callback
  919. # is for pubsub channel/pattern resubscription
  920. # first, remove any dead weakrefs
  921. self._connect_callbacks = [ref for ref in self._connect_callbacks if ref()]
  922. for ref in self._connect_callbacks:
  923. callback = ref()
  924. if callback:
  925. callback(self)
  926. @abstractmethod
  927. def _connect(self):
  928. pass
  929. @abstractmethod
  930. def _host_error(self):
  931. pass
  932. def _error_message(self, exception):
  933. return format_error_message(self._host_error(), exception)
  934. def on_connect(self):
  935. self.on_connect_check_health(check_health=True)
  936. def on_connect_check_health(self, check_health: bool = True):
  937. "Initialize the connection, authenticate and select a database"
  938. self._parser.on_connect(self)
  939. parser = self._parser
  940. auth_args = None
  941. # if credential provider or username and/or password are set, authenticate
  942. if self.credential_provider or (self.username or self.password):
  943. cred_provider = (
  944. self.credential_provider
  945. or UsernamePasswordCredentialProvider(self.username, self.password)
  946. )
  947. auth_args = cred_provider.get_credentials()
  948. # if resp version is specified and we have auth args,
  949. # we need to send them via HELLO
  950. if auth_args and self.protocol not in [2, "2"]:
  951. if isinstance(self._parser, _RESP2Parser):
  952. self.set_parser(_RESP3Parser)
  953. # update cluster exception classes
  954. self._parser.EXCEPTION_CLASSES = parser.EXCEPTION_CLASSES
  955. self._parser.on_connect(self)
  956. if len(auth_args) == 1:
  957. auth_args = ["default", auth_args[0]]
  958. # avoid checking health here -- PING will fail if we try
  959. # to check the health prior to the AUTH
  960. self.send_command(
  961. "HELLO", self.protocol, "AUTH", *auth_args, check_health=False
  962. )
  963. self.handshake_metadata = self.read_response()
  964. # if response.get(b"proto") != self.protocol and response.get(
  965. # "proto"
  966. # ) != self.protocol:
  967. # raise ConnectionError("Invalid RESP version")
  968. elif auth_args:
  969. # avoid checking health here -- PING will fail if we try
  970. # to check the health prior to the AUTH
  971. self.send_command("AUTH", *auth_args, check_health=False)
  972. try:
  973. auth_response = self.read_response()
  974. except AuthenticationWrongNumberOfArgsError:
  975. # a username and password were specified but the Redis
  976. # server seems to be < 6.0.0 which expects a single password
  977. # arg. retry auth with just the password.
  978. # https://github.com/andymccurdy/redis-py/issues/1274
  979. self.send_command("AUTH", auth_args[-1], check_health=False)
  980. auth_response = self.read_response()
  981. if str_if_bytes(auth_response) != "OK":
  982. raise AuthenticationError("Invalid Username or Password")
  983. # if resp version is specified, switch to it
  984. elif self.protocol not in [2, "2"]:
  985. if isinstance(self._parser, _RESP2Parser):
  986. self.set_parser(_RESP3Parser)
  987. # update cluster exception classes
  988. self._parser.EXCEPTION_CLASSES = parser.EXCEPTION_CLASSES
  989. self._parser.on_connect(self)
  990. self.send_command("HELLO", self.protocol, check_health=check_health)
  991. self.handshake_metadata = self.read_response()
  992. if (
  993. self.handshake_metadata.get(b"proto") != self.protocol
  994. and self.handshake_metadata.get("proto") != self.protocol
  995. ):
  996. raise ConnectionError("Invalid RESP version")
  997. # Activate maintenance notifications for this connection
  998. # if enabled in the configuration
  999. # This is a no-op if maintenance notifications are not enabled
  1000. self.activate_maint_notifications_handling_if_enabled(check_health=check_health)
  1001. # if a client_name is given, set it
  1002. if self.client_name:
  1003. self.send_command(
  1004. "CLIENT",
  1005. "SETNAME",
  1006. self.client_name,
  1007. check_health=check_health,
  1008. )
  1009. if str_if_bytes(self.read_response()) != "OK":
  1010. raise ConnectionError("Error setting client name")
  1011. # Set the library name and version from driver_info
  1012. try:
  1013. if self.driver_info and self.driver_info.formatted_name:
  1014. self.send_command(
  1015. "CLIENT",
  1016. "SETINFO",
  1017. "LIB-NAME",
  1018. self.driver_info.formatted_name,
  1019. check_health=check_health,
  1020. )
  1021. self.read_response()
  1022. except ResponseError:
  1023. pass
  1024. try:
  1025. if self.driver_info and self.driver_info.lib_version:
  1026. self.send_command(
  1027. "CLIENT",
  1028. "SETINFO",
  1029. "LIB-VER",
  1030. self.driver_info.lib_version,
  1031. check_health=check_health,
  1032. )
  1033. self.read_response()
  1034. except ResponseError:
  1035. pass
  1036. # if a database is specified, switch to it
  1037. if self.db:
  1038. self.send_command("SELECT", self.db, check_health=check_health)
  1039. if str_if_bytes(self.read_response()) != "OK":
  1040. raise ConnectionError("Invalid Database")
  1041. def disconnect(self, *args, **kwargs):
  1042. "Disconnects from the Redis server"
  1043. self._parser.on_disconnect()
  1044. conn_sock = self._sock
  1045. self._sock = None
  1046. # reset the reconnect flag
  1047. self.reset_should_reconnect()
  1048. if conn_sock is None:
  1049. return
  1050. if os.getpid() == self.pid:
  1051. try:
  1052. conn_sock.shutdown(socket.SHUT_RDWR)
  1053. except (OSError, TypeError):
  1054. pass
  1055. try:
  1056. conn_sock.close()
  1057. except OSError:
  1058. pass
  1059. error = kwargs.get("error")
  1060. failure_count = kwargs.get("failure_count")
  1061. health_check_failed = kwargs.get("health_check_failed")
  1062. if error:
  1063. if health_check_failed:
  1064. close_reason = CloseReason.HEALTHCHECK_FAILED
  1065. else:
  1066. close_reason = CloseReason.ERROR
  1067. if failure_count is not None and failure_count > self.retry.get_retries():
  1068. record_error_count(
  1069. server_address=self.host,
  1070. server_port=self.port,
  1071. network_peer_address=self.host,
  1072. network_peer_port=self.port,
  1073. error_type=error,
  1074. retry_attempts=failure_count,
  1075. )
  1076. record_connection_closed(
  1077. close_reason=close_reason,
  1078. error_type=error,
  1079. )
  1080. else:
  1081. record_connection_closed(
  1082. close_reason=CloseReason.APPLICATION_CLOSE,
  1083. )
  1084. if self.maintenance_state == MaintenanceState.MAINTENANCE:
  1085. # this block will be executed only if the connection was in maintenance state
  1086. # and the connection was closed.
  1087. # The state change won't be applied on connections that are in Moving state
  1088. # because their state and configurations will be handled when the moving ttl expires.
  1089. self.reset_tmp_settings(reset_relaxed_timeout=True)
  1090. self.maintenance_state = MaintenanceState.NONE
  1091. # reset the sets that keep track of received start maint
  1092. # notifications and skipped end maint notifications
  1093. self.reset_received_notifications()
  1094. def mark_for_reconnect(self):
  1095. self._should_reconnect = True
  1096. def should_reconnect(self):
  1097. return self._should_reconnect
  1098. def reset_should_reconnect(self):
  1099. self._should_reconnect = False
  1100. def _send_ping(self):
  1101. """Send PING, expect PONG in return"""
  1102. self.send_command("PING", check_health=False)
  1103. if str_if_bytes(self.read_response()) != "PONG":
  1104. raise ConnectionError("Bad response from PING health check")
  1105. def _ping_failed(self, error, failure_count):
  1106. """Function to call when PING fails"""
  1107. self.disconnect(
  1108. error=error, failure_count=failure_count, health_check_failed=True
  1109. )
  1110. def check_health(self):
  1111. """Check the health of the connection with a PING/PONG"""
  1112. if self.health_check_interval and time.monotonic() > self.next_health_check:
  1113. self.retry.call_with_retry(
  1114. self._send_ping,
  1115. self._ping_failed,
  1116. with_failure_count=True,
  1117. )
  1118. def send_packed_command(self, command, check_health=True):
  1119. """Send an already packed command to the Redis server"""
  1120. if not self._sock:
  1121. self.connect_check_health(check_health=False)
  1122. # guard against health check recursion
  1123. if check_health:
  1124. self.check_health()
  1125. try:
  1126. if isinstance(command, str):
  1127. command = [command]
  1128. for item in command:
  1129. self._sock.sendall(item)
  1130. except socket.timeout:
  1131. self.disconnect()
  1132. raise TimeoutError("Timeout writing to socket")
  1133. except OSError as e:
  1134. self.disconnect()
  1135. if len(e.args) == 1:
  1136. errno, errmsg = "UNKNOWN", e.args[0]
  1137. else:
  1138. errno = e.args[0]
  1139. errmsg = e.args[1]
  1140. raise ConnectionError(f"Error {errno} while writing to socket. {errmsg}.")
  1141. except BaseException:
  1142. # BaseExceptions can be raised when a socket send operation is not
  1143. # finished, e.g. due to a timeout. Ideally, a caller could then re-try
  1144. # to send un-sent data. However, the send_packed_command() API
  1145. # does not support it so there is no point in keeping the connection open.
  1146. self.disconnect()
  1147. raise
  1148. def send_command(self, *args, **kwargs):
  1149. """Pack and send a command to the Redis server"""
  1150. self.send_packed_command(
  1151. self._command_packer.pack(*args),
  1152. check_health=kwargs.get("check_health", True),
  1153. )
  1154. def can_read(self, timeout=0):
  1155. """Poll the socket to see if there's data that can be read."""
  1156. sock = self._sock
  1157. if not sock:
  1158. self.connect()
  1159. host_error = self._host_error()
  1160. try:
  1161. return self._parser.can_read(timeout)
  1162. except OSError as e:
  1163. self.disconnect()
  1164. raise ConnectionError(f"Error while reading from {host_error}: {e.args}")
  1165. def read_response(
  1166. self,
  1167. disable_decoding=False,
  1168. *,
  1169. disconnect_on_error=True,
  1170. push_request=False,
  1171. ):
  1172. """Read the response from a previously sent command"""
  1173. host_error = self._host_error()
  1174. try:
  1175. if self.protocol in ["3", 3]:
  1176. response = self._parser.read_response(
  1177. disable_decoding=disable_decoding, push_request=push_request
  1178. )
  1179. else:
  1180. response = self._parser.read_response(disable_decoding=disable_decoding)
  1181. except socket.timeout:
  1182. if disconnect_on_error:
  1183. self.disconnect()
  1184. raise TimeoutError(f"Timeout reading from {host_error}")
  1185. except OSError as e:
  1186. if disconnect_on_error:
  1187. self.disconnect()
  1188. raise ConnectionError(f"Error while reading from {host_error} : {e.args}")
  1189. except BaseException:
  1190. # Also by default close in case of BaseException. A lot of code
  1191. # relies on this behaviour when doing Command/Response pairs.
  1192. # See #1128.
  1193. if disconnect_on_error:
  1194. self.disconnect()
  1195. raise
  1196. if self.health_check_interval:
  1197. self.next_health_check = time.monotonic() + self.health_check_interval
  1198. if isinstance(response, ResponseError):
  1199. try:
  1200. raise response
  1201. finally:
  1202. del response # avoid creating ref cycles
  1203. return response
  1204. def pack_command(self, *args):
  1205. """Pack a series of arguments into the Redis protocol"""
  1206. return self._command_packer.pack(*args)
  1207. def pack_commands(self, commands):
  1208. """Pack multiple commands into the Redis protocol"""
  1209. output = []
  1210. pieces = []
  1211. buffer_length = 0
  1212. buffer_cutoff = self._buffer_cutoff
  1213. for cmd in commands:
  1214. for chunk in self._command_packer.pack(*cmd):
  1215. chunklen = len(chunk)
  1216. if (
  1217. buffer_length > buffer_cutoff
  1218. or chunklen > buffer_cutoff
  1219. or isinstance(chunk, memoryview)
  1220. ):
  1221. if pieces:
  1222. output.append(SYM_EMPTY.join(pieces))
  1223. buffer_length = 0
  1224. pieces = []
  1225. if chunklen > buffer_cutoff or isinstance(chunk, memoryview):
  1226. output.append(chunk)
  1227. else:
  1228. pieces.append(chunk)
  1229. buffer_length += chunklen
  1230. if pieces:
  1231. output.append(SYM_EMPTY.join(pieces))
  1232. return output
  1233. def get_protocol(self) -> Union[int, str]:
  1234. return self.protocol
  1235. @property
  1236. def handshake_metadata(self) -> Union[Dict[bytes, bytes], Dict[str, str]]:
  1237. return self._handshake_metadata
  1238. @handshake_metadata.setter
  1239. def handshake_metadata(self, value: Union[Dict[bytes, bytes], Dict[str, str]]):
  1240. self._handshake_metadata = value
  1241. def set_re_auth_token(self, token: TokenInterface):
  1242. self._re_auth_token = token
  1243. def re_auth(self):
  1244. if self._re_auth_token is not None:
  1245. self.send_command(
  1246. "AUTH",
  1247. self._re_auth_token.try_get("oid"),
  1248. self._re_auth_token.get_value(),
  1249. )
  1250. self.read_response()
  1251. self._re_auth_token = None
  1252. def _get_socket(self) -> Optional[socket.socket]:
  1253. return self._sock
  1254. @property
  1255. def socket_timeout(self) -> Optional[Union[float, int]]:
  1256. return self._socket_timeout
  1257. @socket_timeout.setter
  1258. def socket_timeout(self, value: Optional[Union[float, int]]):
  1259. self._socket_timeout = value
  1260. @property
  1261. def socket_connect_timeout(self) -> Optional[Union[float, int]]:
  1262. return self._socket_connect_timeout
  1263. @socket_connect_timeout.setter
  1264. def socket_connect_timeout(self, value: Optional[Union[float, int]]):
  1265. self._socket_connect_timeout = value
  1266. class Connection(AbstractConnection):
  1267. "Manages TCP communication to and from a Redis server"
  1268. def __init__(
  1269. self,
  1270. host="localhost",
  1271. port=6379,
  1272. socket_keepalive=False,
  1273. socket_keepalive_options=None,
  1274. socket_type=0,
  1275. **kwargs,
  1276. ):
  1277. self._host = host
  1278. self.port = int(port)
  1279. self.socket_keepalive = socket_keepalive
  1280. self.socket_keepalive_options = socket_keepalive_options or {}
  1281. self.socket_type = socket_type
  1282. super().__init__(**kwargs)
  1283. def repr_pieces(self):
  1284. pieces = [("host", self.host), ("port", self.port), ("db", self.db)]
  1285. if self.client_name:
  1286. pieces.append(("client_name", self.client_name))
  1287. return pieces
  1288. def _connect(self):
  1289. "Create a TCP socket connection"
  1290. # we want to mimic what socket.create_connection does to support
  1291. # ipv4/ipv6, but we want to set options prior to calling
  1292. # socket.connect()
  1293. err = None
  1294. for res in socket.getaddrinfo(
  1295. self.host, self.port, self.socket_type, socket.SOCK_STREAM
  1296. ):
  1297. family, socktype, proto, canonname, socket_address = res
  1298. sock = None
  1299. try:
  1300. sock = socket.socket(family, socktype, proto)
  1301. # TCP_NODELAY
  1302. sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  1303. # TCP_KEEPALIVE
  1304. if self.socket_keepalive:
  1305. sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
  1306. for k, v in self.socket_keepalive_options.items():
  1307. sock.setsockopt(socket.IPPROTO_TCP, k, v)
  1308. # set the socket_connect_timeout before we connect
  1309. sock.settimeout(self.socket_connect_timeout)
  1310. # connect
  1311. sock.connect(socket_address)
  1312. # set the socket_timeout now that we're connected
  1313. sock.settimeout(self.socket_timeout)
  1314. return sock
  1315. except OSError as _:
  1316. err = _
  1317. if sock is not None:
  1318. try:
  1319. sock.shutdown(socket.SHUT_RDWR) # ensure a clean close
  1320. except OSError:
  1321. pass
  1322. sock.close()
  1323. if err is not None:
  1324. raise err
  1325. raise OSError("socket.getaddrinfo returned an empty list")
  1326. def _host_error(self):
  1327. return f"{self.host}:{self.port}"
  1328. @property
  1329. def host(self) -> str:
  1330. return self._host
  1331. @host.setter
  1332. def host(self, value: str):
  1333. self._host = value
  1334. class CacheProxyConnection(MaintNotificationsAbstractConnection, ConnectionInterface):
  1335. DUMMY_CACHE_VALUE = b"foo"
  1336. MIN_ALLOWED_VERSION = "7.4.0"
  1337. DEFAULT_SERVER_NAME = "redis"
  1338. def __init__(
  1339. self,
  1340. conn: ConnectionInterface,
  1341. cache: CacheInterface,
  1342. pool_lock: threading.RLock,
  1343. ):
  1344. self.pid = os.getpid()
  1345. self._conn = conn
  1346. self.retry = self._conn.retry
  1347. self.host = self._conn.host
  1348. self.port = self._conn.port
  1349. self.db = self._conn.db
  1350. self._event_dispatcher = self._conn._event_dispatcher
  1351. self.credential_provider = conn.credential_provider
  1352. self._pool_lock = pool_lock
  1353. self._cache = cache
  1354. self._cache_lock = threading.RLock()
  1355. self._current_command_cache_key = None
  1356. self._current_options = None
  1357. self.register_connect_callback(self._enable_tracking_callback)
  1358. if isinstance(self._conn, MaintNotificationsAbstractConnection):
  1359. MaintNotificationsAbstractConnection.__init__(
  1360. self,
  1361. self._conn.maint_notifications_config,
  1362. self._conn._maint_notifications_pool_handler,
  1363. self._conn.maintenance_state,
  1364. self._conn.maintenance_notification_hash,
  1365. self._conn.host,
  1366. self._conn.socket_timeout,
  1367. self._conn.socket_connect_timeout,
  1368. self._conn._oss_cluster_maint_notifications_handler,
  1369. self._conn._get_parser(),
  1370. event_dispatcher=self._conn.event_dispatcher,
  1371. )
  1372. def repr_pieces(self):
  1373. return self._conn.repr_pieces()
  1374. def register_connect_callback(self, callback):
  1375. self._conn.register_connect_callback(callback)
  1376. def deregister_connect_callback(self, callback):
  1377. self._conn.deregister_connect_callback(callback)
  1378. def set_parser(self, parser_class):
  1379. self._conn.set_parser(parser_class)
  1380. def set_maint_notifications_pool_handler_for_connection(
  1381. self, maint_notifications_pool_handler
  1382. ):
  1383. if isinstance(self._conn, MaintNotificationsAbstractConnection):
  1384. self._conn.set_maint_notifications_pool_handler_for_connection(
  1385. maint_notifications_pool_handler
  1386. )
  1387. def set_maint_notifications_cluster_handler_for_connection(
  1388. self, oss_cluster_maint_notifications_handler
  1389. ):
  1390. if isinstance(self._conn, MaintNotificationsAbstractConnection):
  1391. self._conn.set_maint_notifications_cluster_handler_for_connection(
  1392. oss_cluster_maint_notifications_handler
  1393. )
  1394. def get_protocol(self):
  1395. return self._conn.get_protocol()
  1396. def connect(self):
  1397. self._conn.connect()
  1398. server_name = self._conn.handshake_metadata.get(b"server", None)
  1399. if server_name is None:
  1400. server_name = self._conn.handshake_metadata.get("server", None)
  1401. server_ver = self._conn.handshake_metadata.get(b"version", None)
  1402. if server_ver is None:
  1403. server_ver = self._conn.handshake_metadata.get("version", None)
  1404. if server_ver is None or server_name is None:
  1405. raise ConnectionError("Cannot retrieve information about server version")
  1406. server_ver = ensure_string(server_ver)
  1407. server_name = ensure_string(server_name)
  1408. if (
  1409. server_name != self.DEFAULT_SERVER_NAME
  1410. or compare_versions(server_ver, self.MIN_ALLOWED_VERSION) == 1
  1411. ):
  1412. raise ConnectionError(
  1413. "To maximize compatibility with all Redis products, client-side caching is supported by Redis 7.4 or later" # noqa: E501
  1414. )
  1415. def on_connect(self):
  1416. self._conn.on_connect()
  1417. def disconnect(self, *args, **kwargs):
  1418. with self._cache_lock:
  1419. self._cache.flush()
  1420. self._conn.disconnect(*args, **kwargs)
  1421. def check_health(self):
  1422. self._conn.check_health()
  1423. def send_packed_command(self, command, check_health=True):
  1424. # TODO: Investigate if it's possible to unpack command
  1425. # or extract keys from packed command
  1426. self._conn.send_packed_command(command)
  1427. def send_command(self, *args, **kwargs):
  1428. self._process_pending_invalidations()
  1429. with self._cache_lock:
  1430. # Command is write command or not allowed
  1431. # to be cached.
  1432. if not self._cache.is_cachable(
  1433. CacheKey(command=args[0], redis_keys=(), redis_args=())
  1434. ):
  1435. self._current_command_cache_key = None
  1436. self._conn.send_command(*args, **kwargs)
  1437. return
  1438. if kwargs.get("keys") is None:
  1439. raise ValueError("Cannot create cache key.")
  1440. # Creates cache key.
  1441. self._current_command_cache_key = CacheKey(
  1442. command=args[0], redis_keys=tuple(kwargs.get("keys")), redis_args=args
  1443. )
  1444. with self._cache_lock:
  1445. # We have to trigger invalidation processing in case if
  1446. # it was cached by another connection to avoid
  1447. # queueing invalidations in stale connections.
  1448. if self._cache.get(self._current_command_cache_key):
  1449. entry = self._cache.get(self._current_command_cache_key)
  1450. if entry.connection_ref != self._conn:
  1451. with self._pool_lock:
  1452. while entry.connection_ref.can_read():
  1453. entry.connection_ref.read_response(push_request=True)
  1454. return
  1455. # Set temporary entry value to prevent
  1456. # race condition from another connection.
  1457. self._cache.set(
  1458. CacheEntry(
  1459. cache_key=self._current_command_cache_key,
  1460. cache_value=self.DUMMY_CACHE_VALUE,
  1461. status=CacheEntryStatus.IN_PROGRESS,
  1462. connection_ref=self._conn,
  1463. )
  1464. )
  1465. # Send command over socket only if it's allowed
  1466. # read-only command that not yet cached.
  1467. self._conn.send_command(*args, **kwargs)
  1468. def can_read(self, timeout=0):
  1469. return self._conn.can_read(timeout)
  1470. def read_response(
  1471. self, disable_decoding=False, *, disconnect_on_error=True, push_request=False
  1472. ):
  1473. with self._cache_lock:
  1474. # Check if command response exists in a cache and it's not in progress.
  1475. if self._current_command_cache_key is not None:
  1476. if (
  1477. self._cache.get(self._current_command_cache_key) is not None
  1478. and self._cache.get(self._current_command_cache_key).status
  1479. != CacheEntryStatus.IN_PROGRESS
  1480. ):
  1481. res = copy.deepcopy(
  1482. self._cache.get(self._current_command_cache_key).cache_value
  1483. )
  1484. self._current_command_cache_key = None
  1485. record_csc_request(
  1486. result=CSCResult.HIT,
  1487. )
  1488. record_csc_network_saved(
  1489. bytes_saved=len(res),
  1490. )
  1491. return res
  1492. record_csc_request(
  1493. result=CSCResult.MISS,
  1494. )
  1495. response = self._conn.read_response(
  1496. disable_decoding=disable_decoding,
  1497. disconnect_on_error=disconnect_on_error,
  1498. push_request=push_request,
  1499. )
  1500. with self._cache_lock:
  1501. # Prevent not-allowed command from caching.
  1502. if self._current_command_cache_key is None:
  1503. return response
  1504. # If response is None prevent from caching.
  1505. if response is None:
  1506. self._cache.delete_by_cache_keys([self._current_command_cache_key])
  1507. return response
  1508. cache_entry = self._cache.get(self._current_command_cache_key)
  1509. # Cache only responses that still valid
  1510. # and wasn't invalidated by another connection in meantime.
  1511. if cache_entry is not None:
  1512. cache_entry.status = CacheEntryStatus.VALID
  1513. cache_entry.cache_value = response
  1514. self._cache.set(cache_entry)
  1515. self._current_command_cache_key = None
  1516. return response
  1517. def pack_command(self, *args):
  1518. return self._conn.pack_command(*args)
  1519. def pack_commands(self, commands):
  1520. return self._conn.pack_commands(commands)
  1521. @property
  1522. def handshake_metadata(self) -> Union[Dict[bytes, bytes], Dict[str, str]]:
  1523. return self._conn.handshake_metadata
  1524. def set_re_auth_token(self, token: TokenInterface):
  1525. self._conn.set_re_auth_token(token)
  1526. def re_auth(self):
  1527. self._conn.re_auth()
  1528. def mark_for_reconnect(self):
  1529. self._conn.mark_for_reconnect()
  1530. def should_reconnect(self):
  1531. return self._conn.should_reconnect()
  1532. def reset_should_reconnect(self):
  1533. self._conn.reset_should_reconnect()
  1534. @property
  1535. def host(self) -> str:
  1536. return self._conn.host
  1537. @host.setter
  1538. def host(self, value: str):
  1539. self._conn.host = value
  1540. @property
  1541. def socket_timeout(self) -> Optional[Union[float, int]]:
  1542. return self._conn.socket_timeout
  1543. @socket_timeout.setter
  1544. def socket_timeout(self, value: Optional[Union[float, int]]):
  1545. self._conn.socket_timeout = value
  1546. @property
  1547. def socket_connect_timeout(self) -> Optional[Union[float, int]]:
  1548. return self._conn.socket_connect_timeout
  1549. @socket_connect_timeout.setter
  1550. def socket_connect_timeout(self, value: Optional[Union[float, int]]):
  1551. self._conn.socket_connect_timeout = value
  1552. @property
  1553. def _maint_notifications_connection_handler(
  1554. self,
  1555. ) -> Optional[MaintNotificationsConnectionHandler]:
  1556. if isinstance(self._conn, MaintNotificationsAbstractConnection):
  1557. return self._conn._maint_notifications_connection_handler
  1558. @_maint_notifications_connection_handler.setter
  1559. def _maint_notifications_connection_handler(
  1560. self, value: Optional[MaintNotificationsConnectionHandler]
  1561. ):
  1562. self._conn._maint_notifications_connection_handler = value
  1563. def _get_socket(self) -> Optional[socket.socket]:
  1564. if isinstance(self._conn, MaintNotificationsAbstractConnection):
  1565. return self._conn._get_socket()
  1566. else:
  1567. raise NotImplementedError(
  1568. "Maintenance notifications are not supported by this connection type"
  1569. )
  1570. def _get_maint_notifications_connection_instance(
  1571. self, connection
  1572. ) -> MaintNotificationsAbstractConnection:
  1573. """
  1574. Validate that connection instance supports maintenance notifications.
  1575. With this helper method we ensure that we are working
  1576. with the correct connection type.
  1577. After twe validate that connection instance supports maintenance notifications
  1578. we can safely return the connection instance
  1579. as MaintNotificationsAbstractConnection.
  1580. """
  1581. if not isinstance(connection, MaintNotificationsAbstractConnection):
  1582. raise NotImplementedError(
  1583. "Maintenance notifications are not supported by this connection type"
  1584. )
  1585. else:
  1586. return connection
  1587. @property
  1588. def maintenance_state(self) -> MaintenanceState:
  1589. con = self._get_maint_notifications_connection_instance(self._conn)
  1590. return con.maintenance_state
  1591. @maintenance_state.setter
  1592. def maintenance_state(self, state: MaintenanceState):
  1593. con = self._get_maint_notifications_connection_instance(self._conn)
  1594. con.maintenance_state = state
  1595. def getpeername(self):
  1596. con = self._get_maint_notifications_connection_instance(self._conn)
  1597. return con.getpeername()
  1598. def get_resolved_ip(self):
  1599. con = self._get_maint_notifications_connection_instance(self._conn)
  1600. return con.get_resolved_ip()
  1601. def update_current_socket_timeout(self, relaxed_timeout: Optional[float] = None):
  1602. con = self._get_maint_notifications_connection_instance(self._conn)
  1603. con.update_current_socket_timeout(relaxed_timeout)
  1604. def set_tmp_settings(
  1605. self,
  1606. tmp_host_address: Optional[str] = None,
  1607. tmp_relaxed_timeout: Optional[float] = None,
  1608. ):
  1609. con = self._get_maint_notifications_connection_instance(self._conn)
  1610. con.set_tmp_settings(tmp_host_address, tmp_relaxed_timeout)
  1611. def reset_tmp_settings(
  1612. self,
  1613. reset_host_address: bool = False,
  1614. reset_relaxed_timeout: bool = False,
  1615. ):
  1616. con = self._get_maint_notifications_connection_instance(self._conn)
  1617. con.reset_tmp_settings(reset_host_address, reset_relaxed_timeout)
  1618. def _connect(self):
  1619. self._conn._connect()
  1620. def _host_error(self):
  1621. self._conn._host_error()
  1622. def _enable_tracking_callback(self, conn: ConnectionInterface) -> None:
  1623. conn.send_command("CLIENT", "TRACKING", "ON")
  1624. conn.read_response()
  1625. conn._parser.set_invalidation_push_handler(self._on_invalidation_callback)
  1626. def _process_pending_invalidations(self):
  1627. while self.can_read():
  1628. self._conn.read_response(push_request=True)
  1629. def _on_invalidation_callback(self, data: List[Union[str, Optional[List[bytes]]]]):
  1630. with self._cache_lock:
  1631. # Flush cache when DB flushed on server-side
  1632. if data[1] is None:
  1633. self._cache.flush()
  1634. else:
  1635. keys_deleted = self._cache.delete_by_redis_keys(data[1])
  1636. if len(keys_deleted) > 0:
  1637. record_csc_eviction(
  1638. count=len(keys_deleted),
  1639. reason=CSCReason.INVALIDATION,
  1640. )
  1641. class SSLConnection(Connection):
  1642. """Manages SSL connections to and from the Redis server(s).
  1643. This class extends the Connection class, adding SSL functionality, and making
  1644. use of ssl.SSLContext (https://docs.python.org/3/library/ssl.html#ssl.SSLContext)
  1645. """ # noqa
  1646. def __init__(
  1647. self,
  1648. ssl_keyfile=None,
  1649. ssl_certfile=None,
  1650. ssl_cert_reqs="required",
  1651. ssl_include_verify_flags: Optional[List["VerifyFlags"]] = None,
  1652. ssl_exclude_verify_flags: Optional[List["VerifyFlags"]] = None,
  1653. ssl_ca_certs=None,
  1654. ssl_ca_data=None,
  1655. ssl_check_hostname=True,
  1656. ssl_ca_path=None,
  1657. ssl_password=None,
  1658. ssl_validate_ocsp=False,
  1659. ssl_validate_ocsp_stapled=False,
  1660. ssl_ocsp_context=None,
  1661. ssl_ocsp_expected_cert=None,
  1662. ssl_min_version=None,
  1663. ssl_ciphers=None,
  1664. **kwargs,
  1665. ):
  1666. """Constructor
  1667. Args:
  1668. ssl_keyfile: Path to an ssl private key. Defaults to None.
  1669. ssl_certfile: Path to an ssl certificate. Defaults to None.
  1670. ssl_cert_reqs: The string value for the SSLContext.verify_mode (none, optional, required),
  1671. or an ssl.VerifyMode. Defaults to "required".
  1672. ssl_include_verify_flags: A list of flags to be included in the SSLContext.verify_flags. Defaults to None.
  1673. ssl_exclude_verify_flags: A list of flags to be excluded from the SSLContext.verify_flags. Defaults to None.
  1674. ssl_ca_certs: The path to a file of concatenated CA certificates in PEM format. Defaults to None.
  1675. ssl_ca_data: Either an ASCII string of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates.
  1676. ssl_check_hostname: If set, match the hostname during the SSL handshake. Defaults to True.
  1677. ssl_ca_path: The path to a directory containing several CA certificates in PEM format. Defaults to None.
  1678. ssl_password: Password for unlocking an encrypted private key. Defaults to None.
  1679. ssl_validate_ocsp: If set, perform a full ocsp validation (i.e not a stapled verification)
  1680. ssl_validate_ocsp_stapled: If set, perform a validation on a stapled ocsp response
  1681. ssl_ocsp_context: A fully initialized OpenSSL.SSL.Context object to be used in verifying the ssl_ocsp_expected_cert
  1682. ssl_ocsp_expected_cert: A PEM armoured string containing the expected certificate to be returned from the ocsp verification service.
  1683. ssl_min_version: The lowest supported SSL version. It affects the supported SSL versions of the SSLContext. None leaves the default provided by ssl module.
  1684. ssl_ciphers: A string listing the ciphers that are allowed to be used. Defaults to None, which means that the default ciphers are used. See https://docs.python.org/3/library/ssl.html#ssl.SSLContext.set_ciphers for more information.
  1685. Raises:
  1686. RedisError
  1687. """ # noqa
  1688. if not SSL_AVAILABLE:
  1689. raise RedisError("Python wasn't built with SSL support")
  1690. self.keyfile = ssl_keyfile
  1691. self.certfile = ssl_certfile
  1692. if ssl_cert_reqs is None:
  1693. ssl_cert_reqs = ssl.CERT_NONE
  1694. elif isinstance(ssl_cert_reqs, str):
  1695. CERT_REQS = { # noqa: N806
  1696. "none": ssl.CERT_NONE,
  1697. "optional": ssl.CERT_OPTIONAL,
  1698. "required": ssl.CERT_REQUIRED,
  1699. }
  1700. if ssl_cert_reqs not in CERT_REQS:
  1701. raise RedisError(
  1702. f"Invalid SSL Certificate Requirements Flag: {ssl_cert_reqs}"
  1703. )
  1704. ssl_cert_reqs = CERT_REQS[ssl_cert_reqs]
  1705. self.cert_reqs = ssl_cert_reqs
  1706. self.ssl_include_verify_flags = ssl_include_verify_flags
  1707. self.ssl_exclude_verify_flags = ssl_exclude_verify_flags
  1708. self.ca_certs = ssl_ca_certs
  1709. self.ca_data = ssl_ca_data
  1710. self.ca_path = ssl_ca_path
  1711. self.check_hostname = (
  1712. ssl_check_hostname if self.cert_reqs != ssl.CERT_NONE else False
  1713. )
  1714. self.certificate_password = ssl_password
  1715. self.ssl_validate_ocsp = ssl_validate_ocsp
  1716. self.ssl_validate_ocsp_stapled = ssl_validate_ocsp_stapled
  1717. self.ssl_ocsp_context = ssl_ocsp_context
  1718. self.ssl_ocsp_expected_cert = ssl_ocsp_expected_cert
  1719. self.ssl_min_version = ssl_min_version
  1720. self.ssl_ciphers = ssl_ciphers
  1721. super().__init__(**kwargs)
  1722. def _connect(self):
  1723. """
  1724. Wrap the socket with SSL support, handling potential errors.
  1725. """
  1726. sock = super()._connect()
  1727. try:
  1728. return self._wrap_socket_with_ssl(sock)
  1729. except (OSError, RedisError):
  1730. sock.close()
  1731. raise
  1732. def _wrap_socket_with_ssl(self, sock):
  1733. """
  1734. Wraps the socket with SSL support.
  1735. Args:
  1736. sock: The plain socket to wrap with SSL.
  1737. Returns:
  1738. An SSL wrapped socket.
  1739. """
  1740. context = ssl.create_default_context()
  1741. context.check_hostname = self.check_hostname
  1742. context.verify_mode = self.cert_reqs
  1743. if self.ssl_include_verify_flags:
  1744. for flag in self.ssl_include_verify_flags:
  1745. context.verify_flags |= flag
  1746. if self.ssl_exclude_verify_flags:
  1747. for flag in self.ssl_exclude_verify_flags:
  1748. context.verify_flags &= ~flag
  1749. if self.certfile or self.keyfile:
  1750. context.load_cert_chain(
  1751. certfile=self.certfile,
  1752. keyfile=self.keyfile,
  1753. password=self.certificate_password,
  1754. )
  1755. if (
  1756. self.ca_certs is not None
  1757. or self.ca_path is not None
  1758. or self.ca_data is not None
  1759. ):
  1760. context.load_verify_locations(
  1761. cafile=self.ca_certs, capath=self.ca_path, cadata=self.ca_data
  1762. )
  1763. if self.ssl_min_version is not None:
  1764. context.minimum_version = self.ssl_min_version
  1765. if self.ssl_ciphers:
  1766. context.set_ciphers(self.ssl_ciphers)
  1767. if self.ssl_validate_ocsp is True and CRYPTOGRAPHY_AVAILABLE is False:
  1768. raise RedisError("cryptography is not installed.")
  1769. if self.ssl_validate_ocsp_stapled and self.ssl_validate_ocsp:
  1770. raise RedisError(
  1771. "Either an OCSP staple or pure OCSP connection must be validated "
  1772. "- not both."
  1773. )
  1774. sslsock = context.wrap_socket(sock, server_hostname=self.host)
  1775. # validation for the stapled case
  1776. if self.ssl_validate_ocsp_stapled:
  1777. import OpenSSL
  1778. from .ocsp import ocsp_staple_verifier
  1779. # if a context is provided use it - otherwise, a basic context
  1780. if self.ssl_ocsp_context is None:
  1781. staple_ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
  1782. staple_ctx.use_certificate_file(self.certfile)
  1783. staple_ctx.use_privatekey_file(self.keyfile)
  1784. else:
  1785. staple_ctx = self.ssl_ocsp_context
  1786. staple_ctx.set_ocsp_client_callback(
  1787. ocsp_staple_verifier, self.ssl_ocsp_expected_cert
  1788. )
  1789. # need another socket
  1790. con = OpenSSL.SSL.Connection(staple_ctx, socket.socket())
  1791. con.request_ocsp()
  1792. con.connect((self.host, self.port))
  1793. con.do_handshake()
  1794. con.shutdown()
  1795. return sslsock
  1796. # pure ocsp validation
  1797. if self.ssl_validate_ocsp is True and CRYPTOGRAPHY_AVAILABLE:
  1798. from .ocsp import OCSPVerifier
  1799. o = OCSPVerifier(sslsock, self.host, self.port, self.ca_certs)
  1800. if o.is_valid():
  1801. return sslsock
  1802. else:
  1803. raise ConnectionError("ocsp validation error")
  1804. return sslsock
  1805. class UnixDomainSocketConnection(AbstractConnection):
  1806. "Manages UDS communication to and from a Redis server"
  1807. def __init__(self, path="", socket_timeout=None, **kwargs):
  1808. super().__init__(**kwargs)
  1809. self.path = path
  1810. self.socket_timeout = socket_timeout
  1811. def repr_pieces(self):
  1812. pieces = [("path", self.path), ("db", self.db)]
  1813. if self.client_name:
  1814. pieces.append(("client_name", self.client_name))
  1815. return pieces
  1816. def _connect(self):
  1817. "Create a Unix domain socket connection"
  1818. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  1819. sock.settimeout(self.socket_connect_timeout)
  1820. try:
  1821. sock.connect(self.path)
  1822. except OSError:
  1823. # Prevent ResourceWarnings for unclosed sockets.
  1824. try:
  1825. sock.shutdown(socket.SHUT_RDWR) # ensure a clean close
  1826. except OSError:
  1827. pass
  1828. sock.close()
  1829. raise
  1830. sock.settimeout(self.socket_timeout)
  1831. return sock
  1832. def _host_error(self):
  1833. return self.path
  1834. FALSE_STRINGS = ("0", "F", "FALSE", "N", "NO")
  1835. def to_bool(value):
  1836. if value is None or value == "":
  1837. return None
  1838. if isinstance(value, str) and value.upper() in FALSE_STRINGS:
  1839. return False
  1840. return bool(value)
  1841. def parse_ssl_verify_flags(value):
  1842. # flags are passed in as a string representation of a list,
  1843. # e.g. VERIFY_X509_STRICT, VERIFY_X509_PARTIAL_CHAIN
  1844. verify_flags_str = value.replace("[", "").replace("]", "")
  1845. verify_flags = []
  1846. for flag in verify_flags_str.split(","):
  1847. flag = flag.strip()
  1848. if not hasattr(VerifyFlags, flag):
  1849. raise ValueError(f"Invalid ssl verify flag: {flag}")
  1850. verify_flags.append(getattr(VerifyFlags, flag))
  1851. return verify_flags
  1852. URL_QUERY_ARGUMENT_PARSERS = {
  1853. "db": int,
  1854. "socket_timeout": float,
  1855. "socket_connect_timeout": float,
  1856. "socket_keepalive": to_bool,
  1857. "retry_on_timeout": to_bool,
  1858. "retry_on_error": list,
  1859. "max_connections": int,
  1860. "health_check_interval": int,
  1861. "ssl_check_hostname": to_bool,
  1862. "ssl_include_verify_flags": parse_ssl_verify_flags,
  1863. "ssl_exclude_verify_flags": parse_ssl_verify_flags,
  1864. "timeout": float,
  1865. }
  1866. def parse_url(url):
  1867. if not (
  1868. url.startswith("redis://")
  1869. or url.startswith("rediss://")
  1870. or url.startswith("unix://")
  1871. ):
  1872. raise ValueError(
  1873. "Redis URL must specify one of the following "
  1874. "schemes (redis://, rediss://, unix://)"
  1875. )
  1876. url = urlparse(url)
  1877. kwargs = {}
  1878. for name, value in parse_qs(url.query).items():
  1879. if value and len(value) > 0:
  1880. value = unquote(value[0])
  1881. parser = URL_QUERY_ARGUMENT_PARSERS.get(name)
  1882. if parser:
  1883. try:
  1884. kwargs[name] = parser(value)
  1885. except (TypeError, ValueError):
  1886. raise ValueError(f"Invalid value for '{name}' in connection URL.")
  1887. else:
  1888. kwargs[name] = value
  1889. if url.username:
  1890. kwargs["username"] = unquote(url.username)
  1891. if url.password:
  1892. kwargs["password"] = unquote(url.password)
  1893. # We only support redis://, rediss:// and unix:// schemes.
  1894. if url.scheme == "unix":
  1895. if url.path:
  1896. kwargs["path"] = unquote(url.path)
  1897. kwargs["connection_class"] = UnixDomainSocketConnection
  1898. else: # implied: url.scheme in ("redis", "rediss"):
  1899. if url.hostname:
  1900. kwargs["host"] = unquote(url.hostname)
  1901. if url.port:
  1902. kwargs["port"] = int(url.port)
  1903. # If there's a path argument, use it as the db argument if a
  1904. # querystring value wasn't specified
  1905. if url.path and "db" not in kwargs:
  1906. try:
  1907. kwargs["db"] = int(unquote(url.path).replace("/", ""))
  1908. except (AttributeError, ValueError):
  1909. pass
  1910. if url.scheme == "rediss":
  1911. kwargs["connection_class"] = SSLConnection
  1912. return kwargs
  1913. _CP = TypeVar("_CP", bound="ConnectionPool")
  1914. class ConnectionPoolInterface(ABC):
  1915. @abstractmethod
  1916. def get_protocol(self):
  1917. pass
  1918. @abstractmethod
  1919. def reset(self):
  1920. pass
  1921. @abstractmethod
  1922. @deprecated_args(
  1923. args_to_warn=["*"],
  1924. reason="Use get_connection() without args instead",
  1925. version="5.3.0",
  1926. )
  1927. def get_connection(
  1928. self, command_name: Optional[str], *keys, **options
  1929. ) -> ConnectionInterface:
  1930. pass
  1931. @abstractmethod
  1932. def get_encoder(self):
  1933. pass
  1934. @abstractmethod
  1935. def release(self, connection: ConnectionInterface):
  1936. pass
  1937. @abstractmethod
  1938. def disconnect(self, inuse_connections: bool = True):
  1939. pass
  1940. @abstractmethod
  1941. def close(self):
  1942. pass
  1943. @abstractmethod
  1944. def set_retry(self, retry: Retry):
  1945. pass
  1946. @abstractmethod
  1947. def re_auth_callback(self, token: TokenInterface):
  1948. pass
  1949. @abstractmethod
  1950. def get_connection_count(self) -> list[tuple[int, dict]]:
  1951. """
  1952. Returns a connection count (both idle and in use).
  1953. """
  1954. pass
  1955. class MaintNotificationsAbstractConnectionPool:
  1956. """
  1957. Abstract class for handling maintenance notifications logic.
  1958. This class is mixed into the ConnectionPool classes.
  1959. This class is not intended to be used directly!
  1960. All logic related to maintenance notifications and
  1961. connection pool handling is encapsulated in this class.
  1962. """
  1963. def __init__(
  1964. self,
  1965. maint_notifications_config: Optional[MaintNotificationsConfig] = None,
  1966. oss_cluster_maint_notifications_handler: Optional[
  1967. OSSMaintNotificationsHandler
  1968. ] = None,
  1969. **kwargs,
  1970. ):
  1971. # Initialize maintenance notifications
  1972. is_protocol_supported = check_protocol_version(kwargs.get("protocol"), 3)
  1973. if maint_notifications_config is None and is_protocol_supported:
  1974. maint_notifications_config = MaintNotificationsConfig()
  1975. if maint_notifications_config and maint_notifications_config.enabled:
  1976. if not is_protocol_supported:
  1977. raise RedisError(
  1978. "Maintenance notifications handlers on connection are only supported with RESP version 3"
  1979. )
  1980. self._event_dispatcher = kwargs.get("event_dispatcher", None)
  1981. if self._event_dispatcher is None:
  1982. self._event_dispatcher = EventDispatcher()
  1983. self._maint_notifications_pool_handler = MaintNotificationsPoolHandler(
  1984. self, maint_notifications_config
  1985. )
  1986. if oss_cluster_maint_notifications_handler:
  1987. self._oss_cluster_maint_notifications_handler = (
  1988. oss_cluster_maint_notifications_handler
  1989. )
  1990. self._update_connection_kwargs_for_maint_notifications(
  1991. oss_cluster_maint_notifications_handler=self._oss_cluster_maint_notifications_handler
  1992. )
  1993. self._maint_notifications_pool_handler = None
  1994. else:
  1995. self._oss_cluster_maint_notifications_handler = None
  1996. self._maint_notifications_pool_handler = MaintNotificationsPoolHandler(
  1997. self, maint_notifications_config
  1998. )
  1999. self._update_connection_kwargs_for_maint_notifications(
  2000. maint_notifications_pool_handler=self._maint_notifications_pool_handler
  2001. )
  2002. else:
  2003. self._maint_notifications_pool_handler = None
  2004. self._oss_cluster_maint_notifications_handler = None
  2005. @property
  2006. @abstractmethod
  2007. def connection_kwargs(self) -> Dict[str, Any]:
  2008. pass
  2009. @connection_kwargs.setter
  2010. @abstractmethod
  2011. def connection_kwargs(self, value: Dict[str, Any]):
  2012. pass
  2013. @abstractmethod
  2014. def _get_pool_lock(self) -> threading.RLock:
  2015. pass
  2016. @abstractmethod
  2017. def _get_free_connections(self) -> Iterable["MaintNotificationsAbstractConnection"]:
  2018. pass
  2019. @abstractmethod
  2020. def _get_in_use_connections(
  2021. self,
  2022. ) -> Iterable["MaintNotificationsAbstractConnection"]:
  2023. pass
  2024. def maint_notifications_enabled(self):
  2025. """
  2026. Returns:
  2027. True if the maintenance notifications are enabled, False otherwise.
  2028. The maintenance notifications config is stored in the pool handler.
  2029. If the pool handler is not set, the maintenance notifications are not enabled.
  2030. """
  2031. if self._oss_cluster_maint_notifications_handler:
  2032. maint_notifications_config = (
  2033. self._oss_cluster_maint_notifications_handler.config
  2034. )
  2035. else:
  2036. maint_notifications_config = (
  2037. self._maint_notifications_pool_handler.config
  2038. if self._maint_notifications_pool_handler
  2039. else None
  2040. )
  2041. return maint_notifications_config and maint_notifications_config.enabled
  2042. def update_maint_notifications_config(
  2043. self,
  2044. maint_notifications_config: MaintNotificationsConfig,
  2045. oss_cluster_maint_notifications_handler: Optional[
  2046. OSSMaintNotificationsHandler
  2047. ] = None,
  2048. ):
  2049. """
  2050. Updates the maintenance notifications configuration.
  2051. This method should be called only if the pool was created
  2052. without enabling the maintenance notifications and
  2053. in a later point in time maintenance notifications
  2054. are requested to be enabled.
  2055. """
  2056. if (
  2057. self.maint_notifications_enabled()
  2058. and not maint_notifications_config.enabled
  2059. ):
  2060. raise ValueError(
  2061. "Cannot disable maintenance notifications after enabling them"
  2062. )
  2063. if oss_cluster_maint_notifications_handler:
  2064. self._oss_cluster_maint_notifications_handler = (
  2065. oss_cluster_maint_notifications_handler
  2066. )
  2067. else:
  2068. # first update pool settings
  2069. if not self._maint_notifications_pool_handler:
  2070. self._maint_notifications_pool_handler = MaintNotificationsPoolHandler(
  2071. self, maint_notifications_config
  2072. )
  2073. else:
  2074. self._maint_notifications_pool_handler.config = (
  2075. maint_notifications_config
  2076. )
  2077. # then update connection kwargs and existing connections
  2078. self._update_connection_kwargs_for_maint_notifications(
  2079. maint_notifications_pool_handler=self._maint_notifications_pool_handler,
  2080. oss_cluster_maint_notifications_handler=self._oss_cluster_maint_notifications_handler,
  2081. )
  2082. self._update_maint_notifications_configs_for_connections(
  2083. maint_notifications_pool_handler=self._maint_notifications_pool_handler,
  2084. oss_cluster_maint_notifications_handler=self._oss_cluster_maint_notifications_handler,
  2085. )
  2086. def _update_connection_kwargs_for_maint_notifications(
  2087. self,
  2088. maint_notifications_pool_handler: Optional[
  2089. MaintNotificationsPoolHandler
  2090. ] = None,
  2091. oss_cluster_maint_notifications_handler: Optional[
  2092. OSSMaintNotificationsHandler
  2093. ] = None,
  2094. ):
  2095. """
  2096. Update the connection kwargs for all future connections.
  2097. """
  2098. if not self.maint_notifications_enabled():
  2099. return
  2100. if maint_notifications_pool_handler:
  2101. self.connection_kwargs.update(
  2102. {
  2103. "maint_notifications_pool_handler": maint_notifications_pool_handler,
  2104. "maint_notifications_config": maint_notifications_pool_handler.config,
  2105. }
  2106. )
  2107. if oss_cluster_maint_notifications_handler:
  2108. self.connection_kwargs.update(
  2109. {
  2110. "oss_cluster_maint_notifications_handler": oss_cluster_maint_notifications_handler,
  2111. "maint_notifications_config": oss_cluster_maint_notifications_handler.config,
  2112. }
  2113. )
  2114. # Store original connection parameters for maintenance notifications.
  2115. if self.connection_kwargs.get("orig_host_address", None) is None:
  2116. # If orig_host_address is None it means we haven't
  2117. # configured the original values yet
  2118. self.connection_kwargs.update(
  2119. {
  2120. "orig_host_address": self.connection_kwargs.get("host"),
  2121. "orig_socket_timeout": self.connection_kwargs.get(
  2122. "socket_timeout", None
  2123. ),
  2124. "orig_socket_connect_timeout": self.connection_kwargs.get(
  2125. "socket_connect_timeout", None
  2126. ),
  2127. }
  2128. )
  2129. def _update_maint_notifications_configs_for_connections(
  2130. self,
  2131. maint_notifications_pool_handler: Optional[
  2132. MaintNotificationsPoolHandler
  2133. ] = None,
  2134. oss_cluster_maint_notifications_handler: Optional[
  2135. OSSMaintNotificationsHandler
  2136. ] = None,
  2137. ):
  2138. """Update the maintenance notifications config for all connections in the pool."""
  2139. with self._get_pool_lock():
  2140. for conn in self._get_free_connections():
  2141. if oss_cluster_maint_notifications_handler:
  2142. # set cluster handler for conn
  2143. conn.set_maint_notifications_cluster_handler_for_connection(
  2144. oss_cluster_maint_notifications_handler
  2145. )
  2146. conn.maint_notifications_config = (
  2147. oss_cluster_maint_notifications_handler.config
  2148. )
  2149. elif maint_notifications_pool_handler:
  2150. conn.set_maint_notifications_pool_handler_for_connection(
  2151. maint_notifications_pool_handler
  2152. )
  2153. conn.maint_notifications_config = (
  2154. maint_notifications_pool_handler.config
  2155. )
  2156. else:
  2157. raise ValueError(
  2158. "Either maint_notifications_pool_handler or oss_cluster_maint_notifications_handler must be set"
  2159. )
  2160. conn.disconnect()
  2161. for conn in self._get_in_use_connections():
  2162. if oss_cluster_maint_notifications_handler:
  2163. conn.maint_notifications_config = (
  2164. oss_cluster_maint_notifications_handler.config
  2165. )
  2166. conn._configure_maintenance_notifications(
  2167. oss_cluster_maint_notifications_handler=oss_cluster_maint_notifications_handler
  2168. )
  2169. elif maint_notifications_pool_handler:
  2170. conn.set_maint_notifications_pool_handler_for_connection(
  2171. maint_notifications_pool_handler
  2172. )
  2173. conn.maint_notifications_config = (
  2174. maint_notifications_pool_handler.config
  2175. )
  2176. else:
  2177. raise ValueError(
  2178. "Either maint_notifications_pool_handler or oss_cluster_maint_notifications_handler must be set"
  2179. )
  2180. conn.mark_for_reconnect()
  2181. def _should_update_connection(
  2182. self,
  2183. conn: "MaintNotificationsAbstractConnection",
  2184. matching_pattern: Literal[
  2185. "connected_address", "configured_address", "notification_hash"
  2186. ] = "connected_address",
  2187. matching_address: Optional[str] = None,
  2188. matching_notification_hash: Optional[int] = None,
  2189. ) -> bool:
  2190. """
  2191. Check if the connection should be updated based on the matching criteria.
  2192. """
  2193. if matching_pattern == "connected_address":
  2194. if matching_address and conn.getpeername() != matching_address:
  2195. return False
  2196. elif matching_pattern == "configured_address":
  2197. if matching_address and conn.host != matching_address:
  2198. return False
  2199. elif matching_pattern == "notification_hash":
  2200. if (
  2201. matching_notification_hash
  2202. and conn.maintenance_notification_hash != matching_notification_hash
  2203. ):
  2204. return False
  2205. return True
  2206. def update_connection_settings(
  2207. self,
  2208. conn: "MaintNotificationsAbstractConnection",
  2209. state: Optional["MaintenanceState"] = None,
  2210. maintenance_notification_hash: Optional[int] = None,
  2211. host_address: Optional[str] = None,
  2212. relaxed_timeout: Optional[float] = None,
  2213. update_notification_hash: bool = False,
  2214. reset_host_address: bool = False,
  2215. reset_relaxed_timeout: bool = False,
  2216. ):
  2217. """
  2218. Update the settings for a single connection.
  2219. """
  2220. if state:
  2221. conn.maintenance_state = state
  2222. if update_notification_hash:
  2223. # update the notification hash only if requested
  2224. conn.maintenance_notification_hash = maintenance_notification_hash
  2225. if host_address is not None:
  2226. conn.set_tmp_settings(tmp_host_address=host_address)
  2227. if relaxed_timeout is not None:
  2228. conn.set_tmp_settings(tmp_relaxed_timeout=relaxed_timeout)
  2229. if reset_relaxed_timeout or reset_host_address:
  2230. conn.reset_tmp_settings(
  2231. reset_host_address=reset_host_address,
  2232. reset_relaxed_timeout=reset_relaxed_timeout,
  2233. )
  2234. conn.update_current_socket_timeout(relaxed_timeout)
  2235. def update_connections_settings(
  2236. self,
  2237. state: Optional["MaintenanceState"] = None,
  2238. maintenance_notification_hash: Optional[int] = None,
  2239. host_address: Optional[str] = None,
  2240. relaxed_timeout: Optional[float] = None,
  2241. matching_address: Optional[str] = None,
  2242. matching_notification_hash: Optional[int] = None,
  2243. matching_pattern: Literal[
  2244. "connected_address", "configured_address", "notification_hash"
  2245. ] = "connected_address",
  2246. update_notification_hash: bool = False,
  2247. reset_host_address: bool = False,
  2248. reset_relaxed_timeout: bool = False,
  2249. include_free_connections: bool = True,
  2250. ):
  2251. """
  2252. Update the settings for all matching connections in the pool.
  2253. This method does not create new connections.
  2254. This method does not affect the connection kwargs.
  2255. :param state: The maintenance state to set for the connection.
  2256. :param maintenance_notification_hash: The hash of the maintenance notification
  2257. to set for the connection.
  2258. :param host_address: The host address to set for the connection.
  2259. :param relaxed_timeout: The relaxed timeout to set for the connection.
  2260. :param matching_address: The address to match for the connection.
  2261. :param matching_notification_hash: The notification hash to match for the connection.
  2262. :param matching_pattern: The pattern to match for the connection.
  2263. :param update_notification_hash: Whether to update the notification hash for the connection.
  2264. :param reset_host_address: Whether to reset the host address to the original address.
  2265. :param reset_relaxed_timeout: Whether to reset the relaxed timeout to the original timeout.
  2266. :param include_free_connections: Whether to include free/available connections.
  2267. """
  2268. with self._get_pool_lock():
  2269. for conn in self._get_in_use_connections():
  2270. if self._should_update_connection(
  2271. conn,
  2272. matching_pattern,
  2273. matching_address,
  2274. matching_notification_hash,
  2275. ):
  2276. self.update_connection_settings(
  2277. conn,
  2278. state=state,
  2279. maintenance_notification_hash=maintenance_notification_hash,
  2280. host_address=host_address,
  2281. relaxed_timeout=relaxed_timeout,
  2282. update_notification_hash=update_notification_hash,
  2283. reset_host_address=reset_host_address,
  2284. reset_relaxed_timeout=reset_relaxed_timeout,
  2285. )
  2286. if include_free_connections:
  2287. for conn in self._get_free_connections():
  2288. if self._should_update_connection(
  2289. conn,
  2290. matching_pattern,
  2291. matching_address,
  2292. matching_notification_hash,
  2293. ):
  2294. self.update_connection_settings(
  2295. conn,
  2296. state=state,
  2297. maintenance_notification_hash=maintenance_notification_hash,
  2298. host_address=host_address,
  2299. relaxed_timeout=relaxed_timeout,
  2300. update_notification_hash=update_notification_hash,
  2301. reset_host_address=reset_host_address,
  2302. reset_relaxed_timeout=reset_relaxed_timeout,
  2303. )
  2304. def update_connection_kwargs(
  2305. self,
  2306. **kwargs,
  2307. ):
  2308. """
  2309. Update the connection kwargs for all future connections.
  2310. This method updates the connection kwargs for all future connections created by the pool.
  2311. Existing connections are not affected.
  2312. """
  2313. self.connection_kwargs.update(kwargs)
  2314. def update_active_connections_for_reconnect(
  2315. self,
  2316. moving_address_src: Optional[str] = None,
  2317. ):
  2318. """
  2319. Mark all active connections for reconnect.
  2320. This is used when a cluster node is migrated to a different address.
  2321. :param moving_address_src: The address of the node that is being moved.
  2322. """
  2323. with self._get_pool_lock():
  2324. for conn in self._get_in_use_connections():
  2325. if self._should_update_connection(
  2326. conn, "connected_address", moving_address_src
  2327. ):
  2328. conn.mark_for_reconnect()
  2329. def disconnect_free_connections(
  2330. self,
  2331. moving_address_src: Optional[str] = None,
  2332. ):
  2333. """
  2334. Disconnect all free/available connections.
  2335. This is used when a cluster node is migrated to a different address.
  2336. :param moving_address_src: The address of the node that is being moved.
  2337. """
  2338. with self._get_pool_lock():
  2339. for conn in self._get_free_connections():
  2340. if self._should_update_connection(
  2341. conn, "connected_address", moving_address_src
  2342. ):
  2343. conn.disconnect()
  2344. class ConnectionPool(MaintNotificationsAbstractConnectionPool, ConnectionPoolInterface):
  2345. """
  2346. Create a connection pool. ``If max_connections`` is set, then this
  2347. object raises :py:class:`~redis.exceptions.ConnectionError` when the pool's
  2348. limit is reached.
  2349. By default, TCP connections are created unless ``connection_class``
  2350. is specified. Use class:`.UnixDomainSocketConnection` for
  2351. unix sockets.
  2352. :py:class:`~redis.SSLConnection` can be used for SSL enabled connections.
  2353. If ``maint_notifications_config`` is provided, the connection pool will support
  2354. maintenance notifications.
  2355. Maintenance notifications are supported only with RESP3.
  2356. If the ``maint_notifications_config`` is not provided but the ``protocol`` is 3,
  2357. the maintenance notifications will be enabled by default.
  2358. Any additional keyword arguments are passed to the constructor of
  2359. ``connection_class``.
  2360. """
  2361. @classmethod
  2362. def from_url(cls: Type[_CP], url: str, **kwargs) -> _CP:
  2363. """
  2364. Return a connection pool configured from the given URL.
  2365. For example::
  2366. redis://[[username]:[password]]@localhost:6379/0
  2367. rediss://[[username]:[password]]@localhost:6379/0
  2368. unix://[username@]/path/to/socket.sock?db=0[&password=password]
  2369. Three URL schemes are supported:
  2370. - `redis://` creates a TCP socket connection. See more at:
  2371. <https://www.iana.org/assignments/uri-schemes/prov/redis>
  2372. - `rediss://` creates a SSL wrapped TCP socket connection. See more at:
  2373. <https://www.iana.org/assignments/uri-schemes/prov/rediss>
  2374. - ``unix://``: creates a Unix Domain Socket connection.
  2375. The username, password, hostname, path and all querystring values
  2376. are passed through urllib.parse.unquote in order to replace any
  2377. percent-encoded values with their corresponding characters.
  2378. There are several ways to specify a database number. The first value
  2379. found will be used:
  2380. 1. A ``db`` querystring option, e.g. redis://localhost?db=0
  2381. 2. If using the redis:// or rediss:// schemes, the path argument
  2382. of the url, e.g. redis://localhost/0
  2383. 3. A ``db`` keyword argument to this function.
  2384. If none of these options are specified, the default db=0 is used.
  2385. All querystring options are cast to their appropriate Python types.
  2386. Boolean arguments can be specified with string values "True"/"False"
  2387. or "Yes"/"No". Values that cannot be properly cast cause a
  2388. ``ValueError`` to be raised. Once parsed, the querystring arguments
  2389. and keyword arguments are passed to the ``ConnectionPool``'s
  2390. class initializer. In the case of conflicting arguments, querystring
  2391. arguments always win.
  2392. """
  2393. url_options = parse_url(url)
  2394. if "connection_class" in kwargs:
  2395. url_options["connection_class"] = kwargs["connection_class"]
  2396. kwargs.update(url_options)
  2397. return cls(**kwargs)
  2398. def __init__(
  2399. self,
  2400. connection_class=Connection,
  2401. max_connections: Optional[int] = None,
  2402. cache_factory: Optional[CacheFactoryInterface] = None,
  2403. maint_notifications_config: Optional[MaintNotificationsConfig] = None,
  2404. **connection_kwargs,
  2405. ):
  2406. max_connections = max_connections or 2**31
  2407. if not isinstance(max_connections, int) or max_connections < 0:
  2408. raise ValueError('"max_connections" must be a positive integer')
  2409. self.connection_class = connection_class
  2410. self._connection_kwargs = connection_kwargs
  2411. self.max_connections = max_connections
  2412. self.cache = None
  2413. self._cache_factory = cache_factory
  2414. self._event_dispatcher = self._connection_kwargs.get("event_dispatcher", None)
  2415. if self._event_dispatcher is None:
  2416. self._event_dispatcher = EventDispatcher()
  2417. if connection_kwargs.get("cache_config") or connection_kwargs.get("cache"):
  2418. if not check_protocol_version(self._connection_kwargs.get("protocol"), 3):
  2419. raise RedisError("Client caching is only supported with RESP version 3")
  2420. cache = self._connection_kwargs.get("cache")
  2421. if cache is not None:
  2422. if not isinstance(cache, CacheInterface):
  2423. raise ValueError("Cache must implement CacheInterface")
  2424. self.cache = cache
  2425. else:
  2426. if self._cache_factory is not None:
  2427. self.cache = CacheProxy(self._cache_factory.get_cache())
  2428. else:
  2429. self.cache = CacheFactory(
  2430. self._connection_kwargs.get("cache_config")
  2431. ).get_cache()
  2432. init_csc_items()
  2433. register_csc_items_callback(
  2434. callback=lambda: self.cache.size,
  2435. pool_name=get_pool_name(self),
  2436. )
  2437. connection_kwargs.pop("cache", None)
  2438. connection_kwargs.pop("cache_config", None)
  2439. # a lock to protect the critical section in _checkpid().
  2440. # this lock is acquired when the process id changes, such as
  2441. # after a fork. during this time, multiple threads in the child
  2442. # process could attempt to acquire this lock. the first thread
  2443. # to acquire the lock will reset the data structures and lock
  2444. # object of this pool. subsequent threads acquiring this lock
  2445. # will notice the first thread already did the work and simply
  2446. # release the lock.
  2447. self._fork_lock = threading.RLock()
  2448. self._lock = threading.RLock()
  2449. # Generate unique pool ID for observability (matches go-redis behavior)
  2450. import secrets
  2451. self._pool_id = secrets.token_hex(4)
  2452. MaintNotificationsAbstractConnectionPool.__init__(
  2453. self,
  2454. maint_notifications_config=maint_notifications_config,
  2455. **connection_kwargs,
  2456. )
  2457. self.reset()
  2458. # Keys that should be redacted in __repr__ to avoid exposing sensitive information
  2459. SENSITIVE_REPR_KEYS = frozenset(
  2460. {
  2461. "password",
  2462. "username",
  2463. "ssl_password",
  2464. "credential_provider",
  2465. }
  2466. )
  2467. def __repr__(self) -> str:
  2468. conn_kwargs = ",".join(
  2469. [
  2470. f"{k}={'<REDACTED>' if k in self.SENSITIVE_REPR_KEYS else v}"
  2471. for k, v in self.connection_kwargs.items()
  2472. ]
  2473. )
  2474. return (
  2475. f"<{self.__class__.__module__}.{self.__class__.__name__}"
  2476. f"(<{self.connection_class.__module__}.{self.connection_class.__name__}"
  2477. f"({conn_kwargs})>)>"
  2478. )
  2479. @property
  2480. def connection_kwargs(self) -> Dict[str, Any]:
  2481. return self._connection_kwargs
  2482. @connection_kwargs.setter
  2483. def connection_kwargs(self, value: Dict[str, Any]):
  2484. self._connection_kwargs = value
  2485. def get_protocol(self):
  2486. """
  2487. Returns:
  2488. The RESP protocol version, or ``None`` if the protocol is not specified,
  2489. in which case the server default will be used.
  2490. """
  2491. return self.connection_kwargs.get("protocol", None)
  2492. def reset(self) -> None:
  2493. # Record metrics for connections being removed before clearing
  2494. # (only if attributes exist - they won't during __init__)
  2495. if hasattr(self, "_available_connections") and hasattr(
  2496. self, "_in_use_connections"
  2497. ):
  2498. with self._lock:
  2499. idle_count = len(self._available_connections)
  2500. in_use_count = len(self._in_use_connections)
  2501. if idle_count > 0 or in_use_count > 0:
  2502. pool_name = get_pool_name(self)
  2503. if idle_count > 0:
  2504. record_connection_count(
  2505. pool_name=pool_name,
  2506. connection_state=ConnectionState.IDLE,
  2507. counter=-idle_count,
  2508. )
  2509. if in_use_count > 0:
  2510. record_connection_count(
  2511. pool_name=pool_name,
  2512. connection_state=ConnectionState.USED,
  2513. counter=-in_use_count,
  2514. )
  2515. self._created_connections = 0
  2516. self._available_connections = []
  2517. self._in_use_connections = set()
  2518. # this must be the last operation in this method. while reset() is
  2519. # called when holding _fork_lock, other threads in this process
  2520. # can call _checkpid() which compares self.pid and os.getpid() without
  2521. # holding any lock (for performance reasons). keeping this assignment
  2522. # as the last operation ensures that those other threads will also
  2523. # notice a pid difference and block waiting for the first thread to
  2524. # release _fork_lock. when each of these threads eventually acquire
  2525. # _fork_lock, they will notice that another thread already called
  2526. # reset() and they will immediately release _fork_lock and continue on.
  2527. self.pid = os.getpid()
  2528. def __del__(self) -> None:
  2529. """Clean up connection pool and record metrics when garbage collected."""
  2530. try:
  2531. if not hasattr(self, "_available_connections") or not hasattr(
  2532. self, "_in_use_connections"
  2533. ):
  2534. return
  2535. # Record metrics for all connections being removed
  2536. idle_count = len(self._available_connections)
  2537. in_use_count = len(self._in_use_connections)
  2538. if idle_count > 0 or in_use_count > 0:
  2539. pool_name = get_pool_name(self)
  2540. if idle_count > 0:
  2541. record_connection_count(
  2542. pool_name=pool_name,
  2543. connection_state=ConnectionState.IDLE,
  2544. counter=-idle_count,
  2545. )
  2546. if in_use_count > 0:
  2547. record_connection_count(
  2548. pool_name=pool_name,
  2549. connection_state=ConnectionState.USED,
  2550. counter=-in_use_count,
  2551. )
  2552. except Exception:
  2553. pass
  2554. def _checkpid(self) -> None:
  2555. # _checkpid() attempts to keep ConnectionPool fork-safe on modern
  2556. # systems. this is called by all ConnectionPool methods that
  2557. # manipulate the pool's state such as get_connection() and release().
  2558. #
  2559. # _checkpid() determines whether the process has forked by comparing
  2560. # the current process id to the process id saved on the ConnectionPool
  2561. # instance. if these values are the same, _checkpid() simply returns.
  2562. #
  2563. # when the process ids differ, _checkpid() assumes that the process
  2564. # has forked and that we're now running in the child process. the child
  2565. # process cannot use the parent's file descriptors (e.g., sockets).
  2566. # therefore, when _checkpid() sees the process id change, it calls
  2567. # reset() in order to reinitialize the child's ConnectionPool. this
  2568. # will cause the child to make all new connection objects.
  2569. #
  2570. # _checkpid() is protected by self._fork_lock to ensure that multiple
  2571. # threads in the child process do not call reset() multiple times.
  2572. #
  2573. # there is an extremely small chance this could fail in the following
  2574. # scenario:
  2575. # 1. process A calls _checkpid() for the first time and acquires
  2576. # self._fork_lock.
  2577. # 2. while holding self._fork_lock, process A forks (the fork()
  2578. # could happen in a different thread owned by process A)
  2579. # 3. process B (the forked child process) inherits the
  2580. # ConnectionPool's state from the parent. that state includes
  2581. # a locked _fork_lock. process B will not be notified when
  2582. # process A releases the _fork_lock and will thus never be
  2583. # able to acquire the _fork_lock.
  2584. #
  2585. # to mitigate this possible deadlock, _checkpid() will only wait 5
  2586. # seconds to acquire _fork_lock. if _fork_lock cannot be acquired in
  2587. # that time it is assumed that the child is deadlocked and a
  2588. # redis.ChildDeadlockedError error is raised.
  2589. if self.pid != os.getpid():
  2590. acquired = self._fork_lock.acquire(timeout=5)
  2591. if not acquired:
  2592. raise ChildDeadlockedError
  2593. # reset() the instance for the new process if another thread
  2594. # hasn't already done so
  2595. try:
  2596. if self.pid != os.getpid():
  2597. self.reset()
  2598. finally:
  2599. self._fork_lock.release()
  2600. @deprecated_args(
  2601. args_to_warn=["*"],
  2602. reason="Use get_connection() without args instead",
  2603. version="5.3.0",
  2604. )
  2605. def get_connection(self, command_name=None, *keys, **options) -> "Connection":
  2606. "Get a connection from the pool"
  2607. # Start timing for observability
  2608. self._checkpid()
  2609. is_created = False
  2610. with self._lock:
  2611. try:
  2612. connection = self._available_connections.pop()
  2613. except IndexError:
  2614. # Start timing for observability
  2615. start_time_created = time.monotonic()
  2616. connection = self.make_connection()
  2617. is_created = True
  2618. self._in_use_connections.add(connection)
  2619. # Record state transition: IDLE -> USED
  2620. # (make_connection already recorded IDLE +1 for new connections)
  2621. # This ensures counters stay balanced if connect() fails and release() is called
  2622. pool_name = get_pool_name(self)
  2623. record_connection_count(
  2624. pool_name=pool_name,
  2625. connection_state=ConnectionState.IDLE,
  2626. counter=-1,
  2627. )
  2628. record_connection_count(
  2629. pool_name=pool_name,
  2630. connection_state=ConnectionState.USED,
  2631. counter=1,
  2632. )
  2633. try:
  2634. # ensure this connection is connected to Redis
  2635. connection.connect()
  2636. # connections that the pool provides should be ready to send
  2637. # a command. if not, the connection was either returned to the
  2638. # pool before all data has been read or the socket has been
  2639. # closed. either way, reconnect and verify everything is good.
  2640. try:
  2641. if (
  2642. connection.can_read()
  2643. and self.cache is None
  2644. and not self.maint_notifications_enabled()
  2645. ):
  2646. raise ConnectionError("Connection has data")
  2647. except (ConnectionError, TimeoutError, OSError):
  2648. connection.disconnect()
  2649. connection.connect()
  2650. if connection.can_read():
  2651. raise ConnectionError("Connection not ready")
  2652. except BaseException:
  2653. # release the connection back to the pool so that we don't
  2654. # leak it
  2655. self.release(connection)
  2656. raise
  2657. if is_created:
  2658. record_connection_create_time(
  2659. connection_pool=self,
  2660. duration_seconds=time.monotonic() - start_time_created,
  2661. )
  2662. return connection
  2663. def get_encoder(self) -> Encoder:
  2664. "Return an encoder based on encoding settings"
  2665. kwargs = self.connection_kwargs
  2666. return Encoder(
  2667. encoding=kwargs.get("encoding", "utf-8"),
  2668. encoding_errors=kwargs.get("encoding_errors", "strict"),
  2669. decode_responses=kwargs.get("decode_responses", False),
  2670. )
  2671. def make_connection(self) -> "ConnectionInterface":
  2672. "Create a new connection"
  2673. if self._created_connections >= self.max_connections:
  2674. raise MaxConnectionsError("Too many connections")
  2675. self._created_connections += 1
  2676. kwargs = dict(self.connection_kwargs)
  2677. # Create the connection first, then record metrics only on success
  2678. if self.cache is not None:
  2679. connection = CacheProxyConnection(
  2680. self.connection_class(**kwargs), self.cache, self._lock
  2681. )
  2682. else:
  2683. connection = self.connection_class(**kwargs)
  2684. # Record new connection created (starts as IDLE) - only after successful construction
  2685. record_connection_count(
  2686. pool_name=get_pool_name(self),
  2687. connection_state=ConnectionState.IDLE,
  2688. counter=1,
  2689. )
  2690. return connection
  2691. def release(self, connection: "Connection") -> None:
  2692. "Releases the connection back to the pool"
  2693. self._checkpid()
  2694. with self._lock:
  2695. try:
  2696. self._in_use_connections.remove(connection)
  2697. except KeyError:
  2698. # Gracefully fail when a connection is returned to this pool
  2699. # that the pool doesn't actually own
  2700. return
  2701. if self.owns_connection(connection):
  2702. if connection.should_reconnect():
  2703. connection.disconnect()
  2704. self._available_connections.append(connection)
  2705. self._event_dispatcher.dispatch(
  2706. AfterConnectionReleasedEvent(connection)
  2707. )
  2708. # Record state transition: USED -> IDLE
  2709. pool_name = get_pool_name(self)
  2710. record_connection_count(
  2711. pool_name=pool_name,
  2712. connection_state=ConnectionState.USED,
  2713. counter=-1,
  2714. )
  2715. record_connection_count(
  2716. pool_name=pool_name,
  2717. connection_state=ConnectionState.IDLE,
  2718. counter=1,
  2719. )
  2720. else:
  2721. # Pool doesn't own this connection, do not add it back
  2722. # to the pool.
  2723. # The created connections count should not be changed,
  2724. # because the connection was not created by the pool.
  2725. # Still need to decrement USED since it was counted in get_connection()
  2726. connection.disconnect()
  2727. record_connection_count(
  2728. pool_name="unknown_pool",
  2729. connection_state=ConnectionState.USED,
  2730. counter=-1,
  2731. )
  2732. return
  2733. def owns_connection(self, connection: "Connection") -> int:
  2734. return connection.pid == self.pid
  2735. def disconnect(self, inuse_connections: bool = True) -> None:
  2736. """
  2737. Disconnects connections in the pool
  2738. If ``inuse_connections`` is True, disconnect connections that are
  2739. currently in use, potentially by other threads. Otherwise only disconnect
  2740. connections that are idle in the pool.
  2741. """
  2742. self._checkpid()
  2743. with self._lock:
  2744. if inuse_connections:
  2745. connections = chain(
  2746. self._available_connections, self._in_use_connections
  2747. )
  2748. else:
  2749. connections = self._available_connections
  2750. for connection in connections:
  2751. connection.disconnect()
  2752. def close(self) -> None:
  2753. """Close the pool, disconnecting all connections"""
  2754. self.disconnect()
  2755. def set_retry(self, retry: Retry) -> None:
  2756. self.connection_kwargs.update({"retry": retry})
  2757. for conn in self._available_connections:
  2758. conn.retry = retry
  2759. for conn in self._in_use_connections:
  2760. conn.retry = retry
  2761. def re_auth_callback(self, token: TokenInterface):
  2762. with self._lock:
  2763. for conn in self._available_connections:
  2764. conn.retry.call_with_retry(
  2765. lambda: conn.send_command(
  2766. "AUTH", token.try_get("oid"), token.get_value()
  2767. ),
  2768. lambda error: self._mock(error),
  2769. )
  2770. conn.retry.call_with_retry(
  2771. lambda: conn.read_response(), lambda error: self._mock(error)
  2772. )
  2773. for conn in self._in_use_connections:
  2774. conn.set_re_auth_token(token)
  2775. def _get_pool_lock(self):
  2776. return self._lock
  2777. def _get_free_connections(self):
  2778. with self._lock:
  2779. return list(self._available_connections)
  2780. def _get_in_use_connections(self):
  2781. with self._lock:
  2782. return set(self._in_use_connections)
  2783. def _mock(self, error: RedisError):
  2784. """
  2785. Dummy functions, needs to be passed as error callback to retry object.
  2786. :param error:
  2787. :return:
  2788. """
  2789. pass
  2790. def get_connection_count(self) -> List[tuple[int, dict]]:
  2791. from redis.observability.attributes import get_pool_name
  2792. attributes = AttributeBuilder.build_base_attributes()
  2793. attributes[DB_CLIENT_CONNECTION_POOL_NAME] = get_pool_name(self)
  2794. free_connections_attributes = attributes.copy()
  2795. in_use_connections_attributes = attributes.copy()
  2796. free_connections_attributes[DB_CLIENT_CONNECTION_STATE] = (
  2797. ConnectionState.IDLE.value
  2798. )
  2799. in_use_connections_attributes[DB_CLIENT_CONNECTION_STATE] = (
  2800. ConnectionState.USED.value
  2801. )
  2802. return [
  2803. (len(self._get_free_connections()), free_connections_attributes),
  2804. (len(self._get_in_use_connections()), in_use_connections_attributes),
  2805. ]
  2806. class BlockingConnectionPool(ConnectionPool):
  2807. """
  2808. Thread-safe blocking connection pool::
  2809. >>> from redis.client import Redis
  2810. >>> client = Redis(connection_pool=BlockingConnectionPool())
  2811. It performs the same function as the default
  2812. :py:class:`~redis.ConnectionPool` implementation, in that,
  2813. it maintains a pool of reusable connections that can be shared by
  2814. multiple redis clients (safely across threads if required).
  2815. The difference is that, in the event that a client tries to get a
  2816. connection from the pool when all of connections are in use, rather than
  2817. raising a :py:class:`~redis.ConnectionError` (as the default
  2818. :py:class:`~redis.ConnectionPool` implementation does), it
  2819. makes the client wait ("blocks") for a specified number of seconds until
  2820. a connection becomes available.
  2821. Use ``max_connections`` to increase / decrease the pool size::
  2822. >>> pool = BlockingConnectionPool(max_connections=10)
  2823. Use ``timeout`` to tell it either how many seconds to wait for a connection
  2824. to become available, or to block forever:
  2825. >>> # Block forever.
  2826. >>> pool = BlockingConnectionPool(timeout=None)
  2827. >>> # Raise a ``ConnectionError`` after five seconds if a connection is
  2828. >>> # not available.
  2829. >>> pool = BlockingConnectionPool(timeout=5)
  2830. """
  2831. def __init__(
  2832. self,
  2833. max_connections=50,
  2834. timeout=20,
  2835. connection_class=Connection,
  2836. queue_class=LifoQueue,
  2837. **connection_kwargs,
  2838. ):
  2839. self.queue_class = queue_class
  2840. self.timeout = timeout
  2841. self._in_maintenance = False
  2842. self._locked = False
  2843. super().__init__(
  2844. connection_class=connection_class,
  2845. max_connections=max_connections,
  2846. **connection_kwargs,
  2847. )
  2848. def reset(self):
  2849. # Create and fill up a thread safe queue with ``None`` values.
  2850. try:
  2851. if self._in_maintenance:
  2852. self._lock.acquire()
  2853. self._locked = True
  2854. # Record metrics for connections being removed before clearing
  2855. # Note: Access pool.queue directly to avoid deadlock since we may
  2856. # already hold self._lock (which is non-reentrant)
  2857. if (
  2858. hasattr(self, "_connections")
  2859. and self._connections
  2860. and hasattr(self, "pool")
  2861. ):
  2862. with self._lock:
  2863. connections_in_queue = {conn for conn in self.pool.queue if conn}
  2864. idle_count = len(connections_in_queue)
  2865. in_use_count = len(self._connections) - idle_count
  2866. if idle_count > 0 or in_use_count > 0:
  2867. pool_name = get_pool_name(self)
  2868. if idle_count > 0:
  2869. record_connection_count(
  2870. pool_name=pool_name,
  2871. connection_state=ConnectionState.IDLE,
  2872. counter=-idle_count,
  2873. )
  2874. if in_use_count > 0:
  2875. record_connection_count(
  2876. pool_name=pool_name,
  2877. connection_state=ConnectionState.USED,
  2878. counter=-in_use_count,
  2879. )
  2880. self.pool = self.queue_class(self.max_connections)
  2881. while True:
  2882. try:
  2883. self.pool.put_nowait(None)
  2884. except Full:
  2885. break
  2886. # Keep a list of actual connection instances so that we can
  2887. # disconnect them later.
  2888. self._connections = []
  2889. finally:
  2890. if self._locked:
  2891. try:
  2892. self._lock.release()
  2893. except Exception:
  2894. pass
  2895. self._locked = False
  2896. # this must be the last operation in this method. while reset() is
  2897. # called when holding _fork_lock, other threads in this process
  2898. # can call _checkpid() which compares self.pid and os.getpid() without
  2899. # holding any lock (for performance reasons). keeping this assignment
  2900. # as the last operation ensures that those other threads will also
  2901. # notice a pid difference and block waiting for the first thread to
  2902. # release _fork_lock. when each of these threads eventually acquire
  2903. # _fork_lock, they will notice that another thread already called
  2904. # reset() and they will immediately release _fork_lock and continue on.
  2905. self.pid = os.getpid()
  2906. def __del__(self) -> None:
  2907. """Clean up connection pool and record metrics when garbage collected."""
  2908. try:
  2909. # Note: Access pool.queue directly to avoid potential deadlock
  2910. # if GC runs while the lock is held by the same thread
  2911. if (
  2912. hasattr(self, "_connections")
  2913. and self._connections
  2914. and hasattr(self, "pool")
  2915. ):
  2916. connections_in_queue = {conn for conn in self.pool.queue if conn}
  2917. idle_count = len(connections_in_queue)
  2918. in_use_count = len(self._connections) - idle_count
  2919. if idle_count > 0 or in_use_count > 0:
  2920. pool_name = get_pool_name(self)
  2921. if idle_count > 0:
  2922. record_connection_count(
  2923. pool_name=pool_name,
  2924. connection_state=ConnectionState.IDLE,
  2925. counter=-idle_count,
  2926. )
  2927. if in_use_count > 0:
  2928. record_connection_count(
  2929. pool_name=pool_name,
  2930. connection_state=ConnectionState.USED,
  2931. counter=-in_use_count,
  2932. )
  2933. except Exception:
  2934. pass
  2935. def make_connection(self):
  2936. "Make a fresh connection."
  2937. try:
  2938. if self._in_maintenance:
  2939. self._lock.acquire()
  2940. self._locked = True
  2941. if self.cache is not None:
  2942. connection = CacheProxyConnection(
  2943. self.connection_class(**self.connection_kwargs),
  2944. self.cache,
  2945. self._lock,
  2946. )
  2947. else:
  2948. connection = self.connection_class(**self.connection_kwargs)
  2949. self._connections.append(connection)
  2950. # Record new connection created (starts as IDLE)
  2951. record_connection_count(
  2952. pool_name=get_pool_name(self),
  2953. connection_state=ConnectionState.IDLE,
  2954. counter=1,
  2955. )
  2956. return connection
  2957. finally:
  2958. if self._locked:
  2959. try:
  2960. self._lock.release()
  2961. except Exception:
  2962. pass
  2963. self._locked = False
  2964. @deprecated_args(
  2965. args_to_warn=["*"],
  2966. reason="Use get_connection() without args instead",
  2967. version="5.3.0",
  2968. )
  2969. def get_connection(self, command_name=None, *keys, **options):
  2970. """
  2971. Get a connection, blocking for ``self.timeout`` until a connection
  2972. is available from the pool.
  2973. If the connection returned is ``None`` then creates a new connection.
  2974. Because we use a last-in first-out queue, the existing connections
  2975. (having been returned to the pool after the initial ``None`` values
  2976. were added) will be returned before ``None`` values. This means we only
  2977. create new connections when we need to, i.e.: the actual number of
  2978. connections will only increase in response to demand.
  2979. """
  2980. start_time_acquired = time.monotonic()
  2981. # Make sure we haven't changed process.
  2982. self._checkpid()
  2983. is_created = False
  2984. # Try and get a connection from the pool. If one isn't available within
  2985. # self.timeout then raise a ``ConnectionError``.
  2986. connection = None
  2987. try:
  2988. if self._in_maintenance:
  2989. self._lock.acquire()
  2990. self._locked = True
  2991. try:
  2992. connection = self.pool.get(block=True, timeout=self.timeout)
  2993. except Empty:
  2994. # Note that this is not caught by the redis client and will be
  2995. # raised unless handled by application code. If you want never to
  2996. raise ConnectionError("No connection available.")
  2997. # If the ``connection`` is actually ``None`` then that's a cue to make
  2998. # a new connection to add to the pool.
  2999. if connection is None:
  3000. # Start timing for observability
  3001. start_time_created = time.monotonic()
  3002. connection = self.make_connection()
  3003. is_created = True
  3004. finally:
  3005. if self._locked:
  3006. try:
  3007. self._lock.release()
  3008. except Exception:
  3009. pass
  3010. self._locked = False
  3011. # Record state transition: IDLE -> USED
  3012. # (make_connection already recorded IDLE +1 for new connections)
  3013. # This ensures counters stay balanced if connect() fails and release() is called
  3014. pool_name = get_pool_name(self)
  3015. record_connection_count(
  3016. pool_name=pool_name,
  3017. connection_state=ConnectionState.IDLE,
  3018. counter=-1,
  3019. )
  3020. record_connection_count(
  3021. pool_name=pool_name,
  3022. connection_state=ConnectionState.USED,
  3023. counter=1,
  3024. )
  3025. try:
  3026. # ensure this connection is connected to Redis
  3027. connection.connect()
  3028. # connections that the pool provides should be ready to send
  3029. # a command. if not, the connection was either returned to the
  3030. # pool before all data has been read or the socket has been
  3031. # closed. either way, reconnect and verify everything is good.
  3032. try:
  3033. if connection.can_read():
  3034. raise ConnectionError("Connection has data")
  3035. except (ConnectionError, TimeoutError, OSError):
  3036. connection.disconnect()
  3037. connection.connect()
  3038. if connection.can_read():
  3039. raise ConnectionError("Connection not ready")
  3040. except BaseException:
  3041. # release the connection back to the pool so that we don't leak it
  3042. self.release(connection)
  3043. raise
  3044. if is_created:
  3045. record_connection_create_time(
  3046. connection_pool=self,
  3047. duration_seconds=time.monotonic() - start_time_created,
  3048. )
  3049. record_connection_wait_time(
  3050. pool_name=pool_name,
  3051. duration_seconds=time.monotonic() - start_time_acquired,
  3052. )
  3053. return connection
  3054. def release(self, connection):
  3055. "Releases the connection back to the pool."
  3056. # Make sure we haven't changed process.
  3057. self._checkpid()
  3058. try:
  3059. if self._in_maintenance:
  3060. self._lock.acquire()
  3061. self._locked = True
  3062. if not self.owns_connection(connection):
  3063. # pool doesn't own this connection. do not add it back
  3064. # to the pool. instead add a None value which is a placeholder
  3065. # that will cause the pool to recreate the connection if
  3066. # its needed.
  3067. connection.disconnect()
  3068. self.pool.put_nowait(None)
  3069. # Still need to decrement USED since it was counted in get_connection()
  3070. record_connection_count(
  3071. pool_name="unknown_pool",
  3072. connection_state=ConnectionState.USED,
  3073. counter=-1,
  3074. )
  3075. return
  3076. if connection.should_reconnect():
  3077. connection.disconnect()
  3078. # Put the connection back into the pool.
  3079. pool_name = get_pool_name(self)
  3080. try:
  3081. self.pool.put_nowait(connection)
  3082. # Record state transition: USED -> IDLE
  3083. record_connection_count(
  3084. pool_name=pool_name,
  3085. connection_state=ConnectionState.USED,
  3086. counter=-1,
  3087. )
  3088. record_connection_count(
  3089. pool_name=pool_name,
  3090. connection_state=ConnectionState.IDLE,
  3091. counter=1,
  3092. )
  3093. except Full:
  3094. pass
  3095. finally:
  3096. if self._locked:
  3097. try:
  3098. self._lock.release()
  3099. except Exception:
  3100. pass
  3101. self._locked = False
  3102. def disconnect(self, inuse_connections: bool = True):
  3103. """
  3104. Disconnects either all connections in the pool or just the free connections.
  3105. """
  3106. self._checkpid()
  3107. try:
  3108. if self._in_maintenance:
  3109. self._lock.acquire()
  3110. self._locked = True
  3111. if inuse_connections:
  3112. connections = self._connections
  3113. else:
  3114. connections = self._get_free_connections()
  3115. for connection in connections:
  3116. connection.disconnect()
  3117. finally:
  3118. if self._locked:
  3119. try:
  3120. self._lock.release()
  3121. except Exception:
  3122. pass
  3123. self._locked = False
  3124. def _get_free_connections(self):
  3125. with self._lock:
  3126. return {conn for conn in self.pool.queue if conn}
  3127. def _get_in_use_connections(self):
  3128. with self._lock:
  3129. # free connections
  3130. connections_in_queue = {conn for conn in self.pool.queue if conn}
  3131. # in self._connections we keep all created connections
  3132. # so the ones that are not in the queue are the in use ones
  3133. return {
  3134. conn for conn in self._connections if conn not in connections_in_queue
  3135. }
  3136. def set_in_maintenance(self, in_maintenance: bool):
  3137. """
  3138. Sets a flag that this Blocking ConnectionPool is in maintenance mode.
  3139. This is used to prevent new connections from being created while we are in maintenance mode.
  3140. The pool will be in maintenance mode only when we are processing a MOVING notification.
  3141. """
  3142. self._in_maintenance = in_maintenance