@@ -67,10 +67,12 @@ def __init__(
6767 logger .debug (self .config .fatal_status_codes )
6868
6969 self .retry_grace_period = config .retry_grace_period
70+ self .retry_backoff_max_seconds = config .retry_backoff_max_ms * 0.001
7071 self .streamline_deadline_seconds = config .stream_deadline_ms * 0.001
7172 self .deadline = config .deadline_ms * 0.001
7273 self .connected = False
7374 self ._is_fatal = False
75+ self ._shutdown_event = threading .Event ()
7476 self .channel = self ._generate_channel (config )
7577 self .stub = evaluation_pb2_grpc .ServiceStub (self .channel )
7678 self .selector_metadata : tuple [tuple [str , str ], ...] | None = (
@@ -140,6 +142,7 @@ def initialize(self, evaluation_context: EvaluationContext) -> None:
140142
141143 def shutdown (self ) -> None :
142144 self .active = False
145+ self ._shutdown_event .set ()
143146 self .channel .unsubscribe (self ._state_change_callback )
144147 self .channel .close ()
145148 if self .timer and self .timer .is_alive ():
@@ -150,6 +153,7 @@ def shutdown(self) -> None:
150153
151154 def connect (self ) -> None :
152155 self .active = True
156+ self ._shutdown_event .clear ()
153157
154158 # Run monitoring in a separate thread
155159 self .monitor_thread = threading .Thread (
@@ -228,6 +232,41 @@ def _stream_call_args(self) -> GrpcMultiCallableArgs:
228232 call_args ["metadata" ] = self .selector_metadata
229233 return call_args
230234
235+ def _wait_before_reconnect (self ) -> None :
236+ self ._shutdown_event .wait (self .retry_backoff_max_seconds )
237+
238+ def _handle_rpc_error (self , e : grpc .RpcError ) -> bool :
239+ # code() blocks until final status, finalizing the dead RPC before reconnect; keep it, do not inline into logging only
240+ # https://grpc.github.io/grpc/python/grpc.html#grpc.Call.code
241+ code = e .code ()
242+ if code .name in self .config .fatal_status_codes :
243+ logger .error (f"EventStream fatal error, { code = } { e .details ()= } " )
244+ self ._is_fatal = True
245+ self .active = False
246+ self .emit_provider_error (
247+ ProviderEventDetails (
248+ message = f"Fatal gRPC status code: { code } " ,
249+ error_code = ErrorCode .PROVIDER_FATAL ,
250+ )
251+ )
252+ return True
253+ # non-fatal errors just reconnect; real loss surfaces as a STALE, and eventually, ERROR event
254+ logger .debug (f"EventStream error, reconnecting, { code = } { e .details ()= } " )
255+ return False
256+
257+ def _handle_event_stream_message (
258+ self , message : evaluation_pb2 .EventStreamResponse
259+ ) -> None :
260+ if message .type == "provider_ready" :
261+ self .emit_provider_ready (
262+ ProviderEventDetails (message = "gRPC sync connection established" )
263+ )
264+ self .connected = True
265+ elif message .type == "configuration_change" :
266+ msg_dict = MessageToDict (message )
267+ data = msg_dict .get ("data" , {})
268+ self .handle_changed_flags (data )
269+
231270 def listen (self ) -> None :
232271 logger .debug ("gRPC starting listener thread" )
233272 call_args = self ._stream_call_args ()
@@ -238,38 +277,20 @@ def listen(self) -> None:
238277 try :
239278 logger .debug ("Setting up gRPC sync flags connection" )
240279 for message in self .stub .EventStream (request , ** call_args ):
241- if message .type == "provider_ready" :
242- self .emit_provider_ready (
243- ProviderEventDetails (
244- message = "gRPC sync connection established"
245- )
246- )
247- self .connected = True
248- elif message .type == "configuration_change" :
249- msg_dict = MessageToDict (message )
250- data = msg_dict .get ("data" , {})
251- self .handle_changed_flags (data )
280+ self ._handle_event_stream_message (message )
252281
253282 if not self .active :
254283 logger .info ("Terminating gRPC sync thread" )
255284 return
256- except grpc .RpcError as e : # noqa: PERF203
257- # although it seems like this error log is not interesting, without it, the retry is not working as expected
258- logger .debug (f"SyncFlags stream error, { e .code ()= } { e .details ()= } " )
259- if e .code ().name in self .config .fatal_status_codes :
260- self ._is_fatal = True
261- self .active = False
262- self .emit_provider_error (
263- ProviderEventDetails (
264- message = f"Fatal gRPC status code: { e .code ()} " ,
265- error_code = ErrorCode .PROVIDER_FATAL ,
266- )
267- )
285+ except grpc .RpcError as e :
286+ if self ._handle_rpc_error (e ):
268287 return
269288 except ParseError :
270289 logger .exception (
271290 f"Could not parse flag data using flagd syntax: { message = } "
272291 )
292+ if self .active :
293+ self ._wait_before_reconnect ()
273294
274295 def handle_changed_flags (self , data : typing .Any ) -> None :
275296 changed_flags = list (data .get ("flags" , {}).keys ())
0 commit comments