Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

ext_redis.py 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import functools
  2. import logging
  3. import ssl
  4. from collections.abc import Callable
  5. from datetime import timedelta
  6. from typing import TYPE_CHECKING, Any, Optional, Union
  7. import redis
  8. from redis import RedisError
  9. from redis.cache import CacheConfig
  10. from redis.cluster import ClusterNode, RedisCluster
  11. from redis.connection import Connection, SSLConnection
  12. from redis.lock import Lock
  13. from redis.sentinel import Sentinel
  14. from configs import dify_config
  15. from dify_app import DifyApp
  16. if TYPE_CHECKING:
  17. from redis.lock import Lock
  18. logger = logging.getLogger(__name__)
  19. class RedisClientWrapper:
  20. """
  21. A wrapper class for the Redis client that addresses the issue where the global
  22. `redis_client` variable cannot be updated when a new Redis instance is returned
  23. by Sentinel.
  24. This class allows for deferred initialization of the Redis client, enabling the
  25. client to be re-initialized with a new instance when necessary. This is particularly
  26. useful in scenarios where the Redis instance may change dynamically, such as during
  27. a failover in a Sentinel-managed Redis setup.
  28. Attributes:
  29. _client: The actual Redis client instance. It remains None until
  30. initialized with the `initialize` method.
  31. Methods:
  32. initialize(client): Initializes the Redis client if it hasn't been initialized already.
  33. __getattr__(item): Delegates attribute access to the Redis client, raising an error
  34. if the client is not initialized.
  35. """
  36. _client: Union[redis.Redis, RedisCluster, None]
  37. def __init__(self) -> None:
  38. self._client = None
  39. def initialize(self, client: Union[redis.Redis, RedisCluster]) -> None:
  40. if self._client is None:
  41. self._client = client
  42. if TYPE_CHECKING:
  43. # Type hints for IDE support and static analysis
  44. # These are not executed at runtime but provide type information
  45. def get(self, name: str | bytes) -> Any: ...
  46. def set(
  47. self,
  48. name: str | bytes,
  49. value: Any,
  50. ex: int | None = None,
  51. px: int | None = None,
  52. nx: bool = False,
  53. xx: bool = False,
  54. keepttl: bool = False,
  55. get: bool = False,
  56. exat: int | None = None,
  57. pxat: int | None = None,
  58. ) -> Any: ...
  59. def setex(self, name: str | bytes, time: int | timedelta, value: Any) -> Any: ...
  60. def setnx(self, name: str | bytes, value: Any) -> Any: ...
  61. def delete(self, *names: str | bytes) -> Any: ...
  62. def incr(self, name: str | bytes, amount: int = 1) -> Any: ...
  63. def expire(
  64. self,
  65. name: str | bytes,
  66. time: int | timedelta,
  67. nx: bool = False,
  68. xx: bool = False,
  69. gt: bool = False,
  70. lt: bool = False,
  71. ) -> Any: ...
  72. def lock(
  73. self,
  74. name: str,
  75. timeout: float | None = None,
  76. sleep: float = 0.1,
  77. blocking: bool = True,
  78. blocking_timeout: float | None = None,
  79. thread_local: bool = True,
  80. ) -> Lock: ...
  81. def zadd(
  82. self,
  83. name: str | bytes,
  84. mapping: dict[str | bytes | int | float, float | int | str | bytes],
  85. nx: bool = False,
  86. xx: bool = False,
  87. ch: bool = False,
  88. incr: bool = False,
  89. gt: bool = False,
  90. lt: bool = False,
  91. ) -> Any: ...
  92. def zremrangebyscore(self, name: str | bytes, min: float | str, max: float | str) -> Any: ...
  93. def zcard(self, name: str | bytes) -> Any: ...
  94. def getdel(self, name: str | bytes) -> Any: ...
  95. def __getattr__(self, item: str) -> Any:
  96. if self._client is None:
  97. raise RuntimeError("Redis client is not initialized. Call init_app first.")
  98. return getattr(self._client, item)
  99. redis_client: RedisClientWrapper = RedisClientWrapper()
  100. def _get_ssl_configuration() -> tuple[type[Union[Connection, SSLConnection]], dict[str, Any]]:
  101. """Get SSL configuration for Redis connection."""
  102. if not dify_config.REDIS_USE_SSL:
  103. return Connection, {}
  104. cert_reqs_map = {
  105. "CERT_NONE": ssl.CERT_NONE,
  106. "CERT_OPTIONAL": ssl.CERT_OPTIONAL,
  107. "CERT_REQUIRED": ssl.CERT_REQUIRED,
  108. }
  109. ssl_cert_reqs = cert_reqs_map.get(dify_config.REDIS_SSL_CERT_REQS, ssl.CERT_NONE)
  110. ssl_kwargs = {
  111. "ssl_cert_reqs": ssl_cert_reqs,
  112. "ssl_ca_certs": dify_config.REDIS_SSL_CA_CERTS,
  113. "ssl_certfile": dify_config.REDIS_SSL_CERTFILE,
  114. "ssl_keyfile": dify_config.REDIS_SSL_KEYFILE,
  115. }
  116. return SSLConnection, ssl_kwargs
  117. def _get_cache_configuration() -> CacheConfig | None:
  118. """Get client-side cache configuration if enabled."""
  119. if not dify_config.REDIS_ENABLE_CLIENT_SIDE_CACHE:
  120. return None
  121. resp_protocol = dify_config.REDIS_SERIALIZATION_PROTOCOL
  122. if resp_protocol < 3:
  123. raise ValueError("Client side cache is only supported in RESP3")
  124. return CacheConfig()
  125. def _get_base_redis_params() -> dict[str, Any]:
  126. """Get base Redis connection parameters."""
  127. return {
  128. "username": dify_config.REDIS_USERNAME,
  129. "password": dify_config.REDIS_PASSWORD or None,
  130. "db": dify_config.REDIS_DB,
  131. "encoding": "utf-8",
  132. "encoding_errors": "strict",
  133. "decode_responses": False,
  134. "protocol": dify_config.REDIS_SERIALIZATION_PROTOCOL,
  135. "cache_config": _get_cache_configuration(),
  136. }
  137. def _create_sentinel_client(redis_params: dict[str, Any]) -> Union[redis.Redis, RedisCluster]:
  138. """Create Redis client using Sentinel configuration."""
  139. if not dify_config.REDIS_SENTINELS:
  140. raise ValueError("REDIS_SENTINELS must be set when REDIS_USE_SENTINEL is True")
  141. if not dify_config.REDIS_SENTINEL_SERVICE_NAME:
  142. raise ValueError("REDIS_SENTINEL_SERVICE_NAME must be set when REDIS_USE_SENTINEL is True")
  143. sentinel_hosts = [(node.split(":")[0], int(node.split(":")[1])) for node in dify_config.REDIS_SENTINELS.split(",")]
  144. sentinel = Sentinel(
  145. sentinel_hosts,
  146. sentinel_kwargs={
  147. "socket_timeout": dify_config.REDIS_SENTINEL_SOCKET_TIMEOUT,
  148. "username": dify_config.REDIS_SENTINEL_USERNAME,
  149. "password": dify_config.REDIS_SENTINEL_PASSWORD,
  150. },
  151. )
  152. master: redis.Redis = sentinel.master_for(dify_config.REDIS_SENTINEL_SERVICE_NAME, **redis_params)
  153. return master
  154. def _create_cluster_client() -> Union[redis.Redis, RedisCluster]:
  155. """Create Redis cluster client."""
  156. if not dify_config.REDIS_CLUSTERS:
  157. raise ValueError("REDIS_CLUSTERS must be set when REDIS_USE_CLUSTERS is True")
  158. nodes = [
  159. ClusterNode(host=node.split(":")[0], port=int(node.split(":")[1]))
  160. for node in dify_config.REDIS_CLUSTERS.split(",")
  161. ]
  162. cluster: RedisCluster = RedisCluster(
  163. startup_nodes=nodes,
  164. password=dify_config.REDIS_CLUSTERS_PASSWORD,
  165. protocol=dify_config.REDIS_SERIALIZATION_PROTOCOL,
  166. cache_config=_get_cache_configuration(),
  167. )
  168. return cluster
  169. def _create_standalone_client(redis_params: dict[str, Any]) -> Union[redis.Redis, RedisCluster]:
  170. """Create standalone Redis client."""
  171. connection_class, ssl_kwargs = _get_ssl_configuration()
  172. redis_params.update(
  173. {
  174. "host": dify_config.REDIS_HOST,
  175. "port": dify_config.REDIS_PORT,
  176. "connection_class": connection_class,
  177. }
  178. )
  179. if ssl_kwargs:
  180. redis_params.update(ssl_kwargs)
  181. pool = redis.ConnectionPool(**redis_params)
  182. client: redis.Redis = redis.Redis(connection_pool=pool)
  183. return client
  184. def init_app(app: DifyApp):
  185. """Initialize Redis client and attach it to the app."""
  186. global redis_client
  187. # Determine Redis mode and create appropriate client
  188. if dify_config.REDIS_USE_SENTINEL:
  189. redis_params = _get_base_redis_params()
  190. client = _create_sentinel_client(redis_params)
  191. elif dify_config.REDIS_USE_CLUSTERS:
  192. client = _create_cluster_client()
  193. else:
  194. redis_params = _get_base_redis_params()
  195. client = _create_standalone_client(redis_params)
  196. # Initialize the wrapper and attach to app
  197. redis_client.initialize(client)
  198. app.extensions["redis"] = redis_client
  199. def redis_fallback(default_return: Optional[Any] = None):
  200. """
  201. decorator to handle Redis operation exceptions and return a default value when Redis is unavailable.
  202. Args:
  203. default_return: The value to return when a Redis operation fails. Defaults to None.
  204. """
  205. def decorator(func: Callable):
  206. @functools.wraps(func)
  207. def wrapper(*args, **kwargs):
  208. try:
  209. return func(*args, **kwargs)
  210. except RedisError as e:
  211. logger.warning("Redis operation failed in %s: %s", func.__name__, str(e), exc_info=True)
  212. return default_return
  213. return wrapper
  214. return decorator