Skip to content
51 changes: 51 additions & 0 deletions client/src/com/aerospike/client/AerospikeException.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class AerospikeException extends RuntimeException {
protected transient Policy policy;
protected List<AerospikeException> subExceptions;
protected int resultCode = ResultCode.CLIENT_ERROR;
protected int subcode = SubCode.NONE;
protected ExpressionTrace expTrace;
protected int iteration = -1;
protected boolean inDoubt;

Expand All @@ -40,6 +42,12 @@ public AerospikeException(int resultCode, String message) {
this.resultCode = resultCode;
}

public AerospikeException(int resultCode, String message, int subcode) {
super(message);
this.resultCode = resultCode;
this.subcode = subcode;
}

public AerospikeException(int resultCode, Throwable e) {
super(e);
this.resultCode = resultCode;
Expand Down Expand Up @@ -184,6 +192,49 @@ public final int getResultCode() {
return resultCode;
}

/**
* Get the server-supplied error subcode, or {@link SubCode#NONE} (0) when the
* server did not return one (verbosity disabled, or the failing branch had no
* dispatchable subcode).
* <p>
* A subcode is only meaningful when interpreted together with
* {@link #getResultCode()}: subcode integer values are scoped to their parent
* result code and are NOT globally unique. Dispatch on the
* {@code (resultCode, subcode)} pair. See {@link SubCode}.
*/
public final int getSubcode() {
return subcode;
}

/**
* Set the server-supplied error subcode.
*/
public final void setSubcode(int subcode) {
this.subcode = subcode;
}

/**
* Get the server-supplied expression build trace, or {@code null} when absent.
* <p>
* Populated only at error-detail verbosity 3 (see
* {@link com.aerospike.client.policy.Policy#errorDetailVerbosity}) on an expression
* build failure — a metadata filter ({@code filter_exp}) or an {@code exp_read}/
* {@code exp_write} operation that the server could not build. Such failures carry
* {@link ResultCode#PARAMETER_ERROR} and {@link SubCode#NONE}. {@code null} on every
* other failure (including non-expression failures at verbosity 3). See
* {@link ExpressionTrace}.
*/
public final ExpressionTrace getExpressionTrace() {
return expTrace;
}

/**
* Set the server-supplied expression build trace.
*/
public final void setExpressionTrace(ExpressionTrace expTrace) {
this.expTrace = expTrace;
}

/**
* Get number of attempts before failing.
*/
Expand Down
245 changes: 245 additions & 0 deletions client/src/com/aerospike/client/ExpressionTrace.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/*
* Copyright 2012-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.
*/
package com.aerospike.client;

import java.io.Serializable;
import java.util.Arrays;

/**
* Structured expression build/eval trace surfaced at error-detail verbosity 3.
* <p>
* When extended error detail is requested at verbosity 3 (see
* {@link com.aerospike.client.policy.Policy#errorDetailVerbosity}) and the server
* fails to build an expression — a metadata/predicate filter ({@code filter_exp})
* or an {@code exp_read}/{@code exp_write} operation — it attaches this trace as a
* nested map under the field-45 error-detail key {@link #AS_ERROR_DETAIL_KEY_EXP_TRACE}.
* It is surfaced on {@link AerospikeException#getExpressionTrace()}.
* <p>
* Expression build failures carry {@link ResultCode#PARAMETER_ERROR} and
* {@link SubCode#NONE} (no subcode); the contextual message is on the exception.
* The trace is purely additive diagnostic detail — it never changes the result
* code, subcode, or message-string format.
* <p>
* <b>Every field is optional.</b> The server caps the whole error-detail payload
* and drops {@code snippet} first, then {@code path}, when the budget is tight, so
* those may be absent even within a present trace. Absent integer fields read as
* {@code -1} (except {@link #getLang()}, which defaults to {@link #LANG_MSGPACK});
* absent object fields read as {@code null}. Never require any field.
* <p>
* <b>Two coordinate spaces — do not conflate them.</b> {@link #getByteOffset()} is a
* byte offset into the <i>msgpack expression payload</i> the client sent. The
* {@link #getAelOffset()}/{@link #getAelSpan()} pair are offsets into <i>AEL source
* text</i> — a different coordinate space, reserved for a future server branch and
* absent on today's msgpack build traces.
* <p>
* The nested-map key/value constants below mirror the server's {@code proto.h} names
* so they stay greppable across repositories. They are append-only.
*/
public final class ExpressionTrace implements Serializable {
private static final long serialVersionUID = 1L;

//-------------------------------------------------------
// Wire constants (mirror server proto.h).
//-------------------------------------------------------

/** Top-level field-45 error-detail key carrying the nested expression-trace map. */
public static final int AS_ERROR_DETAIL_KEY_EXP_TRACE = 3;

/** Nested trace key: phase (uint; {@link #PHASE_BUILD} / {@link #PHASE_EVAL}). */
public static final int KEY_PHASE = 1;
/** Nested trace key: byte_offset into the msgpack expression payload (uint). */
public static final int KEY_BYTE_OFFSET = 2;
/** Nested trace key: failing op name (str). */
public static final int KEY_OP = 3;
/** Nested trace key: true nesting depth of the fault (uint). */
public static final int KEY_DEPTH = 4;
/** Nested trace key: op-name chain root&rarr;fault (array of str). */
public static final int KEY_PATH = 5;
/** Nested trace key: human-only rendered snippet of the failing element (str). */
public static final int KEY_SNIPPET = 6;
/** Nested trace key: eval-phase outcome (uint; reserved, SERVER-1138). */
public static final int KEY_OUTCOME = 7;
/** Nested trace key: source language (uint; {@link #LANG_MSGPACK} / {@link #LANG_AEL}). */
public static final int KEY_LANG = 8;
/** Nested trace key: char offset into AEL source text (uint; reserved). */
public static final int KEY_AEL_OFFSET = 9;
/** Nested trace key: byte width of the offending AEL source region (uint; reserved). */
public static final int KEY_AEL_SPAN = 10;
/** Nested trace key: 1-based line in AEL source (uint; reserved). */
public static final int KEY_AEL_LINE = 11;
/** Nested trace key: 1-based column in AEL source (uint; reserved). */
public static final int KEY_AEL_COL = 12;

/** Phase value: expression build failed. */
public static final int PHASE_BUILD = 1;
/** Phase value: expression evaluation failed (reserved, SERVER-1138). */
public static final int PHASE_EVAL = 2;

/** Source language: msgpack (the implied default when {@code lang} is absent). */
public static final int LANG_MSGPACK = 1;
/** Source language: AEL DSL (reserved for a future server branch). */
public static final int LANG_AEL = 2;

/**
* The {@code "..."} sentinel the server splices into {@link #getPath()} when the
* true nesting depth exceeds the path-frame cap. {@link #getDepth()} still reports
* the true count.
*/
public static final String PATH_TRUNCATION_SENTINEL = "...";

//-------------------------------------------------------
// Fields (all optional; sentinels mark "absent").
//-------------------------------------------------------

private final int phase;
private final int byteOffset;
private final String op;
private final int depth;
private final String[] path;
private final String snippet;
private final int lang;
private final int aelOffset;
private final int aelSpan;

/**
* Construct a trace. Use {@code -1} / {@code null} for any absent field.
*
* @param phase {@link #PHASE_BUILD} / {@link #PHASE_EVAL}, or {@code -1} if absent
* @param byteOffset byte offset into the msgpack expression payload, or {@code -1}
* @param op failing op name, or {@code null}
* @param depth true nesting depth of the fault, or {@code -1}
* @param path op-name chain root&rarr;fault, or {@code null}
* @param snippet rendered snippet of the failing element, or {@code null}
* @param lang {@link #LANG_MSGPACK} / {@link #LANG_AEL}, or {@code -1} (&rArr; msgpack)
* @param aelOffset char offset into AEL source text, or {@code -1}
* @param aelSpan byte width of the offending AEL source region, or {@code -1}
*/
public ExpressionTrace(int phase, int byteOffset, String op, int depth, String[] path,
String snippet, int lang, int aelOffset, int aelSpan) {
this.phase = phase;
this.byteOffset = byteOffset;
this.op = op;
this.depth = depth;
this.path = path;
this.snippet = snippet;
this.lang = lang;
this.aelOffset = aelOffset;
this.aelSpan = aelSpan;
}

/**
* Phase that failed: {@link #PHASE_BUILD} or {@link #PHASE_EVAL}. Returns {@code -1}
* when absent. Today the server emits build traces only ({@code PHASE_BUILD}).
*/
public int getPhase() {
return phase;
}

/**
* Byte offset into the msgpack expression payload of the failing element, or
* {@code -1} when absent. This is a coordinate into the wire payload the client
* sent — not into AEL source text (see {@link #getAelOffset()}).
*/
public int getByteOffset() {
return byteOffset;
}

/**
* Failing op name (pre-rendered server-side), or {@code null} when absent.
*/
public String getOp() {
return op;
}

/**
* True nesting depth of the fault, or {@code -1} when absent. Reports the true
* count even when {@link #getPath()} was truncated to the frame cap.
*/
public int getDepth() {
return depth;
}

/**
* Op-name chain from root to fault, or {@code null} when absent. May contain a
* {@link #PATH_TRUNCATION_SENTINEL} ({@code "..."}) element mid-array when the true
* nesting exceeded the server's path-frame cap; {@link #getDepth()} still reports
* the true count.
*/
public String[] getPath() {
return path;
}

/**
* Human-only rendered snippet of the failing element, or {@code null} when absent
* (it is the first field the server drops under a tight byte budget).
*/
public String getSnippet() {
return snippet;
}

/**
* Source language: {@link #LANG_MSGPACK} or {@link #LANG_AEL}. An absent {@code lang}
* key means msgpack (the default), so this returns {@link #LANG_MSGPACK} when the
* server omitted it.
*/
public int getLang() {
return (lang < 0) ? LANG_MSGPACK : lang;
}

/**
* Char offset into the AEL source text, or {@code -1} when absent. Reserved for the
* AEL DSL branch; absent on today's msgpack build traces. A different coordinate
* space from {@link #getByteOffset()}.
*/
public int getAelOffset() {
return aelOffset;
}

/**
* Byte width of the offending AEL source region, or {@code -1} when absent.
* Reserved for the AEL DSL branch.
*/
public int getAelSpan() {
return aelSpan;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder(128);
sb.append("ExpressionTrace[phase=").append(phase);
sb.append(", byteOffset=").append(byteOffset);
if (op != null) {
sb.append(", op=").append(op);
}
sb.append(", depth=").append(depth);
if (path != null) {
sb.append(", path=").append(Arrays.toString(path));
}
if (snippet != null) {
sb.append(", snippet=").append(snippet);
}
sb.append(", lang=").append(getLang());
if (aelOffset >= 0) {
sb.append(", aelOffset=").append(aelOffset);
}
if (aelSpan >= 0) {
sb.append(", aelSpan=").append(aelSpan);
}
sb.append(']');
return sb.toString();
}
}
Loading
Loading