userver: en/testsuite/daemons/service_daemon.py Source File
Loading...
Searching...
No Matches
service_daemon.py
1# pylint: disable=not-async-context-manager
2
3
4import asyncio
5import contextlib
6import os
7import signal
8import subprocess
9import time
10from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence
11from typing import (
12 AsyncGenerator,
13 Awaitable,
14 Callable,
15)
16
17import aiohttp
18
19from testsuite.daemons import spawn
20from testsuite.daemons.spawn import __tracebackhide__ # noqa: F401
21
22POLL_RETRIES = 2000
23PING_REQUEST_TIMEOUT = 1.0
24PING_RESPONSE_CODES = (200,)
25
26HealthCheckType = Callable[..., Awaitable[bool]]
27
28ClientSessionFactory = Callable[..., aiohttp.ClientSession]
29
30
31@contextlib.asynccontextmanager
32async def start(
33 args: Sequence[str],
34 *,
35 health_check: HealthCheckType,
36 session_factory: ClientSessionFactory = aiohttp.ClientSession,
37 env: dict[str, str] | None = None,
38 shutdown_signal: int = signal.SIGINT,
39 shutdown_timeout: float = 120,
40 poll_retries: int = POLL_RETRIES,
41 subprocess_options=None,
42 setup_service=None,
43 subprocess_spawner=None,
44 stdout_handler=None,
45 stderr_handler=None,
46) -> AsyncGenerator[subprocess.Popen | None, None]:
47 async with session_factory() as session:
48 async with _service_daemon(
49 args=args,
50 env=env,
51 shutdown_signal=shutdown_signal,
52 shutdown_timeout=shutdown_timeout,
53 poll_retries=poll_retries,
54 subprocess_options=subprocess_options,
55 setup_service=setup_service,
56 subprocess_spawner=subprocess_spawner,
57 health_check=health_check,
58 session=session,
59 stdout_handler=stdout_handler,
60 stderr_handler=stderr_handler,
61 ) as process:
62 yield process
63
64
65async def service_wait(
66 args: Sequence[str],
67 *,
68 health_check: HealthCheckType,
69 session_factory: ClientSessionFactory = aiohttp.ClientSession,
70 reporter,
71):
72 process = None
73 flush_supported = hasattr(reporter, 'flush')
74 async with session_factory() as session:
75 if not await _run_health_check(
76 health_check,
77 session=session,
78 process=process,
79 ):
80 command = ' '.join(args)
81 reporter.write_line('')
82 reporter.write_line(
83 'Service is not running yet you may want to start it from '
84 'outside of testsuite, e.g. using gdb:',
85 yellow=True,
86 )
87 reporter.write_line('')
88 reporter.write_line(f'gdb --args {command}', green=True)
89 reporter.write_line('')
90 reporter.write('Waiting for service to start...')
91 while not await _run_health_check(
92 health_check,
93 session=session,
94 process=process,
95 sleep=0.2,
96 ):
97 reporter.write('.')
98 if flush_supported:
99 reporter.flush()
100 reporter.write_line('')
101
102
103@contextlib.asynccontextmanager
104async def start_dummy_process():
105 yield None
106
107
108def make_health_check(
109 *,
110 health_check: HealthCheckType | None = None,
111 ping_url: str | None,
112 ping_request_timeout: float = PING_REQUEST_TIMEOUT,
113 ping_response_codes: tuple[int] = PING_RESPONSE_CODES,
114) -> HealthCheckType:
115 if ping_url:
116 return _make_ping_health_check(
117 ping_url=ping_url,
118 ping_request_timeout=ping_request_timeout,
119 ping_response_codes=ping_response_codes,
120 )
121 if health_check:
122 return health_check
123
124 raise RuntimeError('Either `ping_url` or `health_check` must be set')
125
126
127async def _run_health_check(
128 health_check: HealthCheckType,
129 *,
130 session: aiohttp.ClientSession,
131 process: subprocess.Popen | None,
132 sleep: float = 0.05,
133):
134 if process and process.poll() is not None:
135 raise spawn.HealthCheckError('Process already finished')
136
137 begin = time.perf_counter()
138 if await health_check(session=session, process=process):
139 return True
140 end = time.perf_counter()
141 to_sleep = begin + sleep - end
142 if to_sleep > 0:
143 await asyncio.sleep(to_sleep)
144 return False
145
146
147def _make_ping_health_check(
148 *,
149 ping_url: str,
150 ping_request_timeout: float,
151 ping_response_codes: tuple[int],
152) -> HealthCheckType:
153 async def ping_health_check(
154 session: aiohttp.ClientSession,
155 process: subprocess.Popen | None,
156 ) -> bool:
157 try:
158 response = await session.get(
159 ping_url,
160 timeout=ping_request_timeout, # type: ignore[arg-type]
161 )
162 if response.status in ping_response_codes:
163 return True
164 except asyncio.TimeoutError:
165 return False # skip sleep as we've waited enough
166 except aiohttp.ClientConnectorError:
167 pass
168 return False
169
170 return ping_health_check
171
172
173async def _service_wait(
174 process: subprocess.Popen | None,
175 *,
176 poll_retries: int,
177 health_check: HealthCheckType,
178 session: aiohttp.ClientSession,
179) -> bool:
180 for _ in range(poll_retries):
181 if await _run_health_check(
182 health_check,
183 session=session,
184 process=process,
185 ):
186 return True
187 raise spawn.HealthCheckError('service daemon is not ready')
188
189
190def _prepare_env(*envs: dict[str, str] | None) -> dict[str, str]:
191 result = os.environ.copy()
192 for env in envs:
193 if env is not None:
194 result.update(env)
195 asan_preload = os.getenv('ASAN_PRELOAD')
196 if asan_preload is not None:
197 result['LD_PRELOAD'] = asan_preload
198 return result
199
200
201@contextlib.asynccontextmanager
202async def _service_daemon(
203 args: Sequence[str],
204 *,
205 env: dict[str, str] | None,
206 shutdown_signal: int,
207 shutdown_timeout: float,
208 poll_retries: int,
209 subprocess_options=None,
210 setup_service=None,
211 subprocess_spawner=None,
212 health_check,
213 session: aiohttp.ClientSession,
214 stdout_handler=None,
215 stderr_handler=None,
216) -> AsyncGenerator[subprocess.Popen, None]:
217 options = subprocess_options.copy() if subprocess_options else {}
218 options['env'] = _prepare_env(env, options.get('env'))
219 async with spawn.spawned(
220 args,
221 shutdown_signal=shutdown_signal,
222 shutdown_timeout=shutdown_timeout,
223 subprocess_spawner=subprocess_spawner,
224 stdout_handler=stdout_handler,
225 stderr_handler=stderr_handler,
226 **options,
227 ) as process:
228 if setup_service is not None:
229 setup_service(process)
230 await _service_wait(
231 process=process,
232 poll_retries=poll_retries,
233 health_check=health_check,
234 session=session,
235 )
236 yield process