Skip to content

Fix partial failures in AWS OpenSearch bulk inserts - #855

Open
norrishuang wants to merge 2 commits into
zilliztech:mainfrom
norrishuang:fix/aws-opensearch-bulk-partial-failures
Open

Fix partial failures in AWS OpenSearch bulk inserts#855
norrishuang wants to merge 2 commits into
zilliztech:mainfrom
norrishuang:fix/aws-opensearch-bulk-partial-failures

Conversation

@norrishuang

@norrishuang norrishuang commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What changed

  • inspect AWS OpenSearch bulk response items instead of treating every HTTP 200 response as a fully successful batch
  • count only confirmed successful documents and retry only rejected action/source pairs
  • use exponential backoff across at most 30 total attempts, starting at 2 seconds and capped at 60 seconds
  • retry request-level HTTP 429 failures, while avoiding retries for ambiguous AOSS auto-ID requests such as connection timeouts
  • return partial counts and non-retryable errors through the existing VectorDB.insert_embeddings contract
  • fail the benchmark with OpenSearchBulkInsertError when any documents remain rejected after all attempts
  • apply the same handling to parallel clients and preserve labels_data when falling back to a single client

Why

The OpenSearch bulk API can return HTTP 200 while individual documents fail with errors: true. The previous implementation counted the entire batch as inserted without inspecting item statuses, allowing incomplete datasets to be used for benchmark and recall results.

For OpenSearch Serverless, response-level item failures and request-level HTTP 429 responses are safe to retry. Other request exceptions can have an ambiguous outcome because AOSS uses auto-generated document IDs; those failures are returned as non-retryable instead of risking duplicate documents.

Retry timing

With 30 total attempts there are at most 29 waits. The 2, 4, 8, 16, 32, 60... schedule covers about 25 minutes of sustained throttling before the operation fails hard.

Testing

  • python3 -m pytest tests/test_aws_opensearch.py -q (8 passed)
  • black --check on the changed implementation and tests
  • ruff check on the changed implementation and tests

@sre-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: norrishuang
To complete the pull request process, please assign xuanyang-cn after the PR has been reviewed.
You can assign the PR to them by writing /assign @xuanyang-cn in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

)
total_inserted += inserted
if error is not None:
raise error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vectordb_bench/backend/clients/aws_opensearch/aws_opensearch.py line:303
Medium ---- _insert_with_single_client now raises OpenSearchBulkInsertError instead of returning (total_inserted, error), which bypasses the framework contract that VectorDB.insert_embeddings returns tuple[int, Exception] (api.py) and that concurrent_runner._insert_batch_with_retry uses getattr(error, "non_retryable", False) on the RETURNED error to short-circuit retries. The newly declared non_retryable=True flag is therefore never consulted, partial insert counts are lost to the runner, and this diverges from the OSS sibling client, which returns (count, error) from its insert path. Suggest returning (total_inserted, error) and letting the runner decide, or documenting why raising is required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a89780. _insert_with_single_client now returns (total_inserted, error) again, preserving the VectorDB contract and the partial count. OpenSearchBulkInsertError.non_retryable is now consumed by the runner as intended.

try:
self.client.bulk(body=insert_data)
total_inserted += len(batch_embeddings)
response = client.bulk(body=pending_data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vectordb_bench/backend/clients/aws_opensearch/aws_opensearch.py line:358
Medium ---- On a request-level exception the entire pending_data is retried even though the server may already have applied part of the batch (e.g. a timeout after server-side processing). For OpenSearch Serverless the index action carries no _id (auto-generated), so re-sending an already-applied document creates a duplicate with a different _id but the same _source.id, inflating index counts and corrupting recall - exactly the data-integrity problem this PR is meant to fix. Consider documenting the ambiguity or making the retry idempotent for the auto-_id path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a89780. For the AOSS auto-ID path, request-level HTTP 429 remains retryable because the request was explicitly rejected, while ambiguous failures such as connection timeouts now return a non-retryable error without resending the batch. Response-level item failures are still retried selectively. Provisioned paths with deterministic IDs retain request retries.

log.error(str(error))
return total_inserted, error

error = OpenSearchBulkInsertError(f"Bulk insert for {context} exhausted its retry loop")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vectordb_bench/backend/clients/aws_opensearch/aws_opensearch.py line:412
Low ---- The trailing error = OpenSearchBulkInsertError("...exhausted its retry loop") followed by return total_inserted, error is unreachable: every iteration of the for attempt in range(1, BULK_MAX_ATTEMPTS + 1) loop returns (or continues) on all 30 iterations, so control never falls through to this block. Remove the dead code or make the loop-exit the single error-construction point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a89780. The unreachable fallback was removed. Terminal request/item failures now set final_error, break the loop, and use one reachable error logging/return point after the loop.

except Exception as retry_e:
log.warning(f"Retry failed for batch: {retry_e!s}")
return total_inserted, retry_e
request_error = e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vectordb_bench/backend/clients/aws_opensearch/aws_opensearch.py line:360
Low ---- The request-exception retry branch (client.bulk raising, lines 357-382) has no test coverage; all six new tests exercise response-level errors only. A test that makes client.bulk raise on the first N attempts would lock in the backoff and counting behavior of this branch, which is the most common real-world failure mode (timeouts, connection errors).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a89780. Added request-exception coverage for two request-level 429 failures followed by success (including 2s/4s backoff assertions), plus an AOSS connection-timeout case that verifies the ambiguous auto-ID request is not resent. The focused test file now has 8 passing tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants