Add Unix socket support for Redis connections (#15644)

Adds a new configuration setting to connect to Redis via a Unix
socket instead of over TCP. Disabled by default.
This commit is contained in:
Jason Little 2023-05-26 14:28:39 -05:00 committed by GitHub
parent 50918c4940
commit c835befd10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 100 additions and 23 deletions

View File

@ -0,0 +1 @@
Add Unix socket support for Redis connections. Contributed by Jason Little.

View File

@ -3979,6 +3979,8 @@ This setting has the following sub-options:
* `enabled`: whether to use Redis support. Defaults to false. * `enabled`: whether to use Redis support. Defaults to false.
* `host` and `port`: Optional host and port to use to connect to redis. Defaults to * `host` and `port`: Optional host and port to use to connect to redis. Defaults to
localhost and 6379 localhost and 6379
* `path`: The full path to a local Unix socket file. **If this is used, `host` and
`port` are ignored.** Defaults to `/tmp/redis.sock'
* `password`: Optional password if configured on the Redis instance. * `password`: Optional password if configured on the Redis instance.
* `dbid`: Optional redis dbid if needs to connect to specific redis logical db. * `dbid`: Optional redis dbid if needs to connect to specific redis logical db.
* `use_tls`: Whether to use tls connection. Defaults to false. * `use_tls`: Whether to use tls connection. Defaults to false.
@ -3991,6 +3993,8 @@ This setting has the following sub-options:
_Changed in Synapse 1.84.0: Added use\_tls, certificate\_file, private\_key\_file, ca\_file and ca\_path attributes_ _Changed in Synapse 1.84.0: Added use\_tls, certificate\_file, private\_key\_file, ca\_file and ca\_path attributes_
_Changed in Synapse 1.85.0: Added path option to use a local Unix socket_
Example configuration: Example configuration:
```yaml ```yaml
redis: redis:

View File

@ -61,6 +61,9 @@ def lazyConnection(
# most methods to it via ConnectionHandler.__getattr__. # most methods to it via ConnectionHandler.__getattr__.
class ConnectionHandler(RedisProtocol): class ConnectionHandler(RedisProtocol):
def disconnect(self) -> "Deferred[None]": ... def disconnect(self) -> "Deferred[None]": ...
def __repr__(self) -> str: ...
class UnixConnectionHandler(ConnectionHandler): ...
class RedisFactory(protocol.ReconnectingClientFactory): class RedisFactory(protocol.ReconnectingClientFactory):
continueTrying: bool continueTrying: bool

View File

@ -33,6 +33,7 @@ class RedisConfig(Config):
self.redis_host = redis_config.get("host", "localhost") self.redis_host = redis_config.get("host", "localhost")
self.redis_port = redis_config.get("port", 6379) self.redis_port = redis_config.get("port", 6379)
self.redis_path = redis_config.get("path", None)
self.redis_dbid = redis_config.get("dbid", None) self.redis_dbid = redis_config.get("dbid", None)
self.redis_password = redis_config.get("password") self.redis_password = redis_config.get("password")

View File

@ -352,7 +352,15 @@ class ReplicationCommandHandler:
reactor = hs.get_reactor() reactor = hs.get_reactor()
redis_config = hs.config.redis redis_config = hs.config.redis
if hs.config.redis.redis_use_tls: if redis_config.redis_path is not None:
reactor.connectUNIX(
redis_config.redis_path,
self._factory,
timeout=30,
checkPID=False,
)
elif hs.config.redis.redis_use_tls:
ssl_context_factory = ClientContextFactory(hs.config.redis) ssl_context_factory = ClientContextFactory(hs.config.redis)
reactor.connectSSL( reactor.connectSSL(
redis_config.redis_host, redis_config.redis_host,

View File

@ -17,7 +17,12 @@ from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Generic, List, Optional, Type, TypeVar, cast from typing import TYPE_CHECKING, Any, Generic, List, Optional, Type, TypeVar, cast
import attr import attr
import txredisapi from txredisapi import (
ConnectionHandler,
RedisFactory,
SubscriberProtocol,
UnixConnectionHandler,
)
from zope.interface import implementer from zope.interface import implementer
from twisted.internet.address import IPv4Address, IPv6Address from twisted.internet.address import IPv4Address, IPv6Address
@ -68,7 +73,7 @@ class ConstantProperty(Generic[T, V]):
@implementer(IReplicationConnection) @implementer(IReplicationConnection)
class RedisSubscriber(txredisapi.SubscriberProtocol): class RedisSubscriber(SubscriberProtocol):
"""Connection to redis subscribed to replication stream. """Connection to redis subscribed to replication stream.
This class fulfils two functions: This class fulfils two functions:
@ -95,7 +100,7 @@ class RedisSubscriber(txredisapi.SubscriberProtocol):
synapse_handler: "ReplicationCommandHandler" synapse_handler: "ReplicationCommandHandler"
synapse_stream_prefix: str synapse_stream_prefix: str
synapse_channel_names: List[str] synapse_channel_names: List[str]
synapse_outbound_redis_connection: txredisapi.ConnectionHandler synapse_outbound_redis_connection: ConnectionHandler
def __init__(self, *args: Any, **kwargs: Any): def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -229,7 +234,7 @@ class RedisSubscriber(txredisapi.SubscriberProtocol):
) )
class SynapseRedisFactory(txredisapi.RedisFactory): class SynapseRedisFactory(RedisFactory):
"""A subclass of RedisFactory that periodically sends pings to ensure that """A subclass of RedisFactory that periodically sends pings to ensure that
we detect dead connections. we detect dead connections.
""" """
@ -245,7 +250,7 @@ class SynapseRedisFactory(txredisapi.RedisFactory):
dbid: Optional[int], dbid: Optional[int],
poolsize: int, poolsize: int,
isLazy: bool = False, isLazy: bool = False,
handler: Type = txredisapi.ConnectionHandler, handler: Type = ConnectionHandler,
charset: str = "utf-8", charset: str = "utf-8",
password: Optional[str] = None, password: Optional[str] = None,
replyTimeout: int = 30, replyTimeout: int = 30,
@ -326,7 +331,7 @@ class RedisDirectTcpReplicationClientFactory(SynapseRedisFactory):
def __init__( def __init__(
self, self,
hs: "HomeServer", hs: "HomeServer",
outbound_redis_connection: txredisapi.ConnectionHandler, outbound_redis_connection: ConnectionHandler,
channel_names: List[str], channel_names: List[str],
): ):
super().__init__( super().__init__(
@ -368,7 +373,7 @@ def lazyConnection(
reconnect: bool = True, reconnect: bool = True,
password: Optional[str] = None, password: Optional[str] = None,
replyTimeout: int = 30, replyTimeout: int = 30,
) -> txredisapi.ConnectionHandler: ) -> ConnectionHandler:
"""Creates a connection to Redis that is lazily set up and reconnects if the """Creates a connection to Redis that is lazily set up and reconnects if the
connections is lost. connections is lost.
""" """
@ -380,7 +385,7 @@ def lazyConnection(
dbid=dbid, dbid=dbid,
poolsize=1, poolsize=1,
isLazy=True, isLazy=True,
handler=txredisapi.ConnectionHandler, handler=ConnectionHandler,
password=password, password=password,
replyTimeout=replyTimeout, replyTimeout=replyTimeout,
) )
@ -408,3 +413,44 @@ def lazyConnection(
) )
return factory.handler return factory.handler
def lazyUnixConnection(
hs: "HomeServer",
path: str = "/tmp/redis.sock",
dbid: Optional[int] = None,
reconnect: bool = True,
password: Optional[str] = None,
replyTimeout: int = 30,
) -> ConnectionHandler:
"""Creates a connection to Redis that is lazily set up and reconnects if the
connection is lost.
Returns:
A subclass of ConnectionHandler, which is a UnixConnectionHandler in this case.
"""
uuid = path
factory = SynapseRedisFactory(
hs,
uuid=uuid,
dbid=dbid,
poolsize=1,
isLazy=True,
handler=UnixConnectionHandler,
password=password,
replyTimeout=replyTimeout,
)
factory.continueTrying = reconnect
reactor = hs.get_reactor()
reactor.connectUNIX(
path,
factory,
timeout=30,
checkPID=False,
)
return factory.handler

View File

@ -864,22 +864,36 @@ class HomeServer(metaclass=abc.ABCMeta):
# We only want to import redis module if we're using it, as we have # We only want to import redis module if we're using it, as we have
# `txredisapi` as an optional dependency. # `txredisapi` as an optional dependency.
from synapse.replication.tcp.redis import lazyConnection from synapse.replication.tcp.redis import lazyConnection, lazyUnixConnection
logger.info( if self.config.redis.redis_path is None:
"Connecting to redis (host=%r port=%r) for external cache", logger.info(
self.config.redis.redis_host, "Connecting to redis (host=%r port=%r) for external cache",
self.config.redis.redis_port, self.config.redis.redis_host,
) self.config.redis.redis_port,
)
return lazyConnection( return lazyConnection(
hs=self, hs=self,
host=self.config.redis.redis_host, host=self.config.redis.redis_host,
port=self.config.redis.redis_port, port=self.config.redis.redis_port,
dbid=self.config.redis.redis_dbid, dbid=self.config.redis.redis_dbid,
password=self.config.redis.redis_password, password=self.config.redis.redis_password,
reconnect=True, reconnect=True,
) )
else:
logger.info(
"Connecting to redis (path=%r) for external cache",
self.config.redis.redis_path,
)
return lazyUnixConnection(
hs=self,
path=self.config.redis.redis_path,
dbid=self.config.redis.redis_dbid,
password=self.config.redis.redis_password,
reconnect=True,
)
def should_send_federation(self) -> bool: def should_send_federation(self) -> bool:
"Should this server be sending federation traffic directly?" "Should this server be sending federation traffic directly?"