Skip to content

Commit 347bacc

Browse files
Merge pull request #432 from Ratio1/develop
fix: preserve wrapper async request ids
2 parents 4190cc6 + 0dff933 commit 347bacc

7 files changed

Lines changed: 167 additions & 11 deletions

File tree

extensions/business/edge_inference_api/base_inference_api.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2596,6 +2596,39 @@ def maybe_mark_request_timeout(self, request_id: str, request_data: Dict[str, An
25962596
self._decrement_active_requests()
25972597
return True
25982598

2599+
def _extract_request_id_override(self, kwargs: Dict[str, Any]):
2600+
"""Extract an optional caller-provided request id from endpoint kwargs.
2601+
2602+
Existing callers do not pass a request id and keep the generated-id path.
2603+
New paired apps may pass `request_id` so wrapper and inference tracking use
2604+
the same id without a separate mapping store.
2605+
"""
2606+
if not isinstance(kwargs, dict):
2607+
return None
2608+
values = []
2609+
for key in ('request_id', 'REQUEST_ID'):
2610+
if key in kwargs:
2611+
value = kwargs.pop(key)
2612+
if value is not None:
2613+
values.append(value)
2614+
if not values:
2615+
return None
2616+
request_id = values[0]
2617+
for value in values[1:]:
2618+
if value != request_id:
2619+
raise ValueError("Conflicting request_id and REQUEST_ID values.")
2620+
if not isinstance(request_id, str):
2621+
raise ValueError("request_id must be a string.")
2622+
request_id = request_id.strip()
2623+
if not request_id:
2624+
raise ValueError("request_id must not be empty.")
2625+
if len(request_id) > 256:
2626+
raise ValueError("request_id must not exceed 256 characters.")
2627+
allowed_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._:-")
2628+
if any(ch not in allowed_chars for ch in request_id):
2629+
raise ValueError("request_id contains unsupported characters.")
2630+
return request_id
2631+
25992632
def solve_postponed_request(self, request_id: str):
26002633
"""
26012634
Resolve or requeue a postponed request by checking its current status.
@@ -2666,6 +2699,8 @@ def register_request(
26662699
Generated request_id and the stored request data dictionary.
26672700
"""
26682701
request_id = request_id or self.uuid()
2702+
if request_id in self._requests:
2703+
raise ValueError(f"Request ID {request_id} already exists.")
26692704
start_time = self.time()
26702705
request_data = {
26712706
"request_id": request_id,
@@ -2857,6 +2892,7 @@ def predict(
28572892
def predict_async(
28582893
self,
28592894
authorization: Optional[str] = None,
2895+
request_id: Optional[str] = None,
28602896
**kwargs
28612897
):
28622898
"""
@@ -2866,6 +2902,9 @@ def predict_async(
28662902
----------
28672903
authorization : str or None, optional
28682904
Authorization token supplied by the caller.
2905+
request_id : str or None, optional
2906+
Caller-provided id to use for request tracking. If omitted, the API
2907+
keeps the legacy generated-id behavior.
28692908
**kwargs
28702909
Additional parameters forwarded to request handling.
28712910
@@ -2877,6 +2916,7 @@ def predict_async(
28772916
return self._predict_entrypoint(
28782917
authorization=authorization,
28792918
async_request=True,
2919+
request_id=request_id,
28802920
**kwargs
28812921
)
28822922
"""END API ENDPOINTS"""
@@ -2992,6 +3032,20 @@ def _predict_entrypoint(
29923032
return {'error': f"Unexpected error: {str(exc)}", 'status': 'error'}
29933033
# endtry
29943034

3035+
request_id_override = None
3036+
if delegated_execution:
3037+
request_id_override = delegation_context.get('delegation_id')
3038+
elif async_request:
3039+
try:
3040+
request_id_override = self._extract_request_id_override(kwargs)
3041+
except ValueError as exc:
3042+
return {'error': str(exc), 'status': 'error'}
3043+
else:
3044+
# `request_id` is reserved for async tracking and must not leak into
3045+
# synchronous model parameters when clients reuse async payload shapes.
3046+
kwargs.pop('request_id', None)
3047+
kwargs.pop('REQUEST_ID', None)
3048+
29953049
err = self.check_predict_params(**kwargs)
29963050
if err is not None:
29973051
return {'error': err}
@@ -3000,16 +3054,16 @@ def _predict_entrypoint(
30003054
if 'metadata' in parameters:
30013055
metadata = parameters.pop('metadata') or {}
30023056
# endif 'metadata' in parameters
3003-
request_id_override = None
3004-
if delegated_execution:
3005-
request_id_override = delegation_context.get('delegation_id')
3006-
request_id, request_data = self.register_request(
3007-
subject=subject,
3008-
parameters=parameters,
3009-
metadata=metadata,
3010-
timeout=parameters.get('timeout'),
3011-
request_id=request_id_override,
3012-
)
3057+
try:
3058+
request_id, request_data = self.register_request(
3059+
subject=subject,
3060+
parameters=parameters,
3061+
metadata=metadata,
3062+
timeout=parameters.get('timeout'),
3063+
request_id=request_id_override,
3064+
)
3065+
except ValueError as exc:
3066+
return {'error': str(exc), 'status': 'error'}
30133067
request_data['endpoint_name'] = endpoint_name
30143068
request_data['async_request'] = async_request
30153069

extensions/business/edge_inference_api/cv_inference_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def predict_async(
236236
image_data: str = '',
237237
metadata: Optional[Dict[str, Any]] = None,
238238
authorization: Optional[str] = None,
239+
request_id: Optional[str] = None,
239240
**kwargs
240241
):
241242
"""
@@ -249,6 +250,9 @@ def predict_async(
249250
Optional metadata accompanying the request.
250251
authorization : str or None, optional
251252
Bearer token used for authentication.
253+
request_id : str or None, optional
254+
Caller-provided id to use for request tracking. If omitted, the API
255+
keeps the legacy generated-id behavior.
252256
**kwargs
253257
Extra parameters forwarded to the base handler.
254258
@@ -261,6 +265,7 @@ def predict_async(
261265
image_data=image_data,
262266
metadata=metadata,
263267
authorization=authorization,
268+
request_id=request_id,
264269
**kwargs
265270
)
266271
"""END API ENDPOINTS"""

extensions/business/edge_inference_api/llm_inference_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ def predict_async(
399399
response_format: Optional[Dict[str, Any]] = None,
400400
metadata: Optional[Dict[str, Any]] = None,
401401
authorization: Optional[str] = None,
402+
request_id: Optional[str] = None,
402403
**kwargs
403404
):
404405
"""
@@ -422,6 +423,9 @@ def predict_async(
422423
Additional metadata to store with the request.
423424
authorization : str or None, optional
424425
Bearer token used for authentication.
426+
request_id : str or None, optional
427+
Caller-provided id to use for request tracking. If omitted, the API
428+
keeps the legacy generated-id behavior.
425429
**kwargs
426430
Extra parameters forwarded to the base handler.
427431
@@ -439,6 +443,7 @@ def predict_async(
439443
response_format=response_format,
440444
metadata=metadata,
441445
authorization=authorization,
446+
request_id=request_id,
442447
**kwargs
443448
)
444449

extensions/business/edge_inference_api/sd_inference_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ def predict_async(
283283
struct_data: Any = None,
284284
metadata: Optional[Dict[str, Any]] = None,
285285
authorization: Optional[str] = None,
286+
request_id: Optional[str] = None,
286287
**kwargs
287288
):
288289
"""
@@ -296,6 +297,9 @@ def predict_async(
296297
Optional metadata accompanying the request.
297298
authorization : str or None, optional
298299
Bearer token used for authentication.
300+
request_id : str or None, optional
301+
Caller-provided id to use for request tracking. If omitted, the API
302+
keeps the legacy generated-id behavior.
299303
**kwargs
300304
Extra parameters forwarded to the base handler.
301305
@@ -308,6 +312,7 @@ def predict_async(
308312
struct_data=struct_data,
309313
metadata=metadata,
310314
authorization=authorization,
315+
request_id=request_id,
311316
**kwargs
312317
)
313318
"""END API ENDPOINTS"""

extensions/business/edge_inference_api/test_base_inference_api_balancing.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,88 @@ def test_predict_entrypoint_queues_when_full_and_no_peer(self):
580580
request_id = result["request_id"]
581581
self.assertEqual(plugin._requests[request_id]["queue_state"], "queued") # pylint: disable=protected-access
582582

583+
def test_predict_entrypoint_accepts_caller_request_id(self):
584+
plugin = self._make_plugin()
585+
586+
result = plugin._predict_entrypoint( # pylint: disable=protected-access
587+
authorization=None,
588+
async_request=True,
589+
request_id="client-req_1.2:3",
590+
metadata={"source": "test"},
591+
)
592+
593+
self.assertEqual(result["request_id"], "client-req_1.2:3")
594+
self.assertIn("client-req_1.2:3", plugin._requests) # pylint: disable=protected-access
595+
self.assertEqual(plugin.payloads[-1]["REQUEST_ID"], "client-req_1.2:3")
596+
self.assertNotIn("request_id", plugin._requests["client-req_1.2:3"]["parameters"]) # pylint: disable=protected-access
597+
598+
def test_predict_async_without_request_id_keeps_generated_id_behavior(self):
599+
plugin = self._make_plugin()
600+
601+
result = plugin.predict_async(authorization=None, request_id=None)
602+
603+
self.assertEqual(result["request_id"], "req-1")
604+
self.assertIn("req-1", plugin._requests) # pylint: disable=protected-access
605+
606+
def test_sync_predict_does_not_treat_request_id_as_tracking_override(self):
607+
plugin = self._make_plugin()
608+
609+
plugin._predict_entrypoint( # pylint: disable=protected-access
610+
authorization=None,
611+
async_request=False,
612+
request_id="client-sync-id",
613+
REQUEST_ID="client-sync-id-upper",
614+
)
615+
616+
self.assertIn("req-1", plugin._requests) # pylint: disable=protected-access
617+
self.assertNotIn("client-sync-id", plugin._requests) # pylint: disable=protected-access
618+
self.assertNotIn("request_id", plugin._requests["req-1"]["parameters"]) # pylint: disable=protected-access
619+
self.assertNotIn("REQUEST_ID", plugin._requests["req-1"]["parameters"]) # pylint: disable=protected-access
620+
self.assertEqual(plugin.payloads[-1]["REQUEST_ID"], "req-1")
621+
622+
def test_predict_entrypoint_accepts_uppercase_request_id_alias(self):
623+
plugin = self._make_plugin()
624+
625+
result = plugin._predict_entrypoint( # pylint: disable=protected-access
626+
authorization=None,
627+
async_request=True,
628+
REQUEST_ID="client-req-2",
629+
)
630+
631+
self.assertEqual(result["request_id"], "client-req-2")
632+
self.assertIn("client-req-2", plugin._requests) # pylint: disable=protected-access
633+
634+
def test_predict_entrypoint_rejects_duplicate_caller_request_id(self):
635+
plugin = self._make_plugin()
636+
637+
first = plugin._predict_entrypoint( # pylint: disable=protected-access
638+
authorization=None,
639+
async_request=True,
640+
request_id="client-req-dup",
641+
)
642+
second = plugin._predict_entrypoint( # pylint: disable=protected-access
643+
authorization=None,
644+
async_request=True,
645+
request_id="client-req-dup",
646+
)
647+
648+
self.assertEqual(first["request_id"], "client-req-dup")
649+
self.assertEqual(second["status"], "error")
650+
self.assertIn("already exists", second["error"])
651+
652+
def test_predict_entrypoint_rejects_invalid_caller_request_id(self):
653+
plugin = self._make_plugin()
654+
655+
result = plugin._predict_entrypoint( # pylint: disable=protected-access
656+
authorization=None,
657+
async_request=True,
658+
request_id="../bad",
659+
)
660+
661+
self.assertEqual(result["status"], "error")
662+
self.assertIn("unsupported characters", result["error"])
663+
self.assertNotIn("../bad", plugin._requests) # pylint: disable=protected-access
664+
583665
def test_predict_entrypoint_fails_cleanly_when_delegated_request_cannot_encode(self):
584666
plugin = self._make_plugin()
585667
plugin._active_execution_slots.add("busy") # pylint: disable=protected-access

extensions/business/edge_inference_api/text_classifier_inference_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def predict_async(
276276
text: str = "",
277277
metadata: Optional[Dict[str, Any]] = None,
278278
authorization: Optional[str] = None,
279+
request_id: Optional[str] = None,
279280
**kwargs
280281
):
281282
"""
@@ -289,6 +290,9 @@ def predict_async(
289290
Optional metadata accompanying the request.
290291
authorization : str or None, optional
291292
Bearer token used for authentication.
293+
request_id : str or None, optional
294+
Caller-provided id to use for request tracking. If omitted, the API
295+
keeps the legacy generated-id behavior.
292296
**kwargs
293297
Extra parameters forwarded to the base handler.
294298
@@ -301,6 +305,7 @@ def predict_async(
301305
text=text,
302306
metadata=metadata,
303307
authorization=authorization,
308+
request_id=request_id,
304309
**kwargs
305310
)
306311
"""END API ENDPOINTS"""

ver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__VER__ = '2.10.290'
1+
__VER__ = '2.10.300'

0 commit comments

Comments
 (0)