tigrcorn-runtime 0.3.16.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrcorn_runtime/__init__.py +20 -0
- tigrcorn_runtime/api.py +351 -0
- tigrcorn_runtime/app_interfaces.py +105 -0
- tigrcorn_runtime/cli.py +191 -0
- tigrcorn_runtime/embedded.py +77 -0
- tigrcorn_runtime/py.typed +1 -0
- tigrcorn_runtime/server/__init__.py +9 -0
- tigrcorn_runtime/server/app_loader.py +42 -0
- tigrcorn_runtime/server/bootstrap.py +225 -0
- tigrcorn_runtime/server/hooks.py +24 -0
- tigrcorn_runtime/server/reloader.py +109 -0
- tigrcorn_runtime/server/runner.py +878 -0
- tigrcorn_runtime/server/shutdown.py +12 -0
- tigrcorn_runtime/server/signals.py +33 -0
- tigrcorn_runtime/server/state.py +13 -0
- tigrcorn_runtime/server/supervisor.py +110 -0
- tigrcorn_runtime/workers/__init__.py +6 -0
- tigrcorn_runtime/workers/local.py +36 -0
- tigrcorn_runtime/workers/model.py +9 -0
- tigrcorn_runtime/workers/process.py +120 -0
- tigrcorn_runtime/workers/supervisor.py +58 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/METADATA +245 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/RECORD +26 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/WHEEL +5 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
- tigrcorn_runtime-0.3.16.dev5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import random
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tigrcorn_asgi.receive import HTTPRequestReceive, HTTPStreamingRequestReceive
|
|
9
|
+
from tigrcorn_asgi.scopes.http import build_http_scope
|
|
10
|
+
from tigrcorn_asgi.send import FileBodySegment, HTTPResponseCollector, iter_response_body_segments, response_body_segments_have_bytes
|
|
11
|
+
from tigrcorn_runtime.app_interfaces import resolve_app_dispatch
|
|
12
|
+
from tigrcorn_core.errors import ProtocolError
|
|
13
|
+
from tigrcorn_config.model import ListenerConfig, ServerConfig
|
|
14
|
+
from tigrcorn_core.constants import H2_PREFACE
|
|
15
|
+
from tigrcorn_transports.listeners.inproc import InProcListener
|
|
16
|
+
from tigrcorn_transports.listeners.pipe import PipeListener
|
|
17
|
+
from tigrcorn_transports.listeners.tcp import TCPListener
|
|
18
|
+
from tigrcorn_transports.listeners.udp import UDPListener
|
|
19
|
+
from tigrcorn_transports.listeners.unix import UnixListener
|
|
20
|
+
from tigrcorn_observability.logging import AccessLogger, configure_logging, resolve_logging_config
|
|
21
|
+
from tigrcorn_observability.metrics import StatsdExporter
|
|
22
|
+
from tigrcorn_observability.tracing import OtelExporter, span
|
|
23
|
+
from tigrcorn_protocols.connect import is_connect_allowed, parse_connect_authority
|
|
24
|
+
from tigrcorn_http.alt_svc import configured_alt_svc_values
|
|
25
|
+
from tigrcorn_http.entity import apply_response_entity_semantics, plan_file_backed_response_entity_semantics
|
|
26
|
+
from tigrcorn_protocols.http1.keepalive import apply_keep_alive_policy
|
|
27
|
+
from tigrcorn_protocols.http1.parser import ParsedRequestHead, read_http11_request_head
|
|
28
|
+
from tigrcorn_protocols.http1.serializer import finalize_chunked_body, serialize_http11_response_chunk, serialize_http11_response_head, serialize_http11_response_whole
|
|
29
|
+
from tigrcorn_protocols.http2.handler import HTTP2ConnectionHandler
|
|
30
|
+
from tigrcorn_protocols.http3.handler import HTTP3DatagramHandler
|
|
31
|
+
from tigrcorn_protocols.lifespan.driver import LifespanManager
|
|
32
|
+
from tigrcorn_protocols.rawframed.handler import RawFramedApplicationHandler
|
|
33
|
+
from tigrcorn_protocols.websocket.handler import WebSocketConnectionHandler
|
|
34
|
+
from tigrcorn_protocols.scheduler import ProductionScheduler, SchedulerPolicy
|
|
35
|
+
from tigrcorn_security.tls import build_server_ssl_context, tls_extension_payload
|
|
36
|
+
from tigrcorn_runtime.server.hooks import run_async_hooks
|
|
37
|
+
from tigrcorn_runtime.server.state import ServerState
|
|
38
|
+
from tigrcorn_transports.tcp.reader import PrebufferedReader
|
|
39
|
+
from tigrcorn_core.types import ASGIApp, StreamReaderLike
|
|
40
|
+
from tigrcorn_core.utils.authority import authority_allowed
|
|
41
|
+
from tigrcorn_core.utils.headers import get_header
|
|
42
|
+
from tigrcorn_core.utils.net import peer_parts
|
|
43
|
+
from tigrcorn_core.utils.proxy import resolve_proxy_view
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TigrCornServer:
|
|
47
|
+
def __init__(self, app: ASGIApp, config: ServerConfig) -> None:
|
|
48
|
+
selection = resolve_app_dispatch(app, config.app.interface)
|
|
49
|
+
self.app = selection.app
|
|
50
|
+
self.app_interface = selection.interface
|
|
51
|
+
self.config = config
|
|
52
|
+
self._resolved_logging = resolve_logging_config(config.log_level, config=config.logging)
|
|
53
|
+
self.logger = configure_logging(config.log_level, config=config.logging)
|
|
54
|
+
self.access_logger = AccessLogger(
|
|
55
|
+
self.logger,
|
|
56
|
+
enabled=self._resolved_logging.access_log,
|
|
57
|
+
fmt=self._resolved_logging.access_log_format,
|
|
58
|
+
)
|
|
59
|
+
self.state = ServerState()
|
|
60
|
+
self.lifespan = LifespanManager(app, mode=config.lifespan)
|
|
61
|
+
self._listeners: list[TCPListener | UDPListener | UnixListener | PipeListener | InProcListener] = []
|
|
62
|
+
self._datagram_handlers: list[HTTP3DatagramHandler] = []
|
|
63
|
+
self._should_exit = asyncio.Event()
|
|
64
|
+
self._started = False
|
|
65
|
+
self._metrics_server: asyncio.AbstractServer | None = None
|
|
66
|
+
self._request_budget_task: asyncio.Task[None] | None = None
|
|
67
|
+
self._statsd_exporter = StatsdExporter(config.metrics.statsd_host, logger=self.logger) if config.metrics.statsd_host else None
|
|
68
|
+
self._otel_exporter = OtelExporter(config.metrics.otel_endpoint, logger=self.logger) if config.metrics.otel_endpoint else None
|
|
69
|
+
policy = SchedulerPolicy()
|
|
70
|
+
if config.scheduler.max_connections is not None:
|
|
71
|
+
policy.max_connections = config.scheduler.max_connections
|
|
72
|
+
if config.scheduler.max_tasks is not None:
|
|
73
|
+
policy.max_tasks = config.scheduler.max_tasks
|
|
74
|
+
if config.scheduler.max_streams is not None:
|
|
75
|
+
policy.max_streams_per_session = config.scheduler.max_streams
|
|
76
|
+
if config.scheduler.limit_concurrency is not None:
|
|
77
|
+
policy.limit_concurrency = config.scheduler.limit_concurrency
|
|
78
|
+
self.scheduler = ProductionScheduler(policy)
|
|
79
|
+
self._request_budget = None
|
|
80
|
+
if config.process.limit_max_requests is not None:
|
|
81
|
+
jitter = max(0, config.process.max_requests_jitter)
|
|
82
|
+
self._request_budget = config.process.limit_max_requests + (random.randint(0, jitter) if jitter else 0)
|
|
83
|
+
|
|
84
|
+
async def start(self) -> None:
|
|
85
|
+
if self._started:
|
|
86
|
+
return
|
|
87
|
+
with span('server.start', attrs={'listener_count': len(self.config.listeners)}, sink=self._otel_exporter.record_span if self._otel_exporter is not None else None):
|
|
88
|
+
await self.lifespan.startup()
|
|
89
|
+
await run_async_hooks(self.config.hooks.on_startup, self)
|
|
90
|
+
for listener_cfg in self.config.listeners:
|
|
91
|
+
listener = await self._make_listener(listener_cfg)
|
|
92
|
+
await listener.start(self._make_client_handler(listener_cfg))
|
|
93
|
+
self._sync_listener_bound_address(listener_cfg, listener)
|
|
94
|
+
self._listeners.append(listener)
|
|
95
|
+
self.logger.info('listening on %s', listener_cfg.label)
|
|
96
|
+
if self.config.metrics.enabled and self.config.metrics.bind:
|
|
97
|
+
self._metrics_server = await self._start_metrics_endpoint(self.config.metrics.bind)
|
|
98
|
+
if self._statsd_exporter is not None:
|
|
99
|
+
await self._statsd_exporter.start(self.state.metrics)
|
|
100
|
+
if self._otel_exporter is not None:
|
|
101
|
+
await self._otel_exporter.start(self.state.metrics)
|
|
102
|
+
if self._request_budget is not None:
|
|
103
|
+
self._request_budget_task = asyncio.create_task(self._monitor_request_budget(), name='tigrcorn-request-budget')
|
|
104
|
+
self._started = True
|
|
105
|
+
|
|
106
|
+
async def serve_forever(self) -> None:
|
|
107
|
+
await self.start()
|
|
108
|
+
try:
|
|
109
|
+
await self._should_exit.wait()
|
|
110
|
+
finally:
|
|
111
|
+
await self.close()
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _sync_listener_bound_address(cfg: ListenerConfig, listener: Any) -> None:
|
|
115
|
+
server = getattr(listener, 'server', None)
|
|
116
|
+
sockets = getattr(server, 'sockets', None) if server is not None else None
|
|
117
|
+
if sockets:
|
|
118
|
+
sockname = sockets[0].getsockname()
|
|
119
|
+
if isinstance(sockname, tuple) and len(sockname) >= 2:
|
|
120
|
+
cfg.host = str(sockname[0])
|
|
121
|
+
cfg.port = int(sockname[1])
|
|
122
|
+
return
|
|
123
|
+
if isinstance(sockname, str):
|
|
124
|
+
cfg.path = sockname
|
|
125
|
+
return
|
|
126
|
+
transport = getattr(listener, 'transport', None)
|
|
127
|
+
if transport is not None:
|
|
128
|
+
sockname = transport.get_extra_info('sockname')
|
|
129
|
+
if isinstance(sockname, tuple) and len(sockname) >= 2:
|
|
130
|
+
cfg.host = str(sockname[0])
|
|
131
|
+
cfg.port = int(sockname[1])
|
|
132
|
+
return
|
|
133
|
+
if isinstance(sockname, str):
|
|
134
|
+
cfg.path = sockname
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
def request_shutdown(self) -> None:
|
|
138
|
+
self._should_exit.set()
|
|
139
|
+
|
|
140
|
+
async def close(self) -> None:
|
|
141
|
+
if self.state.shutting_down:
|
|
142
|
+
return
|
|
143
|
+
self.state.shutting_down = True
|
|
144
|
+
with span('server.shutdown', attrs={'active_listeners': len(self._listeners)}, sink=self._otel_exporter.record_span if self._otel_exporter is not None else None):
|
|
145
|
+
if self._request_budget_task is not None:
|
|
146
|
+
self._request_budget_task.cancel()
|
|
147
|
+
with suppress(Exception):
|
|
148
|
+
await self._request_budget_task
|
|
149
|
+
if self._metrics_server is not None:
|
|
150
|
+
self._metrics_server.close()
|
|
151
|
+
with suppress(Exception):
|
|
152
|
+
await self._metrics_server.wait_closed()
|
|
153
|
+
self._metrics_server = None
|
|
154
|
+
for listener in self._listeners:
|
|
155
|
+
with suppress(Exception):
|
|
156
|
+
await listener.close()
|
|
157
|
+
self._listeners.clear()
|
|
158
|
+
for handler in self._datagram_handlers:
|
|
159
|
+
with suppress(Exception):
|
|
160
|
+
await handler.close()
|
|
161
|
+
self._datagram_handlers.clear()
|
|
162
|
+
with suppress(Exception):
|
|
163
|
+
await asyncio.wait_for(self.scheduler.close(), timeout=self.config.http.shutdown_timeout)
|
|
164
|
+
with suppress(Exception):
|
|
165
|
+
await self.lifespan.shutdown()
|
|
166
|
+
with suppress(Exception):
|
|
167
|
+
await run_async_hooks(self.config.hooks.on_shutdown, self)
|
|
168
|
+
if self._statsd_exporter is not None:
|
|
169
|
+
with suppress(Exception):
|
|
170
|
+
await self._statsd_exporter.stop(self.state.metrics)
|
|
171
|
+
if self._otel_exporter is not None:
|
|
172
|
+
with suppress(Exception):
|
|
173
|
+
await self._otel_exporter.stop(self.state.metrics)
|
|
174
|
+
|
|
175
|
+
async def _make_listener(self, cfg: ListenerConfig):
|
|
176
|
+
if cfg.kind == 'tcp':
|
|
177
|
+
ssl_ctx = build_server_ssl_context(cfg)
|
|
178
|
+
return TCPListener(
|
|
179
|
+
cfg.host,
|
|
180
|
+
cfg.port,
|
|
181
|
+
cfg.backlog,
|
|
182
|
+
ssl=ssl_ctx,
|
|
183
|
+
reuse_port=cfg.reuse_port,
|
|
184
|
+
reuse_address=cfg.reuse_address,
|
|
185
|
+
nodelay=cfg.nodelay,
|
|
186
|
+
fd=cfg.fd,
|
|
187
|
+
)
|
|
188
|
+
if cfg.kind == 'udp':
|
|
189
|
+
return UDPListener(cfg.host, cfg.port, reuse_port=cfg.reuse_port, fd=cfg.fd)
|
|
190
|
+
if cfg.kind == 'unix':
|
|
191
|
+
ssl_ctx = build_server_ssl_context(cfg)
|
|
192
|
+
return UnixListener(cfg.path or '', cfg.backlog, ssl=ssl_ctx, fd=cfg.fd)
|
|
193
|
+
if cfg.kind == 'pipe':
|
|
194
|
+
return PipeListener(cfg.path or '')
|
|
195
|
+
return InProcListener()
|
|
196
|
+
|
|
197
|
+
def _make_client_handler(self, listener_cfg: ListenerConfig):
|
|
198
|
+
if listener_cfg.kind == 'udp':
|
|
199
|
+
h3_handler = HTTP3DatagramHandler(
|
|
200
|
+
app=self.app,
|
|
201
|
+
config=self.config,
|
|
202
|
+
listener=listener_cfg,
|
|
203
|
+
access_logger=self.access_logger,
|
|
204
|
+
scheduler=self.scheduler,
|
|
205
|
+
metrics=self.state.metrics,
|
|
206
|
+
)
|
|
207
|
+
self._datagram_handlers.append(h3_handler)
|
|
208
|
+
|
|
209
|
+
async def udp_handler(packet, endpoint) -> None:
|
|
210
|
+
sessions_before = len(h3_handler.sessions)
|
|
211
|
+
responses_before = sum(len(session.responded_streams) for session in h3_handler.sessions.values())
|
|
212
|
+
await h3_handler.handle_packet(packet, endpoint)
|
|
213
|
+
if len(h3_handler.sessions) > sessions_before:
|
|
214
|
+
self.state.metrics.connection_opened()
|
|
215
|
+
responses_after = sum(len(session.responded_streams) for session in h3_handler.sessions.values())
|
|
216
|
+
if responses_after > responses_before:
|
|
217
|
+
self.state.metrics.requests_served += responses_after - responses_before
|
|
218
|
+
|
|
219
|
+
return udp_handler
|
|
220
|
+
|
|
221
|
+
if listener_cfg.kind == 'pipe':
|
|
222
|
+
raw_handler = RawFramedApplicationHandler(
|
|
223
|
+
app=self.app,
|
|
224
|
+
config=self.config,
|
|
225
|
+
listener=listener_cfg,
|
|
226
|
+
access_logger=self.access_logger,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def pipe_handler(connection, data) -> None:
|
|
230
|
+
handled = await raw_handler.feed_bytes(connection, data, path=listener_cfg.path)
|
|
231
|
+
self.state.metrics.requests_served += handled
|
|
232
|
+
self.state.metrics.bytes_received += len(data)
|
|
233
|
+
|
|
234
|
+
return pipe_handler
|
|
235
|
+
|
|
236
|
+
async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
237
|
+
await self._handle_client(reader, writer, listener_cfg)
|
|
238
|
+
|
|
239
|
+
return handler
|
|
240
|
+
|
|
241
|
+
async def _handle_client(
|
|
242
|
+
self,
|
|
243
|
+
reader: asyncio.StreamReader,
|
|
244
|
+
writer: asyncio.StreamWriter,
|
|
245
|
+
listener_cfg: ListenerConfig,
|
|
246
|
+
) -> None:
|
|
247
|
+
lease = self.scheduler.acquire_connection()
|
|
248
|
+
if lease is None:
|
|
249
|
+
writer.close()
|
|
250
|
+
with suppress(Exception):
|
|
251
|
+
await writer.wait_closed()
|
|
252
|
+
return
|
|
253
|
+
self.state.metrics.connection_opened()
|
|
254
|
+
peername = writer.get_extra_info('peername')
|
|
255
|
+
sockname = writer.get_extra_info('sockname')
|
|
256
|
+
ssl_obj = writer.get_extra_info('ssl_object')
|
|
257
|
+
selected_alpn = ssl_obj.selected_alpn_protocol() if ssl_obj else None
|
|
258
|
+
tls_payload = tls_extension_payload(writer)
|
|
259
|
+
scope_tls_extensions = {'tls': tls_payload} if tls_payload is not None else None
|
|
260
|
+
client_host, client_port = peer_parts(peername)
|
|
261
|
+
server_host, server_port = peer_parts(sockname)
|
|
262
|
+
client = (client_host, client_port) if client_host is not None and client_port is not None else None
|
|
263
|
+
server = (server_host or '', server_port)
|
|
264
|
+
scheme = 'https' if ssl_obj else (listener_cfg.scheme or 'http')
|
|
265
|
+
ws_scheme = 'wss' if ssl_obj else 'ws'
|
|
266
|
+
try:
|
|
267
|
+
if selected_alpn == 'h2' and '2' in listener_cfg.http_versions:
|
|
268
|
+
h2_handler = HTTP2ConnectionHandler(
|
|
269
|
+
app=self.app,
|
|
270
|
+
config=self.config,
|
|
271
|
+
access_logger=self.access_logger,
|
|
272
|
+
scheduler=self.scheduler,
|
|
273
|
+
metrics=self.state.metrics,
|
|
274
|
+
reader=reader,
|
|
275
|
+
writer=writer,
|
|
276
|
+
client=client,
|
|
277
|
+
server=server,
|
|
278
|
+
scheme=scheme,
|
|
279
|
+
scope_extensions=scope_tls_extensions,
|
|
280
|
+
)
|
|
281
|
+
await h2_handler.handle()
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
initial = b''
|
|
285
|
+
if '2' in listener_cfg.http_versions and self.config.enable_h2c:
|
|
286
|
+
initial = await self._read_preface_probe(reader)
|
|
287
|
+
if initial == H2_PREFACE:
|
|
288
|
+
h2_handler = HTTP2ConnectionHandler(
|
|
289
|
+
app=self.app,
|
|
290
|
+
config=self.config,
|
|
291
|
+
access_logger=self.access_logger,
|
|
292
|
+
scheduler=self.scheduler,
|
|
293
|
+
metrics=self.state.metrics,
|
|
294
|
+
reader=reader,
|
|
295
|
+
writer=writer,
|
|
296
|
+
client=client,
|
|
297
|
+
server=server,
|
|
298
|
+
scheme=scheme,
|
|
299
|
+
prebuffer=initial,
|
|
300
|
+
scope_extensions=scope_tls_extensions,
|
|
301
|
+
)
|
|
302
|
+
await h2_handler.handle()
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
buffered_reader: StreamReaderLike = PrebufferedReader(reader, initial)
|
|
306
|
+
await self._handle_http11_connection(
|
|
307
|
+
buffered_reader,
|
|
308
|
+
writer,
|
|
309
|
+
listener_cfg,
|
|
310
|
+
client=client,
|
|
311
|
+
server=server,
|
|
312
|
+
scheme=scheme,
|
|
313
|
+
ws_scheme=ws_scheme,
|
|
314
|
+
scope_extensions=scope_tls_extensions,
|
|
315
|
+
)
|
|
316
|
+
finally:
|
|
317
|
+
lease.release()
|
|
318
|
+
self.state.metrics.connection_closed()
|
|
319
|
+
writer.close()
|
|
320
|
+
with suppress(Exception):
|
|
321
|
+
await writer.wait_closed()
|
|
322
|
+
|
|
323
|
+
async def _read_preface_probe(self, reader: asyncio.StreamReader) -> bytes:
|
|
324
|
+
data = await asyncio.wait_for(reader.read(len(H2_PREFACE)), timeout=self.config.http.read_timeout)
|
|
325
|
+
if not data:
|
|
326
|
+
return b''
|
|
327
|
+
if H2_PREFACE.startswith(data) and data != H2_PREFACE:
|
|
328
|
+
with suppress(Exception):
|
|
329
|
+
data += await asyncio.wait_for(reader.readexactly(len(H2_PREFACE) - len(data)), timeout=0.05)
|
|
330
|
+
return data
|
|
331
|
+
|
|
332
|
+
async def _handle_http11_connection(
|
|
333
|
+
self,
|
|
334
|
+
reader: StreamReaderLike,
|
|
335
|
+
writer: asyncio.StreamWriter,
|
|
336
|
+
listener_cfg: ListenerConfig,
|
|
337
|
+
*,
|
|
338
|
+
client: tuple[str, int] | None,
|
|
339
|
+
server: tuple[str, int] | tuple[str, None] | None,
|
|
340
|
+
scheme: str,
|
|
341
|
+
ws_scheme: str,
|
|
342
|
+
scope_extensions: dict | None = None,
|
|
343
|
+
) -> None:
|
|
344
|
+
keep_handling = True
|
|
345
|
+
handled_requests = 0
|
|
346
|
+
while keep_handling and not self.state.shutting_down:
|
|
347
|
+
request_timeout = self.config.http.keep_alive_timeout if handled_requests else self.config.http.read_timeout
|
|
348
|
+
if self.config.http.http1_header_read_timeout is not None:
|
|
349
|
+
request_timeout = min(request_timeout, self.config.http.http1_header_read_timeout)
|
|
350
|
+
try:
|
|
351
|
+
request = await asyncio.wait_for(
|
|
352
|
+
read_http11_request_head(
|
|
353
|
+
reader,
|
|
354
|
+
max_body_size=self.config.max_body_size,
|
|
355
|
+
max_header_size=self.config.max_header_size,
|
|
356
|
+
max_incomplete_event_size=self.config.http.http1_max_incomplete_event_size,
|
|
357
|
+
buffer_size=self.config.http.http1_buffer_size,
|
|
358
|
+
),
|
|
359
|
+
timeout=request_timeout,
|
|
360
|
+
)
|
|
361
|
+
except asyncio.TimeoutError:
|
|
362
|
+
break
|
|
363
|
+
except Exception as exc:
|
|
364
|
+
self.state.metrics.protocol_errors += 1
|
|
365
|
+
self.logger.warning('protocol error from %s: %s', client, exc)
|
|
366
|
+
await self._write_error(writer, 400, b'bad request', keep_alive=False)
|
|
367
|
+
break
|
|
368
|
+
if request is None:
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
proxy_view = resolve_proxy_view(
|
|
372
|
+
request.headers,
|
|
373
|
+
client=client,
|
|
374
|
+
server=server,
|
|
375
|
+
scheme=scheme,
|
|
376
|
+
root_path=self.config.proxy.root_path,
|
|
377
|
+
enabled=self.config.proxy.proxy_headers,
|
|
378
|
+
forwarded_allow_ips=self.config.proxy.forwarded_allow_ips,
|
|
379
|
+
)
|
|
380
|
+
request_client = proxy_view.client
|
|
381
|
+
request_server = proxy_view.server
|
|
382
|
+
request_scheme = proxy_view.scheme
|
|
383
|
+
request_ws_scheme = 'wss' if request_scheme == 'https' else 'ws'
|
|
384
|
+
request.keep_alive = apply_keep_alive_policy(request.keep_alive, enabled=self.config.http.http1_keep_alive)
|
|
385
|
+
|
|
386
|
+
if request.method.upper() == 'CONNECT':
|
|
387
|
+
await self._handle_http11_connect_tunnel(reader, writer, request, client=request_client)
|
|
388
|
+
keep_handling = False
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
if request.websocket_upgrade:
|
|
392
|
+
if not listener_cfg.websocket:
|
|
393
|
+
await self._write_error(writer, 426, b'websocket not enabled', keep_alive=False)
|
|
394
|
+
break
|
|
395
|
+
work_lease = self.scheduler.acquire_work()
|
|
396
|
+
if work_lease is None:
|
|
397
|
+
self.state.metrics.scheduler_task_rejected()
|
|
398
|
+
await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
|
|
399
|
+
break
|
|
400
|
+
handler = WebSocketConnectionHandler(
|
|
401
|
+
app=self.app,
|
|
402
|
+
config=self.config,
|
|
403
|
+
access_logger=self.access_logger,
|
|
404
|
+
request=request,
|
|
405
|
+
reader=reader,
|
|
406
|
+
writer=writer,
|
|
407
|
+
client=request_client,
|
|
408
|
+
server=request_server,
|
|
409
|
+
scheme=request_ws_scheme,
|
|
410
|
+
scope_extensions=scope_extensions,
|
|
411
|
+
metrics=self.state.metrics,
|
|
412
|
+
)
|
|
413
|
+
try:
|
|
414
|
+
self.state.metrics.websocket_opened()
|
|
415
|
+
await handler.handle()
|
|
416
|
+
finally:
|
|
417
|
+
work_lease.release()
|
|
418
|
+
self.state.metrics.websocket_closed()
|
|
419
|
+
keep_handling = False
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
work_lease = self.scheduler.acquire_work()
|
|
423
|
+
if work_lease is None:
|
|
424
|
+
self.state.metrics.scheduler_task_rejected()
|
|
425
|
+
await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
|
|
426
|
+
break
|
|
427
|
+
try:
|
|
428
|
+
keep_handling = await self._serve_http11_request(
|
|
429
|
+
reader,
|
|
430
|
+
writer,
|
|
431
|
+
request,
|
|
432
|
+
client=request_client,
|
|
433
|
+
server=request_server,
|
|
434
|
+
scheme=request_scheme,
|
|
435
|
+
scope_extensions=scope_extensions,
|
|
436
|
+
)
|
|
437
|
+
finally:
|
|
438
|
+
work_lease.release()
|
|
439
|
+
handled_requests += 1
|
|
440
|
+
|
|
441
|
+
async def _drain_writer(self, writer: asyncio.StreamWriter) -> None:
|
|
442
|
+
await asyncio.wait_for(writer.drain(), timeout=self.config.http.write_timeout)
|
|
443
|
+
|
|
444
|
+
async def _write_continue(self, writer: asyncio.StreamWriter) -> None:
|
|
445
|
+
writer.write(b'HTTP/1.1 100 Continue\r\n\r\n')
|
|
446
|
+
await self._drain_writer(writer)
|
|
447
|
+
|
|
448
|
+
def _build_http11_receive(
|
|
449
|
+
self,
|
|
450
|
+
reader: StreamReaderLike,
|
|
451
|
+
writer: asyncio.StreamWriter,
|
|
452
|
+
request: ParsedRequestHead,
|
|
453
|
+
) -> HTTPRequestReceive | HTTPStreamingRequestReceive:
|
|
454
|
+
if request.body_kind == 'none':
|
|
455
|
+
return HTTPRequestReceive(b'')
|
|
456
|
+
return HTTPStreamingRequestReceive(
|
|
457
|
+
reader=reader,
|
|
458
|
+
content_length=request.content_length if request.body_kind == 'content-length' else None,
|
|
459
|
+
chunked=request.body_kind == 'chunked',
|
|
460
|
+
max_body_size=self.config.max_body_size,
|
|
461
|
+
expect_continue=request.expect_continue,
|
|
462
|
+
on_expect_continue=lambda: self._write_continue(writer),
|
|
463
|
+
max_chunk_size=self.config.http.http1_buffer_size,
|
|
464
|
+
trailer_policy=self.config.http.trailer_policy,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def _http11_scope_extensions(self, request: ParsedRequestHead, *, scope_extensions: dict | None = None) -> dict:
|
|
468
|
+
extensions: dict = dict(scope_extensions or {})
|
|
469
|
+
if request.body_kind == 'chunked' and self.config.http.trailer_policy != 'drop':
|
|
470
|
+
extensions['tigrcorn.http.request_trailers'] = {}
|
|
471
|
+
if request.method.upper() == 'CONNECT':
|
|
472
|
+
extensions['tigrcorn.http.connect'] = {'authority': request.target}
|
|
473
|
+
extensions['tigrcorn.http.response.file'] = {'protocol': 'http/1.1', 'streaming': True, 'sendfile': True}
|
|
474
|
+
extensions['http.response.pathsend'] = {}
|
|
475
|
+
return extensions
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _parse_connect_authority(authority: str) -> tuple[str, int]:
|
|
479
|
+
return parse_connect_authority(authority)
|
|
480
|
+
|
|
481
|
+
async def _relay_stream(self, reader: StreamReaderLike, writer: asyncio.StreamWriter) -> None:
|
|
482
|
+
try:
|
|
483
|
+
while True:
|
|
484
|
+
chunk = await asyncio.wait_for(reader.read(65536), timeout=self.config.http.idle_timeout)
|
|
485
|
+
if not chunk:
|
|
486
|
+
break
|
|
487
|
+
writer.write(chunk)
|
|
488
|
+
await self._drain_writer(writer)
|
|
489
|
+
self.state.metrics.bytes_sent += len(chunk)
|
|
490
|
+
finally:
|
|
491
|
+
writer.close()
|
|
492
|
+
with suppress(Exception):
|
|
493
|
+
await writer.wait_closed()
|
|
494
|
+
|
|
495
|
+
async def _try_http11_sendfile(self, writer: asyncio.StreamWriter, segment: FileBodySegment) -> bool:
|
|
496
|
+
if segment.count is not None and segment.count <= 0:
|
|
497
|
+
return True
|
|
498
|
+
if writer.get_extra_info('ssl_object') is not None or writer.get_extra_info('sslcontext') is not None:
|
|
499
|
+
return False
|
|
500
|
+
transport = getattr(writer, 'transport', None) or getattr(writer, '_transport', None)
|
|
501
|
+
if transport is None:
|
|
502
|
+
return False
|
|
503
|
+
loop = asyncio.get_running_loop()
|
|
504
|
+
try:
|
|
505
|
+
with open(segment.path, 'rb') as handle:
|
|
506
|
+
await loop.sendfile(transport, handle, offset=segment.offset, count=segment.count, fallback=False)
|
|
507
|
+
return True
|
|
508
|
+
except Exception:
|
|
509
|
+
return False
|
|
510
|
+
|
|
511
|
+
async def _send_http11_body_segments(self, writer: asyncio.StreamWriter, body_segments: list, *, chunked: bool = False) -> None:
|
|
512
|
+
if not chunked and len(body_segments) == 1 and isinstance(body_segments[0], FileBodySegment):
|
|
513
|
+
if await self._try_http11_sendfile(writer, body_segments[0]):
|
|
514
|
+
return
|
|
515
|
+
async for chunk in iter_response_body_segments(body_segments):
|
|
516
|
+
self.state.metrics.bytes_sent += len(chunk)
|
|
517
|
+
if chunked:
|
|
518
|
+
writer.write(serialize_http11_response_chunk(chunk))
|
|
519
|
+
else:
|
|
520
|
+
writer.write(chunk)
|
|
521
|
+
if len(chunk) >= 64 * 1024:
|
|
522
|
+
await self._drain_writer(writer)
|
|
523
|
+
await self._drain_writer(writer)
|
|
524
|
+
|
|
525
|
+
async def _send_http11_streamed_response(
|
|
526
|
+
self,
|
|
527
|
+
writer: asyncio.StreamWriter,
|
|
528
|
+
*,
|
|
529
|
+
request: ParsedRequestHead,
|
|
530
|
+
status: int,
|
|
531
|
+
headers: list[tuple[bytes, bytes]],
|
|
532
|
+
body_segments: list,
|
|
533
|
+
trailers: list[tuple[bytes, bytes]],
|
|
534
|
+
) -> None:
|
|
535
|
+
has_body = response_body_segments_have_bytes(body_segments)
|
|
536
|
+
if trailers:
|
|
537
|
+
writer.write(
|
|
538
|
+
serialize_http11_response_head(
|
|
539
|
+
status=status,
|
|
540
|
+
headers=headers,
|
|
541
|
+
keep_alive=request.keep_alive,
|
|
542
|
+
server_header=self.config.server_header_value,
|
|
543
|
+
chunked=True,
|
|
544
|
+
include_date_header=self.config.include_date_header,
|
|
545
|
+
default_headers=self.config.default_response_headers,
|
|
546
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
await self._drain_writer(writer)
|
|
550
|
+
if has_body:
|
|
551
|
+
await self._send_http11_body_segments(writer, body_segments, chunked=True)
|
|
552
|
+
writer.write(finalize_chunked_body(trailers))
|
|
553
|
+
await self._drain_writer(writer)
|
|
554
|
+
return
|
|
555
|
+
if not has_body:
|
|
556
|
+
writer.write(
|
|
557
|
+
serialize_http11_response_whole(
|
|
558
|
+
status=status,
|
|
559
|
+
headers=headers,
|
|
560
|
+
body=b'',
|
|
561
|
+
keep_alive=request.keep_alive,
|
|
562
|
+
server_header=self.config.server_header_value,
|
|
563
|
+
include_date_header=self.config.include_date_header,
|
|
564
|
+
default_headers=self.config.default_response_headers,
|
|
565
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
await self._drain_writer(writer)
|
|
569
|
+
return
|
|
570
|
+
writer.write(
|
|
571
|
+
serialize_http11_response_head(
|
|
572
|
+
status=status,
|
|
573
|
+
headers=headers,
|
|
574
|
+
keep_alive=request.keep_alive,
|
|
575
|
+
server_header=self.config.server_header_value,
|
|
576
|
+
chunked=False,
|
|
577
|
+
include_date_header=self.config.include_date_header,
|
|
578
|
+
default_headers=self.config.default_response_headers,
|
|
579
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
await self._drain_writer(writer)
|
|
583
|
+
await self._send_http11_body_segments(writer, body_segments, chunked=False)
|
|
584
|
+
|
|
585
|
+
async def _handle_http11_connect_tunnel(
|
|
586
|
+
self,
|
|
587
|
+
reader: StreamReaderLike,
|
|
588
|
+
writer: asyncio.StreamWriter,
|
|
589
|
+
request: ParsedRequestHead,
|
|
590
|
+
*,
|
|
591
|
+
client: tuple[str, int] | None,
|
|
592
|
+
) -> None:
|
|
593
|
+
try:
|
|
594
|
+
host, port = self._parse_connect_authority(request.target)
|
|
595
|
+
except Exception:
|
|
596
|
+
await self._write_error(writer, 400, b'bad connect target', keep_alive=False)
|
|
597
|
+
return
|
|
598
|
+
if self.config.http.connect_policy == 'deny':
|
|
599
|
+
await self._write_error(writer, 403, b'connect denied', keep_alive=False)
|
|
600
|
+
return
|
|
601
|
+
if self.config.http.connect_policy == 'allowlist' and not is_connect_allowed(host, port, self.config.http.connect_allow):
|
|
602
|
+
await self._write_error(writer, 403, b'connect denied', keep_alive=False)
|
|
603
|
+
return
|
|
604
|
+
if request.body_kind != 'none':
|
|
605
|
+
await self._write_error(writer, 400, b'connect request body not supported', keep_alive=False)
|
|
606
|
+
return
|
|
607
|
+
work_lease = self.scheduler.acquire_work()
|
|
608
|
+
if work_lease is None:
|
|
609
|
+
self.state.metrics.scheduler_task_rejected()
|
|
610
|
+
await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
|
|
611
|
+
return
|
|
612
|
+
try:
|
|
613
|
+
upstream_reader, upstream_writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=self.config.http.read_timeout)
|
|
614
|
+
except Exception:
|
|
615
|
+
work_lease.release()
|
|
616
|
+
await self._write_error(writer, 502, b'bad gateway', keep_alive=False)
|
|
617
|
+
return
|
|
618
|
+
writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
|
|
619
|
+
await self._drain_writer(writer)
|
|
620
|
+
self.access_logger.log_http(client, 'CONNECT', request.target, 200, f'HTTP/{request.http_version}')
|
|
621
|
+
try:
|
|
622
|
+
self.state.metrics.scheduler_task_spawned()
|
|
623
|
+
relay_up = self.scheduler.spawn(self._relay_stream(reader, upstream_writer), owner=f'connect:{request.target}:up')
|
|
624
|
+
self.state.metrics.scheduler_task_spawned()
|
|
625
|
+
relay_down = self.scheduler.spawn(self._relay_stream(upstream_reader, writer), owner=f'connect:{request.target}:down')
|
|
626
|
+
except RuntimeError:
|
|
627
|
+
self.state.metrics.scheduler_task_rejected()
|
|
628
|
+
await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
|
|
629
|
+
return
|
|
630
|
+
try:
|
|
631
|
+
done, pending = await asyncio.wait({relay_up, relay_down}, return_when=asyncio.FIRST_COMPLETED)
|
|
632
|
+
for task in pending:
|
|
633
|
+
task.cancel()
|
|
634
|
+
with suppress(Exception):
|
|
635
|
+
await task
|
|
636
|
+
for task in done:
|
|
637
|
+
with suppress(Exception):
|
|
638
|
+
await task
|
|
639
|
+
finally:
|
|
640
|
+
work_lease.release()
|
|
641
|
+
|
|
642
|
+
async def _serve_http11_request(
|
|
643
|
+
self,
|
|
644
|
+
reader: StreamReaderLike,
|
|
645
|
+
writer: asyncio.StreamWriter,
|
|
646
|
+
request: ParsedRequestHead,
|
|
647
|
+
*,
|
|
648
|
+
client: tuple[str, int] | None,
|
|
649
|
+
server: tuple[str, int] | tuple[str, None] | None,
|
|
650
|
+
scheme: str,
|
|
651
|
+
scope_extensions: dict | None = None,
|
|
652
|
+
) -> bool:
|
|
653
|
+
host_header = get_header(request.headers, b'host')
|
|
654
|
+
if self.config.allowed_server_names and not authority_allowed(host_header, self.config.allowed_server_names):
|
|
655
|
+
await self._write_error(writer, 421, b'misdirected request', keep_alive=False)
|
|
656
|
+
return False
|
|
657
|
+
scope = build_http_scope(
|
|
658
|
+
request,
|
|
659
|
+
client=client,
|
|
660
|
+
server=server,
|
|
661
|
+
scheme=scheme,
|
|
662
|
+
extensions=self._http11_scope_extensions(request, scope_extensions=scope_extensions),
|
|
663
|
+
root_path=self.config.proxy.root_path,
|
|
664
|
+
proxy=self.config.proxy,
|
|
665
|
+
)
|
|
666
|
+
receive = self._build_http11_receive(reader, writer, request)
|
|
667
|
+
send = HTTPResponseCollector()
|
|
668
|
+
status = 500
|
|
669
|
+
trailers: list[tuple[bytes, bytes]] = []
|
|
670
|
+
try:
|
|
671
|
+
await self.app(scope, receive, send)
|
|
672
|
+
send.finalize()
|
|
673
|
+
assert send.status is not None
|
|
674
|
+
status = send.status
|
|
675
|
+
headers = list(send.headers)
|
|
676
|
+
trailers = list(send.trailers)
|
|
677
|
+
body = b''
|
|
678
|
+
body_segments = list(send.body_segments) if send.uses_streamed_body else None
|
|
679
|
+
for interim_status, interim_headers in send.informational_responses:
|
|
680
|
+
writer.write(
|
|
681
|
+
serialize_http11_response_head(
|
|
682
|
+
status=interim_status,
|
|
683
|
+
headers=interim_headers,
|
|
684
|
+
keep_alive=request.keep_alive,
|
|
685
|
+
server_header=self.config.server_header_value,
|
|
686
|
+
chunked=False,
|
|
687
|
+
include_date_header=self.config.include_date_header,
|
|
688
|
+
default_headers=self.config.default_response_headers,
|
|
689
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
if body_segments is None and send.has_spooled_body():
|
|
693
|
+
spooled_segments = send.spooled_body_segments()
|
|
694
|
+
spooled_path = ''
|
|
695
|
+
if spooled_segments:
|
|
696
|
+
first_segment = spooled_segments[0]
|
|
697
|
+
if isinstance(first_segment, FileBodySegment):
|
|
698
|
+
spooled_path = first_segment.path
|
|
699
|
+
planned = plan_file_backed_response_entity_semantics(
|
|
700
|
+
method=request.method,
|
|
701
|
+
request_headers=request.headers,
|
|
702
|
+
response_headers=headers,
|
|
703
|
+
status=status,
|
|
704
|
+
body_path=spooled_path,
|
|
705
|
+
body_length=send.body_length,
|
|
706
|
+
generated_etag=send.generated_entity_tag(),
|
|
707
|
+
apply_content_coding=True,
|
|
708
|
+
trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
|
|
709
|
+
)
|
|
710
|
+
if planned.requires_materialization:
|
|
711
|
+
body = await send.materialize_body()
|
|
712
|
+
processed = apply_response_entity_semantics(
|
|
713
|
+
method=request.method,
|
|
714
|
+
request_headers=request.headers,
|
|
715
|
+
response_headers=headers,
|
|
716
|
+
body=body,
|
|
717
|
+
status=status,
|
|
718
|
+
content_coding_policy=self.config.http.content_coding_policy,
|
|
719
|
+
supported_codings=tuple(self.config.http.content_codings),
|
|
720
|
+
apply_content_coding=True,
|
|
721
|
+
generate_etag=True,
|
|
722
|
+
trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
|
|
723
|
+
)
|
|
724
|
+
status = processed.status
|
|
725
|
+
headers = processed.headers
|
|
726
|
+
body = processed.body
|
|
727
|
+
if processed.head_response:
|
|
728
|
+
trailers = []
|
|
729
|
+
elif planned.use_body_segments:
|
|
730
|
+
status = planned.status
|
|
731
|
+
headers = planned.headers
|
|
732
|
+
body_segments = list(planned.body_segments)
|
|
733
|
+
body = b''
|
|
734
|
+
else:
|
|
735
|
+
status = planned.status
|
|
736
|
+
headers = planned.headers
|
|
737
|
+
body = planned.body
|
|
738
|
+
trailers = []
|
|
739
|
+
elif body_segments is None:
|
|
740
|
+
body = await send.materialize_body()
|
|
741
|
+
processed = apply_response_entity_semantics(
|
|
742
|
+
method=request.method,
|
|
743
|
+
request_headers=request.headers,
|
|
744
|
+
response_headers=headers,
|
|
745
|
+
body=body,
|
|
746
|
+
status=status,
|
|
747
|
+
content_coding_policy=self.config.http.content_coding_policy,
|
|
748
|
+
supported_codings=tuple(self.config.http.content_codings),
|
|
749
|
+
apply_content_coding=True,
|
|
750
|
+
generate_etag=True,
|
|
751
|
+
trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
|
|
752
|
+
)
|
|
753
|
+
status = processed.status
|
|
754
|
+
headers = processed.headers
|
|
755
|
+
body = processed.body
|
|
756
|
+
if processed.head_response:
|
|
757
|
+
trailers = []
|
|
758
|
+
if body_segments is None:
|
|
759
|
+
if trailers:
|
|
760
|
+
writer.write(
|
|
761
|
+
serialize_http11_response_head(
|
|
762
|
+
status=status,
|
|
763
|
+
headers=headers,
|
|
764
|
+
keep_alive=request.keep_alive,
|
|
765
|
+
server_header=self.config.server_header_value,
|
|
766
|
+
chunked=True,
|
|
767
|
+
include_date_header=self.config.include_date_header,
|
|
768
|
+
default_headers=self.config.default_response_headers,
|
|
769
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
if body:
|
|
773
|
+
writer.write(serialize_http11_response_chunk(body))
|
|
774
|
+
writer.write(finalize_chunked_body(trailers))
|
|
775
|
+
await self._drain_writer(writer)
|
|
776
|
+
else:
|
|
777
|
+
writer.write(
|
|
778
|
+
serialize_http11_response_whole(
|
|
779
|
+
status=status,
|
|
780
|
+
headers=headers,
|
|
781
|
+
body=body,
|
|
782
|
+
keep_alive=request.keep_alive,
|
|
783
|
+
server_header=self.config.server_header_value,
|
|
784
|
+
include_date_header=self.config.include_date_header,
|
|
785
|
+
default_headers=self.config.default_response_headers,
|
|
786
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
await self._drain_writer(writer)
|
|
790
|
+
else:
|
|
791
|
+
await self._send_http11_streamed_response(
|
|
792
|
+
writer,
|
|
793
|
+
request=request,
|
|
794
|
+
status=status,
|
|
795
|
+
headers=headers,
|
|
796
|
+
body_segments=body_segments,
|
|
797
|
+
trailers=trailers,
|
|
798
|
+
)
|
|
799
|
+
self.state.metrics.requests_served += 1
|
|
800
|
+
except ProtocolError:
|
|
801
|
+
self.state.metrics.requests_failed += 1
|
|
802
|
+
await self._write_error(writer, 400, b'bad request trailers', keep_alive=False)
|
|
803
|
+
return False
|
|
804
|
+
except Exception:
|
|
805
|
+
self.state.metrics.requests_failed += 1
|
|
806
|
+
self.logger.exception('application error')
|
|
807
|
+
await self._write_error(writer, 500, b'internal server error', keep_alive=False)
|
|
808
|
+
return False
|
|
809
|
+
finally:
|
|
810
|
+
send.cleanup()
|
|
811
|
+
self.access_logger.log_http(client, request.method, request.path, status, f'HTTP/{request.http_version}')
|
|
812
|
+
body_complete = getattr(receive, 'body_complete', True)
|
|
813
|
+
return request.keep_alive and body_complete
|
|
814
|
+
|
|
815
|
+
async def _write_error(
|
|
816
|
+
self,
|
|
817
|
+
writer: asyncio.StreamWriter,
|
|
818
|
+
status: int,
|
|
819
|
+
body: bytes,
|
|
820
|
+
*,
|
|
821
|
+
keep_alive: bool,
|
|
822
|
+
) -> None:
|
|
823
|
+
writer.write(
|
|
824
|
+
serialize_http11_response_whole(
|
|
825
|
+
status=status,
|
|
826
|
+
headers=[(b'content-type', b'text/plain; charset=utf-8')],
|
|
827
|
+
body=body,
|
|
828
|
+
keep_alive=keep_alive,
|
|
829
|
+
server_header=self.config.server_header_value,
|
|
830
|
+
include_date_header=self.config.include_date_header,
|
|
831
|
+
default_headers=self.config.default_response_headers,
|
|
832
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version='1.1'),
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
await self._drain_writer(writer)
|
|
836
|
+
|
|
837
|
+
async def _start_metrics_endpoint(self, bind: str) -> asyncio.AbstractServer:
|
|
838
|
+
host, port = self._parse_bind_target(bind)
|
|
839
|
+
server = await asyncio.start_server(self._handle_metrics_request, host=host, port=port)
|
|
840
|
+
self.logger.info('metrics endpoint listening on %s', bind)
|
|
841
|
+
return server
|
|
842
|
+
|
|
843
|
+
async def _handle_metrics_request(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
844
|
+
with suppress(Exception):
|
|
845
|
+
await asyncio.wait_for(reader.readuntil(b'\r\n\r\n'), timeout=1.0)
|
|
846
|
+
payload = self.state.metrics.render_prometheus().encode('utf-8')
|
|
847
|
+
response = serialize_http11_response_whole(
|
|
848
|
+
status=200,
|
|
849
|
+
headers=[(b'content-type', b'text/plain; version=0.0.4')],
|
|
850
|
+
body=payload,
|
|
851
|
+
keep_alive=False,
|
|
852
|
+
server_header=self.config.server_header_value,
|
|
853
|
+
include_date_header=self.config.include_date_header,
|
|
854
|
+
default_headers=self.config.default_response_headers,
|
|
855
|
+
)
|
|
856
|
+
writer.write(response)
|
|
857
|
+
with suppress(Exception):
|
|
858
|
+
await writer.drain()
|
|
859
|
+
writer.close()
|
|
860
|
+
with suppress(Exception):
|
|
861
|
+
await writer.wait_closed()
|
|
862
|
+
|
|
863
|
+
@staticmethod
|
|
864
|
+
def _parse_bind_target(bind: str) -> tuple[str, int]:
|
|
865
|
+
if bind.startswith('[') and ']:' in bind:
|
|
866
|
+
host, port = bind.rsplit(':', 1)
|
|
867
|
+
return host[1:-1], int(port)
|
|
868
|
+
host, port = bind.rsplit(':', 1)
|
|
869
|
+
return host, int(port)
|
|
870
|
+
|
|
871
|
+
async def _monitor_request_budget(self) -> None:
|
|
872
|
+
assert self._request_budget is not None
|
|
873
|
+
while not self._should_exit.is_set() and not self.state.shutting_down:
|
|
874
|
+
if self.state.metrics.requests_served >= self._request_budget:
|
|
875
|
+
self.logger.info('request budget reached, shutting down worker')
|
|
876
|
+
self.request_shutdown()
|
|
877
|
+
return
|
|
878
|
+
await asyncio.sleep(0.1)
|