|
11 | 11 | """This module contains the ASGI server for the restate framework.""" |
12 | 12 |
|
13 | 13 | import asyncio |
| 14 | +import functools |
14 | 15 | import logging |
15 | 16 | import signal |
16 | | -from typing import Dict, Set, TypedDict, Literal |
| 17 | +from typing import Dict, Optional, Set, TypedDict, Literal |
17 | 18 |
|
18 | 19 | from restate.discovery import compute_discovery_json |
19 | 20 | from restate.endpoint import Endpoint |
| 21 | +from restate.entry_codec import JournalValueCodec |
20 | 22 | from restate.server_context import ServerInvocationContext, DisconnectedException |
21 | 23 | from restate.server_types import Receive, ReceiveChannel, RestateAppT, Scope, Send, binary_to_header, header_to_binary # pylint: disable=line-too-long |
22 | 24 | from restate.vm import VMWrapper |
@@ -125,7 +127,12 @@ async def send_health_check(send: Send): |
125 | 127 |
|
126 | 128 |
|
127 | 129 | async def process_invocation_to_completion( |
128 | | - vm: VMWrapper, handler, attempt_headers: Dict[str, str], receive: ReceiveChannel, send: Send |
| 130 | + vm: VMWrapper, |
| 131 | + handler, |
| 132 | + attempt_headers: Dict[str, str], |
| 133 | + receive: ReceiveChannel, |
| 134 | + send: Send, |
| 135 | + journal_codec: Optional[JournalValueCodec] = None, |
129 | 136 | ): |
130 | 137 | """Invoke the user code.""" |
131 | 138 | status, res_headers = vm.get_response_head() |
@@ -155,7 +162,13 @@ async def process_invocation_to_completion( |
155 | 162 | # ======================================== |
156 | 163 | invocation = vm.sys_input() |
157 | 164 | context = ServerInvocationContext( |
158 | | - vm=vm, handler=handler, invocation=invocation, attempt_headers=attempt_headers, send=send, receive=receive |
| 165 | + vm=vm, |
| 166 | + handler=handler, |
| 167 | + invocation=invocation, |
| 168 | + attempt_headers=attempt_headers, |
| 169 | + send=send, |
| 170 | + receive=receive, |
| 171 | + journal_codec=journal_codec, |
159 | 172 | ) |
160 | 173 | try: |
161 | 174 | await context.enter() |
@@ -217,6 +230,27 @@ def asgi_app(endpoint: Endpoint) -> RestateAppT: |
217 | 230 | active_channels: Set[ReceiveChannel] = set() |
218 | 231 | sigterm_installed = False |
219 | 232 |
|
| 233 | + # Journal value codec resolution. |
| 234 | + # A codec (or an async provider building one) may be configured on the endpoint. Whether one is |
| 235 | + # configured is known synchronously and drives the VM's payload-checks flag. An async provider |
| 236 | + # is resolved once, lazily on the first invocation, and the resulting codec is reused. |
| 237 | + codec_config = endpoint.journal_value_codec |
| 238 | + disable_payload_checks = codec_config is not None |
| 239 | + |
| 240 | + # Memoize the provider's resolution. functools.cache can't wrap the async provider directly |
| 241 | + # (it would cache a coroutine, which can't be awaited twice), so we cache a sync factory that |
| 242 | + # returns the Task instead: the provider runs once and every caller awaits the same Task. |
| 243 | + @functools.cache |
| 244 | + def _codec_task() -> "asyncio.Task[JournalValueCodec]": |
| 245 | + assert callable(codec_config) |
| 246 | + return asyncio.ensure_future(codec_config()) |
| 247 | + |
| 248 | + async def get_codec() -> Optional[JournalValueCodec]: |
| 249 | + if codec_config is None or isinstance(codec_config, JournalValueCodec): |
| 250 | + # Nothing configured, or an already-built instance: nothing to await. |
| 251 | + return codec_config |
| 252 | + return await _codec_task() |
| 253 | + |
220 | 254 | def _on_sigterm() -> None: |
221 | 255 | """Notify all active receive channels of graceful shutdown.""" |
222 | 256 | for ch in active_channels: |
@@ -284,8 +318,14 @@ async def app(scope: Scope, receive: Receive, send: Send): |
284 | 318 | receive_channel = ReceiveChannel(receive) |
285 | 319 | active_channels.add(receive_channel) |
286 | 320 | try: |
| 321 | + journal_codec = await get_codec() |
287 | 322 | await process_invocation_to_completion( |
288 | | - VMWrapper(request_headers), handler, dict(request_headers), receive_channel, send |
| 323 | + VMWrapper(request_headers, disable_payload_checks=disable_payload_checks), |
| 324 | + handler, |
| 325 | + dict(request_headers), |
| 326 | + receive_channel, |
| 327 | + send, |
| 328 | + journal_codec=journal_codec, |
289 | 329 | ) |
290 | 330 | finally: |
291 | 331 | active_channels.discard(receive_channel) |
|
0 commit comments