tamar-model-client 0.1.27__py3-none-any.whl → 0.1.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tamar_model_client/async_client.py +42 -8
- tamar_model_client/circuit_breaker.py +6 -3
- tamar_model_client/core/__init__.py +5 -1
- tamar_model_client/core/base_client.py +132 -37
- tamar_model_client/core/http_fallback.py +238 -17
- tamar_model_client/core/logging_setup.py +15 -1
- tamar_model_client/core/utils.py +27 -1
- tamar_model_client/error_handler.py +106 -13
- tamar_model_client/sync_client.py +150 -26
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.28.dist-info}/METADATA +96 -3
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.28.dist-info}/RECORD +14 -14
- tests/test_google_azure_final.py +17 -17
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.28.dist-info}/WHEEL +0 -0
- {tamar_model_client-0.1.27.dist-info → tamar_model_client-0.1.28.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ connections fail, supporting both synchronous and asynchronous clients.
|
|
7
7
|
|
8
8
|
import json
|
9
9
|
import logging
|
10
|
-
from typing import Optional, Iterator, AsyncIterator, Dict, Any
|
10
|
+
from typing import Optional, Iterator, AsyncIterator, Dict, Any, List
|
11
11
|
|
12
12
|
from . import generate_request_id, get_protected_logger
|
13
13
|
from ..schemas import ModelRequest, ModelResponse
|
@@ -16,7 +16,16 @@ logger = get_protected_logger(__name__)
|
|
16
16
|
|
17
17
|
|
18
18
|
class HttpFallbackMixin:
|
19
|
-
"""HTTP fallback functionality for synchronous clients
|
19
|
+
"""HTTP fallback functionality for synchronous clients
|
20
|
+
|
21
|
+
This mixin requires the following attributes from the host class:
|
22
|
+
- http_fallback_url: str - The HTTP fallback URL (from BaseClient._init_resilient_features)
|
23
|
+
- jwt_token: Optional[str] - JWT token for authentication (from BaseClient)
|
24
|
+
|
25
|
+
Usage:
|
26
|
+
This mixin should be used with BaseClient or its subclasses that have
|
27
|
+
initialized the resilient features with _init_resilient_features().
|
28
|
+
"""
|
20
29
|
|
21
30
|
def _ensure_http_client(self) -> None:
|
22
31
|
"""Ensure HTTP client is initialized"""
|
@@ -29,7 +38,6 @@ class HttpFallbackMixin:
|
|
29
38
|
|
30
39
|
# Set default headers
|
31
40
|
self._http_client.headers.update({
|
32
|
-
'Content-Type': 'application/json',
|
33
41
|
'User-Agent': 'TamarModelClient/1.0'
|
34
42
|
})
|
35
43
|
|
@@ -92,8 +100,13 @@ class HttpFallbackMixin:
|
|
92
100
|
|
93
101
|
def _invoke_http_fallback(self, model_request: ModelRequest,
|
94
102
|
timeout: Optional[float] = None,
|
95
|
-
request_id: Optional[str] = None
|
103
|
+
request_id: Optional[str] = None,
|
104
|
+
origin_request_id: Optional[str] = None) -> Any:
|
96
105
|
"""HTTP fallback implementation"""
|
106
|
+
# Check if http_fallback_url is available
|
107
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
108
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
109
|
+
|
97
110
|
self._ensure_http_client()
|
98
111
|
|
99
112
|
# Generate request ID if not provided
|
@@ -104,10 +117,14 @@ class HttpFallbackMixin:
|
|
104
117
|
logger.warning(
|
105
118
|
f"🔻 Using HTTP fallback for request",
|
106
119
|
extra={
|
120
|
+
"log_type": "info",
|
107
121
|
"request_id": request_id,
|
108
|
-
"
|
109
|
-
|
110
|
-
|
122
|
+
"data": {
|
123
|
+
"origin_request_id": origin_request_id,
|
124
|
+
"provider": model_request.provider.value,
|
125
|
+
"model": model_request.model,
|
126
|
+
"fallback_url": self.http_fallback_url
|
127
|
+
}
|
111
128
|
}
|
112
129
|
)
|
113
130
|
|
@@ -117,8 +134,18 @@ class HttpFallbackMixin:
|
|
117
134
|
# Construct URL
|
118
135
|
url = f"{self.http_fallback_url}/v1/invoke"
|
119
136
|
|
120
|
-
# Build headers with authentication
|
121
|
-
headers = {
|
137
|
+
# Build headers with authentication and request tracking
|
138
|
+
headers = {
|
139
|
+
'X-Request-ID': request_id,
|
140
|
+
'Content-Type': 'application/json',
|
141
|
+
'User-Agent': 'TamarModelClient/1.0'
|
142
|
+
}
|
143
|
+
|
144
|
+
# Add origin request ID if provided
|
145
|
+
if origin_request_id:
|
146
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
147
|
+
|
148
|
+
# Add JWT authentication if available
|
122
149
|
if hasattr(self, 'jwt_token') and self.jwt_token:
|
123
150
|
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
124
151
|
|
@@ -138,10 +165,101 @@ class HttpFallbackMixin:
|
|
138
165
|
# Parse response
|
139
166
|
data = response.json()
|
140
167
|
return ModelResponse(**data)
|
168
|
+
|
169
|
+
def _invoke_batch_http_fallback(self, batch_request: 'BatchModelRequest',
|
170
|
+
timeout: Optional[float] = None,
|
171
|
+
request_id: Optional[str] = None,
|
172
|
+
origin_request_id: Optional[str] = None) -> List['BatchModelResponse']:
|
173
|
+
"""HTTP batch fallback implementation"""
|
174
|
+
# Import here to avoid circular import
|
175
|
+
from ..schemas import BatchModelRequest, BatchModelResponse
|
176
|
+
|
177
|
+
# Check if http_fallback_url is available
|
178
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
179
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
180
|
+
|
181
|
+
self._ensure_http_client()
|
182
|
+
|
183
|
+
# Generate request ID if not provided
|
184
|
+
if not request_id:
|
185
|
+
request_id = generate_request_id()
|
186
|
+
|
187
|
+
# Log fallback usage
|
188
|
+
logger.warning(
|
189
|
+
f"🔻 Using HTTP fallback for batch request",
|
190
|
+
extra={
|
191
|
+
"log_type": "info",
|
192
|
+
"request_id": request_id,
|
193
|
+
"data": {
|
194
|
+
"origin_request_id": origin_request_id,
|
195
|
+
"batch_size": len(batch_request.items),
|
196
|
+
"fallback_url": self.http_fallback_url
|
197
|
+
}
|
198
|
+
}
|
199
|
+
)
|
200
|
+
|
201
|
+
# Convert to HTTP format
|
202
|
+
http_payload = {
|
203
|
+
"user_context": batch_request.user_context.model_dump(),
|
204
|
+
"items": []
|
205
|
+
}
|
206
|
+
|
207
|
+
# Convert each item
|
208
|
+
for item in batch_request.items:
|
209
|
+
item_payload = self._convert_to_http_format(item)
|
210
|
+
if hasattr(item, 'custom_id') and item.custom_id:
|
211
|
+
item_payload['custom_id'] = item.custom_id
|
212
|
+
if hasattr(item, 'priority') and item.priority is not None:
|
213
|
+
item_payload['priority'] = item.priority
|
214
|
+
http_payload['items'].append(item_payload)
|
215
|
+
|
216
|
+
# Construct URL
|
217
|
+
url = f"{self.http_fallback_url}/v1/batch-invoke"
|
218
|
+
|
219
|
+
# Build headers with authentication and request tracking
|
220
|
+
headers = {
|
221
|
+
'X-Request-ID': request_id,
|
222
|
+
'Content-Type': 'application/json',
|
223
|
+
'User-Agent': 'TamarModelClient/1.0'
|
224
|
+
}
|
225
|
+
|
226
|
+
# Add origin request ID if provided
|
227
|
+
if origin_request_id:
|
228
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
229
|
+
|
230
|
+
# Add JWT authentication if available
|
231
|
+
if hasattr(self, 'jwt_token') and self.jwt_token:
|
232
|
+
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
233
|
+
|
234
|
+
# Send batch request
|
235
|
+
response = self._http_client.post(
|
236
|
+
url,
|
237
|
+
json=http_payload,
|
238
|
+
timeout=timeout or 120, # Longer timeout for batch requests
|
239
|
+
headers=headers
|
240
|
+
)
|
241
|
+
response.raise_for_status()
|
242
|
+
|
243
|
+
# Parse response
|
244
|
+
data = response.json()
|
245
|
+
results = []
|
246
|
+
for item_data in data.get('results', []):
|
247
|
+
results.append(BatchModelResponse(**item_data))
|
248
|
+
|
249
|
+
return results
|
141
250
|
|
142
251
|
|
143
252
|
class AsyncHttpFallbackMixin:
|
144
|
-
"""HTTP fallback functionality for asynchronous clients
|
253
|
+
"""HTTP fallback functionality for asynchronous clients
|
254
|
+
|
255
|
+
This mixin requires the following attributes from the host class:
|
256
|
+
- http_fallback_url: str - The HTTP fallback URL (from BaseClient._init_resilient_features)
|
257
|
+
- jwt_token: Optional[str] - JWT token for authentication (from BaseClient)
|
258
|
+
|
259
|
+
Usage:
|
260
|
+
This mixin should be used with BaseClient or its subclasses that have
|
261
|
+
initialized the resilient features with _init_resilient_features().
|
262
|
+
"""
|
145
263
|
|
146
264
|
async def _ensure_http_client(self) -> None:
|
147
265
|
"""Ensure async HTTP client is initialized"""
|
@@ -149,7 +267,6 @@ class AsyncHttpFallbackMixin:
|
|
149
267
|
import aiohttp
|
150
268
|
self._http_session = aiohttp.ClientSession(
|
151
269
|
headers={
|
152
|
-
'Content-Type': 'application/json',
|
153
270
|
'User-Agent': 'AsyncTamarModelClient/1.0'
|
154
271
|
}
|
155
272
|
)
|
@@ -192,8 +309,13 @@ class AsyncHttpFallbackMixin:
|
|
192
309
|
|
193
310
|
async def _invoke_http_fallback(self, model_request: ModelRequest,
|
194
311
|
timeout: Optional[float] = None,
|
195
|
-
request_id: Optional[str] = None
|
312
|
+
request_id: Optional[str] = None,
|
313
|
+
origin_request_id: Optional[str] = None) -> Any:
|
196
314
|
"""Async HTTP fallback implementation"""
|
315
|
+
# Check if http_fallback_url is available
|
316
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
317
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
318
|
+
|
197
319
|
await self._ensure_http_client()
|
198
320
|
|
199
321
|
# Generate request ID if not provided
|
@@ -204,10 +326,14 @@ class AsyncHttpFallbackMixin:
|
|
204
326
|
logger.warning(
|
205
327
|
f"🔻 Using HTTP fallback for request",
|
206
328
|
extra={
|
329
|
+
"log_type": "info",
|
207
330
|
"request_id": request_id,
|
208
|
-
"
|
209
|
-
|
210
|
-
|
331
|
+
"data": {
|
332
|
+
"origin_request_id": origin_request_id,
|
333
|
+
"provider": model_request.provider.value,
|
334
|
+
"model": model_request.model,
|
335
|
+
"fallback_url": self.http_fallback_url
|
336
|
+
}
|
211
337
|
}
|
212
338
|
)
|
213
339
|
|
@@ -217,8 +343,18 @@ class AsyncHttpFallbackMixin:
|
|
217
343
|
# Construct URL
|
218
344
|
url = f"{self.http_fallback_url}/v1/invoke"
|
219
345
|
|
220
|
-
# Build headers with authentication
|
221
|
-
headers = {
|
346
|
+
# Build headers with authentication and request tracking
|
347
|
+
headers = {
|
348
|
+
'X-Request-ID': request_id,
|
349
|
+
'Content-Type': 'application/json',
|
350
|
+
'User-Agent': 'AsyncTamarModelClient/1.0'
|
351
|
+
}
|
352
|
+
|
353
|
+
# Add origin request ID if provided
|
354
|
+
if origin_request_id:
|
355
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
356
|
+
|
357
|
+
# Add JWT authentication if available
|
222
358
|
if hasattr(self, 'jwt_token') and self.jwt_token:
|
223
359
|
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
224
360
|
|
@@ -242,6 +378,91 @@ class AsyncHttpFallbackMixin:
|
|
242
378
|
data = await response.json()
|
243
379
|
return ModelResponse(**data)
|
244
380
|
|
381
|
+
async def _invoke_batch_http_fallback(self, batch_request: 'BatchModelRequest',
|
382
|
+
timeout: Optional[float] = None,
|
383
|
+
request_id: Optional[str] = None,
|
384
|
+
origin_request_id: Optional[str] = None) -> List['BatchModelResponse']:
|
385
|
+
"""Async HTTP batch fallback implementation"""
|
386
|
+
# Import here to avoid circular import
|
387
|
+
from ..schemas import BatchModelRequest, BatchModelResponse
|
388
|
+
|
389
|
+
# Check if http_fallback_url is available
|
390
|
+
if not hasattr(self, 'http_fallback_url') or not self.http_fallback_url:
|
391
|
+
raise RuntimeError("HTTP fallback URL not configured. Please set MODEL_CLIENT_HTTP_FALLBACK_URL environment variable.")
|
392
|
+
|
393
|
+
await self._ensure_http_client()
|
394
|
+
|
395
|
+
# Generate request ID if not provided
|
396
|
+
if not request_id:
|
397
|
+
request_id = generate_request_id()
|
398
|
+
|
399
|
+
# Log fallback usage
|
400
|
+
logger.warning(
|
401
|
+
f"🔻 Using HTTP fallback for batch request",
|
402
|
+
extra={
|
403
|
+
"log_type": "info",
|
404
|
+
"request_id": request_id,
|
405
|
+
"data": {
|
406
|
+
"origin_request_id": origin_request_id,
|
407
|
+
"batch_size": len(batch_request.items),
|
408
|
+
"fallback_url": self.http_fallback_url
|
409
|
+
}
|
410
|
+
}
|
411
|
+
)
|
412
|
+
|
413
|
+
# Convert to HTTP format
|
414
|
+
http_payload = {
|
415
|
+
"user_context": batch_request.user_context.model_dump(),
|
416
|
+
"items": []
|
417
|
+
}
|
418
|
+
|
419
|
+
# Convert each item
|
420
|
+
for item in batch_request.items:
|
421
|
+
item_payload = self._convert_to_http_format(item)
|
422
|
+
if hasattr(item, 'custom_id') and item.custom_id:
|
423
|
+
item_payload['custom_id'] = item.custom_id
|
424
|
+
if hasattr(item, 'priority') and item.priority is not None:
|
425
|
+
item_payload['priority'] = item.priority
|
426
|
+
http_payload['items'].append(item_payload)
|
427
|
+
|
428
|
+
# Construct URL
|
429
|
+
url = f"{self.http_fallback_url}/v1/batch-invoke"
|
430
|
+
|
431
|
+
# Build headers with authentication and request tracking
|
432
|
+
headers = {
|
433
|
+
'X-Request-ID': request_id,
|
434
|
+
'Content-Type': 'application/json',
|
435
|
+
'User-Agent': 'AsyncTamarModelClient/1.0'
|
436
|
+
}
|
437
|
+
|
438
|
+
# Add origin request ID if provided
|
439
|
+
if origin_request_id:
|
440
|
+
headers['X-Origin-Request-ID'] = origin_request_id
|
441
|
+
|
442
|
+
# Add JWT authentication if available
|
443
|
+
if hasattr(self, 'jwt_token') and self.jwt_token:
|
444
|
+
headers['Authorization'] = f'Bearer {self.jwt_token}'
|
445
|
+
|
446
|
+
# Send batch request
|
447
|
+
import aiohttp
|
448
|
+
timeout_obj = aiohttp.ClientTimeout(total=timeout or 120) if timeout else None
|
449
|
+
|
450
|
+
async with self._http_session.post(
|
451
|
+
url,
|
452
|
+
json=http_payload,
|
453
|
+
timeout=timeout_obj,
|
454
|
+
headers=headers
|
455
|
+
) as response:
|
456
|
+
response.raise_for_status()
|
457
|
+
|
458
|
+
# Parse response
|
459
|
+
data = await response.json()
|
460
|
+
results = []
|
461
|
+
for item_data in data.get('results', []):
|
462
|
+
results.append(BatchModelResponse(**item_data))
|
463
|
+
|
464
|
+
return results
|
465
|
+
|
245
466
|
async def _cleanup_http_session(self) -> None:
|
246
467
|
"""Clean up HTTP session"""
|
247
468
|
if hasattr(self, '_http_session') and self._http_session:
|
@@ -10,7 +10,7 @@ import threading
|
|
10
10
|
from typing import Optional, Dict
|
11
11
|
|
12
12
|
from ..json_formatter import JSONFormatter
|
13
|
-
from .utils import get_request_id
|
13
|
+
from .utils import get_request_id, get_origin_request_id
|
14
14
|
|
15
15
|
# gRPC 消息长度限制(32位系统兼容)
|
16
16
|
MAX_MESSAGE_LENGTH = 2 ** 31 - 1
|
@@ -45,6 +45,20 @@ class RequestIdFilter(logging.Filter):
|
|
45
45
|
"""
|
46
46
|
# 从 ContextVar 中获取当前的 request_id
|
47
47
|
record.request_id = get_request_id()
|
48
|
+
|
49
|
+
# 添加 origin_request_id 到 data 字段
|
50
|
+
origin_request_id = get_origin_request_id()
|
51
|
+
if origin_request_id:
|
52
|
+
# 确保 data 字段存在且是字典类型
|
53
|
+
if not hasattr(record, 'data'):
|
54
|
+
record.data = {}
|
55
|
+
elif record.data is None:
|
56
|
+
record.data = {}
|
57
|
+
elif isinstance(record.data, dict):
|
58
|
+
# 只有在 data 是字典且没有 origin_request_id 时才添加
|
59
|
+
if 'origin_request_id' not in record.data:
|
60
|
+
record.data['origin_request_id'] = origin_request_id
|
61
|
+
|
48
62
|
return True
|
49
63
|
|
50
64
|
|
tamar_model_client/core/utils.py
CHANGED
@@ -15,6 +15,7 @@ from pydantic import BaseModel
|
|
15
15
|
|
16
16
|
# 使用 contextvars 管理请求ID,支持异步和同步上下文中的请求追踪
|
17
17
|
_request_id: ContextVar[str] = ContextVar('request_id', default='-')
|
18
|
+
_origin_request_id: ContextVar[str] = ContextVar('origin_request_id', default=None)
|
18
19
|
|
19
20
|
|
20
21
|
def is_effective_value(value) -> bool:
|
@@ -168,4 +169,29 @@ def get_request_id() -> str:
|
|
168
169
|
Returns:
|
169
170
|
str: 当前的请求ID或默认值
|
170
171
|
"""
|
171
|
-
return _request_id.get()
|
172
|
+
return _request_id.get()
|
173
|
+
|
174
|
+
|
175
|
+
def set_origin_request_id(origin_request_id: str):
|
176
|
+
"""
|
177
|
+
设置当前上下文的原始请求ID
|
178
|
+
|
179
|
+
在 ContextVar 中设置原始请求ID,使得在整个异步调用链中
|
180
|
+
都能访问到同一个原始请求ID,便于追踪请求来源。
|
181
|
+
|
182
|
+
Args:
|
183
|
+
origin_request_id: 要设置的原始请求ID字符串
|
184
|
+
"""
|
185
|
+
_origin_request_id.set(origin_request_id)
|
186
|
+
|
187
|
+
|
188
|
+
def get_origin_request_id() -> str:
|
189
|
+
"""
|
190
|
+
获取当前上下文的原始请求ID
|
191
|
+
|
192
|
+
从 ContextVar 中获取当前的原始请求ID,如果没有设置则返回 None
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
str: 当前的原始请求ID或 None
|
196
|
+
"""
|
197
|
+
return _origin_request_id.get()
|
@@ -230,19 +230,106 @@ class EnhancedRetryHandler:
|
|
230
230
|
except (grpc.RpcError, grpc.aio.AioRpcError) as e:
|
231
231
|
# 创建错误上下文
|
232
232
|
error_context = ErrorContext(e, context)
|
233
|
+
current_duration = time.time() - method_start_time
|
234
|
+
context['duration'] = current_duration
|
233
235
|
|
234
236
|
# 判断是否可以重试
|
235
|
-
|
237
|
+
should_retry = self._should_retry(e, attempt)
|
238
|
+
|
239
|
+
# 检查是否应该尝试快速降级(需要从外部注入client引用)
|
240
|
+
should_try_fallback = False
|
241
|
+
if hasattr(self.error_handler, 'client') and hasattr(self.error_handler.client, '_should_try_fallback'):
|
242
|
+
should_try_fallback = self.error_handler.client._should_try_fallback(e.code(), attempt)
|
243
|
+
|
244
|
+
if should_try_fallback:
|
245
|
+
# 尝试快速降级到HTTP
|
246
|
+
logger.warning(
|
247
|
+
f"🚀 Fast fallback triggered for {e.code().name} after {attempt + 1} attempts",
|
248
|
+
extra={
|
249
|
+
"log_type": "fast_fallback",
|
250
|
+
"request_id": error_context.request_id,
|
251
|
+
"data": {
|
252
|
+
"error_code": e.code().name,
|
253
|
+
"attempt": attempt,
|
254
|
+
"fallback_reason": "immediate" if hasattr(self.error_handler.client, 'immediate_fallback_errors') and e.code() in self.error_handler.client.immediate_fallback_errors else "after_retries"
|
255
|
+
}
|
256
|
+
}
|
257
|
+
)
|
258
|
+
|
259
|
+
try:
|
260
|
+
# 尝试HTTP降级(需要从context获取必要参数)
|
261
|
+
if hasattr(self.error_handler, 'client'):
|
262
|
+
# 检查是否是批量请求
|
263
|
+
if hasattr(self.error_handler.client, '_current_batch_request'):
|
264
|
+
batch_request = self.error_handler.client._current_batch_request
|
265
|
+
origin_request_id = getattr(self.error_handler.client, '_current_origin_request_id', None)
|
266
|
+
timeout = context.get('timeout')
|
267
|
+
request_id = context.get('request_id')
|
268
|
+
|
269
|
+
# 尝试批量HTTP降级
|
270
|
+
result = await self.error_handler.client._invoke_batch_http_fallback(batch_request, timeout, request_id, origin_request_id)
|
271
|
+
elif hasattr(self.error_handler.client, '_current_model_request'):
|
272
|
+
model_request = self.error_handler.client._current_model_request
|
273
|
+
origin_request_id = getattr(self.error_handler.client, '_current_origin_request_id', None)
|
274
|
+
timeout = context.get('timeout')
|
275
|
+
request_id = context.get('request_id')
|
276
|
+
|
277
|
+
# 尝试HTTP降级
|
278
|
+
result = await self.error_handler.client._invoke_http_fallback(model_request, timeout, request_id, origin_request_id)
|
279
|
+
|
280
|
+
logger.info(
|
281
|
+
f"✅ Fast fallback to HTTP succeeded",
|
282
|
+
extra={
|
283
|
+
"log_type": "fast_fallback_success",
|
284
|
+
"request_id": request_id,
|
285
|
+
"data": {
|
286
|
+
"grpc_attempts": attempt + 1,
|
287
|
+
"fallback_duration": time.time() - method_start_time
|
288
|
+
}
|
289
|
+
}
|
290
|
+
)
|
291
|
+
|
292
|
+
return result
|
293
|
+
except Exception as fallback_error:
|
294
|
+
# 降级失败,记录日志但继续原有重试逻辑
|
295
|
+
logger.warning(
|
296
|
+
f"⚠️ Fast fallback to HTTP failed: {str(fallback_error)}",
|
297
|
+
extra={
|
298
|
+
"log_type": "fast_fallback_failed",
|
299
|
+
"request_id": error_context.request_id,
|
300
|
+
"data": {
|
301
|
+
"fallback_error": str(fallback_error),
|
302
|
+
"will_continue_grpc_retry": should_retry and attempt < self.max_retries
|
303
|
+
}
|
304
|
+
}
|
305
|
+
)
|
306
|
+
|
307
|
+
if not should_retry:
|
236
308
|
# 不可重试或已达到最大重试次数
|
237
|
-
|
238
|
-
|
309
|
+
# 记录最终失败日志
|
310
|
+
log_data = {
|
311
|
+
"log_type": "info",
|
312
|
+
"request_id": error_context.request_id,
|
313
|
+
"data": {
|
314
|
+
"error_code": error_context.error_code.name if error_context.error_code else 'UNKNOWN',
|
315
|
+
"error_message": error_context.error_message,
|
316
|
+
"retry_count": attempt,
|
317
|
+
"max_retries": self.max_retries,
|
318
|
+
"category": error_context._get_error_category(),
|
319
|
+
"is_retryable": False,
|
320
|
+
"method": error_context.method,
|
321
|
+
"final_failure": True
|
322
|
+
},
|
323
|
+
"duration": current_duration
|
324
|
+
}
|
325
|
+
logger.warning(
|
326
|
+
f"Final attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()} (no more retries)",
|
327
|
+
extra=log_data
|
328
|
+
)
|
239
329
|
last_exception = self.error_handler.handle_error(e, context)
|
240
330
|
break
|
241
331
|
|
242
|
-
#
|
243
|
-
current_duration = time.time() - method_start_time
|
244
|
-
|
245
|
-
# 记录重试日志
|
332
|
+
# 可以重试,记录重试日志
|
246
333
|
log_data = {
|
247
334
|
"log_type": "info",
|
248
335
|
"request_id": error_context.request_id,
|
@@ -252,13 +339,15 @@ class EnhancedRetryHandler:
|
|
252
339
|
"retry_count": attempt,
|
253
340
|
"max_retries": self.max_retries,
|
254
341
|
"category": error_context._get_error_category(),
|
255
|
-
"is_retryable": True,
|
256
|
-
"method": error_context.method
|
342
|
+
"is_retryable": True,
|
343
|
+
"method": error_context.method,
|
344
|
+
"will_retry": True,
|
345
|
+
"fallback_attempted": should_try_fallback
|
257
346
|
},
|
258
347
|
"duration": current_duration
|
259
348
|
}
|
260
349
|
logger.warning(
|
261
|
-
f"Attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()}",
|
350
|
+
f"Attempt {attempt + 1}/{self.max_retries + 1} failed: {e.code()} (will retry)",
|
262
351
|
extra=log_data
|
263
352
|
)
|
264
353
|
|
@@ -267,8 +356,8 @@ class EnhancedRetryHandler:
|
|
267
356
|
delay = self._calculate_backoff(attempt)
|
268
357
|
await asyncio.sleep(delay)
|
269
358
|
|
270
|
-
|
271
|
-
last_exception =
|
359
|
+
# 保存异常,以备后续使用
|
360
|
+
last_exception = e
|
272
361
|
|
273
362
|
except Exception as e:
|
274
363
|
# 非 gRPC 错误,直接包装抛出
|
@@ -280,7 +369,11 @@ class EnhancedRetryHandler:
|
|
280
369
|
|
281
370
|
# 抛出最后的异常
|
282
371
|
if last_exception:
|
283
|
-
|
372
|
+
if isinstance(last_exception, TamarModelException):
|
373
|
+
raise last_exception
|
374
|
+
else:
|
375
|
+
# 对于原始的 gRPC 异常,需要包装
|
376
|
+
raise self.error_handler.handle_error(last_exception, context)
|
284
377
|
else:
|
285
378
|
raise TamarModelException("Unknown error occurred")
|
286
379
|
|