44import dataclasses
55import json
66import logging
7+ import os
78from types import SimpleNamespace
8- from typing import Any , Optional , Sequence , Union
9+ from typing import Any , Optional , Sequence , Union , Iterable
910
1011import grpc
1112
2425INSECURE_PROTOCOLS = ["http://" , "grpc://" ]
2526
2627
28+ def _get_env_bool (name : str , default : bool ) -> bool :
29+ """ helper to convert the environment variable to a bool"""
30+ val = os .environ .get (name )
31+ if val is None :
32+ return default
33+ return val .strip ().lower () in {"1" , "true" , "t" , "yes" , "y" }
34+
35+
36+ def _get_env_int (name : str , default : int ) -> int :
37+ """ helper to convert the env var to an int or if we could not to the default value given """
38+ val = os .environ .get (name )
39+ if val is None :
40+ return default
41+ try :
42+ return int (val )
43+ except Exception :
44+ return default
45+
46+
47+ def _get_env_float (name : str , default : float ) -> float :
48+ """ helper to convert the env var to a float or if we could not to the default value given """
49+ val = os .environ .get (name )
50+ if val is None :
51+ return default
52+ try :
53+ return float (val )
54+ except Exception :
55+ return default
56+
57+
58+ def _get_env_csv (name : str , default_csv : str ) -> list [str ]:
59+ """ helper to convert the env var to a list or if we could not to the default value given """
60+ val = os .environ .get (name , default_csv )
61+ return [s .strip ().upper () for s in val .split ("," ) if s .strip ()]
62+
63+
64+ def get_grpc_keepalive_options () -> list [tuple [str , Any ]]:
65+ """Build gRPC keepalive channel options from environment variables.
66+
67+ Environment variables (defaults in parentheses):
68+ - DAPR_GRPC_KEEPALIVE_ENABLED (false)
69+ - DAPR_GRPC_KEEPALIVE_TIME_MS (120000)
70+ - DAPR_GRPC_KEEPALIVE_TIMEOUT_MS (20000)
71+ - DAPR_GRPC_KEEPALIVE_PERMIT_WITHOUT_CALLS (false)
72+ """
73+ enabled = _get_env_bool ("DAPR_GRPC_KEEPALIVE_ENABLED" , False )
74+ if not enabled :
75+ return []
76+ time_ms = _get_env_int ("DAPR_GRPC_KEEPALIVE_TIME_MS" , 120000 )
77+ timeout_ms = _get_env_int ("DAPR_GRPC_KEEPALIVE_TIMEOUT_MS" , 20000 )
78+ permit_without_calls = (
79+ 1 if _get_env_bool ("DAPR_GRPC_KEEPALIVE_PERMIT_WITHOUT_CALLS" , False ) else 0
80+ )
81+ return [
82+ ("grpc.keepalive_time_ms" , time_ms ),
83+ ("grpc.keepalive_timeout_ms" , timeout_ms ),
84+ ("grpc.keepalive_permit_without_calls" , permit_without_calls ),
85+ ]
86+
87+
88+ def get_grpc_retry_service_config_option () -> Optional [tuple [str , str ]]:
89+ """Return ("grpc.service_config", json_str) if retry is enabled via env; else None.
90+
91+ Environment variables (defaults in parentheses):
92+ - DAPR_GRPC_RETRY_ENABLED (false)
93+ - DAPR_GRPC_RETRY_MAX_ATTEMPTS (4)
94+ - DAPR_GRPC_RETRY_INITIAL_BACKOFF_MS (100)
95+ - DAPR_GRPC_RETRY_MAX_BACKOFF_MS (1000)
96+ - DAPR_GRPC_RETRY_BACKOFF_MULTIPLIER (2.0)
97+ - DAPR_GRPC_RETRY_CODES (UNAVAILABLE,DEADLINE_EXCEEDED)
98+ """
99+ enabled = _get_env_bool ("DAPR_GRPC_RETRY_ENABLED" , False )
100+ if not enabled :
101+ return None
102+
103+ max_attempts = _get_env_int ("DAPR_GRPC_RETRY_MAX_ATTEMPTS" , 4 )
104+ initial_backoff_ms = _get_env_int ("DAPR_GRPC_RETRY_INITIAL_BACKOFF_MS" , 100 )
105+ max_backoff_ms = _get_env_int ("DAPR_GRPC_RETRY_MAX_BACKOFF_MS" , 1000 )
106+ backoff_multiplier = _get_env_float ("DAPR_GRPC_RETRY_BACKOFF_MULTIPLIER" , 2.0 )
107+ codes = _get_env_csv ("DAPR_GRPC_RETRY_CODES" , "UNAVAILABLE,DEADLINE_EXCEEDED" )
108+
109+ # service_config ref => https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto#L44
110+ service_config = {
111+ "methodConfig" : [
112+ {
113+ "name" : [{"service" : "" }],
114+ "retryPolicy" : {
115+ "maxAttempts" : max_attempts ,
116+ "initialBackoff" : f"{ initial_backoff_ms / 1000.0 } s" ,
117+ "maxBackoff" : f"{ max_backoff_ms / 1000.0 } s" ,
118+ "backoffMultiplier" : backoff_multiplier ,
119+ "retryableStatusCodes" : codes ,
120+ },
121+ }
122+ ]
123+ }
124+ # we are not applying retry throttling policy (but a user can pass the whole option string via options)
125+ return "grpc.service_config" , json .dumps (service_config )
126+
127+
128+ def build_grpc_channel_options (
129+ base_options : Optional [Iterable [tuple [str , Any ]]] = None ,
130+ ) -> Optional [list [tuple [str , Any ]]]:
131+ """Combine base options + env-driven keepalive and retry service config.
132+
133+ The returned list is safe to pass as the `options` argument to grpc.secure_channel/insecure_channel.
134+ """
135+ combined : list [tuple [str , Any ]] = []
136+ if base_options :
137+ combined .extend (list (base_options ))
138+
139+ keepalive = get_grpc_keepalive_options ()
140+ if keepalive :
141+ combined .extend (keepalive )
142+ retry_opt = get_grpc_retry_service_config_option ()
143+ if retry_opt is not None :
144+ combined .append (retry_opt )
145+ return combined if combined else None
146+
147+
27148def get_default_host_address () -> str :
28149 """Resolve the default Durable Task sidecar address.
29150
@@ -53,7 +174,9 @@ def get_default_host_address() -> str:
53174def get_grpc_channel (
54175 host_address : Optional [str ],
55176 secure_channel : bool = False ,
56- interceptors : Optional [Sequence [ClientInterceptor ]] = None ) -> grpc .Channel :
177+ interceptors : Optional [Sequence [ClientInterceptor ]] = None ,
178+ options : Optional [Sequence [tuple [str , Any ]]] = None ,
179+ ) -> grpc .Channel :
57180 if host_address is None :
58181 host_address = get_default_host_address ()
59182
@@ -71,11 +194,22 @@ def get_grpc_channel(
71194 host_address = host_address [len (protocol ):]
72195 break
73196
197+ # Build channel options (merge provided options with env-driven keepalive/retry)
198+ channel_options = build_grpc_channel_options (options )
199+
74200 # Create the base channel
75201 if secure_channel :
76- channel = grpc .secure_channel (host_address , grpc .ssl_channel_credentials ())
202+ if channel_options is not None :
203+ channel = grpc .secure_channel (
204+ host_address , grpc .ssl_channel_credentials (), options = channel_options
205+ )
206+ else :
207+ channel = grpc .secure_channel (host_address , grpc .ssl_channel_credentials ())
77208 else :
78- channel = grpc .insecure_channel (host_address )
209+ if channel_options is not None :
210+ channel = grpc .insecure_channel (host_address , options = channel_options )
211+ else :
212+ channel = grpc .insecure_channel (host_address )
79213
80214 # Apply interceptors ONLY if they exist
81215 if interceptors :
0 commit comments