userver: en/testsuite/databases/redis/service.py Source File
Loading...
Searching...
No Matches
service.py
1import os
2import pathlib
3import socket
4import typing
5import warnings
6
7from testsuite.environment import service, utils
8
9from . import genredis
10
11DEFAULT_MASTER_PORTS = (16379, 16389)
12DEFAULT_SENTINEL_PORT = 26379
13DEFAULT_SLAVE_PORTS = (16380, 16390, 16381)
14DEFAULT_CLUSTER_PORTS = (17380, 17381, 17382, 17383, 17384, 17385)
15DEFAULT_CLUSTER_REPLICAS = 1
16DEFAULT_STANDALONE_PORT = 7000
17
18SERVICE_SCRIPT_PATH = pathlib.Path(__file__).parent.joinpath(
19 'scripts/service-redis',
20)
21CLUSTER_SERVICE_SCRIPT_PATH = pathlib.Path(__file__).parent.joinpath(
22 'scripts/service-cluster-redis',
23)
24STANDALONE_SERVICE_SCRIPT_PATH = pathlib.Path(__file__).parent.joinpath(
25 'scripts/service-standalone-redis',
26)
27
28
29class BaseError(Exception):
30 pass
31
32
34 pass
35
36
37class ServiceSettings(typing.NamedTuple):
38 host: str
39 master_ports: tuple[int, ...]
40 sentinel_port: int
41 slave_ports: tuple[int, ...]
42
43 def validate(self):
44 if len(self.master_ports) != len(DEFAULT_MASTER_PORTS):
45 raise NotEnoughPorts(
46 f'Need exactly {len(DEFAULT_MASTER_PORTS)} masters!',
47 )
48 if len(self.slave_ports) != len(DEFAULT_SLAVE_PORTS):
49 raise NotEnoughPorts(
50 f'Need exactly {len(DEFAULT_SLAVE_PORTS)} slaves!',
51 )
52
53
54class ClusterServiceSettings(typing.NamedTuple):
55 host: str
56 cluster_ports: tuple[int, ...]
57 cluster_replicas: int
58
59 def validate(self):
60 if len(self.cluster_ports) % (self.cluster_replicas + 1) != 0:
61 raise NotEnoughPorts(
62 f'Number of nodes does not match number of replicas ({self.cluster_replicas})',
63 )
64 min_masters = (self.cluster_replicas + 1) * 3
65 if len(self.cluster_ports) < min_masters:
66 raise NotEnoughPorts(
67 f'Need at least {min_masters} cluster nodes!',
68 )
69
70
71class StandaloneServiceSettings(typing.NamedTuple):
72 host: str
73 port: int
74
75
76def get_service_settings():
77 return ServiceSettings(
78 host=_get_hostname(),
79 master_ports=utils.getenv_ints(
80 key='TESTSUITE_REDIS_MASTER_PORTS',
81 default=DEFAULT_MASTER_PORTS,
82 ),
83 sentinel_port=utils.getenv_int(
84 key='TESTSUITE_REDIS_SENTINEL_PORT',
85 default=DEFAULT_SENTINEL_PORT,
86 ),
87 slave_ports=utils.getenv_ints(
88 key='TESTSUITE_REDIS_SLAVE_PORTS',
89 default=DEFAULT_SLAVE_PORTS,
90 ),
91 )
92
93
94def get_cluster_service_settings():
96 host=_get_hostname(),
97 cluster_ports=utils.getenv_ints(
98 key='TESTSUITE_REDIS_CLUSTER_PORTS',
99 default=DEFAULT_CLUSTER_PORTS,
100 ),
101 cluster_replicas=utils.getenv_int(
102 key='TESTSUITE_REDIS_CLUSTER_REPLICAS',
103 default=DEFAULT_CLUSTER_REPLICAS,
104 ),
105 )
106
107
108def get_standalone_service_settings():
110 host=_get_hostname(),
111 port=utils.getenv_int(
112 key='TESTSUITE_REDIS_STANDALONE_PORT',
113 default=DEFAULT_STANDALONE_PORT,
114 ),
115 )
116
117
118def create_redis_service(
119 service_name,
120 working_dir,
121 settings: ServiceSettings | None = None,
122 env=None,
123):
124 if settings is None:
125 settings = get_service_settings()
126 configs_dir = pathlib.Path(working_dir).joinpath('configs')
127 check_ports = [
128 settings.sentinel_port,
129 *settings.master_ports,
130 *settings.slave_ports,
131 ]
132
133 def prestart_hook():
134 configs_dir.mkdir(parents=True, exist_ok=True)
135 settings.validate()
136 genredis.generate_redis_configs(
137 output_path=configs_dir,
138 host=settings.host,
139 master0_port=settings.master_ports[0],
140 master1_port=settings.master_ports[1],
141 slave0_port=settings.slave_ports[0],
142 slave1_port=settings.slave_ports[1],
143 slave2_port=settings.slave_ports[2],
144 sentinel_port=settings.sentinel_port,
145 )
146
148 service_name=service_name,
149 script_path=str(SERVICE_SCRIPT_PATH),
150 working_dir=working_dir,
151 environment={
152 'REDIS_TMPDIR': working_dir,
153 'REDIS_CONFIGS_DIR': str(configs_dir),
154 'REDIS_SENTINEL_PORT': str(settings.sentinel_port),
155 'REDIS_SENTINEL_HOST': settings.host,
156 **(env or {}),
157 },
158 check_host=settings.host,
159 check_ports=check_ports,
160 prestart_hook=prestart_hook,
161 )
162
163
164def create_cluster_redis_service(
165 service_name,
166 working_dir,
167 settings: ClusterServiceSettings | None = None,
168 env=None,
169):
170 if settings is None:
171 settings = get_cluster_service_settings()
172 configs_dir = pathlib.Path(working_dir).joinpath('configs')
173 check_ports = [
174 *settings.cluster_ports,
175 ]
176
177 def prestart_hook():
178 configs_dir.mkdir(parents=True, exist_ok=True)
179 settings.validate()
180 genredis.generate_cluster_redis_configs(
181 output_path=configs_dir,
182 host=settings.host,
183 cluster_ports=settings.cluster_ports,
184 )
185
187 service_name=service_name,
188 script_path=str(CLUSTER_SERVICE_SCRIPT_PATH),
189 working_dir=working_dir,
190 environment={
191 'REDIS_TMPDIR': working_dir,
192 'REDIS_CONFIGS_DIR': str(configs_dir),
193 'REDIS_HOST': settings.host,
194 'REDIS_CLUSTER_PORTS': ' '.join(
195 [str(port) for port in settings.cluster_ports]
196 ),
197 'REDIS_CLUSTER_REPLICAS': str(settings.cluster_replicas),
198 **(env or {}),
199 },
200 check_host=settings.host,
201 check_ports=check_ports,
202 prestart_hook=prestart_hook,
203 )
204
205
206def create_standalone_redis_service(
207 service_name,
208 working_dir,
209 settings: StandaloneServiceSettings | None = None,
210 env=None,
211):
212 if settings is None:
213 settings = get_standalone_service_settings()
214 configs_dir = pathlib.Path(working_dir).joinpath('configs')
215
216 def prestart_hook():
217 configs_dir.mkdir(parents=True, exist_ok=True)
218 genredis.generate_standalone_redis_config(
219 output_path=configs_dir,
220 host=settings.host,
221 port=settings.port,
222 )
223
225 service_name=service_name,
226 script_path=str(STANDALONE_SERVICE_SCRIPT_PATH),
227 working_dir=working_dir,
228 environment={
229 'REDIS_CONFIGS_DIR': str(configs_dir),
230 **(env or {}),
231 },
232 check_host=settings.host,
233 check_ports=[settings.port],
234 prestart_hook=prestart_hook,
235 )
236
237
238def _get_hostname():
239 hostname = 'localhost'
240 for var in ('TESTSUITE_REDIS_HOSTNAME', 'HOSTNAME'):
241 if var in os.environ:
242 hostname = os.environ[var]
243 break
244 return _resolve_hostname(hostname)
245
246
247def _resolve_hostname(hostname: str) -> str:
248 for family in socket.AF_INET6, socket.AF_INET:
249 try:
250 result = socket.getaddrinfo(
251 hostname,
252 None,
253 family=family,
254 type=socket.SOCK_STREAM,
255 )
256 except OSError:
257 continue
258 if result:
259 return result[0][4][0] # type: ignore[return-value]
260 warnings.warn(f'Failed to resolve hostname {hostname}')
261 return hostname