From 8595b425a96611d48fe0881823f8d37ea3353f12 Mon Sep 17 00:00:00 2001 From: zirongluo Date: Mon, 8 Jun 2026 12:09:52 +0800 Subject: [PATCH 01/11] [http] cancel --- .../reinforcement/component/server/server.py | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/finetune/reinforcement/component/server/server.py index 02330d8..e913dbf 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/finetune/reinforcement/component/server/server.py @@ -37,6 +37,7 @@ GET /health Health check """ +import asyncio import logging import os import time @@ -72,7 +73,7 @@ "0", "no", ) -_THREAD_POOL_WORKERS = int(os.getenv("THREAD_POOL_WORKERS", "4")) +_THREAD_POOL_WORKERS = int(os.getenv("THREAD_POOL_WORKERS", "32")) _THREAD_POOL_QUEUE = int(os.getenv("THREAD_POOL_QUEUE", "100")) if not _ENABLE_LOGGING: @@ -386,6 +387,10 @@ async def handle_endpoint(request: Request) -> JSONResponse: request body, executes business logic using configured processor, and returns serialized result. + Monitors client connection state via ASGI disconnect messages. If the + client disconnects before processing completes, the processing task is + cancelled to avoid wasting resources. + Request body format: JSON, fields determined by FuncType: - reward: See RewardInput - rollout: See RolloutInput @@ -398,8 +403,27 @@ async def handle_endpoint(request: Request) -> JSONResponse: """ start_time = time.time() success = False + cancelled = False processor_input = None + # --- Disconnect detection setup --- + disconnected = asyncio.Event() + + async def _listen_for_disconnect(): + """Background listener for ASGI disconnect messages.""" + try: + while True: + message = await request.receive() + if message.get("type") == "http.disconnect": + disconnected.set() + break + except Exception: + # If receive() raises (e.g. connection already closed), + # treat as disconnected. + disconnected.set() + + disconnect_listener = asyncio.create_task(_listen_for_disconnect()) + # Extract trace context from request headers _otel_ctx_token, _upstream_tokens = await _extract_trace_context(request) @@ -413,8 +437,36 @@ async def handle_endpoint(request: Request) -> JSONResponse: # Check thread pool queue capacity await _check_queue_capacity() - # Execute processor - result = await func_manager.processes(processor_input) + # Execute processor as a task so we can cancel it on disconnect + process_task = asyncio.create_task( + func_manager.processes(processor_input), + ) + + # Wait for either the processing to finish or client disconnect + done, _pending = await asyncio.wait( + [process_task, disconnect_listener], + return_when=asyncio.FIRST_COMPLETED, + ) + + if disconnected.is_set(): + # Client disconnected — cancel the processing task + process_task.cancel() + try: + await process_task + except asyncio.CancelledError: + pass + cancelled = True + logger.warning( + "[Server] Client disconnected during processing, " + "task cancelled.", + ) + return JSONResponse( + status_code=499, + content={"message": "Client disconnected, request cancelled."}, + ) + + # Processing completed normally + result = process_task.result() success = True # Serialize result @@ -425,6 +477,13 @@ async def handle_endpoint(request: Request) -> JSONResponse: except HTTPException: # Re-raise HTTP exceptions as-is raise + except asyncio.CancelledError: + cancelled = True + logger.warning("[Server] Request processing was cancelled.") + return JSONResponse( + status_code=499, + content={"message": "Request cancelled."}, + ) except Exception as ex: logger.error(f"[Server] Unexpected error: {ex}", exc_info=True) return JSONResponse( @@ -432,6 +491,13 @@ async def handle_endpoint(request: Request) -> JSONResponse: content={"message": str(ex)}, ) finally: + # Cancel the disconnect listener if still running + disconnect_listener.cancel() + try: + await disconnect_listener + except asyncio.CancelledError: + pass + # Clean up trace context await _cleanup_trace_context(_otel_ctx_token, _upstream_tokens) @@ -439,7 +505,8 @@ async def handle_endpoint(request: Request) -> JSONResponse: elapsed = round(time.time() - start_time, 4) logger.info( f"[Server] /api/v1 | func_type={func_type.value} | " - f"success={success} | elapsed={elapsed}s", + f"success={success} | cancelled={cancelled} | " + f"elapsed={elapsed}s", ) # Best-effort force flush based on platform/internal env config From ed83a14ee992e6c9c2b555dfed283785d5b1a6d8 Mon Sep 17 00:00:00 2001 From: zirongluo Date: Thu, 11 Jun 2026 12:04:08 +0800 Subject: [PATCH 02/11] [log] request id --- .../finetune/reinforcement/common/utils.py | 40 +++++++++++++++++++ .../reinforcement/component/server/server.py | 14 ++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/dashscope/finetune/reinforcement/common/utils.py b/dashscope/finetune/reinforcement/common/utils.py index d6ee0f7..9f0c491 100644 --- a/dashscope/finetune/reinforcement/common/utils.py +++ b/dashscope/finetune/reinforcement/common/utils.py @@ -1003,3 +1003,43 @@ def serialize_for_output(data: Any) -> Any: # Return basic types directly return data + + +def get_fc_request_id(request) -> str: + """Extract Function Compute request ID from request headers. + + Retrieves the 'x-fc-request-id' header from a FastAPI/Starlette Request + object. This is useful for correlating logs with specific FC invocations. + + Args: + request: FastAPI/Starlette Request object (or any object with a + `.headers` mapping). + + Returns: + The FC request ID string, or "unknown" if the header is not present. + """ + if request is None: + return "unknown" + return request.headers.get("x-fc-request-id", "unknown") + + +def get_business_summary(processor_input) -> str: + """Extract summary business information from processor input. + + Returns the string representation of request_metadata if present, + otherwise returns an empty string. + + Args: + processor_input: The processor input object (BaseDataModel subclass). + + Returns: + String representation of request_metadata, or empty string. + """ + if processor_input is None: + return "" + + request_metadata = getattr(processor_input, "request_metadata", None) + if request_metadata is None: + return "" + + return str(request_metadata) diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/finetune/reinforcement/component/server/server.py index e913dbf..d43d056 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/finetune/reinforcement/component/server/server.py @@ -48,6 +48,10 @@ from fastapi.responses import JSONResponse from dashscope.finetune.reinforcement.common.log import logger +from dashscope.finetune.reinforcement.common.utils import ( + get_fc_request_id, + get_business_summary, +) from dashscope.finetune.reinforcement.common.model_types import ( FunctionType as FuncType, ) @@ -456,9 +460,11 @@ async def _listen_for_disconnect(): except asyncio.CancelledError: pass cancelled = True + fc_request_id = get_fc_request_id(request) logger.warning( "[Server] Client disconnected during processing, " - "task cancelled.", + "task cancelled. x-fc-request-id: %s", + fc_request_id, ) return JSONResponse( status_code=499, @@ -503,10 +509,14 @@ async def _listen_for_disconnect(): # Log request metrics elapsed = round(time.time() - start_time, 4) + fc_req_id = get_fc_request_id(request) + biz_summary = get_business_summary(processor_input) + biz_part = f" | {biz_summary}" if biz_summary else "" logger.info( f"[Server] /api/v1 | func_type={func_type.value} | " + f"fc_request_id={fc_req_id} | " f"success={success} | cancelled={cancelled} | " - f"elapsed={elapsed}s", + f"elapsed={elapsed}s{biz_part}", ) # Best-effort force flush based on platform/internal env config From b9518999c8266de058a443fc87a291e3f8108649 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Mon, 22 Jun 2026 17:10:43 +0800 Subject: [PATCH 03/11] Preserve error codes and surface root cause in agentic RL errors - Re-raise exceptions that already carry an error_code instead of wrapping them, so the original code propagates to callers - Add _format_cause() to AgenticRLError, RuntimeErrorWithCode and ValueErrorWithCode so __str__ includes the root cause type and message when the exception was triggered by another error - Let OSSConnectionError escape FunctionComponentModel without being re-wrapped Co-Authored-By: Claude Opus 4.7 --- dashscope/finetune/agentic_rl.py | 6 ++++ .../finetune/reinforcement/common/errors.py | 30 +++++++++++++++++-- .../finetune/reinforcement/common/model.py | 4 +++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index fe3b4c6..4f24f4a 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -106,6 +106,8 @@ async def register_functions( ) logger.info("Function components registered") except Exception as e: + if hasattr(e, 'error_code'): + raise raise RegistrationError( "Function registration failed", error_code=3002, @@ -249,6 +251,8 @@ def submit_job( **kwargs, ) except Exception as e: + if hasattr(e, 'error_code'): + raise raise RuntimeErrorWithCode( "Job submission failed", error_code=3005, @@ -312,6 +316,8 @@ async def run( **kwargs, ) except Exception as e: + if hasattr(e, 'error_code'): + raise raise RuntimeErrorWithCode( "RL tuning workflow failed", error_code=3006, diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/finetune/reinforcement/common/errors.py index 288c934..ae1c1a2 100644 --- a/dashscope/finetune/reinforcement/common/errors.py +++ b/dashscope/finetune/reinforcement/common/errors.py @@ -23,8 +23,16 @@ def root_cause(self) -> Exception: root = root.__cause__ return root + def _format_cause(self) -> str: + """Format root cause information if available.""" + if self.__cause__ is not None and self is not self.root_cause: + root = self.root_cause + cause_msg = str(root).split("\n")[0][:100] + return f" (caused by: {type(root).__name__}: {cause_msg})" + return "" + def __str__(self): - return f"[{self.error_code}] {self.message} (at {self.timestamp})" + return f"[{self.error_code}] {self.message} (at {self.timestamp}){self._format_cause()}" class IOErrorWithCode(AgenticRLError): @@ -58,8 +66,16 @@ def root_cause(self) -> Exception: root = root.__cause__ return root + def _format_cause(self) -> str: + """Format root cause information if available.""" + if self.__cause__ is not None and self is not self.root_cause: + root = self.root_cause + cause_msg = str(root).split("\n")[0][:100] + return f" (caused by: {type(root).__name__}: {cause_msg})" + return "" + def __str__(self): - return f"[{self.error_code}] {self.message}" + return f"[{self.error_code}] {self.message}{self._format_cause()}" class ValueErrorWithCode(ValueError): @@ -78,8 +94,16 @@ def root_cause(self) -> Exception: root = root.__cause__ return root + def _format_cause(self) -> str: + """Format root cause information if available.""" + if self.__cause__ is not None and self is not self.root_cause: + root = self.root_cause + cause_msg = str(root).split("\n")[0][:100] + return f" (caused by: {type(root).__name__}: {cause_msg})" + return "" + def __str__(self): - return f"[{self.error_code}] {self.message}" + return f"[{self.error_code}] {self.message}{self._format_cause()}" class InputError(AgenticRLError): diff --git a/dashscope/finetune/reinforcement/common/model.py b/dashscope/finetune/reinforcement/common/model.py index 7313396..0dede60 100644 --- a/dashscope/finetune/reinforcement/common/model.py +++ b/dashscope/finetune/reinforcement/common/model.py @@ -348,6 +348,8 @@ async def get_oss(self, oss_id: Optional[str] = None) -> str: ) return self.oss_signed_url + except OSSConnectionError: + raise except Exception as e: raise OSSConnectionError( "Failed to obtain OSS URL", @@ -1243,6 +1245,8 @@ async def register_functions( ) except Exception as e: + if hasattr(e, 'error_code'): + raise raise RegistrationError( "Function component registration failed", error_code=2055, From dad233b9cb1b5be0c0c00d5050066ff22da7a949 Mon Sep 17 00:00:00 2001 From: luozirong2025 <798138983@qq.com> Date: Mon, 22 Jun 2026 18:00:20 +0800 Subject: [PATCH 04/11] Update dashscope/finetune/reinforcement/component/server/server.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- dashscope/finetune/reinforcement/component/server/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/finetune/reinforcement/component/server/server.py index d43d056..0b47e02 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/finetune/reinforcement/component/server/server.py @@ -410,7 +410,7 @@ async def handle_endpoint(request: Request) -> JSONResponse: cancelled = False processor_input = None - # --- Disconnect detection setup --- + disconnect_listener = None disconnected = asyncio.Event() async def _listen_for_disconnect(): @@ -426,8 +426,6 @@ async def _listen_for_disconnect(): # treat as disconnected. disconnected.set() - disconnect_listener = asyncio.create_task(_listen_for_disconnect()) - # Extract trace context from request headers _otel_ctx_token, _upstream_tokens = await _extract_trace_context(request) From 64c70aa24712b4d7f78a9a74e02e5e9e86a88073 Mon Sep 17 00:00:00 2001 From: luozirong2025 <798138983@qq.com> Date: Mon, 22 Jun 2026 18:00:52 +0800 Subject: [PATCH 05/11] Update dashscope/finetune/reinforcement/common/utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- dashscope/finetune/reinforcement/common/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dashscope/finetune/reinforcement/common/utils.py b/dashscope/finetune/reinforcement/common/utils.py index 9efa4f2..f0915c9 100644 --- a/dashscope/finetune/reinforcement/common/utils.py +++ b/dashscope/finetune/reinforcement/common/utils.py @@ -1021,7 +1021,10 @@ def get_fc_request_id(request) -> str: """ if request is None: return "unknown" - return request.headers.get("x-fc-request-id", "unknown") + headers = getattr(request, "headers", None) + if headers is not None and hasattr(headers, "get"): + return headers.get("x-fc-request-id", "unknown") + return "unknown" def get_business_summary(processor_input) -> str: From cbbf03f8c712c116774911bc11bccc46c54417d5 Mon Sep 17 00:00:00 2001 From: luozirong2025 <798138983@qq.com> Date: Mon, 22 Jun 2026 18:01:10 +0800 Subject: [PATCH 06/11] Update dashscope/finetune/reinforcement/component/server/server.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../finetune/reinforcement/component/server/server.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/finetune/reinforcement/component/server/server.py index 0b47e02..599fe99 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/finetune/reinforcement/component/server/server.py @@ -496,11 +496,12 @@ async def _listen_for_disconnect(): ) finally: # Cancel the disconnect listener if still running - disconnect_listener.cancel() - try: - await disconnect_listener - except asyncio.CancelledError: - pass + if disconnect_listener is not None: + disconnect_listener.cancel() + try: + await disconnect_listener + except asyncio.CancelledError: + pass # Clean up trace context await _cleanup_trace_context(_otel_ctx_token, _upstream_tokens) From c925f24b84780652d4a2730555ab0d9c2593ffba Mon Sep 17 00:00:00 2001 From: zirongluo Date: Tue, 23 Jun 2026 10:38:39 +0800 Subject: [PATCH 07/11] Fix race condition and DRY refactor in AgenticRL - Fix critical race condition: delay disconnect listener until after request body parsing to prevent concurrent receive() calls - Refactor error classes: extract _RootCauseMixin to eliminate duplicated _format_cause and root_cause methods - Extract helper functions to reduce function complexity - Fix pre-commit issues: mypy types, flake8 line length, pylint warnings Addresses PR #152 review feedback from Gemini Code Assist. Co-Authored-By: Claude Opus 4.5 --- .../finetune/reinforcement/common/errors.py | 64 +++++--------- .../reinforcement/component/server/server.py | 85 ++++++++++++++----- 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/finetune/reinforcement/common/errors.py index ae1c1a2..8966a66 100644 --- a/dashscope/finetune/reinforcement/common/errors.py +++ b/dashscope/finetune/reinforcement/common/errors.py @@ -7,18 +7,13 @@ from typing import Optional, Dict -class AgenticRLError(Exception): - """Base class for all Agentic RL exceptions.""" - - def __init__(self, message: str, error_code: int = 1000): - super().__init__(message) - self.error_code = error_code - self.timestamp = datetime.now().isoformat() - self.message = message +class _RootCauseMixin: + """Mixin that provides root-cause traversal and formatting for exceptions + that carry an error code.""" @property - def root_cause(self) -> Exception: - root = self + def root_cause(self) -> "Exception": + root: "Exception" = self # type: ignore[assignment] while root.__cause__: root = root.__cause__ return root @@ -27,12 +22,23 @@ def _format_cause(self) -> str: """Format root cause information if available.""" if self.__cause__ is not None and self is not self.root_cause: root = self.root_cause - cause_msg = str(root).split("\n")[0][:100] + cause_msg = str(root).split("\n", maxsplit=1)[0][:100] return f" (caused by: {type(root).__name__}: {cause_msg})" return "" + +class AgenticRLError(_RootCauseMixin, Exception): + """Base class for all Agentic RL exceptions.""" + + def __init__(self, message: str, error_code: int = 1000): + super().__init__(message) + self.error_code = error_code + self.timestamp = datetime.now().isoformat() + self.message = message + def __str__(self): - return f"[{self.error_code}] {self.message} (at {self.timestamp}){self._format_cause()}" + base = f"[{self.error_code}] {self.message} (at {self.timestamp})" + return f"{base}{self._format_cause()}" class IOErrorWithCode(AgenticRLError): @@ -50,7 +56,7 @@ def __init__( self.operation = operation -class RuntimeErrorWithCode(RuntimeError): +class RuntimeErrorWithCode(_RootCauseMixin, RuntimeError): """Enhanced RuntimeError that supports error codes for better error categorization.""" @@ -59,26 +65,11 @@ def __init__(self, message: str, error_code: int = 0): self.error_code = error_code self.message = message - @property - def root_cause(self) -> Exception: - root = self - while root.__cause__: - root = root.__cause__ - return root - - def _format_cause(self) -> str: - """Format root cause information if available.""" - if self.__cause__ is not None and self is not self.root_cause: - root = self.root_cause - cause_msg = str(root).split("\n")[0][:100] - return f" (caused by: {type(root).__name__}: {cause_msg})" - return "" - def __str__(self): return f"[{self.error_code}] {self.message}{self._format_cause()}" -class ValueErrorWithCode(ValueError): +class ValueErrorWithCode(_RootCauseMixin, ValueError): """Enhanced ValueError that supports error codes for better error categorization.""" @@ -87,21 +78,6 @@ def __init__(self, message: str, error_code: int = 0): self.error_code = error_code self.message = message - @property - def root_cause(self) -> Exception: - root = self - while root.__cause__: - root = root.__cause__ - return root - - def _format_cause(self) -> str: - """Format root cause information if available.""" - if self.__cause__ is not None and self is not self.root_cause: - root = self.root_cause - cause_msg = str(root).split("\n")[0][:100] - return f" (caused by: {type(root).__name__}: {cause_msg})" - return "" - def __str__(self): return f"[{self.error_code}] {self.message}{self._format_cause()}" diff --git a/dashscope/finetune/reinforcement/component/server/server.py b/dashscope/finetune/reinforcement/component/server/server.py index 599fe99..ce2d400 100644 --- a/dashscope/finetune/reinforcement/component/server/server.py +++ b/dashscope/finetune/reinforcement/component/server/server.py @@ -382,6 +382,52 @@ def _serialize_result(result: Any) -> Dict: return {"result": result} +async def _cancel_task_on_disconnect( + process_task: "asyncio.Task", + disconnect_listener: "asyncio.Task", + disconnected: "asyncio.Event", +) -> bool: + """Wait for processing or disconnect, cancel task if disconnected. + + Returns True if disconnected, False if processing completed normally. + """ + _done, _pending = await asyncio.wait( + [process_task, disconnect_listener], + return_when=asyncio.FIRST_COMPLETED, + ) + + if disconnected.is_set(): + process_task.cancel() + try: + await process_task + except asyncio.CancelledError: + pass + return True + + return False + + +async def _log_request_metrics( + request: "Request", + processor_input, + start_time: float, + success: bool, + cancelled: bool, +) -> None: + """Log request metrics and force flush if configured.""" + elapsed = round(time.time() - start_time, 4) + fc_req_id = get_fc_request_id(request) + biz_summary = get_business_summary(processor_input) + biz_part = f" | {biz_summary}" if biz_summary else "" + logger.info( + f"[Server] /api/v1 | func_type={func_type.value} | " + f"fc_request_id={fc_req_id} | " + f"success={success} | cancelled={cancelled} | " + f"elapsed={elapsed}s{biz_part}", + ) + await maybe_force_flush_async(reason="request") + + @app.post("/api/v1") async def handle_endpoint(request: Request) -> JSONResponse: """ @@ -439,24 +485,25 @@ async def _listen_for_disconnect(): # Check thread pool queue capacity await _check_queue_capacity() + # Start listening for disconnect only AFTER the request body has been + # fully read. Otherwise the background listener competes with the body + # reader for ASGI receive() messages, causing hung requests or parse + # failures. + disconnect_listener = asyncio.create_task(_listen_for_disconnect()) + # Execute processor as a task so we can cancel it on disconnect process_task = asyncio.create_task( func_manager.processes(processor_input), ) # Wait for either the processing to finish or client disconnect - done, _pending = await asyncio.wait( - [process_task, disconnect_listener], - return_when=asyncio.FIRST_COMPLETED, + disconnected_flag = await _cancel_task_on_disconnect( + process_task, + disconnect_listener, + disconnected, ) - if disconnected.is_set(): - # Client disconnected — cancel the processing task - process_task.cancel() - try: - await process_task - except asyncio.CancelledError: - pass + if disconnected_flag: cancelled = True fc_request_id = get_fc_request_id(request) logger.warning( @@ -507,20 +554,14 @@ async def _listen_for_disconnect(): await _cleanup_trace_context(_otel_ctx_token, _upstream_tokens) # Log request metrics - elapsed = round(time.time() - start_time, 4) - fc_req_id = get_fc_request_id(request) - biz_summary = get_business_summary(processor_input) - biz_part = f" | {biz_summary}" if biz_summary else "" - logger.info( - f"[Server] /api/v1 | func_type={func_type.value} | " - f"fc_request_id={fc_req_id} | " - f"success={success} | cancelled={cancelled} | " - f"elapsed={elapsed}s{biz_part}", + await _log_request_metrics( + request, + processor_input, + start_time, + success, + cancelled, ) - # Best-effort force flush based on platform/internal env config - await maybe_force_flush_async(reason="request") - @app.get("/health") async def health_check_endpoint() -> JSONResponse: From 7721808fa3e221086e5dd5592ccea7bfbd634034 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 23 Jun 2026 10:55:46 +0800 Subject: [PATCH 08/11] Fix test_register_functions_failure to match new error propagation The previous commit (preserve error codes on re-raise) changed register_functions to propagate the inner RegistrationError (error_code=2052) instead of wrapping it in a generic error (error_code=2055). Update the test assertion to check for the original error message and code. Co-Authored-By: Claude Opus 4.7 --- tests/unit/test_agentic_rl_model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_agentic_rl_model.py b/tests/unit/test_agentic_rl_model.py index 1b0e1fe..0559079 100644 --- a/tests/unit/test_agentic_rl_model.py +++ b/tests/unit/test_agentic_rl_model.py @@ -242,9 +242,8 @@ async def test_register_functions_failure(self, agentic_rl_tuning): with pytest.raises(RegistrationError) as exc_info: await agentic_rl_tuning.tuning.register_functions() - assert "Function component registration failed" in str( - exc_info.value, - ) + assert "Registration failed" in str(exc_info.value) + assert exc_info.value.error_code == 2052 # pylint: disable=redefined-outer-name @pytest.mark.asyncio From fc04b36b5cdba817f261e3f53a618af3ef2edc7a Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 23 Jun 2026 10:57:09 +0800 Subject: [PATCH 09/11] Apply black formatting to error handling code Co-Authored-By: Claude Opus 4.7 --- dashscope/finetune/agentic_rl.py | 6 +++--- dashscope/finetune/reinforcement/common/model.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dashscope/finetune/agentic_rl.py b/dashscope/finetune/agentic_rl.py index 4f24f4a..61b9c18 100644 --- a/dashscope/finetune/agentic_rl.py +++ b/dashscope/finetune/agentic_rl.py @@ -106,7 +106,7 @@ async def register_functions( ) logger.info("Function components registered") except Exception as e: - if hasattr(e, 'error_code'): + if hasattr(e, "error_code"): raise raise RegistrationError( "Function registration failed", @@ -251,7 +251,7 @@ def submit_job( **kwargs, ) except Exception as e: - if hasattr(e, 'error_code'): + if hasattr(e, "error_code"): raise raise RuntimeErrorWithCode( "Job submission failed", @@ -316,7 +316,7 @@ async def run( **kwargs, ) except Exception as e: - if hasattr(e, 'error_code'): + if hasattr(e, "error_code"): raise raise RuntimeErrorWithCode( "RL tuning workflow failed", diff --git a/dashscope/finetune/reinforcement/common/model.py b/dashscope/finetune/reinforcement/common/model.py index 0dede60..8f4123b 100644 --- a/dashscope/finetune/reinforcement/common/model.py +++ b/dashscope/finetune/reinforcement/common/model.py @@ -1245,7 +1245,7 @@ async def register_functions( ) except Exception as e: - if hasattr(e, 'error_code'): + if hasattr(e, "error_code"): raise raise RegistrationError( "Function component registration failed", From de1ae4595833aac350ef4c87890ddfdca860925e Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 23 Jun 2026 11:03:02 +0800 Subject: [PATCH 10/11] Add cycle detection to root_cause traversal in error classes Addresses PR #150 review feedback: the root_cause property now tracks visited exception IDs to prevent infinite loops if a circular __cause__ chain is accidentally introduced. Also handles empty cause messages gracefully in _format_cause. Co-Authored-By: Claude Opus 4.7 --- dashscope/finetune/reinforcement/common/errors.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dashscope/finetune/reinforcement/common/errors.py b/dashscope/finetune/reinforcement/common/errors.py index 8966a66..0d9f31d 100644 --- a/dashscope/finetune/reinforcement/common/errors.py +++ b/dashscope/finetune/reinforcement/common/errors.py @@ -14,16 +14,21 @@ class _RootCauseMixin: @property def root_cause(self) -> "Exception": root: "Exception" = self # type: ignore[assignment] - while root.__cause__: + seen = {id(root)} + while root.__cause__ and id(root.__cause__) not in seen: root = root.__cause__ + seen.add(id(root)) return root def _format_cause(self) -> str: """Format root cause information if available.""" - if self.__cause__ is not None and self is not self.root_cause: + if self.__cause__ is not None: root = self.root_cause - cause_msg = str(root).split("\n", maxsplit=1)[0][:100] - return f" (caused by: {type(root).__name__}: {cause_msg})" + if self is not root: + cause_msg = str(root).split("\n", maxsplit=1)[0][:100].strip() + if cause_msg: + return f" (caused by: {type(root).__name__}: {cause_msg})" + return f" (caused by: {type(root).__name__})" return "" From ebe2b6d9dbba7104b6067e55eb1e075481de8829 Mon Sep 17 00:00:00 2001 From: "zhansheng.lzs" Date: Tue, 23 Jun 2026 11:34:47 +0800 Subject: [PATCH 11/11] bump version to 1.25.24 --- dashscope/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashscope/version.py b/dashscope/version.py index a4c4f3f..eb003d0 100644 --- a/dashscope/version.py +++ b/dashscope/version.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Copyright (c) Alibaba, Inc. and its affiliates. -__version__ = "1.25.23" +__version__ = "1.25.24"