Skip to content

Commit fe00545

Browse files
fix(bedrock): only retry transient botocore errors
The previous 'except Exception' caught and retried KeyError from malformed responses, missing-credential errors, and other programmer or config mistakes -- wasting up to 6s of backoff per failed call and hiding the real cause. Narrow the catch to (ClientError, BotoCoreError) and gate retries with _is_retryable, which only retries throttling, 5xx, transient model errors, and transport-level BotoCoreError. Permanent ClientError codes (ValidationException, AccessDeniedException, ResourceNotFoundException) and any non-botocore exception surface immediately. Addresses the Copilot review comment on PR #69.
1 parent 956cfa5 commit fe00545

1 file changed

Lines changed: 23 additions & 2 deletions

File tree

src/docs2vecs/subcommands/indexer/skills/bedrock_titan_embedding_skill.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,24 @@
33
from typing import List, Optional
44

55
import boto3
6+
from botocore.exceptions import BotoCoreError, ClientError
67

78
from docs2vecs.subcommands.indexer.config.config import Config
89
from docs2vecs.subcommands.indexer.document.document import Document
910
from docs2vecs.subcommands.indexer.skills.skill import IndexerSkill
1011

12+
# Bedrock error codes that represent transient failures worth retrying.
13+
# Permanent errors (ValidationException, AccessDeniedException,
14+
# ResourceNotFoundException, etc.) surface immediately.
15+
_RETRYABLE_BEDROCK_CODES = frozenset({
16+
"ThrottlingException",
17+
"TooManyRequestsException",
18+
"ServiceUnavailableException",
19+
"InternalServerException",
20+
"ModelTimeoutException",
21+
"ModelStreamErrorException",
22+
})
23+
1124

1225
class BedrockTitanEmbeddingSkill(IndexerSkill):
1326
DEFAULT_MODEL_ID = "amazon.titan-embed-text-v2:0"
@@ -27,6 +40,14 @@ def __init__(self, config: dict, global_config: Config):
2740
region_name=self._config.get("region"),
2841
)
2942

43+
def _is_retryable(self, exc: Exception) -> bool:
44+
if isinstance(exc, ClientError):
45+
code = exc.response.get("Error", {}).get("Code", "")
46+
status = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode", 0)
47+
return code in _RETRYABLE_BEDROCK_CODES or 500 <= status < 600
48+
# BotoCoreError covers connection/read timeouts and other transport-level issues
49+
return isinstance(exc, BotoCoreError)
50+
3051
def _embed_text(self, content: str, chunk_id=None):
3152
self.logger.debug(
3253
f"Requesting Bedrock embedding for chunk_id={chunk_id}, content_length={len(content)}"
@@ -51,8 +72,8 @@ def _embed_text(self, content: str, chunk_id=None):
5172
f"Successfully received embedding for chunk_id={chunk_id}, embedding_dim={len(embedding) if embedding else 0}"
5273
)
5374
return embedding
54-
except Exception as exc:
55-
if attempt == self._max_retries - 1:
75+
except (ClientError, BotoCoreError) as exc:
76+
if attempt == self._max_retries - 1 or not self._is_retryable(exc):
5677
raise
5778
wait = self._retry_backoff * (attempt + 1)
5879
self.logger.warning(

0 commit comments

Comments
 (0)