# Copyright 2025-2026 Aerospike, Inc.
#
# Portions may be licensed to Aerospike, Inc. under one or more contributor
# license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Typed exceptions for the SDK client.
Subclasses of :class:`AerospikeError` mirror common server and client outcomes so
callers can handle failures selectively (for example ``except GenerationError``)
instead of comparing result codes everywhere.
At public boundaries, errors from the underlying async client are normalized with
:func:`_convert_pnc_exception`. Callers should chain causes explicitly:
``raise _convert_pnc_exception(exc) from exc``.
"""
from __future__ import annotations
from aerospike_native.exceptions import (
AerospikeError as PncAerospikeError,
CommitFailedError as PncCommitFailedError,
ConnectionError as PncConnectionError,
InvalidNodeError as PncInvalidNodeError,
MaxErrorRate as PncMaxErrorRate,
ServerError as PncServerError,
TimeoutError as PncTimeoutError,
UDFBadResponse as PncUDFBadResponse,
)
# Re-exported for callers that need the raw server result code or the
# PNC-level error types without importing aerospike_native directly.
from aerospike_native import SubCode
from aerospike_native.exceptions import ResultCode
from aerospike_native.exceptions import SecurityNotEnabled as SecurityNotEnabled
from aerospike_native.exceptions import ServerError as ServerError
from aerospike_native.exceptions import ValueError as PncValueError # noqa: F401
# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------
[docs]
class AerospikeError(Exception):
"""Base class for SDK failures.
Raised directly when no more specific subclass applies, including
unmapped server result codes (see :func:`_result_code_to_exception`).
Prefer catching concrete subclasses when you need targeted handling, and
fall back to this type for all other Aerospike-related errors.
Attributes:
result_code: The :class:`~aerospike_native.exceptions.ResultCode` of the
failure. A server failure carries the code the server returned;
a failure the client raised on its own carries a client code
(``ResultCode.SERVER_NOT_AVAILABLE``, ``ResultCode.TXN_FAILED``),
the same negative number every Aerospike client reports for it.
``None`` only on an instance built by hand.
in_doubt: ``True`` when a write may have completed on the server despite
the error; safe retry usually requires a read-verify strategy.
sub_code: Server-supplied numeric subcode refining ``result_code`` when
extended error detail was requested (see ``error_detail_verbosity``).
``None`` means no detail was returned (verbosity off, or a
client-side failure); ``0`` (``SubCode.NONE``) means detail was
returned but this failure has no finer subcode than its result code.
Only meaningful together with ``result_code`` — subcode values are
scoped to their parent code, not globally unique.
server_message: Human-readable message from the server when detail was
requested at message-level verbosity or higher; ``None`` when no
detail was returned. May be present even when ``sub_code`` is ``0``.
hint: Client-side guidance for result codes whose cause is a common,
recognizable misconfiguration -- appended to the message and kept
here separately so callers can log or suppress it independently.
``None`` when the SDK has nothing to add beyond what the server
reported. Unlike ``server_message`` this is generated locally and
never travels the wire.
exp_trace: Structured expression trace the server attaches to
expression-build failures at trace-level verbosity; ``None``
otherwise. Surfaced as an opaque passthrough of the underlying
client value.
node: Cluster node the failing attempt targeted, when the retry loop
recorded one; ``None`` for failures that never reached node
selection (for example, argument validation).
iteration: Number of attempts made before the command failed, when
recorded; ``None`` when the failure precedes the retry loop.
sub_exceptions: Exceptions from prior retry attempts of the same
command, oldest first, each converted to this hierarchy. Empty
when the command was not retried.
base_message: The failure message without the retry-context decoration
the full message carries; ``None`` when no undecorated form was
recorded.
Example::
try:
stream = await session.query(key).bins(["x"]).execute()
await stream.first_or_raise()
except AerospikeError as err:
code = err.result_code
...
See Also:
:func:`_result_code_to_exception`: Maps result codes to this type or a
subclass.
"""
[docs]
def __init__(
self,
message: str = "",
*,
result_code: ResultCode | None = None,
in_doubt: bool = False,
sub_code: int | None = None,
server_message: str | None = None,
exp_trace: object | None = None,
node: str | None = None,
iteration: int | None = None,
sub_exceptions: tuple[AerospikeError, ...] = (),
base_message: str | None = None,
hint: str | None = None,
) -> None:
super().__init__(message)
self.result_code = result_code
self.in_doubt = in_doubt
self.sub_code = sub_code
self.server_message = server_message
self.exp_trace = exp_trace
self.node = node
self.iteration = iteration
self.sub_exceptions = sub_exceptions
self.base_message = base_message
self.hint = hint
# ---------------------------------------------------------------------------
# Timeout / connectivity
# ---------------------------------------------------------------------------
[docs]
class TimeoutError(AerospikeError):
"""Raised when an operation exceeds a client or server timeout.
Covers socket-level timeouts and server-reported timeout result codes.
This type shares a name with Python's built-in :exc:`TimeoutError`; always
import it from :mod:`aerospike_sdk` or this module when handling SDK
client timeouts.
Attributes:
client: ``True`` when the client's own deadline fired (socket or
total timeout expired locally); ``False`` when the server
reported the timeout result code. A client timeout says nothing
about server-side progress — pair it with ``in_doubt`` when
deciding whether a write needs read-verification.
result_code: ``ResultCode.TIMEOUT`` when the server reported it or
the total timeout expired, so both classify alike;
``ResultCode.MAX_RETRIES_EXCEEDED`` when every attempt the policy
allowed failed before that deadline -- including the only attempt
when retries are off, so a single socket timeout with
``max_retries=0`` reports this code too.
See Also:
:class:`ConnectionError`: Cluster reachability rather than deadline
exceeded.
Example::
try:
await stream.first_or_raise()
except TimeoutError as err:
if err.client:
... # local deadline; the server may still be working
else:
... # the server itself timed the operation out
"""
[docs]
def __init__(self, message: str = "", *, client: bool = False, **kwargs: object) -> None:
super().__init__(message, **kwargs) # type: ignore[arg-type]
self.client = client
[docs]
class ConnectionError(AerospikeError):
"""Raised when the client cannot establish or keep a cluster connection.
Typical causes include refused sockets, TLS handshake failure, or loss of
connectivity mid-flight. Distinct from :class:`TimeoutError`, which signals
a deadline rather than an immediate transport failure.
Attributes:
result_code: ``ResultCode.SERVER_NOT_AVAILABLE`` when no node could be
reached, or the code of the failure that cut the connection.
Example::
try:
async with Client(...) as client:
...
except ConnectionError:
... # cluster unreachable
"""
[docs]
class InvalidNodeError(AerospikeError):
"""Raised when the chosen node is unknown, wrong role, or not usable.
Use for diagnosing cluster topology or client routing issues rather than
application-level data errors.
Attributes:
result_code: ``ResultCode.INVALID_NODE_ERROR`` (``-3``).
"""
[docs]
class InvalidNamespaceError(AerospikeError):
"""Raised when the namespace is missing or not defined on the cluster.
Often indicates a configuration mismatch between application and cluster.
Attributes:
result_code: Typically ``ResultCode.INVALID_NAMESPACE`` when mapped from
a server response.
Example::
try:
await session.query(bad_ds).execute()
except InvalidNamespaceError:
... # namespace not configured on cluster
"""
# ---------------------------------------------------------------------------
# Security
# ---------------------------------------------------------------------------
[docs]
class SecurityError(AerospikeError):
"""Base class for authentication, authorization, and security policy errors.
Several distinct server result codes collapse to this type when they do not
warrant a dedicated subclass. Catch :class:`AuthenticationError` or
:class:`AuthorizationError` first if you need finer granularity.
Attributes:
result_code: The security-related code returned by the server, when
applicable.
"""
[docs]
class AuthenticationError(SecurityError):
"""Raised when credentials are rejected or the session is not authenticated.
Examples include invalid user, expired password, or not authenticated
responses from the server.
See Also:
:class:`AuthorizationError`: Valid identity but disallowed operation.
"""
[docs]
class AuthorizationError(SecurityError):
"""Raised when the authenticated principal may not perform the operation.
Distinct from :class:`AuthenticationError`, which indicates identity or
credential problems rather than policy denial.
"""
# ---------------------------------------------------------------------------
# Data integrity
# ---------------------------------------------------------------------------
[docs]
class GenerationError(AerospikeError):
"""Raised when a write fails due to a record generation mismatch.
The record was modified since it was read, or the expected generation did
not match. Retrying blindly usually requires re-reading the record and
reapplying the logical update.
Attributes:
result_code: Typically ``ResultCode.GENERATION_ERROR``.
Example::
try:
await (
session.upsert(key)
.put({"x": 1})
.ensure_generation_is(3)
.execute()
)
except GenerationError:
... # record was modified by another writer
See Also:
:meth:`~aerospike_sdk.aio.session.Session.upsert`: Common write path
that can enforce generations on builders.
"""
[docs]
class QuotaError(AerospikeError):
"""Raised when a server-side quota or limit is exceeded.
Handling is usually operational (throttle, increase limits, or partition
workload) rather than a single-record retry.
"""
[docs]
class SerializationError(AerospikeError):
"""Raised when a value cannot be encoded for the wire or decoded from it.
Check bin types and application serializers when this appears on puts or
reads.
"""
# ---------------------------------------------------------------------------
# Record and key
# ---------------------------------------------------------------------------
[docs]
class RecordNotFoundError(AerospikeError):
"""Raised when no record exists for the requested key.
Surfaces on reads and on writes that require an existing record (update,
replace, touch), and on batch/point reads when
:meth:`~aerospike_sdk.aio.operations.query.QueryBuilder.include_missing_keys`
promotes a not-found key to an error result.
Attributes:
result_code: ``ResultCode.KEY_NOT_FOUND_ERROR``.
Example::
try:
await session.update(key).put({"count": 1}).execute_or_raise()
except RecordNotFoundError:
... # nothing to update
"""
[docs]
class RecordExistsError(AerospikeError):
"""Raised when a create-only write finds the key already present.
Produced by insert-style (create-only) writes when a record already exists
for the key; the idiomatic way to detect a duplicate on insert.
Attributes:
result_code: ``ResultCode.KEY_EXISTS_ERROR``.
Example::
try:
await session.insert(key).put({"count": 1}).execute_or_raise()
except RecordExistsError:
... # key already taken
"""
[docs]
class RecordTooBigError(AerospikeError):
"""Raised when a record exceeds the server's configured size limit.
Reduce the bin payload or raise the namespace's size ceiling; retrying the
same record unchanged will fail again.
Attributes:
result_code: ``ResultCode.RECORD_TOO_BIG``.
"""
[docs]
class FilteredOutError(AerospikeError):
"""Raised when an operation is skipped because its filter expression was false.
Only surfaces as an error when
:meth:`~aerospike_sdk.aio.operations.query.QueryBuilder.fail_on_filtered_out`
is set; otherwise a filtered record is silently omitted from results.
Attributes:
result_code: ``ResultCode.FILTERED_OUT``.
See Also:
:class:`RecordNotFoundError`: A missing record rather than a filter miss.
"""
# ---------------------------------------------------------------------------
# Bin
# ---------------------------------------------------------------------------
[docs]
class BinError(AerospikeError):
"""Base class for bin-level failures.
Covers an over-long bin name and serves as the parent for the more specific
bin errors below; catch this to handle any bin-scoped failure at once.
Attributes:
result_code: A bin-related code such as ``ResultCode.BIN_NAME_TOO_LONG``.
"""
[docs]
class BinExistsError(BinError):
"""Raised when a create-only bin operation finds the bin already present.
Attributes:
result_code: ``ResultCode.BIN_EXISTS_ERROR``.
"""
[docs]
class BinNotFoundError(BinError):
"""Raised when an update-only bin operation targets a missing bin.
Attributes:
result_code: ``ResultCode.BIN_NOT_FOUND``.
"""
[docs]
class BinTypeError(BinError):
"""Raised when an operation is incompatible with the bin's stored type.
For example, arithmetic on a bin holding a string. Distinct from
:class:`BinOpInvalidError`, which concerns the bin's current *value*.
Attributes:
result_code: ``ResultCode.BIN_TYPE_ERROR``.
"""
[docs]
class BinOpInvalidError(BinError):
"""Raised when an operation cannot be applied to the bin's current value.
For example, a list operation against a bin that does not hold a list.
Attributes:
result_code: ``ResultCode.OP_NOT_APPLICABLE``.
"""
# ---------------------------------------------------------------------------
# Collection (CDT) element
# ---------------------------------------------------------------------------
[docs]
class ElementError(AerospikeError):
"""Base class for list/map element-level failures inside CDT operations."""
[docs]
class ElementNotFoundError(ElementError):
"""Raised when a list index or map key is absent under an update-only mode.
Attributes:
result_code: ``ResultCode.ELEMENT_NOT_FOUND``.
"""
[docs]
class ElementExistsError(ElementError):
"""Raised when a list index or map key is present under a create-only mode.
Attributes:
result_code: ``ResultCode.ELEMENT_EXISTS``.
"""
# ---------------------------------------------------------------------------
# Capacity / resource exhaustion
# ---------------------------------------------------------------------------
[docs]
class CapacityError(AerospikeError):
"""Raised on server or client resource exhaustion.
Covers server memory pressure, device I/O overload, and full server-side
queues. Handling is operational (throttle, retry with backoff, or add
capacity) rather than a single-record retry.
Attributes:
result_code: A capacity-related code such as
``ResultCode.SERVER_MEM_ERROR`` or ``ResultCode.DEVICE_OVERLOAD``.
"""
[docs]
class KeyBusyError(CapacityError):
"""Raised when too many concurrent operations target one record (hot key).
A transient contention signal; a brief backoff before retry usually clears
it. Catch :class:`CapacityError` to handle it alongside other exhaustion.
Attributes:
result_code: ``ResultCode.KEY_BUSY``.
"""
# ---------------------------------------------------------------------------
# Secondary index
# ---------------------------------------------------------------------------
[docs]
class SecondaryIndexError(AerospikeError):
"""Base class for secondary-index management failures.
Named ``SecondaryIndexError`` rather than ``IndexError`` so it does not
shadow the built-in :exc:`IndexError`. Covers index creation limits,
out-of-memory, and not-readable states; catch this for any index failure.
Attributes:
result_code: An index-related code such as ``ResultCode.INDEX_OOM``.
"""
[docs]
class IndexNotFoundError(SecondaryIndexError):
"""Raised when a referenced secondary index does not exist.
Attributes:
result_code: ``ResultCode.INDEX_NOT_FOUND``.
"""
[docs]
class IndexAlreadyExistsError(SecondaryIndexError):
"""Raised when creating a secondary index that already exists.
Attributes:
result_code: ``ResultCode.INDEX_FOUND``.
"""
# ---------------------------------------------------------------------------
# Operations
# ---------------------------------------------------------------------------
[docs]
class QueryError(AerospikeError):
"""Base class for query and scan execution failures.
Covers generic query errors, aborted scans, and query network failures.
Timeout-flavored query codes raise :class:`TimeoutError` instead, and a
full query queue raises :class:`CapacityError` — catch this class for
failures of the query itself.
Attributes:
result_code: A query-related code such as ``ResultCode.QUERY_GENERIC``
or ``ResultCode.SCAN_ABORT``.
"""
[docs]
class QueryTerminatedError(QueryError):
"""Raised when a query stops early (aborted, canceled, or server-terminated).
Partial rows may already have been delivered on streaming paths; this error
represents the overall query outcome, not a single-key failure inside a
batch. Catch :class:`QueryError` to handle it alongside other query
failures.
Attributes:
result_code: ``ResultCode.QUERY_ABORTED`` or a related server code, or
``ResultCode.SCAN_TERMINATED`` when the client cut the stream short.
"""
[docs]
class UdfError(AerospikeError):
"""Raised when a server-side UDF execution reports a failure.
The server ran the registered function and it returned an error (or the
response was not a valid UDF result). The failure detail is in the
message; check the UDF's own error string when debugging Lua code.
Attributes:
result_code: ``ResultCode.UDF_BAD_RESPONSE``.
Example::
try:
await session.execute_udf(key).function("mod", "fn").execute()
except UdfError:
... # the function itself failed on the server
"""
[docs]
class BatchError(AerospikeError):
"""Raised when the server's batch subsystem rejects a batch request.
Signals a batch-level condition (for example, batch disabled on the
server) rather than a per-key failure — per-key outcomes surface as
:class:`~aerospike_sdk.record_result.RecordResult` errors in the stream.
Full batch queues raise :class:`CapacityError` instead.
Attributes:
result_code: A batch-related code such as ``ResultCode.BATCH_DISABLED``.
"""
[docs]
class BackoffError(AerospikeError):
"""Raised when the server signals rate limiting or requires backoff.
Callers may retry after a delay or reduce request pressure.
"""
[docs]
class MaxErrorRateError(BackoffError):
"""Raised when the client's per-node circuit breaker trips.
The breaker is governed by ``Client(...)``'s ``max_error_rate`` and
``error_rate_window`` keywords (or :class:`~aerospike_native.ClientPolicy`
fields of the same name). Once a node's error count crosses
``max_error_rate`` within the current window, subsequent commands routed
to that node fail fast with this exception until the next window resets.
Catch :class:`BackoffError` to handle this together with other server-side
backoff signals.
Example::
try:
await session.read(key).execute()
except MaxErrorRateError:
... # node is in cooldown; route around it or wait
"""
[docs]
class TransactionError(AerospikeError):
"""Base class for multi-record transaction (MRT) failures.
Covers a transaction that is blocked, expired, aborted, already committed,
version-mismatched, or has exceeded its write limit. :class:`CommitError`
is the commit-phase specialization; catch this base to handle any MRT
failure at once.
Attributes:
result_code: An MRT-related server code such as
``ResultCode.MRT_EXPIRED``, or ``ResultCode.TXN_FAILED`` when the
client refuses to continue a transaction whose commit already
failed.
"""
[docs]
class CommitError(TransactionError):
"""Raised when a multi-record transaction commit does not complete successfully.
Additional fields expose verify or roll-forward details when the underlying
client provides them.
Attributes:
commit_error_type: Implementation-defined label for the failure phase,
if available. ``ROLL_FORWARD_ABANDONED`` means the writes are not
yet visible; the server will eventually commit them, so a retry
of the same work is not safe.
verify_records: Verify-phase records or summaries, if available.
roll_records: Roll-forward or rollback-phase records, if available.
result_code: The server code that tripped the failing stage when there
was one, otherwise ``ResultCode.TXN_FAILED``.
in_doubt: Inherited; ``True`` when commit outcome may be ambiguous on
the server.
"""
[docs]
def __init__(
self,
message: str = "",
*,
commit_error_type: object | None = None,
verify_records: list | None = None,
roll_records: list | None = None,
result_code: ResultCode | None = None,
in_doubt: bool = False,
**kwargs: object,
) -> None:
# Detail and retry-context fields (sub_code, server_message,
# exp_trace, node, iteration, ...) pass through to the base so a
# commit failure is not the one place they get dropped.
super().__init__(
message, result_code=result_code, in_doubt=in_doubt,
**kwargs, # type: ignore[arg-type]
)
self.commit_error_type = commit_error_type
self.verify_records = verify_records
self.roll_records = roll_records
# ---------------------------------------------------------------------------
# Factory: ResultCode -> typed exception
# ---------------------------------------------------------------------------
# Codes without a meaningfully distinct handling story (for example
# INVALID_GEOJSON, PARAMETER_ERROR) fall through to AerospikeError. Client-side
# failures with their own exception type (timeouts, connection loss) convert
# by that type before this map is consulted; the client codes listed here are
# the ones that arrive on the generic client error.
_RC_TO_TYPE: dict[ResultCode, type[AerospikeError]] = {
# Client-assigned codes
ResultCode.TXN_FAILED: TransactionError,
ResultCode.TXN_ALREADY_ABORTED: TransactionError,
ResultCode.TXN_ALREADY_COMMITTED: TransactionError,
ResultCode.SCAN_TERMINATED: QueryTerminatedError,
ResultCode.QUERY_TERMINATED: QueryTerminatedError,
ResultCode.SERIALIZE_ERROR: SerializationError,
ResultCode.GENERATION_ERROR: GenerationError,
# Authentication (identity / credential problems)
ResultCode.NOT_AUTHENTICATED: AuthenticationError,
ResultCode.INVALID_USER: AuthenticationError,
ResultCode.INVALID_PASSWORD: AuthenticationError,
ResultCode.INVALID_CREDENTIAL: AuthenticationError,
ResultCode.EXPIRED_PASSWORD: AuthenticationError,
# Authorization (valid identity, disallowed operation)
ResultCode.ROLE_VIOLATION: AuthorizationError,
ResultCode.NOT_ALLOWLISTED: AuthorizationError,
# Security (catch-all for remaining security codes)
ResultCode.ILLEGAL_STATE: SecurityError,
ResultCode.USER_ALREADY_EXISTS: SecurityError,
ResultCode.FORBIDDEN_PASSWORD: SecurityError,
ResultCode.SECURITY_NOT_SUPPORTED: SecurityError,
ResultCode.SECURITY_NOT_ENABLED: SecurityError,
ResultCode.SECURITY_SCHEME_NOT_SUPPORTED: SecurityError,
ResultCode.EXPIRED_SESSION: SecurityError,
ResultCode.INVALID_ROLE: SecurityError,
ResultCode.ROLE_ALREADY_EXISTS: SecurityError,
ResultCode.INVALID_PRIVILEGE: SecurityError,
ResultCode.INVALID_ALLOWLIST: SecurityError,
# Quota
ResultCode.QUOTA_EXCEEDED: QuotaError,
ResultCode.QUOTAS_NOT_ENABLED: QuotaError,
ResultCode.INVALID_QUOTA: QuotaError,
# Timeout
ResultCode.TIMEOUT: TimeoutError,
ResultCode.QUERY_TIMEOUT: TimeoutError,
# Namespace
ResultCode.INVALID_NAMESPACE: InvalidNamespaceError,
# Query / scan
ResultCode.QUERY_ABORTED: QueryTerminatedError,
ResultCode.QUERY_GENERIC: QueryError,
ResultCode.SCAN_ABORT: QueryError,
ResultCode.QUERY_NETIO_ERR: QueryError,
ResultCode.QUERY_DUPLICATE: QueryError,
# UDF
ResultCode.UDF_BAD_RESPONSE: UdfError,
# Batch subsystem (full queues are capacity, below)
ResultCode.BATCH_DISABLED: BatchError,
# Record / key
ResultCode.KEY_NOT_FOUND_ERROR: RecordNotFoundError,
ResultCode.KEY_EXISTS_ERROR: RecordExistsError,
ResultCode.RECORD_TOO_BIG: RecordTooBigError,
ResultCode.FILTERED_OUT: FilteredOutError,
# Bin
ResultCode.BIN_NAME_TOO_LONG: BinError,
ResultCode.BIN_EXISTS_ERROR: BinExistsError,
ResultCode.BIN_NOT_FOUND: BinNotFoundError,
ResultCode.BIN_TYPE_ERROR: BinTypeError,
ResultCode.OP_NOT_APPLICABLE: BinOpInvalidError,
# Collection (CDT) element
ResultCode.ELEMENT_NOT_FOUND: ElementNotFoundError,
ResultCode.ELEMENT_EXISTS: ElementExistsError,
# Capacity / resource exhaustion
ResultCode.SERVER_MEM_ERROR: CapacityError,
ResultCode.DEVICE_OVERLOAD: CapacityError,
ResultCode.QUERY_QUEUE_FULL: CapacityError,
ResultCode.BATCH_QUEUES_FULL: CapacityError,
ResultCode.BATCH_MAX_REQUESTS_EXCEEDED: CapacityError,
ResultCode.KEY_BUSY: KeyBusyError,
ResultCode.XDR_KEY_BUSY: KeyBusyError,
# Secondary index
ResultCode.INDEX_NOT_FOUND: IndexNotFoundError,
ResultCode.INDEX_FOUND: IndexAlreadyExistsError,
ResultCode.INDEX_OOM: SecondaryIndexError,
ResultCode.INDEX_MAX_COUNT: SecondaryIndexError,
ResultCode.INDEX_NAME_MAX_LEN: SecondaryIndexError,
ResultCode.INDEX_NOT_READABLE: SecondaryIndexError,
ResultCode.INDEX_GENERIC: SecondaryIndexError,
# Transactions (multi-record)
ResultCode.MRT_BLOCKED: TransactionError,
ResultCode.MRT_EXPIRED: TransactionError,
ResultCode.MRT_VERSION_MISMATCH: TransactionError,
ResultCode.MRT_TOO_MANY_WRITES: TransactionError,
ResultCode.MRT_ABORTED: TransactionError,
ResultCode.MRT_COMMITTED: TransactionError,
ResultCode.MRT_ALREADY_LOCKED: TransactionError,
ResultCode.MRT_MONITOR_EXISTS: TransactionError,
}
# Guidance for failures whose cause is a recognizable condition the result code
# alone does not name. Deliberately small: text that restates the code name is
# noise, and this is appended to a user-facing message.
#
# Precedence is narrowest-first -- an explicit ``hint=`` from a caller holding
# the operation, then the (code, subcode) pair, then the code alone.
# Keyed on the *pair*: subcode integers are scoped to their parent code, not
# globally unique (BIN_NAME_COUNT_TOO_LARGE and FORBID_XDR_FILTER_BLOCKED are
# both 1). Populated only when the server sends detail, which requires
# ``error_detail_verbosity`` and a server that supplies it.
_SUBCODE_GUIDANCE: dict[tuple[ResultCode, int], str] = {
(ResultCode.BIN_NAME_TOO_LONG, SubCode.BIN_NAME_COUNT_TOO_LARGE): (
"The record would exceed the server's per-record bin-count limit. The "
"bin names themselves are fine -- the record has too many bins."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_DURABILITY_VIOLATION): (
"A non-durable delete was refused because it would violate durability. "
"Strong-consistency namespaces require durable deletes: enable durable "
"delete on the operation, or on the Behavior the session carries."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_SET_COUNT_STOP_WRITES): (
"The set reached its record-count stop-writes limit. Writes stay "
"refused until the count falls or the limit is raised."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_SET_SIZE_STOP_WRITES): (
"The set reached its size stop-writes limit. Writes stay refused until "
"the set shrinks or the limit is raised."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_CLOCK_SKEW_STOP_WRITES): (
"Writes are stopped because clocks across the cluster have drifted too "
"far apart. This is a cluster health problem, not a client one -- check "
"time synchronization on the server nodes."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_TRUNCATED): (
"The set or namespace is mid-truncate, so writes are refused until the "
"truncate completes. Retrying after it finishes should succeed."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_XDR_FILTER_BLOCKED): (
"An XDR ship filter at the destination rejected this write. The filter "
"is configured on the destination cluster, not in the client."
),
(ResultCode.FAIL_FORBIDDEN, SubCode.FORBID_REPLACE_CONFLICT_RESOLVING): (
"A replace was refused while the record was being conflict-resolved. "
"Retry, or use an update rather than a replace."
),
}
# Fallback when no subcode is available. These must not rank causes: the code
# covers several, and naming one would misdirect whenever it is not that one.
_RC_GUIDANCE: dict[ResultCode, str] = {
ResultCode.BIN_NAME_TOO_LONG: (
"The server rejected the operation over bin naming. This code covers "
"two causes: a bin name longer than 15 characters, or a record with "
"too many bins. Enable extended error detail to get the subcode that "
"tells them apart."
),
ResultCode.FAIL_FORBIDDEN: (
"The server forbade this operation given its current state. Causes "
"include a durability violation, set-level stop-writes limits, cluster "
"clock skew, an in-progress truncate, XDR filtering, and conflict "
"resolution. Enable extended error detail to get the subcode that "
"identifies which; namespace configuration is visible through "
"session.info().namespace_details(<namespace>)."
),
ResultCode.UNSUPPORTED_FEATURE: (
"The cluster or namespace does not support something this operation "
"requires. Multi-record transactions, for example, need a "
"strong-consistency namespace on a server build that supports them; "
"check the namespace mode with session.namespace_sc_status(<namespace>)."
),
}
# The server validates arguments and holds the reason for rejecting one, but
# only sends it when the request asked. A ParameterError with neither subcode
# nor message is one that never asked, so the guidance is how to.
_PARAMETER_ERROR_DETAIL_HINT = (
"The server rejected an argument of this operation and knows why, but the "
"reason was not requested. Set error_detail_verbosity to "
"ErrorDetailVerbosity.MESSAGE on the session's Behavior to have the "
"server's explanation returned with this error."
)
def _result_code_to_exception(
result_code: ResultCode,
message: str = "",
in_doubt: bool = False,
*,
sub_code: int | None = None,
server_message: str | None = None,
exp_trace: object | None = None,
node: str | None = None,
iteration: int | None = None,
sub_exceptions: tuple[AerospikeError, ...] = (),
base_message: str | None = None,
hint: str | None = None,
) -> AerospikeError:
"""Map a server result code to the appropriate typed exception.
When the code is one whose cause is a recognizable misconfiguration, local
guidance is appended to *message* and kept on the exception as ``hint``. Pass
``hint`` explicitly to override the generic text with something narrower --
a caller holding the operation knows more than the code alone conveys.
"""
cls = _RC_TO_TYPE.get(result_code, AerospikeError)
# A row-level failure arrives as a bare code; the code's descriptive string
# is then both the message and the base message, as it is for a server
# failure that reaches this layer already rendered.
if not message:
message = result_code.description
if base_message is None:
base_message = result_code.description
if hint is not None:
guidance: str | None = hint
elif sub_code:
# A subcode names the exact condition, so it beats the per-code text.
# Falsy covers both "absent" and SubCode.NONE.
guidance = (
_SUBCODE_GUIDANCE.get((result_code, sub_code))
or _RC_GUIDANCE.get(result_code)
)
elif (
result_code == ResultCode.PARAMETER_ERROR
and sub_code is None
and server_message is None
):
guidance = _PARAMETER_ERROR_DETAIL_HINT
else:
guidance = _RC_GUIDANCE.get(result_code)
if guidance:
message = f"{message}\n{guidance}" if message else guidance
return cls(
message,
result_code=result_code,
in_doubt=in_doubt,
sub_code=sub_code,
server_message=server_message,
exp_trace=exp_trace,
node=node,
iteration=iteration,
sub_exceptions=sub_exceptions,
base_message=base_message,
hint=guidance,
)
# ---------------------------------------------------------------------------
# Boundary converter: PNC exception -> PSDK exception
# ---------------------------------------------------------------------------
def _retry_context_kwargs(exc: Exception) -> dict:
"""Extract the retry/diagnostic context a PNC exception carries.
``getattr`` defaults keep this tolerant of PNC versions predating a
field. Prior-attempt exceptions are themselves converted, so
``sub_exceptions`` is homogeneous in this hierarchy (PNC sub-errors
never nest further, so the recursion is single-level).
"""
subs = getattr(exc, "sub_exceptions", None)
return {
"node": getattr(exc, "node", None),
"iteration": getattr(exc, "iteration", None),
"base_message": getattr(exc, "base_message", None),
"sub_exceptions": (
tuple(_convert_pnc_exception(s) for s in subs) if subs else ()
),
}
def _client_error_kwargs(exc: Exception) -> dict:
"""The fields a client-side PNC failure carries, ready for a constructor.
The result code rides along with the retry context: a client failure
carries its client code the way a server failure carries the server's.
"""
return {
"result_code": getattr(exc, "result_code", None),
"in_doubt": getattr(exc, "in_doubt", False),
**_retry_context_kwargs(exc),
}
def _convert_pnc_exception(exc: Exception, *, hint: str | None = None) -> AerospikeError:
"""Convert a PNC exception to the appropriate PSDK typed exception.
The original exception is **not** set as ``__cause__`` here; callers
should use ``raise _convert_pnc_exception(e) from e``.
Args:
exc: The PNC exception to convert.
hint: Guidance narrower than the result code alone supports, from a
caller that still has the operation in scope. Replaces the generic
text for that code. Ignored for failures that carry no result code.
"""
if isinstance(exc, AerospikeError):
return exc
if isinstance(exc, PncServerError):
return _result_code_to_exception(
exc.result_code,
str(exc),
exc.in_doubt,
sub_code=getattr(exc, "sub_code", None),
server_message=getattr(exc, "server_message", None),
exp_trace=getattr(exc, "exp_trace", None),
hint=hint,
**_retry_context_kwargs(exc),
)
if isinstance(exc, PncMaxErrorRate):
return MaxErrorRateError(str(exc), **_client_error_kwargs(exc))
if isinstance(exc, PncTimeoutError):
# PNC's TimeoutError is core's client-side deadline; a server-reported
# timeout arrives as a ServerError with the TIMEOUT result code and
# keeps the default client=False.
return TimeoutError(str(exc), client=True, **_client_error_kwargs(exc))
if isinstance(exc, PncConnectionError):
return ConnectionError(str(exc), **_client_error_kwargs(exc))
if isinstance(exc, PncInvalidNodeError):
return InvalidNodeError(str(exc), **_client_error_kwargs(exc))
if isinstance(exc, PncUDFBadResponse):
return _result_code_to_exception(
ResultCode.UDF_BAD_RESPONSE,
str(exc),
getattr(exc, "in_doubt", False),
**_retry_context_kwargs(exc),
)
if isinstance(exc, PncCommitFailedError):
# The stage, the per-key records, and the code that tripped verify or
# roll all come straight through. The code is absent when the failure
# was client-side, which is why this is classified by type as well.
return CommitError(
str(exc),
result_code=getattr(exc, "result_code", None),
commit_error_type=getattr(exc, "commit_error_type", None),
verify_records=getattr(exc, "verify_records", None),
roll_records=getattr(exc, "roll_records", None),
in_doubt=getattr(exc, "in_doubt", False),
**_retry_context_kwargs(exc),
)
if isinstance(exc, PncAerospikeError):
# A generic client failure still names its condition by code, and
# some of those conditions have a type here (an abort refused after a
# failed commit is a TransactionError).
code = getattr(exc, "result_code", None)
if code is not None:
return _result_code_to_exception(
code, str(exc), getattr(exc, "in_doubt", False),
**_retry_context_kwargs(exc),
)
# A client-side failure that names no condition (a CA file that
# cannot be read, for example) still reports as the client's own
# error, so code-branching callers always have a code to branch on.
return AerospikeError(
str(exc), result_code=ResultCode.CLIENT_ERROR,
in_doubt=getattr(exc, "in_doubt", False),
**_retry_context_kwargs(exc),
)
return AerospikeError(str(exc))