1+ import asyncio
2+ import contextlib
13import os
4+ import sys
25
36import aiorun
47import grpc
58
9+ from pynumaflow .info .server import write as info_server_write
610from pynumaflow .info .types import ContainerType , ServerInfo , MINIMUM_NUMAFLOW_VERSION
711from pynumaflow .sinker .servicer .async_servicer import AsyncSinkServicer
812from pynumaflow .proto .sinker import sink_pb2_grpc
2428 MAX_NUM_THREADS ,
2529)
2630
27- from pynumaflow .shared .server import NumaflowServer , start_async_server
31+ from pynumaflow .shared .server import NumaflowServer
2832from pynumaflow .sinker ._dtypes import SinkAsyncCallable
2933
3034
@@ -118,13 +122,17 @@ def __init__(
118122 ]
119123
120124 self .servicer = AsyncSinkServicer (sinker_instance )
125+ self ._error : BaseException | None = None
121126
122127 def start (self ):
123128 """
124129 Starter function for the Async server class, need a separate caller
125130 so that all the async coroutines can be started from a single context
126131 """
127132 aiorun .run (self .aexec (), use_uvloop = True , shutdown_callback = self .shutdown_callback )
133+ if self ._error :
134+ _LOGGER .critical ("Server exiting due to UDF error: %s" , self ._error )
135+ sys .exit (1 )
128136
129137 async def aexec (self ):
130138 """
@@ -133,17 +141,41 @@ async def aexec(self):
133141 # As the server is async, we need to create a new server instance in the
134142 # same thread as the event loop so that all the async calls are made in the
135143 # same context
136- # Create a new server instance, add the servicer to it and start the server
137144 server = grpc .aio .server (options = self ._server_options )
138145 server .add_insecure_port (self .sock_path )
146+
147+ # The asyncio.Event must be created here (inside aexec) rather than in __init__,
148+ # because it must be bound to the running event loop that aiorun creates.
149+ # At __init__ time no event loop exists yet.
150+ shutdown_event = asyncio .Event ()
151+ self .servicer .set_shutdown_event (shutdown_event )
152+
139153 sink_pb2_grpc .add_SinkServicer_to_server (self .servicer , server )
154+
140155 serv_info = ServerInfo .get_default_server_info ()
141156 serv_info .minimum_numaflow_version = MINIMUM_NUMAFLOW_VERSION [ContainerType .Sinker ]
142- await start_async_server (
143- server_async = server ,
144- sock_path = self .sock_path ,
145- max_threads = self .max_threads ,
146- cleanup_coroutines = list (),
147- server_info_file = self .server_info_file ,
148- server_info = serv_info ,
157+
158+ await server .start ()
159+ info_server_write (server_info = serv_info , info_file = self .server_info_file )
160+
161+ _LOGGER .info (
162+ "Async GRPC Server listening on: %s with max threads: %s" ,
163+ self .sock_path ,
164+ self .max_threads ,
149165 )
166+
167+ async def _watch_for_shutdown ():
168+ """Wait for the shutdown event and stop the server with a grace period."""
169+ await shutdown_event .wait ()
170+ _LOGGER .info ("Shutdown signal received, stopping server gracefully..." )
171+ await server .stop (5 )
172+
173+ shutdown_task = asyncio .create_task (_watch_for_shutdown ())
174+ await server .wait_for_termination ()
175+
176+ # Propagate error so start() can exit with a non-zero code
177+ self ._error = self .servicer ._error
178+
179+ shutdown_task .cancel ()
180+ with contextlib .suppress (asyncio .CancelledError ):
181+ await shutdown_task
0 commit comments