From be33006e6ae0102f767db8e690bd384f6983f1dd Mon Sep 17 00:00:00 2001 From: Mirza Karacic Date: Wed, 1 Apr 2026 09:09:22 -0700 Subject: [PATCH 1/6] CLIENT-4221 Extended errors --- .../com/aerospike/client/command/Command.java | 11 +- .../client/command/DeleteCommand.java | 4 +- .../client/command/ExecuteCommand.java | 4 +- .../client/command/ExistsCommand.java | 4 +- .../aerospike/client/command/FieldType.java | 1 + .../client/command/OperateCommandWrite.java | 4 +- .../aerospike/client/command/ReadCommand.java | 4 +- .../client/command/ReadHeaderCommand.java | 4 +- .../client/command/RecordParser.java | 233 +++++++++++++++++- .../client/command/SyncWriteCommand.java | 5 + .../client/command/TouchCommand.java | 4 +- .../client/command/WriteCommand.java | 4 +- .../dynamicconfig/DynamicReadConfig.java | 8 +- .../dynamicconfig/DynamicWriteConfig.java | 8 +- .../com/aerospike/client/policy/Policy.java | 25 +- .../aerospike/client/policy/WritePolicy.java | 6 + .../com/aerospike/examples/ErrorMessage.java | 176 +++++++++++++ examples/src/com/aerospike/examples/Main.java | 1 + test/src/com/aerospike/test/SuiteSync.java | 2 + .../sync/basic/TestErrorDetailVerbosity.java | 233 ++++++++++++++++++ 20 files changed, 721 insertions(+), 20 deletions(-) create mode 100644 examples/src/com/aerospike/examples/ErrorMessage.java create mode 100644 test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java diff --git a/client/src/com/aerospike/client/command/Command.java b/client/src/com/aerospike/client/command/Command.java index cb936edcc..e55cf3f7a 100644 --- a/client/src/com/aerospike/client/command/Command.java +++ b/client/src/com/aerospike/client/command/Command.java @@ -108,6 +108,7 @@ public class Command { public static final int INFO4_TXN_ROLL_FORWARD = (1 << 1); // Roll forward transaction. public static final int INFO4_TXN_ROLL_BACK = (1 << 2); // Roll back transaction. public static final int INFO4_TXN_ON_LOCKING_ONLY = (1 << 4); // Must be able to lock record in transaction. + public static final int INFO4_ERROR_VERBOSITY_SHIFT = 5; // info4 bits 5-6: error detail verbosity level public static final byte STATE_READ_AUTH_HEADER = 1; public static final byte STATE_READ_HEADER = 2; @@ -2399,6 +2400,8 @@ private final void writeHeaderWrite(WritePolicy policy, int writeAttr, int field txnAttr |= Command.INFO4_TXN_ON_LOCKING_ONLY; } + txnAttr |= (policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); + if (policy.xdr) { readAttr |= Command.INFO1_XDR; } @@ -2477,6 +2480,8 @@ private final void writeHeaderReadWrite( txnAttr |= Command.INFO4_TXN_ON_LOCKING_ONLY; } + txnAttr |= (policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); + if (policy.xdr) { readAttr |= Command.INFO1_XDR; } @@ -2557,8 +2562,9 @@ private final void writeHeaderRead( dataBuffer[9] = (byte)readAttr; dataBuffer[10] = (byte)writeAttr; dataBuffer[11] = (byte)infoAttr; + dataBuffer[12] = (byte)(policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); - for (int i = 12; i < 18; i++) { + for (int i = 13; i < 18; i++) { dataBuffer[i] = 0; } Buffer.intToBytes(policy.readTouchTtlPercent, dataBuffer, 18); @@ -2597,8 +2603,9 @@ private final void writeHeaderReadHeader(Policy policy, int readAttr, int fieldC dataBuffer[9] = (byte)readAttr; dataBuffer[10] = (byte)0; dataBuffer[11] = (byte)infoAttr; + dataBuffer[12] = (byte)(policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); - for (int i = 12; i < 18; i++) { + for (int i = 13; i < 18; i++) { dataBuffer[i] = 0; } Buffer.intToBytes(policy.readTouchTtlPercent, dataBuffer, 18); diff --git a/client/src/com/aerospike/client/command/DeleteCommand.java b/client/src/com/aerospike/client/command/DeleteCommand.java index fcd8d3ec2..fe5126ba1 100644 --- a/client/src/com/aerospike/client/command/DeleteCommand.java +++ b/client/src/com/aerospike/client/command/DeleteCommand.java @@ -59,7 +59,9 @@ protected void parseResult(Node node, Connection conn) throws IOException { return; } - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } public boolean existed() { diff --git a/client/src/com/aerospike/client/command/ExecuteCommand.java b/client/src/com/aerospike/client/command/ExecuteCommand.java index d7c867cb3..862a00e16 100644 --- a/client/src/com/aerospike/client/command/ExecuteCommand.java +++ b/client/src/com/aerospike/client/command/ExecuteCommand.java @@ -79,7 +79,9 @@ record = rp.parseRecord(false); return; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } private void handleUdfError(int resultCode) { diff --git a/client/src/com/aerospike/client/command/ExistsCommand.java b/client/src/com/aerospike/client/command/ExistsCommand.java index 94428f605..f870c271a 100644 --- a/client/src/com/aerospike/client/command/ExistsCommand.java +++ b/client/src/com/aerospike/client/command/ExistsCommand.java @@ -64,7 +64,9 @@ protected void parseResult(Node node, Connection conn) throws IOException { return; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } public boolean exists() { diff --git a/client/src/com/aerospike/client/command/FieldType.java b/client/src/com/aerospike/client/command/FieldType.java index 9a188da8e..24ff7cf7e 100644 --- a/client/src/com/aerospike/client/command/FieldType.java +++ b/client/src/com/aerospike/client/command/FieldType.java @@ -43,4 +43,5 @@ public final class FieldType { public final static int QUERY_BINLIST = 40; public final static int BATCH_INDEX = 41; public final static int FILTER_EXP = 43; + public static final int ERROR_MESSAGE = 45; } diff --git a/client/src/com/aerospike/client/command/OperateCommandWrite.java b/client/src/com/aerospike/client/command/OperateCommandWrite.java index eece21e6d..a0b6d1b17 100644 --- a/client/src/com/aerospike/client/command/OperateCommandWrite.java +++ b/client/src/com/aerospike/client/command/OperateCommandWrite.java @@ -61,7 +61,9 @@ record = rp.parseRecord(true); return; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/ReadCommand.java b/client/src/com/aerospike/client/command/ReadCommand.java index 9f3c35f3b..836ce671f 100644 --- a/client/src/com/aerospike/client/command/ReadCommand.java +++ b/client/src/com/aerospike/client/command/ReadCommand.java @@ -78,7 +78,9 @@ protected void parseResult(Node node, Connection conn) throws IOException { return; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/ReadHeaderCommand.java b/client/src/com/aerospike/client/command/ReadHeaderCommand.java index b6a42aa0a..264cec1d5 100644 --- a/client/src/com/aerospike/client/command/ReadHeaderCommand.java +++ b/client/src/com/aerospike/client/command/ReadHeaderCommand.java @@ -64,7 +64,9 @@ record = new Record(null, rp.generation, rp.expiration); return; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/RecordParser.java b/client/src/com/aerospike/client/command/RecordParser.java index f40ad7d3a..5283ff4fa 100644 --- a/client/src/com/aerospike/client/command/RecordParser.java +++ b/client/src/com/aerospike/client/command/RecordParser.java @@ -38,6 +38,7 @@ public final class RecordParser { public final int opCount; public int dataOffset; public long bytesIn; + public String serverMessage; /** * Sync record parser. @@ -159,7 +160,7 @@ public RecordParser(byte[] buffer, int offset, int receiveSize) { public void parseFields(Txn txn, Key key, boolean hasWrite) { if (txn == null) { - skipFields(); + parseFieldsError(); return; } @@ -180,6 +181,9 @@ public void parseFields(Txn txn, Key key, boolean hasWrite) { throw new AerospikeException("Record version field has invalid size: " + size); } } + else if (type == FieldType.ERROR_MESSAGE && size > 0) { + serverMessage = parseErrorDetails(dataOffset, size); + } dataOffset += size; } @@ -206,12 +210,229 @@ public void parseTranDeadline(Txn txn) { } } - private void skipFields() { - // There can be fields in the response (setname etc). - // But for now, ignore them. Expose them to the API if needed in the future. + private void parseFieldsError() { for (int i = 0; i < fieldCount; i++) { - int fieldlen = Buffer.bytesToInt(dataBuffer, dataOffset); - dataOffset += 4 + fieldlen; + int len = Buffer.bytesToInt(dataBuffer, dataOffset); + dataOffset += 4; + + int type = dataBuffer[dataOffset++]; + int size = len - 1; + + if (type == FieldType.ERROR_MESSAGE && size > 0) { + serverMessage = parseErrorDetails(dataOffset, size); + } + dataOffset += size; + } + } + + /** + * Parse error detail msgpack map from server response. + * Map keys: 1 = subcode (uint), 2 = message (string). + * Returns formatted error message string. + */ + private String parseErrorDetails(int offset, int size) { + int end = offset + size; + + if (offset >= end) { + return null; + } + + // Read fixmap header. + int b = dataBuffer[offset++] & 0xFF; + int count; + + if ((b & 0xF0) == 0x80) { + count = b & 0x0F; + } + else { + return null; + } + + if (count <= 0) { + return null; + } + + String message = null; + long subcode = -1; + + for (int i = 0; i < count && offset < end; i++) { + // Read key (positive fixint or uint8). + int key; + b = dataBuffer[offset++] & 0xFF; + + if (b <= 0x7F) { + key = b; + } + else if (b == 0xCC && offset < end) { + key = dataBuffer[offset++] & 0xFF; + } + else { + break; + } + + switch (key) { + case 1: // AS_ERROR_DETAIL_KEY_SUBCODE + subcode = unpackUint(offset, end); + offset = skipMsgpackValue(offset, end); + break; + + case 2: // AS_ERROR_DETAIL_KEY_MESSAGE + int[] strResult = unpackStr(offset, end); + if (strResult != null) { + message = new String(dataBuffer, strResult[0], strResult[1], java.nio.charset.StandardCharsets.UTF_8); + offset = strResult[0] + strResult[1]; + } + else { + offset = skipMsgpackValue(offset, end); + } + break; + + default: + offset = skipMsgpackValue(offset, end); + break; + } + } + + if (message != null && subcode >= 0) { + return message + " (subcode=" + subcode + ")"; + } + else if (subcode >= 0) { + return "error subcode=" + subcode; + } + else if (message != null) { + return message; + } + return null; + } + + /** + * Unpack a msgpack unsigned integer value. Returns -1 on failure. + */ + private long unpackUint(int offset, int end) { + if (offset >= end) { + return -1; + } + + int b = dataBuffer[offset] & 0xFF; + + if (b <= 0x7F) { + return b; + } + else if (b == 0xCC && offset + 1 < end) { + return dataBuffer[offset + 1] & 0xFF; + } + else if (b == 0xCD && offset + 2 < end) { + return Buffer.bytesToShort(dataBuffer, offset + 1) & 0xFFFF; + } + else if (b == 0xCE && offset + 4 < end) { + return Buffer.bytesToInt(dataBuffer, offset + 1) & 0xFFFFFFFFL; + } + else if (b == 0xCF && offset + 8 < end) { + return Buffer.bytesToLong(dataBuffer, offset + 1); + } + return -1; + } + + /** + * Unpack a msgpack string. Returns [offset, length] or null on failure. + */ + private int[] unpackStr(int offset, int end) { + if (offset >= end) { + return null; + } + + int b = dataBuffer[offset++] & 0xFF; + int len; + + if ((b & 0xE0) == 0xA0) { + len = b & 0x1F; + } + else if (b == 0xD9 && offset < end) { + len = dataBuffer[offset++] & 0xFF; + } + else if (b == 0xDA && offset + 1 < end) { + len = Buffer.bytesToShort(dataBuffer, offset) & 0xFFFF; + offset += 2; + } + else { + return null; + } + + if (offset + len > end) { + return null; + } + + return new int[]{offset, len}; + } + + /** + * Skip a single msgpack value, returning the new offset. + */ + private int skipMsgpackValue(int offset, int end) { + if (offset >= end) { + return end; + } + + int b = dataBuffer[offset++] & 0xFF; + + // Positive fixint / negative fixint + if (b <= 0x7F || b >= 0xE0) { + return offset; + } + // fixstr + if ((b & 0xE0) == 0xA0) { + return offset + (b & 0x1F); + } + // fixmap + if ((b & 0xF0) == 0x80) { + int count = (b & 0x0F) * 2; + for (int i = 0; i < count && offset < end; i++) { + offset = skipMsgpackValue(offset, end); + } + return offset; + } + // fixarray + if ((b & 0xF0) == 0x90) { + int count = b & 0x0F; + for (int i = 0; i < count && offset < end; i++) { + offset = skipMsgpackValue(offset, end); + } + return offset; + } + + switch (b) { + case 0xC0: // nil + case 0xC2: // false + case 0xC3: // true + return offset; + case 0xCC: // uint8 + case 0xD0: // int8 + return offset + 1; + case 0xCD: // uint16 + case 0xD1: // int16 + return offset + 2; + case 0xCE: // uint32 + case 0xD2: // int32 + case 0xCA: // float32 + return offset + 4; + case 0xCF: // uint64 + case 0xD3: // int64 + case 0xCB: // float64 + return offset + 8; + case 0xD9: // str8 + case 0xC4: // bin8 + if (offset < end) { + return offset + 1 + (dataBuffer[offset] & 0xFF); + } + return end; + case 0xDA: // str16 + case 0xC5: // bin16 + if (offset + 1 < end) { + return offset + 2 + (Buffer.bytesToShort(dataBuffer, offset) & 0xFFFF); + } + return end; + default: + return end; } } diff --git a/client/src/com/aerospike/client/command/SyncWriteCommand.java b/client/src/com/aerospike/client/command/SyncWriteCommand.java index 06b241260..0c38692bc 100644 --- a/client/src/com/aerospike/client/command/SyncWriteCommand.java +++ b/client/src/com/aerospike/client/command/SyncWriteCommand.java @@ -73,6 +73,11 @@ protected int parseHeader(Node node, Connection conn) throws IOException { if (node.areMetricsEnabled()) { node.addBytesIn(namespace, rp.bytesIn); } + if (rp.serverMessage != null) { + this.serverMessage = rp.serverMessage; + } return rp.resultCode; } + + protected String serverMessage; } diff --git a/client/src/com/aerospike/client/command/TouchCommand.java b/client/src/com/aerospike/client/command/TouchCommand.java index 49efeb5a2..6b06fa2e6 100644 --- a/client/src/com/aerospike/client/command/TouchCommand.java +++ b/client/src/com/aerospike/client/command/TouchCommand.java @@ -65,7 +65,9 @@ protected void parseResult(Node node, Connection conn) throws IOException { return; } - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } public boolean getTouched() { diff --git a/client/src/com/aerospike/client/command/WriteCommand.java b/client/src/com/aerospike/client/command/WriteCommand.java index 5c77e0a34..f4e889d77 100644 --- a/client/src/com/aerospike/client/command/WriteCommand.java +++ b/client/src/com/aerospike/client/command/WriteCommand.java @@ -57,6 +57,8 @@ protected void parseResult(Node node, Connection conn) throws IOException { return; } - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } } diff --git a/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicReadConfig.java b/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicReadConfig.java index 216f317bf..761b483d9 100644 --- a/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicReadConfig.java +++ b/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicReadConfig.java @@ -38,6 +38,7 @@ public class DynamicReadConfig { public IntProperty totalTimeout; public IntProperty maxRetries; public DoubleProperty sleepMultiplier; + public IntProperty errorDetailVerbosity; public DynamicReadConfig() {} @@ -63,6 +64,8 @@ public DynamicReadConfig() {} public void setSleepMultiplier(DoubleProperty sleepMultiplier) { this.sleepMultiplier = sleepMultiplier; } + public void setErrorDetailVerbosity(IntProperty errorDetailVerbosity) { this.errorDetailVerbosity = errorDetailVerbosity; } + public ReadModeAP getReadModeAP() { return readModeAP; } public ReadModeSC getReadModeSC() { return readModeSC; } @@ -85,6 +88,8 @@ public DynamicReadConfig() {} public DoubleProperty getSleepMultiplier() { return sleepMultiplier; } + public IntProperty getErrorDetailVerbosity() { return errorDetailVerbosity; } + @Override public String toString() { StringBuffer propsString = new StringBuffer("{"); @@ -99,7 +104,8 @@ public String toString() { propsString.append(" timeout_delay=").append(timeoutDelay.value).append(", "); propsString.append(" total_timeout=").append(totalTimeout.value).append(", "); propsString.append(" max_retries=").append(maxRetries.value).append(", "); - propsString.append(" sleep_multiplier=").append(sleepMultiplier.value); + propsString.append(" sleep_multiplier=").append(sleepMultiplier.value).append(", "); + propsString.append(" error_detail_verbosity=").append(errorDetailVerbosity != null ? errorDetailVerbosity.value : "null"); } catch (Exception e) { Log.error(e.toString()); } diff --git a/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicWriteConfig.java b/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicWriteConfig.java index dd52706a3..7a021cebb 100644 --- a/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicWriteConfig.java +++ b/client/src/com/aerospike/client/configuration/serializers/dynamicconfig/DynamicWriteConfig.java @@ -36,6 +36,7 @@ public class DynamicWriteConfig { public IntProperty maxRetries; public BooleanProperty durableDelete; public DoubleProperty sleepMultiplier; + public IntProperty errorDetailVerbosity; public DynamicWriteConfig() {} @@ -63,6 +64,8 @@ public void setReplica(Replica replica) { public void setSleepMultiplier(DoubleProperty sleepMultiplier) { this.sleepMultiplier = sleepMultiplier; } + public void setErrorDetailVerbosity(IntProperty errorDetailVerbosity) { this.errorDetailVerbosity = errorDetailVerbosity; } + public IntProperty getConnectTimeout() { return connectTimeout; } public BooleanProperty getFailOnFilteredOut() { return failOnFilteredOut; } @@ -85,6 +88,8 @@ public void setReplica(Replica replica) { public DoubleProperty getSleepMultiplier() { return sleepMultiplier; } + public IntProperty getErrorDetailVerbosity() { return errorDetailVerbosity; } + @Override public String toString() { StringBuffer propsString = new StringBuffer("{"); @@ -99,7 +104,8 @@ public String toString() { propsString.append(" total_timeout=").append(totalTimeout.value).append(", "); propsString.append(" max_retries=").append(maxRetries.value).append(", "); propsString.append(" durable_delete=").append(durableDelete.value).append(", "); - propsString.append(" sleep_multiplier=").append(sleepMultiplier.value); + propsString.append(" sleep_multiplier=").append(sleepMultiplier.value).append(", "); + propsString.append(" error_detail_verbosity=").append(errorDetailVerbosity != null ? errorDetailVerbosity.value : "null"); } catch (Exception e) { Log.error(e.toString()); } diff --git a/client/src/com/aerospike/client/policy/Policy.java b/client/src/com/aerospike/client/policy/Policy.java index d8be235d2..60eb20970 100644 --- a/client/src/com/aerospike/client/policy/Policy.java +++ b/client/src/com/aerospike/client/policy/Policy.java @@ -272,6 +272,18 @@ public class Policy { */ public boolean failOnFilteredOut; + /** + * Request server error detail fields in responses. + * + *

+ * Default: 0 + */ + public int errorDetailVerbosity; + /** * Copy policy from another policy. */ @@ -292,6 +304,7 @@ public Policy(Policy other) { this.compress = other.compress; this.failOnFilteredOut = other.failOnFilteredOut; this.sleepMultiplier = other.sleepMultiplier; + this.errorDetailVerbosity = other.errorDetailVerbosity; } /** @@ -405,6 +418,8 @@ public void setFailOnFilteredOut(boolean failOnFilteredOut) { public void setSleepMultiplier(double sleepMultiplier) { this.sleepMultiplier = sleepMultiplier; } + public void setErrorDetailVerbosity(int errorDetailVerbosity) { this.errorDetailVerbosity = errorDetailVerbosity; } + @Override public boolean equals(Object o) { if (this == o) { @@ -414,12 +429,12 @@ public boolean equals(Object o) { return false; } Policy policy = (Policy) o; - return connectTimeout == policy.connectTimeout && socketTimeout == policy.socketTimeout && totalTimeout == policy.totalTimeout && timeoutDelay == policy.timeoutDelay && maxRetries == policy.maxRetries && sleepBetweenRetries == policy.sleepBetweenRetries && Double.compare(policy.sleepMultiplier, sleepMultiplier) == 0 && readTouchTtlPercent == policy.readTouchTtlPercent && sendKey == policy.sendKey && compress == policy.compress && failOnFilteredOut == policy.failOnFilteredOut && Objects.equals(txn, policy.txn) && readModeAP == policy.readModeAP && readModeSC == policy.readModeSC && replica == policy.replica && Objects.equals(filterExp, policy.filterExp); + return connectTimeout == policy.connectTimeout && socketTimeout == policy.socketTimeout && totalTimeout == policy.totalTimeout && timeoutDelay == policy.timeoutDelay && maxRetries == policy.maxRetries && sleepBetweenRetries == policy.sleepBetweenRetries && Double.compare(policy.sleepMultiplier, sleepMultiplier) == 0 && readTouchTtlPercent == policy.readTouchTtlPercent && sendKey == policy.sendKey && compress == policy.compress && failOnFilteredOut == policy.failOnFilteredOut && errorDetailVerbosity == policy.errorDetailVerbosity && Objects.equals(txn, policy.txn) && readModeAP == policy.readModeAP && readModeSC == policy.readModeSC && replica == policy.replica && Objects.equals(filterExp, policy.filterExp); } @Override public int hashCode() { - return Objects.hash(txn, readModeAP, readModeSC, replica, filterExp, connectTimeout, socketTimeout, totalTimeout, timeoutDelay, maxRetries, sleepBetweenRetries, sleepMultiplier, readTouchTtlPercent, sendKey, compress, failOnFilteredOut); + return Objects.hash(txn, readModeAP, readModeSC, replica, filterExp, connectTimeout, socketTimeout, totalTimeout, timeoutDelay, maxRetries, sleepBetweenRetries, sleepMultiplier, readTouchTtlPercent, sendKey, compress, failOnFilteredOut, errorDetailVerbosity); } private void updateFromConfig(ConfigurationProvider configProvider, boolean log) { @@ -509,5 +524,11 @@ private void updateFromConfig(ConfigurationProvider configProvider, boolean log) Log.info("Set Policy.maxRetries = " + this.maxRetries); } } + if (dynRC.errorDetailVerbosity != null && this.errorDetailVerbosity != dynRC.errorDetailVerbosity.value) { + this.errorDetailVerbosity = dynRC.errorDetailVerbosity.value; + if (logUpdate) { + Log.info("Set Policy.errorDetailVerbosity = " + this.errorDetailVerbosity); + } + } } } diff --git a/client/src/com/aerospike/client/policy/WritePolicy.java b/client/src/com/aerospike/client/policy/WritePolicy.java index cbc1d460d..260818d6f 100644 --- a/client/src/com/aerospike/client/policy/WritePolicy.java +++ b/client/src/com/aerospike/client/policy/WritePolicy.java @@ -297,6 +297,12 @@ private void updateFromConfig(ConfigurationProvider configProvider, boolean log, Log.info("Set" + preText + " WritePolicy.durableDelete = " + this.durableDelete); } } + if (dynWC.errorDetailVerbosity != null && this.errorDetailVerbosity != dynWC.errorDetailVerbosity.value) { + this.errorDetailVerbosity = dynWC.errorDetailVerbosity.value; + if (logUpdate) { + Log.info("Set" + preText + " WritePolicy.errorDetailVerbosity = " + this.errorDetailVerbosity); + } + } } // Include setters to facilitate Spring's ConfigurationProperties. diff --git a/examples/src/com/aerospike/examples/ErrorMessage.java b/examples/src/com/aerospike/examples/ErrorMessage.java new file mode 100644 index 000000000..9b9ed8e9e --- /dev/null +++ b/examples/src/com/aerospike/examples/ErrorMessage.java @@ -0,0 +1,176 @@ +/* + * Copyright 2012-2025 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.examples; + +import com.aerospike.client.AerospikeException; +import com.aerospike.client.Bin; +import com.aerospike.client.IAerospikeClient; +import com.aerospike.client.Key; +import com.aerospike.client.Operation; +import com.aerospike.client.ResultCode; +import com.aerospike.client.operation.HLLOperation; +import com.aerospike.client.operation.HLLPolicy; +import com.aerospike.client.policy.GenerationPolicy; +import com.aerospike.client.policy.WritePolicy; +import com.aerospike.client.Value; + +import java.util.ArrayList; +import java.util.List; + +public class ErrorMessage extends Example { + + public ErrorMessage(Console console) { + super(console); + } + + @Override + public void runExample(IAerospikeClient client, Parameters params) throws Exception { + String binName = "test-bin"; + Key key = new Key(params.namespace, params.set, "error-message-key"); + + // Write a record with an integer bin. + WritePolicy writePolicy = new WritePolicy(); + writePolicy.errorDetailVerbosity = 2; + client.put(writePolicy, key, new Bin(binName, 1)); + console.info("Write succeeded, running error detail tests."); + + // Test 1: Append string to integer bin. + testAppendToIntegerBin(client, params, writePolicy, key, binName); + + // Test 2: Delete with wrong generation. + testDeleteGenerationMismatch(client, params, key); + + // Test 3: Increment a string bin. + testIncrementStringBin(client, params, writePolicy, binName); + + // Test 4: HLL add on integer bin. + testHllAddOnIntegerBin(client, params, writePolicy, key, binName); + + // Test 5: HLL refresh_count on nonexistent bin. + testHllRefreshCountMissingBin(client, params, writePolicy); + + console.info("Error message example completed successfully."); + } + + private void testAppendToIntegerBin( + IAerospikeClient client, + Parameters params, + WritePolicy writePolicy, + Key key, + String binName + ) { + try { + client.operate(writePolicy, key, Operation.append(new Bin(binName, "bad-append"))); + throw new RuntimeException("Expected error on append to integer bin"); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "cannot append", "subcode=1100"); + console.info("Test 1 passed: append to integer bin - %d: %s", ae.getResultCode(), ae.getBaseMessage()); + } + } + + private void testDeleteGenerationMismatch(IAerospikeClient client, Parameters params, Key key) { + WritePolicy rmPolicy = new WritePolicy(); + rmPolicy.errorDetailVerbosity = 2; + rmPolicy.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL; + rmPolicy.generation = 777; + + try { + client.delete(rmPolicy, key); + throw new RuntimeException("Expected error on generation-mismatch delete"); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.GENERATION_ERROR, "delete generation mismatch", "subcode=1701"); + console.info("Test 2 passed: generation mismatch delete - %d: %s", ae.getResultCode(), ae.getBaseMessage()); + } + } + + private void testIncrementStringBin( + IAerospikeClient client, + Parameters params, + WritePolicy writePolicy, + String binName + ) { + Key key2 = new Key(params.namespace, params.set, "error-message-key-2"); + client.put(writePolicy, key2, new Bin(binName, "hello")); + + try { + client.operate(writePolicy, key2, Operation.add(new Bin(binName, 1))); + throw new RuntimeException("Expected error on incr of string bin"); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "cannot increment", "subcode=1100"); + console.info("Test 3 passed: increment string bin - %d: %s", ae.getResultCode(), ae.getBaseMessage()); + } + } + + private void testHllAddOnIntegerBin( + IAerospikeClient client, + Parameters params, + WritePolicy writePolicy, + Key key, + String binName + ) { + List hllList = new ArrayList<>(); + hllList.add(Value.get("element1")); + + try { + client.operate(writePolicy, key, + HLLOperation.add(HLLPolicy.Default, binName, hllList, 8)); + throw new RuntimeException("Expected error on HLL add to integer bin"); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "bin is not hll type", "subcode=1138"); + console.info("Test 4 passed: HLL add on integer bin - %d: %s", ae.getResultCode(), ae.getBaseMessage()); + } + } + + private void testHllRefreshCountMissingBin( + IAerospikeClient client, + Parameters params, + WritePolicy writePolicy + ) { + Key key3 = new Key(params.namespace, params.set, "error-message-key-3"); + client.put(writePolicy, key3, new Bin("other-bin", 1)); + + try { + client.operate(writePolicy, key3, + HLLOperation.refreshCount("no-hll-bin")); + throw new RuntimeException("Expected error on HLL refresh_count of nonexistent bin"); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_NOT_FOUND, "subcode=1134"); + console.info("Test 5 passed: HLL refresh_count missing bin - %d: %s", ae.getResultCode(), ae.getBaseMessage()); + } + } + + private void assertErrorDetails(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { + if (ae.getResultCode() != expectedResultCode) { + throw new RuntimeException( + "Expected result code " + expectedResultCode + " but got " + ae.getResultCode() + ": " + ae.getBaseMessage()); + } + + String msg = ae.getBaseMessage(); + + for (String expected : expectedSubstrings) { + if (!msg.contains(expected)) { + throw new RuntimeException( + "Expected '" + expected + "' in error message: " + msg); + } + } + } +} diff --git a/examples/src/com/aerospike/examples/Main.java b/examples/src/com/aerospike/examples/Main.java index 41e09877c..43a9ac5a0 100644 --- a/examples/src/com/aerospike/examples/Main.java +++ b/examples/src/com/aerospike/examples/Main.java @@ -52,6 +52,7 @@ public class Main extends JPanel { "Touch", "StoreKey", "DeleteBin", + "ErrorMessage", "ListMap", "Operate", "OperateBit", diff --git a/test/src/com/aerospike/test/SuiteSync.java b/test/src/com/aerospike/test/SuiteSync.java index 9b4c9e272..59b515993 100644 --- a/test/src/com/aerospike/test/SuiteSync.java +++ b/test/src/com/aerospike/test/SuiteSync.java @@ -34,6 +34,7 @@ import com.aerospike.test.sync.basic.TestBitExp; import com.aerospike.test.sync.basic.TestConfigLoadYAML; import com.aerospike.test.sync.basic.TestDeleteBin; +import com.aerospike.test.sync.basic.TestErrorDetailVerbosity; import com.aerospike.test.sync.basic.TestExpOperation; import com.aerospike.test.sync.basic.TestExpire; import com.aerospike.test.sync.basic.TestFilterExp; @@ -84,6 +85,7 @@ TestCdtOperate.class, TestConfigLoadYAML.class, TestDeleteBin.class, + TestErrorDetailVerbosity.class, TestExpire.class, TestExpOperation.class, TestFilterExp.class, diff --git a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java new file mode 100644 index 000000000..593525e95 --- /dev/null +++ b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java @@ -0,0 +1,233 @@ +/* + * Copyright 2012-2025 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.test.sync.basic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.aerospike.client.AerospikeException; +import com.aerospike.client.Bin; +import com.aerospike.client.Key; +import com.aerospike.client.Operation; +import com.aerospike.client.Record; +import com.aerospike.client.ResultCode; +import com.aerospike.client.Value; +import com.aerospike.client.operation.HLLOperation; +import com.aerospike.client.operation.HLLPolicy; +import com.aerospike.client.policy.GenerationPolicy; +import com.aerospike.client.policy.Policy; +import com.aerospike.client.policy.WritePolicy; +import com.aerospike.test.sync.TestSync; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class TestErrorDetailVerbosity extends TestSync { + + private static final String binName = "edv-bin"; + private static Key intKey; + private static Key strKey; + + @BeforeClass + public static void setup() { + WritePolicy wp = new WritePolicy(); + intKey = new Key(args.namespace, args.set, "edv-int-key"); + strKey = new Key(args.namespace, args.set, "edv-str-key"); + + client.put(wp, intKey, new Bin(binName, 1)); + client.put(wp, strKey, new Bin(binName, "hello")); + } + + @Test + public void testDefaultVerbosityIsZero() { + Policy p = new Policy(); + assertEquals(0, p.errorDetailVerbosity); + + WritePolicy wp = new WritePolicy(); + assertEquals(0, wp.errorDetailVerbosity); + } + + @Test + public void testVerbosityDisabled() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 0; + + try { + client.operate(wp, intKey, Operation.append(new Bin(binName, "bad"))); + } + catch (AerospikeException ae) { + assertEquals(ResultCode.BIN_TYPE_ERROR, ae.getResultCode()); + // With verbosity 0, the message should be the default ResultCode string. + String msg = ae.getBaseMessage(); + assertEquals(ResultCode.getResultString(ResultCode.BIN_TYPE_ERROR), msg); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testVerbositySubcodeOnly() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 1; + + try { + client.operate(wp, intKey, Operation.append(new Bin(binName, "bad"))); + } + catch (AerospikeException ae) { + assertEquals(ResultCode.BIN_TYPE_ERROR, ae.getResultCode()); + String msg = ae.getBaseMessage(); + assertNotNull(msg); + assertTrue("Expected subcode in: " + msg, msg.contains("subcode=")); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testVerbositySubcodeAndMessage() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, intKey, Operation.append(new Bin(binName, "bad"))); + } + catch (AerospikeException ae) { + assertEquals(ResultCode.BIN_TYPE_ERROR, ae.getResultCode()); + String msg = ae.getBaseMessage(); + assertNotNull(msg); + assertTrue("Expected 'cannot append' in: " + msg, msg.contains("cannot append")); + assertTrue("Expected subcode in: " + msg, msg.contains("subcode=")); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testAppendToIntegerBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, intKey, Operation.append(new Bin(binName, "bad-append"))); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "cannot append", "subcode=1100"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testDeleteGenerationMismatch() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL; + wp.generation = 777; + + try { + client.delete(wp, intKey); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.GENERATION_ERROR, "delete generation mismatch", "subcode=1701"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testIncrementStringBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, strKey, Operation.add(new Bin(binName, 1))); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "cannot increment", "subcode=1100"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testHllAddOnIntegerBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + List hllList = new ArrayList<>(); + hllList.add(Value.get("element1")); + + try { + client.operate(wp, intKey, + HLLOperation.add(HLLPolicy.Default, binName, hllList, 8)); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "bin is not hll type", "subcode=1138"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testHllRefreshCountMissingBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key3 = new Key(args.namespace, args.set, "edv-no-hll-key"); + client.put(new WritePolicy(), key3, new Bin("other-bin", 1)); + + try { + client.operate(wp, key3, HLLOperation.refreshCount("no-hll-bin")); + } + catch (AerospikeException ae) { + assertErrorDetails(ae, ResultCode.BIN_NOT_FOUND, "subcode=1134"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testSuccessNoErrorDetails() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + // A successful write with verbosity=2 should not cause issues. + Key key = new Key(args.namespace, args.set, "edv-success-key"); + client.put(wp, key, new Bin(binName, 42)); + + Policy rp = new Policy(); + rp.errorDetailVerbosity = 2; + Record record = client.get(rp, key); + + assertNotNull(record); + assertEquals(42, record.getInt(binName)); + } + + private void assertErrorDetails(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { + assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + + String msg = ae.getBaseMessage(); + assertNotNull("Expected server error message", msg); + + for (String expected : expectedSubstrings) { + assertTrue("Expected '" + expected + "' in: " + msg, msg.contains(expected)); + } + } +} From 031d7672c85a13e9934e6ddff98712d3c56cfd00 Mon Sep 17 00:00:00 2001 From: Mirza Karacic Date: Mon, 18 May 2026 12:12:26 -0700 Subject: [PATCH 2/6] Sync latest C client changes for extended error handling --- .../aerospike/client/async/AsyncDelete.java | 8 +- .../aerospike/client/async/AsyncExecute.java | 8 +- .../aerospike/client/async/AsyncExists.java | 8 +- .../client/async/AsyncOperateWrite.java | 8 +- .../com/aerospike/client/async/AsyncRead.java | 8 +- .../client/async/AsyncReadHeader.java | 8 +- .../aerospike/client/async/AsyncTouch.java | 12 +- .../aerospike/client/async/AsyncWrite.java | 8 +- .../client/async/AsyncWriteBase.java | 3 + .../com/aerospike/client/command/Command.java | 9 +- .../client/command/RecordParser.java | 46 ++- test/src/com/aerospike/test/SuiteAsync.java | 3 +- test/src/com/aerospike/test/SuiteSync.java | 2 + .../async/TestAsyncErrorDetailVerbosity.java | 230 +++++++++++ .../sync/basic/TestErrorDetailParser.java | 359 ++++++++++++++++++ 15 files changed, 696 insertions(+), 24 deletions(-) create mode 100644 test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java create mode 100644 test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java diff --git a/client/src/com/aerospike/client/async/AsyncDelete.java b/client/src/com/aerospike/client/async/AsyncDelete.java index 862afc109..a5903b40e 100644 --- a/client/src/com/aerospike/client/async/AsyncDelete.java +++ b/client/src/com/aerospike/client/async/AsyncDelete.java @@ -53,13 +53,17 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } existed = true; return true; } - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncExecute.java b/client/src/com/aerospike/client/async/AsyncExecute.java index 36183747f..0f8754e4f 100644 --- a/client/src/com/aerospike/client/async/AsyncExecute.java +++ b/client/src/com/aerospike/client/async/AsyncExecute.java @@ -74,12 +74,16 @@ record = rp.parseRecord(false); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } return true; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } private void handleUdfError(int resultCode) { diff --git a/client/src/com/aerospike/client/async/AsyncExists.java b/client/src/com/aerospike/client/async/AsyncExists.java index 81171902e..ffa6f4694 100644 --- a/client/src/com/aerospike/client/async/AsyncExists.java +++ b/client/src/com/aerospike/client/async/AsyncExists.java @@ -55,13 +55,17 @@ protected boolean parseResult() { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } exists = true; return true; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncOperateWrite.java b/client/src/com/aerospike/client/async/AsyncOperateWrite.java index 4d0326d59..dc9936644 100644 --- a/client/src/com/aerospike/client/async/AsyncOperateWrite.java +++ b/client/src/com/aerospike/client/async/AsyncOperateWrite.java @@ -53,12 +53,16 @@ record = rp.parseRecord(true); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } return true; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncRead.java b/client/src/com/aerospike/client/async/AsyncRead.java index d95c8b378..9ad15c030 100644 --- a/client/src/com/aerospike/client/async/AsyncRead.java +++ b/client/src/com/aerospike/client/async/AsyncRead.java @@ -66,12 +66,16 @@ protected final boolean parseResult() { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } return true; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncReadHeader.java b/client/src/com/aerospike/client/async/AsyncReadHeader.java index 7ab259aab..393c1d1f8 100644 --- a/client/src/com/aerospike/client/async/AsyncReadHeader.java +++ b/client/src/com/aerospike/client/async/AsyncReadHeader.java @@ -55,12 +55,16 @@ record = new Record(null, rp.generation, rp.expiration); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } return true; } - throw new AerospikeException(rp.resultCode); + throw (rp.serverMessage != null) ? + new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncTouch.java b/client/src/com/aerospike/client/async/AsyncTouch.java index 2cdf57b1f..6632521bc 100644 --- a/client/src/com/aerospike/client/async/AsyncTouch.java +++ b/client/src/com/aerospike/client/async/AsyncTouch.java @@ -57,7 +57,9 @@ protected boolean parseResult() { if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) { if (existsListener == null) { - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } touched = false; return true; @@ -65,13 +67,17 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } touched = false; return true; } - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncWrite.java b/client/src/com/aerospike/client/async/AsyncWrite.java index a2f2883d2..58760c856 100644 --- a/client/src/com/aerospike/client/async/AsyncWrite.java +++ b/client/src/com/aerospike/client/async/AsyncWrite.java @@ -59,12 +59,16 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } return true; } - throw new AerospikeException(resultCode); + throw (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncWriteBase.java b/client/src/com/aerospike/client/async/AsyncWriteBase.java index 0768991a9..b8b3e9eab 100644 --- a/client/src/com/aerospike/client/async/AsyncWriteBase.java +++ b/client/src/com/aerospike/client/async/AsyncWriteBase.java @@ -65,9 +65,12 @@ void onInDoubt() { } } + protected String serverMessage; + protected int parseHeader() { RecordParser rp = new RecordParser(dataBuffer, dataOffset, receiveSize); rp.parseFields(policy.txn, key, true); + this.serverMessage = rp.serverMessage; return rp.resultCode; } } diff --git a/client/src/com/aerospike/client/command/Command.java b/client/src/com/aerospike/client/command/Command.java index e55cf3f7a..6d809a3e9 100644 --- a/client/src/com/aerospike/client/command/Command.java +++ b/client/src/com/aerospike/client/command/Command.java @@ -109,6 +109,7 @@ public class Command { public static final int INFO4_TXN_ROLL_BACK = (1 << 2); // Roll back transaction. public static final int INFO4_TXN_ON_LOCKING_ONLY = (1 << 4); // Must be able to lock record in transaction. public static final int INFO4_ERROR_VERBOSITY_SHIFT = 5; // info4 bits 5-6: error detail verbosity level + public static final int INFO4_ERROR_VERBOSITY_MASK = 0x60; // 0b0110_0000: bits 5-6 only public static final byte STATE_READ_AUTH_HEADER = 1; public static final byte STATE_READ_HEADER = 2; @@ -2400,7 +2401,7 @@ private final void writeHeaderWrite(WritePolicy policy, int writeAttr, int field txnAttr |= Command.INFO4_TXN_ON_LOCKING_ONLY; } - txnAttr |= (policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); + txnAttr |= (policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK; if (policy.xdr) { readAttr |= Command.INFO1_XDR; @@ -2480,7 +2481,7 @@ private final void writeHeaderReadWrite( txnAttr |= Command.INFO4_TXN_ON_LOCKING_ONLY; } - txnAttr |= (policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); + txnAttr |= (policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK; if (policy.xdr) { readAttr |= Command.INFO1_XDR; @@ -2562,7 +2563,7 @@ private final void writeHeaderRead( dataBuffer[9] = (byte)readAttr; dataBuffer[10] = (byte)writeAttr; dataBuffer[11] = (byte)infoAttr; - dataBuffer[12] = (byte)(policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); + dataBuffer[12] = (byte)((policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK); for (int i = 13; i < 18; i++) { dataBuffer[i] = 0; @@ -2603,7 +2604,7 @@ private final void writeHeaderReadHeader(Policy policy, int readAttr, int fieldC dataBuffer[9] = (byte)readAttr; dataBuffer[10] = (byte)0; dataBuffer[11] = (byte)infoAttr; - dataBuffer[12] = (byte)(policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT); + dataBuffer[12] = (byte)((policy.errorDetailVerbosity << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK); for (int i = 13; i < 18; i++) { dataBuffer[i] = 0; diff --git a/client/src/com/aerospike/client/command/RecordParser.java b/client/src/com/aerospike/client/command/RecordParser.java index 5283ff4fa..80cda1598 100644 --- a/client/src/com/aerospike/client/command/RecordParser.java +++ b/client/src/com/aerospike/client/command/RecordParser.java @@ -237,13 +237,21 @@ private String parseErrorDetails(int offset, int size) { return null; } - // Read fixmap header. + // Read map header (fixmap, map16, map32). int b = dataBuffer[offset++] & 0xFF; int count; if ((b & 0xF0) == 0x80) { count = b & 0x0F; } + else if (b == 0xDE && offset + 2 <= end) { + count = Buffer.bytesToShort(dataBuffer, offset) & 0xFFFF; + offset += 2; + } + else if (b == 0xDF && offset + 4 <= end) { + count = Buffer.bytesToInt(dataBuffer, offset); + offset += 4; + } else { return null; } @@ -354,11 +362,15 @@ else if (b == 0xDA && offset + 1 < end) { len = Buffer.bytesToShort(dataBuffer, offset) & 0xFFFF; offset += 2; } + else if (b == 0xDB && offset + 3 < end) { + len = Buffer.bytesToInt(dataBuffer, offset); + offset += 4; + } else { return null; } - if (offset + len > end) { + if (len < 0 || offset + len > end) { return null; } @@ -431,6 +443,36 @@ private int skipMsgpackValue(int offset, int end) { return offset + 2 + (Buffer.bytesToShort(dataBuffer, offset) & 0xFFFF); } return end; + case 0xDB: // str32 + case 0xC6: // bin32 + if (offset + 3 < end) { + return offset + 4 + Buffer.bytesToInt(dataBuffer, offset); + } + return end; + case 0xDC: // array16 + case 0xDE: { // map16 + if (offset + 1 >= end) { + return end; + } + int count = (Buffer.bytesToShort(dataBuffer, offset) & 0xFFFF) * ((b == 0xDE) ? 2 : 1); + offset += 2; + for (int i = 0; i < count && offset < end; i++) { + offset = skipMsgpackValue(offset, end); + } + return offset; + } + case 0xDD: // array32 + case 0xDF: { // map32 + if (offset + 3 >= end) { + return end; + } + int count = Buffer.bytesToInt(dataBuffer, offset) * ((b == 0xDF) ? 2 : 1); + offset += 4; + for (int i = 0; i < count && offset < end; i++) { + offset = skipMsgpackValue(offset, end); + } + return offset; + } default: return end; } diff --git a/test/src/com/aerospike/test/SuiteAsync.java b/test/src/com/aerospike/test/SuiteAsync.java index 3cee9b52a..294f344b3 100644 --- a/test/src/com/aerospike/test/SuiteAsync.java +++ b/test/src/com/aerospike/test/SuiteAsync.java @@ -43,7 +43,8 @@ TestAsyncScan.class, TestAsyncQuery.class, TestAsyncTxn.class, - TestAsyncUDF.class + TestAsyncUDF.class, + TestAsyncErrorDetailVerbosity.class }) public class SuiteAsync { public static IAerospikeClient client = null; diff --git a/test/src/com/aerospike/test/SuiteSync.java b/test/src/com/aerospike/test/SuiteSync.java index 59b515993..89496de15 100644 --- a/test/src/com/aerospike/test/SuiteSync.java +++ b/test/src/com/aerospike/test/SuiteSync.java @@ -34,6 +34,7 @@ import com.aerospike.test.sync.basic.TestBitExp; import com.aerospike.test.sync.basic.TestConfigLoadYAML; import com.aerospike.test.sync.basic.TestDeleteBin; +import com.aerospike.test.sync.basic.TestErrorDetailParser; import com.aerospike.test.sync.basic.TestErrorDetailVerbosity; import com.aerospike.test.sync.basic.TestExpOperation; import com.aerospike.test.sync.basic.TestExpire; @@ -85,6 +86,7 @@ TestCdtOperate.class, TestConfigLoadYAML.class, TestDeleteBin.class, + TestErrorDetailParser.class, TestErrorDetailVerbosity.class, TestExpire.class, TestExpOperation.class, diff --git a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java new file mode 100644 index 000000000..2ec0ab284 --- /dev/null +++ b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java @@ -0,0 +1,230 @@ +/* + * 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.test.async; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.aerospike.client.AerospikeException; +import com.aerospike.client.Bin; +import com.aerospike.client.Key; +import com.aerospike.client.Operation; +import com.aerospike.client.Record; +import com.aerospike.client.ResultCode; +import com.aerospike.client.listener.DeleteListener; +import com.aerospike.client.listener.ExistsListener; +import com.aerospike.client.listener.RecordListener; +import com.aerospike.client.listener.WriteListener; +import com.aerospike.client.policy.GenerationPolicy; +import com.aerospike.client.policy.Policy; +import com.aerospike.client.policy.WritePolicy; + +/** + * Validates that server-supplied error details (subcode + message) reach + * AerospikeException through every async command path. + */ +public class TestAsyncErrorDetailVerbosity extends TestAsync { + private static final String binName = "edv-bin"; + private static Key intKey; + + @BeforeClass + public static void setup() { + WritePolicy wp = new WritePolicy(); + intKey = new Key(args.namespace, args.set, "edv-async-int-key"); + client.put(wp, intKey, new Bin(binName, 1)); + } + + // AsyncOperateWrite — type mismatch surfaces subcode + message + @Test + public void asyncOperateWriteSurfacesDetail() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + AtomicReference caught = new AtomicReference<>(); + + client.operate(eventLoop, new RecordListener() { + public void onSuccess(Key key, Record record) { + setError(new Exception("Expected BIN_TYPE_ERROR, got success")); + notifyComplete(); + } + public void onFailure(AerospikeException e) { + caught.set(e); + notifyComplete(); + } + }, wp, intKey, Operation.append(new Bin(binName, "bad-append"))); + + waitTillComplete(); + assertDetail(caught.get(), ResultCode.BIN_TYPE_ERROR, "cannot append", "subcode="); + } + + // AsyncDelete — generation mismatch surfaces subcode + message + @Test + public void asyncDeleteSurfacesDetail() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL; + wp.generation = 777; + + AtomicReference caught = new AtomicReference<>(); + + client.delete(eventLoop, new DeleteListener() { + public void onSuccess(Key key, boolean existed) { + setError(new Exception("Expected GENERATION_ERROR, got success")); + notifyComplete(); + } + public void onFailure(AerospikeException e) { + caught.set(e); + notifyComplete(); + } + }, wp, intKey); + + waitTillComplete(); + assertDetail(caught.get(), ResultCode.GENERATION_ERROR, "delete generation mismatch", "subcode="); + } + + // AsyncWrite — generation mismatch surfaces subcode + message + @Test + public void asyncWriteSurfacesDetail() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL; + wp.generation = 777; + + AtomicReference caught = new AtomicReference<>(); + + client.put(eventLoop, new WriteListener() { + public void onSuccess(Key key) { + setError(new Exception("Expected GENERATION_ERROR, got success")); + notifyComplete(); + } + public void onFailure(AerospikeException e) { + caught.set(e); + notifyComplete(); + } + }, wp, intKey, new Bin(binName, 2)); + + waitTillComplete(); + assertDetail(caught.get(), ResultCode.GENERATION_ERROR, "subcode="); + } + + // AsyncTouch — generation mismatch surfaces subcode + message + @Test + public void asyncTouchSurfacesDetail() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL; + wp.generation = 777; + + AtomicReference caught = new AtomicReference<>(); + + client.touch(eventLoop, new WriteListener() { + public void onSuccess(Key key) { + setError(new Exception("Expected GENERATION_ERROR, got success")); + notifyComplete(); + } + public void onFailure(AerospikeException e) { + caught.set(e); + notifyComplete(); + } + }, wp, intKey); + + waitTillComplete(); + assertDetail(caught.get(), ResultCode.GENERATION_ERROR, "subcode="); + } + + // AsyncExists — uses Policy (not WritePolicy). Server should not error on plain exists; + // just verifies the configured verbosity does not break the happy path. + @Test + public void asyncExistsVerbositySetHappyPath() { + Policy p = new Policy(); + p.errorDetailVerbosity = 2; + + client.exists(eventLoop, new ExistsListener() { + public void onSuccess(Key key, boolean exists) { + if (! exists) { + setError(new Exception("Expected record to exist")); + } + notifyComplete(); + } + public void onFailure(AerospikeException e) { + setError(e); + notifyComplete(); + } + }, p, intKey); + + waitTillComplete(); + } + + // AsyncRead — verifies happy path with verbosity set + @Test + public void asyncReadVerbositySetHappyPath() { + Policy p = new Policy(); + p.errorDetailVerbosity = 2; + + client.get(eventLoop, new RecordListener() { + public void onSuccess(Key key, Record record) { + if (record == null || record.getInt(binName) != 1) { + setError(new Exception("Unexpected record: " + record)); + } + notifyComplete(); + } + public void onFailure(AerospikeException e) { + setError(e); + notifyComplete(); + } + }, p, intKey); + + waitTillComplete(); + } + + // AsyncReadHeader — verifies happy path with verbosity set + @Test + public void asyncReadHeaderVerbositySetHappyPath() { + Policy p = new Policy(); + p.errorDetailVerbosity = 2; + + client.getHeader(eventLoop, new RecordListener() { + public void onSuccess(Key key, Record record) { + if (record == null) { + setError(new Exception("Expected header")); + } + notifyComplete(); + } + public void onFailure(AerospikeException e) { + setError(e); + notifyComplete(); + } + }, p, intKey); + + waitTillComplete(); + } + + private static void assertDetail(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { + org.junit.Assert.assertNotNull("Expected AerospikeException to be captured", ae); + org.junit.Assert.assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + + String msg = ae.getBaseMessage(); + org.junit.Assert.assertNotNull("Expected server error message, got null. ae=" + ae, msg); + + for (String expected : expectedSubstrings) { + org.junit.Assert.assertTrue("Expected '" + expected + "' in: " + msg, msg.contains(expected)); + } + } + +} diff --git a/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java b/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java new file mode 100644 index 000000000..a0dde0c82 --- /dev/null +++ b/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java @@ -0,0 +1,359 @@ +/* + * 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.test.sync.basic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +import com.aerospike.client.command.Command; +import com.aerospike.client.command.FieldType; +import com.aerospike.client.command.RecordParser; +import com.aerospike.test.util.TestBase; + +/** + * Unit-style tests that exercise RecordParser's msgpack error-detail decoding + * by feeding it synthetic wire-format buffers. No server connection required. + * + *

Also verifies the info4 verbosity bit math (Command.INFO4_ERROR_VERBOSITY_*). + */ +public class TestErrorDetailParser extends TestBase { + + // ---------- Verbosity bit math (fix #3) ---------- + + @Test + public void verbosityShiftAndMaskAreConsistent() { + assertEquals(5, Command.INFO4_ERROR_VERBOSITY_SHIFT); + assertEquals(0x60, Command.INFO4_ERROR_VERBOSITY_MASK); + // Mask must cover exactly two bits at shift position. + assertEquals(0x60, (0x03 << Command.INFO4_ERROR_VERBOSITY_SHIFT)); + } + + @Test + public void verbosityValueInRangeIsPreservedAfterMasking() { + for (int v = 0; v <= 3; v++) { + int actual = (v << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK; + assertEquals("v=" + v, v << Command.INFO4_ERROR_VERBOSITY_SHIFT, actual); + } + } + + @Test + public void verbosityOutOfRangeCannotCorruptOtherInfo4Bits() { + // Key invariant: regardless of input, masking guarantees only bits 5-6 can ever be set. + // No other info4 bit (TXN_VERIFY_READ, TXN_ROLL_FORWARD, TXN_ROLL_BACK, TXN_ON_LOCKING_ONLY, etc.) + // may flip from a stray verbosity value. + int otherBits = ~Command.INFO4_ERROR_VERBOSITY_MASK & 0xFF; + for (int v : new int[] {0, 1, 2, 3, 4, 8, 16, 255, Integer.MAX_VALUE, -1}) { + int written = (v << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK; + assertEquals("other-bit pollution for v=" + v, 0, written & otherBits); + assertTrue("result must fit in mask for v=" + v, written == (written & Command.INFO4_ERROR_VERBOSITY_MASK)); + } + + // Specific spot checks: values that, pre-mask, would set bits OUTSIDE 5-6. + // 4 << 5 = 0x80 (bit 7), 8 << 5 = 0x100 (bit 8), 16 << 5 = 0x200 (bit 9). + // All three masked → 0 because none have bits 5 or 6 lit. + assertEquals(0, (4 << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK); + assertEquals(0, (8 << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK); + assertEquals(0, (16 << Command.INFO4_ERROR_VERBOSITY_SHIFT) & Command.INFO4_ERROR_VERBOSITY_MASK); + } + + // ---------- Parser: fixmap (baseline) ---------- + + @Test + public void parsesFixmapWithSubcodeAndMessage() { + byte[] detail = fixmap2( + pair(intKey(1), fixint(99)), + pair(intKey(2), fixstr("cannot append")) + ); + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertEquals("cannot append (subcode=99)", rp.serverMessage); + } + + @Test + public void parsesFixmapWithSubcodeOnly() { + byte[] detail = fixmap1(pair(intKey(1), fixint(42))); + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertEquals("error subcode=42", rp.serverMessage); + } + + @Test + public void parsesFixmapWithMessageOnly() { + byte[] detail = fixmap1(pair(intKey(2), fixstr("oops"))); + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertEquals("oops", rp.serverMessage); + } + + // ---------- Parser: msgpack types that the original hand-rolled decoder didn't handle (fix #2) ---------- + + @Test + public void parsesMap16Header() { + // Build with 16 entries to force map16. Real keys 1 and 2; pad rest with unknown keys 100..113 → uint8. + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + // map16 header: 0xDE NN NN + payload.write(0xDE); + payload.write(0x00); + payload.write(16); + writeBytes(payload, pair(intKey(1), fixint(7))); + writeBytes(payload, pair(intKey(2), fixstr("boom"))); + for (int i = 0; i < 14; i++) { + // unknown key, uint8 (0xCC NN), value nil (0xC0) + payload.write(0xCC); + payload.write(100 + i); + payload.write(0xC0); + } + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("boom (subcode=7)", rp.serverMessage); + } + + @Test + public void parsesStr32Message() { + // 100-char message that we choose to encode with str32 to verify that path works. + String big = repeat('x', 100); + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); // fixmap, 2 entries + writeBytes(payload, pair(intKey(1), fixint(5))); + // key=2 + writeBytes(payload, intKey(2)); + // str32 prefix + payload.write(0xDB); + payload.write(0x00); + payload.write(0x00); + payload.write(0x00); + payload.write(big.length()); + writeBytes(payload, big.getBytes(StandardCharsets.UTF_8)); + + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals(big + " (subcode=5)", rp.serverMessage); + } + + @Test + public void parsesSubcodeAsUint16() { + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); + // key=1, subcode uint16 = 1100 (0x044C) + writeBytes(payload, intKey(1)); + payload.write(0xCD); + payload.write(0x04); + payload.write(0x4C); + writeBytes(payload, pair(intKey(2), fixstr("hi"))); + + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("hi (subcode=1100)", rp.serverMessage); + } + + @Test + public void parsesSubcodeAsUint32() { + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); + // key=1, subcode uint32 = 70000 (0x00011170) + writeBytes(payload, intKey(1)); + payload.write(0xCE); + payload.write(0x00); + payload.write(0x01); + payload.write(0x11); + payload.write(0x70); + writeBytes(payload, pair(intKey(2), fixstr("x"))); + + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("x (subcode=70000)", rp.serverMessage); + } + + // ---------- Parser: defensive/edge cases ---------- + + @Test + public void emptyMapProducesNoMessage() { + // 0x80 = fixmap, 0 entries → parseErrorDetails returns null. + RecordParser rp = parserFor(new byte[]{(byte)0x80}); + rp.parseFields(null, null, false); + assertNull(rp.serverMessage); + } + + @Test + public void unknownKeysAreSkippedNotFatal() { + // 4-entry fixmap: unknown int, subcode, unknown nil, message + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x84); // fixmap, 4 entries + writeBytes(payload, pair(intKey(50), fixint(0))); // unknown key 50 → fixint value + writeBytes(payload, pair(intKey(1), fixint(7))); + writeBytes(payload, intKey(51)); + payload.write(0xC0); // nil + writeBytes(payload, pair(intKey(2), fixstr("z"))); + + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("z (subcode=7)", rp.serverMessage); + } + + @Test + public void parseSucceedsWhenAdditionalNonErrorFieldsPresent() { + // Build buffer with fieldCount=2: one bogus field type, then ERROR_MESSAGE. + byte[] detail = fixmap2( + pair(intKey(1), fixint(1)), + pair(intKey(2), fixstr("ok")) + ); + byte[] buffer = bufferWithFields( + new int[]{0xCD, FieldType.ERROR_MESSAGE}, // first field type is unknown to parseFieldsError → skipped + new byte[][]{new byte[]{0x01, 0x02, 0x03}, detail} + ); + RecordParser rp = new RecordParser(buffer, 0, buffer.length); + rp.parseFields(null, null, false); + assertEquals("ok (subcode=1)", rp.serverMessage); + } + + @Test + public void missingErrorFieldYieldsNullMessage() { + // fieldCount = 0 + byte[] buffer = bufferWithFields(new int[0], new byte[0][]); + RecordParser rp = new RecordParser(buffer, 0, buffer.length); + rp.parseFields(null, null, false); + assertNull(rp.serverMessage); + } + + // ---------- helpers: wire format & msgpack composition ---------- + + /** + * Build a buffer containing a single ERROR_MESSAGE field with the given msgpack + * payload, then construct a parser sitting at the right offset. + */ + private static RecordParser parserFor(byte[] msgpackDetail) { + byte[] buf = bufferWithFields( + new int[]{FieldType.ERROR_MESSAGE}, + new byte[][]{msgpackDetail} + ); + // Async constructor expects receiveSize >= MSG_REMAINING_HEADER_SIZE (22). + return new RecordParser(buf, 0, buf.length); + } + + /** + * Lay out: [5-byte proto stub][22-byte msg header with fieldCount=N][fields...] + */ + private static byte[] bufferWithFields(int[] fieldTypes, byte[][] fieldDatas) { + assertEquals(fieldTypes.length, fieldDatas.length); + + // Msg-header layout per RecordParser async ctor (22 bytes from buf[0]): + // bytes 0..4 : header-size marker + 4 attr bytes (parser skips via offset += 5) + // byte 5 : result code + // bytes 6..9 : generation + // bytes 10..13 : expiration + // bytes 14..17 : skipped (parser does offset += 8 after expiration) + // bytes 18..19 : fieldCount + // bytes 20..21 : opCount + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (int i = 0; i < 5; i++) out.write(0); // size marker + 4 attrs + out.write(0); // result code + for (int i = 0; i < 4; i++) out.write(0); // generation + for (int i = 0; i < 4; i++) out.write(0); // expiration + for (int i = 0; i < 4; i++) out.write(0); // skip + writeShort(out, fieldTypes.length); // fieldCount + writeShort(out, 0); // opCount + + for (int i = 0; i < fieldTypes.length; i++) { + byte[] data = fieldDatas[i]; + int size = data.length + 1; // includes type byte + writeInt(out, size); + out.write(fieldTypes[i] & 0xFF); + writeBytes(out, data); + } + return out.toByteArray(); + } + + private static byte[] fixmap1(byte[] kv) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x81); + writeBytes(out, kv); + return out.toByteArray(); + } + + private static byte[] fixmap2(byte[] kv1, byte[] kv2) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x82); + writeBytes(out, kv1); + writeBytes(out, kv2); + return out.toByteArray(); + } + + private static byte[] pair(byte[] k, byte[] v) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeBytes(out, k); + writeBytes(out, v); + return out.toByteArray(); + } + + private static byte[] intKey(int v) { + // positive fixint (0–127) + assertTrue(v >= 0 && v <= 0x7F); + return new byte[]{(byte)v}; + } + + private static byte[] fixint(int v) { + assertTrue(v >= 0 && v <= 0x7F); + return new byte[]{(byte)v}; + } + + private static byte[] fixstr(String s) { + byte[] data = s.getBytes(StandardCharsets.UTF_8); + assertTrue("fixstr supports up to 31 bytes", data.length <= 31); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0xA0 | data.length); + writeBytes(out, data); + return out.toByteArray(); + } + + private static void writeBytes(ByteArrayOutputStream out, byte[] b) { + try { + out.write(b); + } + catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + private static void writeShort(ByteArrayOutputStream out, int v) { + out.write((v >> 8) & 0xFF); + out.write(v & 0xFF); + } + + private static void writeInt(ByteArrayOutputStream out, int v) { + out.write((v >> 24) & 0xFF); + out.write((v >> 16) & 0xFF); + out.write((v >> 8) & 0xFF); + out.write(v & 0xFF); + } + + private static String repeat(char c, int n) { + char[] arr = new char[n]; + java.util.Arrays.fill(arr, c); + return new String(arr); + } + + // Ensure assertNotNull is referenced (used for nullity checks in helpers). + @SuppressWarnings("unused") + private static void unused() { assertNotNull(""); } +} From 44e55f725c9ca8c9dd5c9186010e1de62727a534 Mon Sep 17 00:00:00 2001 From: Mirza Karacic Date: Mon, 18 May 2026 16:27:18 -0700 Subject: [PATCH 3/6] Updated api changes --- .../client/command/DeleteCommand.java | 6 +- .../client/command/ExecuteCommand.java | 6 +- .../client/command/ExistsCommand.java | 6 +- .../client/command/OperateCommandWrite.java | 6 +- .../aerospike/client/command/ReadCommand.java | 6 +- .../client/command/ReadHeaderCommand.java | 6 +- .../client/command/RecordParser.java | 12 ++ .../client/command/TouchCommand.java | 8 +- .../client/command/WriteCommand.java | 6 +- .../sync/basic/TestErrorDetailParser.java | 146 ++++++++++++++++++ 10 files changed, 175 insertions(+), 33 deletions(-) diff --git a/client/src/com/aerospike/client/command/DeleteCommand.java b/client/src/com/aerospike/client/command/DeleteCommand.java index fe5126ba1..a10175858 100644 --- a/client/src/com/aerospike/client/command/DeleteCommand.java +++ b/client/src/com/aerospike/client/command/DeleteCommand.java @@ -53,15 +53,13 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.FILTERED_OUT) { if (writePolicy.failOnFilteredOut) { - throw new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } existed = true; return; } - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } public boolean existed() { diff --git a/client/src/com/aerospike/client/command/ExecuteCommand.java b/client/src/com/aerospike/client/command/ExecuteCommand.java index 862a00e16..7d4bb3438 100644 --- a/client/src/com/aerospike/client/command/ExecuteCommand.java +++ b/client/src/com/aerospike/client/command/ExecuteCommand.java @@ -74,14 +74,12 @@ record = rp.parseRecord(false); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } return; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } private void handleUdfError(int resultCode) { diff --git a/client/src/com/aerospike/client/command/ExistsCommand.java b/client/src/com/aerospike/client/command/ExistsCommand.java index f870c271a..9aa6e3fe8 100644 --- a/client/src/com/aerospike/client/command/ExistsCommand.java +++ b/client/src/com/aerospike/client/command/ExistsCommand.java @@ -58,15 +58,13 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } exists = true; return; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } public boolean exists() { diff --git a/client/src/com/aerospike/client/command/OperateCommandWrite.java b/client/src/com/aerospike/client/command/OperateCommandWrite.java index a0b6d1b17..e6ae99c4a 100644 --- a/client/src/com/aerospike/client/command/OperateCommandWrite.java +++ b/client/src/com/aerospike/client/command/OperateCommandWrite.java @@ -56,14 +56,12 @@ record = rp.parseRecord(true); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } return; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/ReadCommand.java b/client/src/com/aerospike/client/command/ReadCommand.java index 836ce671f..fe10f70fe 100644 --- a/client/src/com/aerospike/client/command/ReadCommand.java +++ b/client/src/com/aerospike/client/command/ReadCommand.java @@ -73,14 +73,12 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } return; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/ReadHeaderCommand.java b/client/src/com/aerospike/client/command/ReadHeaderCommand.java index 264cec1d5..04eb59e42 100644 --- a/client/src/com/aerospike/client/command/ReadHeaderCommand.java +++ b/client/src/com/aerospike/client/command/ReadHeaderCommand.java @@ -59,14 +59,12 @@ record = new Record(null, rp.generation, rp.expiration); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } return; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/RecordParser.java b/client/src/com/aerospike/client/command/RecordParser.java index 80cda1598..a23a18249 100644 --- a/client/src/com/aerospike/client/command/RecordParser.java +++ b/client/src/com/aerospike/client/command/RecordParser.java @@ -40,6 +40,18 @@ public final class RecordParser { public long bytesIn; public String serverMessage; + /** + * Build a failure exception that includes the server's extended-error + * detail when present. Route all non-OK throws through here so the + * detail is never silently dropped on special-case result codes such + * as FILTERED_OUT or KEY_NOT_FOUND_ERROR. + */ + public static AerospikeException toException(int resultCode, String serverMessage) { + return (serverMessage != null) ? + new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode); + } + /** * Sync record parser. */ diff --git a/client/src/com/aerospike/client/command/TouchCommand.java b/client/src/com/aerospike/client/command/TouchCommand.java index 6b06fa2e6..c877546ea 100644 --- a/client/src/com/aerospike/client/command/TouchCommand.java +++ b/client/src/com/aerospike/client/command/TouchCommand.java @@ -51,7 +51,7 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) { if (failOnNotFound) { - throw new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } touched = false; return; @@ -59,15 +59,13 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.FILTERED_OUT) { if (writePolicy.failOnFilteredOut) { - throw new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } touched = false; return; } - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } public boolean getTouched() { diff --git a/client/src/com/aerospike/client/command/WriteCommand.java b/client/src/com/aerospike/client/command/WriteCommand.java index f4e889d77..42f00a86f 100644 --- a/client/src/com/aerospike/client/command/WriteCommand.java +++ b/client/src/com/aerospike/client/command/WriteCommand.java @@ -52,13 +52,11 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.FILTERED_OUT) { if (writePolicy.failOnFilteredOut) { - throw new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } return; } - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage); } } diff --git a/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java b/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java index a0dde0c82..5847b410e 100644 --- a/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java +++ b/test/src/com/aerospike/test/sync/basic/TestErrorDetailParser.java @@ -106,6 +106,32 @@ public void parsesFixmapWithMessageOnly() { assertEquals("oops", rp.serverMessage); } + @Test + public void parsesKeysInReverseOrder() { + // Server is allowed to emit the map keys in any order; result must be identical. + byte[] detail = fixmap2( + pair(intKey(2), fixstr("swap")), + pair(intKey(1), fixint(7)) + ); + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertEquals("swap (subcode=7)", rp.serverMessage); + } + + @Test + public void parsesMultiByteUtf8Message() { + // Mix BMP and supplementary-plane code points so we exercise both + // 2/3-byte and 4-byte UTF-8 sequences. + String multibyte = "αβγ · 测试 · 🚀"; + byte[] detail = fixmap2( + pair(intKey(1), fixint(1)), + pair(intKey(2), fixstr(multibyte)) + ); + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertEquals(multibyte + " (subcode=1)", rp.serverMessage); + } + // ---------- Parser: msgpack types that the original hand-rolled decoder didn't handle (fix #2) ---------- @Test @@ -129,6 +155,20 @@ public void parsesMap16Header() { assertEquals("boom (subcode=7)", rp.serverMessage); } + @Test + public void parsesMap32Header() { + // 0xDF + 4-byte big-endian count. Only 2 entries here — exercising the + // header path, not the count. + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0xDF); + writeInt(payload, 2); + writeBytes(payload, pair(intKey(1), fixint(9))); + writeBytes(payload, pair(intKey(2), fixstr("m32"))); + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("m32 (subcode=9)", rp.serverMessage); + } + @Test public void parsesStr32Message() { // 100-char message that we choose to encode with str32 to verify that path works. @@ -151,6 +191,32 @@ public void parsesStr32Message() { assertEquals(big + " (subcode=5)", rp.serverMessage); } + @Test + public void parsesSubcodeAsFixint() { + // 0..127 — encoded as a single positive-fixint byte. + byte[] detail = fixmap2( + pair(intKey(1), fixint(127)), + pair(intKey(2), fixstr("fx")) + ); + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertEquals("fx (subcode=127)", rp.serverMessage); + } + + @Test + public void parsesSubcodeAsUint8() { + // 200 doesn't fit in fixint (max 127) so server would emit uint8 (0xCC NN). + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); + writeBytes(payload, intKey(1)); + payload.write(0xCC); + payload.write(200); + writeBytes(payload, pair(intKey(2), fixstr("u8"))); + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("u8 (subcode=200)", rp.serverMessage); + } + @Test public void parsesSubcodeAsUint16() { ByteArrayOutputStream payload = new ByteArrayOutputStream(); @@ -185,6 +251,56 @@ public void parsesSubcodeAsUint32() { assertEquals("x (subcode=70000)", rp.serverMessage); } + @Test + public void parsesSubcodeAsUint64() { + // 5,000,000,000 doesn't fit in uint32; forces the 0xCF / 8-byte path. + long value = 5_000_000_000L; + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); + writeBytes(payload, intKey(1)); + payload.write(0xCF); + writeLong(payload, value); + writeBytes(payload, pair(intKey(2), fixstr("u64"))); + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals("u64 (subcode=" + value + ")", rp.serverMessage); + } + + @Test + public void parsesMessageAsStr8() { + // 0xD9 + 1-byte length. Server may pick str8 even for short strings; + // the parser must accept whichever encoding it gets. + String msg = "string8"; + byte[] data = msg.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); + writeBytes(payload, pair(intKey(1), fixint(3))); + writeBytes(payload, intKey(2)); + payload.write(0xD9); + payload.write(data.length); + writeBytes(payload, data); + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals(msg + " (subcode=3)", rp.serverMessage); + } + + @Test + public void parsesMessageAsStr16() { + // 0xDA + 2-byte length. + String msg = "string16"; + byte[] data = msg.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + payload.write(0x82); + writeBytes(payload, pair(intKey(1), fixint(4))); + writeBytes(payload, intKey(2)); + payload.write(0xDA); + writeShort(payload, data.length); + writeBytes(payload, data); + RecordParser rp = parserFor(payload.toByteArray()); + rp.parseFields(null, null, false); + assertEquals(msg + " (subcode=4)", rp.serverMessage); + } + // ---------- Parser: defensive/edge cases ---------- @Test @@ -195,6 +311,25 @@ public void emptyMapProducesNoMessage() { assertNull(rp.serverMessage); } + @Test + public void truncatedValueReturnsNullNotThrow() { + // fixmap-1, key=1, uint16 prefix — but value bytes are missing. + // Parser must return null and MUST NOT throw or read past the buffer. + byte[] detail = new byte[]{(byte)0x81, 0x01, (byte)0xCD}; + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertNull(rp.serverMessage); + } + + @Test + public void truncatedMapHeaderReturnsNull() { + // 0xDE (map16 prefix) with NO count bytes following. + byte[] detail = new byte[]{(byte)0xDE}; + RecordParser rp = parserFor(detail); + rp.parseFields(null, null, false); + assertNull(rp.serverMessage); + } + @Test public void unknownKeysAreSkippedNotFatal() { // 4-entry fixmap: unknown int, subcode, unknown nil, message @@ -347,6 +482,17 @@ private static void writeInt(ByteArrayOutputStream out, int v) { out.write(v & 0xFF); } + private static void writeLong(ByteArrayOutputStream out, long v) { + out.write((int)((v >> 56) & 0xFF)); + out.write((int)((v >> 48) & 0xFF)); + out.write((int)((v >> 40) & 0xFF)); + out.write((int)((v >> 32) & 0xFF)); + out.write((int)((v >> 24) & 0xFF)); + out.write((int)((v >> 16) & 0xFF)); + out.write((int)((v >> 8) & 0xFF)); + out.write((int)(v & 0xFF)); + } + private static String repeat(char c, int n) { char[] arr = new char[n]; java.util.Arrays.fill(arr, c); From f7dbc351c6b77d95c82e5909f50cc1d33ad30c2f Mon Sep 17 00:00:00 2001 From: Mirza Karacic Date: Wed, 10 Jun 2026 22:15:20 -0700 Subject: [PATCH 4/6] Sync changes with C client and expended tests --- .../async/TestAsyncErrorDetailVerbosity.java | 54 +- .../sync/basic/TestErrorDetailVerbosity.java | 573 +++++++++++++++++- 2 files changed, 588 insertions(+), 39 deletions(-) diff --git a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java index 2ec0ab284..85c9febdc 100644 --- a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java +++ b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java @@ -21,12 +21,19 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.util.ArrayList; +import java.util.List; + import com.aerospike.client.AerospikeException; import com.aerospike.client.Bin; import com.aerospike.client.Key; -import com.aerospike.client.Operation; import com.aerospike.client.Record; import com.aerospike.client.ResultCode; +import com.aerospike.client.Value; +import com.aerospike.client.cdt.ListOperation; +import com.aerospike.client.cdt.ListOrder; +import com.aerospike.client.cdt.ListPolicy; +import com.aerospike.client.cdt.ListWriteFlags; import com.aerospike.client.listener.DeleteListener; import com.aerospike.client.listener.ExistsListener; import com.aerospike.client.listener.RecordListener; @@ -42,38 +49,49 @@ public class TestAsyncErrorDetailVerbosity extends TestAsync { private static final String binName = "edv-bin"; private static Key intKey; + private static Key listKey; @BeforeClass public static void setup() { WritePolicy wp = new WritePolicy(); intKey = new Key(args.namespace, args.set, "edv-async-int-key"); client.put(wp, intKey, new Bin(binName, 1)); + + listKey = new Key(args.namespace, args.set, "edv-async-list-key"); + List seed = new ArrayList<>(); + seed.add(Value.get(10)); + client.put(wp, listKey, new Bin(binName, seed)); } - // AsyncOperateWrite — type mismatch surfaces subcode + message + // AsyncOperateWrite — a write op that fails surfaces subcode + message. + // A bounded ordered-list insert past the end yields OP_NOT_APPLICABLE with + // AS_SUB_OPNOT_CDT_BOUNDED_LIST_OVERFLOW = 3. @Test public void asyncOperateWriteSurfacesDetail() { WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 2; + ListPolicy bounded = new ListPolicy(ListOrder.ORDERED, ListWriteFlags.INSERT_BOUNDED); + AtomicReference caught = new AtomicReference<>(); client.operate(eventLoop, new RecordListener() { public void onSuccess(Key key, Record record) { - setError(new Exception("Expected BIN_TYPE_ERROR, got success")); + setError(new Exception("Expected OP_NOT_APPLICABLE, got success")); notifyComplete(); } public void onFailure(AerospikeException e) { caught.set(e); notifyComplete(); } - }, wp, intKey, Operation.append(new Bin(binName, "bad-append"))); + }, wp, listKey, ListOperation.insert(bounded, binName, 10, Value.get(5))); waitTillComplete(); - assertDetail(caught.get(), ResultCode.BIN_TYPE_ERROR, "cannot append", "subcode="); + assertDetail(caught.get(), ResultCode.OP_NOT_APPLICABLE, "subcode=3"); } - // AsyncDelete — generation mismatch surfaces subcode + message + // AsyncDelete — generation mismatch surfaces the detail message. The status + // is maximally specific, so the server omits the subcode (AS_SUB_NONE). @Test public void asyncDeleteSurfacesDetail() { WritePolicy wp = new WritePolicy(); @@ -95,10 +113,10 @@ public void onFailure(AerospikeException e) { }, wp, intKey); waitTillComplete(); - assertDetail(caught.get(), ResultCode.GENERATION_ERROR, "delete generation mismatch", "subcode="); + assertSubcodeAbsent(caught.get(), ResultCode.GENERATION_ERROR, "generation mismatch"); } - // AsyncWrite — generation mismatch surfaces subcode + message + // AsyncWrite — generation mismatch surfaces the detail message (AS_SUB_NONE). @Test public void asyncWriteSurfacesDetail() { WritePolicy wp = new WritePolicy(); @@ -120,10 +138,10 @@ public void onFailure(AerospikeException e) { }, wp, intKey, new Bin(binName, 2)); waitTillComplete(); - assertDetail(caught.get(), ResultCode.GENERATION_ERROR, "subcode="); + assertSubcodeAbsent(caught.get(), ResultCode.GENERATION_ERROR, "generation mismatch"); } - // AsyncTouch — generation mismatch surfaces subcode + message + // AsyncTouch — generation mismatch surfaces the detail message (AS_SUB_NONE). @Test public void asyncTouchSurfacesDetail() { WritePolicy wp = new WritePolicy(); @@ -145,7 +163,7 @@ public void onFailure(AerospikeException e) { }, wp, intKey); waitTillComplete(); - assertDetail(caught.get(), ResultCode.GENERATION_ERROR, "subcode="); + assertSubcodeAbsent(caught.get(), ResultCode.GENERATION_ERROR, "generation mismatch"); } // AsyncExists — uses Policy (not WritePolicy). Server should not error on plain exists; @@ -227,4 +245,18 @@ private static void assertDetail(AerospikeException ae, int expectedResultCode, } } + /** + * Assert that the server surfaced a contextual message but NO subcode + * (AS_SUB_NONE): the "(subcode=...)" suffix must never appear. + */ + private static void assertSubcodeAbsent(AerospikeException ae, int expectedResultCode, String expectedSubstring) { + org.junit.Assert.assertNotNull("Expected AerospikeException to be captured", ae); + org.junit.Assert.assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + + String msg = ae.getBaseMessage(); + org.junit.Assert.assertNotNull("Expected server error message, got null. ae=" + ae, msg); + org.junit.Assert.assertTrue("Expected '" + expectedSubstring + "' in: " + msg, msg.contains(expectedSubstring)); + org.junit.Assert.assertFalse("Expected NO subcode suffix in: " + msg, msg.contains("subcode=")); + } + } diff --git a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java index 593525e95..554b49e1d 100644 --- a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java +++ b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2025 Aerospike, Inc. + * 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. @@ -17,6 +17,7 @@ package com.aerospike.test.sync.basic; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -27,34 +28,71 @@ import com.aerospike.client.Record; import com.aerospike.client.ResultCode; import com.aerospike.client.Value; +import com.aerospike.client.cdt.CTX; +import com.aerospike.client.cdt.ListOperation; +import com.aerospike.client.cdt.ListOrder; +import com.aerospike.client.cdt.ListPolicy; +import com.aerospike.client.cdt.ListReturnType; +import com.aerospike.client.cdt.ListWriteFlags; +import com.aerospike.client.cdt.MapOperation; +import com.aerospike.client.cdt.MapOrder; +import com.aerospike.client.cdt.MapPolicy; +import com.aerospike.client.cdt.MapReturnType; +import com.aerospike.client.cdt.MapWriteFlags; +import com.aerospike.client.exp.Exp; +import com.aerospike.client.operation.BitOperation; import com.aerospike.client.operation.HLLOperation; import com.aerospike.client.operation.HLLPolicy; import com.aerospike.client.policy.GenerationPolicy; import com.aerospike.client.policy.Policy; +import com.aerospike.client.policy.RecordExistsAction; import com.aerospike.client.policy.WritePolicy; import com.aerospike.test.sync.TestSync; -import org.junit.BeforeClass; -import org.junit.Test; - import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.junit.BeforeClass; +import org.junit.Test; +/** + * Validates the extended error-detail feature (CLIENT-4221) against a server + * that supports error detail verbosity. + * + *

Subcode expectations track the per-status enum numbering finalized on the + * C client (commits 8b6f8db4 / 2d2495a1) and confirmed against the live server: + * the old flat status-echo subcodes (1100, 1134, 1138, 1701, ...) are retired. + * Where the status is already maximally specific the server emits AS_SUB_NONE + * and omits the subcode map key entirely, so no "(subcode=...)" suffix appears. + */ public class TestErrorDetailVerbosity extends TestSync { private static final String binName = "edv-bin"; private static Key intKey; private static Key strKey; + private static Key listKey; @BeforeClass public static void setup() { WritePolicy wp = new WritePolicy(); intKey = new Key(args.namespace, args.set, "edv-int-key"); strKey = new Key(args.namespace, args.set, "edv-str-key"); + listKey = new Key(args.namespace, args.set, "edv-list-key"); client.put(wp, intKey, new Bin(binName, 1)); client.put(wp, strKey, new Bin(binName, "hello")); + + List seed = new ArrayList<>(); + seed.add(Value.get(10)); + seed.add(Value.get(20)); + seed.add(Value.get(30)); + client.put(wp, listKey, new Bin(binName, seed)); } + // --------------------------------------------------------------------- + // Verbosity level semantics. + // --------------------------------------------------------------------- + @Test public void testDefaultVerbosityIsZero() { Policy p = new Policy(); @@ -84,17 +122,23 @@ public void testVerbosityDisabled() { @Test public void testVerbositySubcodeOnly() { + // Verbosity 1: server sends the subcode but not the message. A subcode + // that resolves to a value (BIN_NOT_FOUND from an HLL count op on a + // missing bin -> subcode 1) surfaces as the bare "error subcode=N" form. WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 1; + Key key = new Key(args.namespace, args.set, "edv-subonly-key"); + client.put(new WritePolicy(), key, new Bin("other-bin", 1)); + try { - client.operate(wp, intKey, Operation.append(new Bin(binName, "bad"))); + client.operate(wp, key, HLLOperation.refreshCount("no-hll-bin")); } catch (AerospikeException ae) { - assertEquals(ResultCode.BIN_TYPE_ERROR, ae.getResultCode()); + assertEquals(ResultCode.BIN_NOT_FOUND, ae.getResultCode()); String msg = ae.getBaseMessage(); assertNotNull(msg); - assertTrue("Expected subcode in: " + msg, msg.contains("subcode=")); + assertTrue("Expected subcode in: " + msg, msg.contains("subcode=1")); return; } assertTrue("Expected AerospikeException", false); @@ -102,23 +146,34 @@ public void testVerbositySubcodeOnly() { @Test public void testVerbositySubcodeAndMessage() { + // Verbosity 2: server sends both message and subcode, formatted as + // " (subcode=)". WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 2; + Key key = new Key(args.namespace, args.set, "edv-submsg-key"); + client.put(new WritePolicy(), key, new Bin("other-bin", 1)); + try { - client.operate(wp, intKey, Operation.append(new Bin(binName, "bad"))); + client.operate(wp, key, HLLOperation.refreshCount("no-hll-bin")); } catch (AerospikeException ae) { - assertEquals(ResultCode.BIN_TYPE_ERROR, ae.getResultCode()); + assertEquals(ResultCode.BIN_NOT_FOUND, ae.getResultCode()); String msg = ae.getBaseMessage(); assertNotNull(msg); - assertTrue("Expected 'cannot append' in: " + msg, msg.contains("cannot append")); - assertTrue("Expected subcode in: " + msg, msg.contains("subcode=")); + assertTrue("Expected message text in: " + msg, msg.contains("count op")); + assertTrue("Expected subcode in: " + msg, msg.contains("(subcode=1)")); return; } assertTrue("Expected AerospikeException", false); } + // --------------------------------------------------------------------- + // Subcode-absent cases (AS_SUB_NONE): the status is already maximally + // specific, so the server omits the subcode map key and the client must + // never format a "(subcode=...)" suffix. The message carries the context. + // --------------------------------------------------------------------- + @Test public void testAppendToIntegerBin() { WritePolicy wp = new WritePolicy(); @@ -128,7 +183,41 @@ public void testAppendToIntegerBin() { client.operate(wp, intKey, Operation.append(new Bin(binName, "bad-append"))); } catch (AerospikeException ae) { - assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "cannot append", "subcode=1100"); + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR, "cannot append"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testIncrementStringBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, strKey, Operation.add(new Bin(binName, 1))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR, "cannot increment"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testHllAddOnIntegerBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + List hllList = new ArrayList<>(); + hllList.add(Value.get("element1")); + + try { + client.operate(wp, intKey, + HLLOperation.add(HLLPolicy.Default, binName, hllList, 8)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR, "bin is not hll type"); return; } assertTrue("Expected AerospikeException", false); @@ -145,70 +234,480 @@ public void testDeleteGenerationMismatch() { client.delete(wp, intKey); } catch (AerospikeException ae) { - assertErrorDetails(ae, ResultCode.GENERATION_ERROR, "delete generation mismatch", "subcode=1701"); + assertSubcodeAbsent(ae, ResultCode.GENERATION_ERROR, "generation"); return; } assertTrue("Expected AerospikeException", false); } + // --------------------------------------------------------------------- + // Subcode-present cases: per-status enum subcode numbering. + // --------------------------------------------------------------------- + @Test - public void testIncrementStringBin() { + public void testHllRefreshCountMissingBin() { WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 2; + Key key = new Key(args.namespace, args.set, "edv-no-hll-key"); + client.put(new WritePolicy(), key, new Bin("other-bin", 1)); + try { - client.operate(wp, strKey, Operation.add(new Bin(binName, 1))); + client.operate(wp, key, HLLOperation.refreshCount("no-hll-bin")); } catch (AerospikeException ae) { - assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "cannot increment", "subcode=1100"); + // AS_SUB_BIN_NOT_FOUND_HLL_CANNOT_CREATE_WITH_OP = 1 + assertErrorDetails(ae, ResultCode.BIN_NOT_FOUND, "subcode=1"); return; } assertTrue("Expected AerospikeException", false); } @Test - public void testHllAddOnIntegerBin() { + public void testListGetIndexOutOfBounds() { WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 2; - List hllList = new ArrayList<>(); - hllList.add(Value.get("element1")); + try { + client.operate(wp, listKey, ListOperation.get(binName, 99)); + } + catch (AerospikeException ae) { + // AS_SUB_OPNOT_CDT_INDEX_OUT_OF_BOUNDS = 1 + assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=1"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testListGetByRankOutOfBounds() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; try { - client.operate(wp, intKey, - HLLOperation.add(HLLPolicy.Default, binName, hllList, 8)); + client.operate(wp, listKey, ListOperation.getByRank(binName, 99, ListReturnType.VALUE)); } catch (AerospikeException ae) { - assertErrorDetails(ae, ResultCode.BIN_TYPE_ERROR, "bin is not hll type", "subcode=1138"); + // AS_SUB_OPNOT_CDT_RANK_OUT_OF_BOUNDS = 2 + assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=2"); return; } assertTrue("Expected AerospikeException", false); } @Test - public void testHllRefreshCountMissingBin() { + public void testListBoundedOverflow() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + ListPolicy bounded = new ListPolicy(ListOrder.ORDERED, ListWriteFlags.INSERT_BOUNDED); + + try { + client.operate(wp, listKey, ListOperation.insert(bounded, binName, 10, Value.get(5))); + } + catch (AerospikeException ae) { + // AS_SUB_OPNOT_CDT_BOUNDED_LIST_OVERFLOW = 3 + assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=3"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testHllFoldTargetTooLarge() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-hll-fold-key"); + client.delete(new WritePolicy(), key); + client.operate(new WritePolicy(), key, HLLOperation.init(HLLPolicy.Default, binName, 8)); + + try { + client.operate(wp, key, HLLOperation.fold(binName, 14)); + } + catch (AerospikeException ae) { + // AS_SUB_OPNOT_HLL_FOLD_INDEX_BITS_TOO_LARGE = 8 + assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=8"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testBitGetOffsetOutOfRange() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-bits-key"); + client.put(new WritePolicy(), key, new Bin(binName, new byte[]{(byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD})); + + try { + client.operate(wp, key, BitOperation.get(binName, 2000000000, 8)); + } + catch (AerospikeException ae) { + // AS_SUB_PARAM_BITS_OFFSET_OUT_OF_RANGE = 2 + assertErrorDetails(ae, ResultCode.PARAMETER_ERROR, "subcode=2"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testBitGetSizeZero() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-bits-key2"); + client.put(new WritePolicy(), key, new Bin(binName, new byte[]{(byte)0xAA, (byte)0xBB, (byte)0xCC, (byte)0xDD})); + + try { + client.operate(wp, key, BitOperation.get(binName, 0, 0)); + } + catch (AerospikeException ae) { + // AS_SUB_PARAM_BITS_SIZE_OUT_OF_RANGE = 3 + assertErrorDetails(ae, ResultCode.PARAMETER_ERROR, "subcode=3"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testReadFilteredOut() { + Policy p = new Policy(); + p.errorDetailVerbosity = 2; + p.filterExp = Exp.build(Exp.eq(Exp.intBin(binName), Exp.val(99))); + p.failOnFilteredOut = true; + + try { + client.get(p, intKey); + } + catch (AerospikeException ae) { + // AS_SUB_FILTERED_BINS = 2 + assertErrorDetails(ae, ResultCode.FILTERED_OUT, "subcode=2"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + // --------------------------------------------------------------------- + // Additional particle modify type mismatches (subcode absent). + // --------------------------------------------------------------------- + + @Test + public void testPrependToIntegerBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, intKey, Operation.prepend(new Bin(binName, "bad-prepend"))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR, "prepend"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testIncrementDoubleOnIntegerBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, strKey, Operation.add(new Bin(binName, 1.5))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + // --------------------------------------------------------------------- + // Additional CDT list ops. + // --------------------------------------------------------------------- + + @Test + public void testListPopIndexOutOfBounds() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + try { + client.operate(wp, listKey, ListOperation.pop(binName, 99)); + } + catch (AerospikeException ae) { + // AS_SUB_OPNOT_CDT_INDEX_OUT_OF_BOUNDS = 1 + assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=1"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testListAddUniqueViolation() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + // Seed [10,20,30]; appending an existing value with ADD_UNIQUE fails. + ListPolicy unique = new ListPolicy(ListOrder.UNORDERED, ListWriteFlags.ADD_UNIQUE); + + try { + client.operate(wp, listKey, ListOperation.append(unique, binName, Value.get(20))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.ELEMENT_EXISTS); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testListOpOnRawBytesBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + // A raw-bytes bin is not a list -> list_get triggers the wrong-type path. + Key key = new Key(args.namespace, args.set, "edv-list-raw-key"); + client.put(new WritePolicy(), key, new Bin(binName, new byte[]{(byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF})); + + try { + client.operate(wp, key, ListOperation.get(binName, 0)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + // --------------------------------------------------------------------- + // CDT map ops. + // --------------------------------------------------------------------- + + @Test + public void testMapCreateOnlyExistingKey() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-map-create-key"); + Map seed = new HashMap<>(); + seed.put(1, "a"); + client.put(new WritePolicy(), key, new Bin(binName, seed)); + + MapPolicy mp = new MapPolicy(MapOrder.UNORDERED, MapWriteFlags.CREATE_ONLY); + + try { + client.operate(wp, key, MapOperation.put(mp, binName, Value.get(1), Value.get("b"))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.ELEMENT_EXISTS); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testMapUpdateOnlyMissingKey() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-map-update-key"); + Map seed = new HashMap<>(); + seed.put(1, "a"); + client.put(new WritePolicy(), key, new Bin(binName, seed)); + + MapPolicy mp = new MapPolicy(MapOrder.UNORDERED, MapWriteFlags.UPDATE_ONLY); + + try { + client.operate(wp, key, MapOperation.put(mp, binName, Value.get(99), Value.get("b"))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.ELEMENT_NOT_FOUND); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testMapOpOnListBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + // listKey holds a list; a map op against it triggers the wrong-type path. + try { + client.operate(wp, listKey, MapOperation.getByKey(binName, Value.get(1), MapReturnType.VALUE)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testMapOpOnRawBytesBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-map-raw-key"); + client.put(new WritePolicy(), key, new Bin(binName, new byte[]{0x42, 0x42})); + + try { + client.operate(wp, key, MapOperation.getByKey(binName, Value.get(1), MapReturnType.VALUE)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testListCtxIntoStringMapValue() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + // Map value at key 1 is a string; descending into it with a list op and + // a map-key context is a type mismatch. + Key key = new Key(args.namespace, args.set, "edv-map-ctx-key"); + Map seed = new HashMap<>(); + seed.put(1, "leaf-string"); + client.put(new WritePolicy(), key, new Bin(binName, seed)); + + try { + client.operate(wp, key, ListOperation.get(binName, 0, CTX.mapKey(Value.get(1)))); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + // --------------------------------------------------------------------- + // Additional HLL ops. + // --------------------------------------------------------------------- + + @Test + public void testHllInitInvalidIndexBits() { WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 2; - Key key3 = new Key(args.namespace, args.set, "edv-no-hll-key"); - client.put(new WritePolicy(), key3, new Bin("other-bin", 1)); + Key key = new Key(args.namespace, args.set, "edv-hll-bad-bits-key"); try { - client.operate(wp, key3, HLLOperation.refreshCount("no-hll-bin")); + // Index bit count out of the legal [4,16] range -> server-side reject. + client.operate(wp, key, HLLOperation.init(HLLPolicy.Default, binName, 30)); } catch (AerospikeException ae) { - assertErrorDetails(ae, ResultCode.BIN_NOT_FOUND, "subcode=1134"); + assertSubcodeAbsent(ae, ResultCode.PARAMETER_ERROR); return; } assertTrue("Expected AerospikeException", false); } + @Test + public void testHllOpOnRawBytesBin() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + + Key key = new Key(args.namespace, args.set, "edv-hll-raw-key"); + client.put(new WritePolicy(), key, new Bin(binName, new byte[]{0x01, 0x02, 0x03})); + + try { + client.operate(wp, key, HLLOperation.getCount(binName)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.BIN_TYPE_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + // --------------------------------------------------------------------- + // Write / delete / read policy (subcode absent unless noted). + // --------------------------------------------------------------------- + + @Test + public void testWriteCreateOnlyExistingRecord() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.recordExistsAction = RecordExistsAction.CREATE_ONLY; + + try { + // intKey already exists. + client.put(wp, intKey, new Bin(binName, 2)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.KEY_EXISTS_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testWriteReplaceOnlyMissingRecord() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.recordExistsAction = RecordExistsAction.REPLACE_ONLY; + + Key key = new Key(args.namespace, args.set, "edv-replace-missing-key"); + client.delete(new WritePolicy(), key); + + try { + client.put(wp, key, new Bin(binName, 1)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.KEY_NOT_FOUND_ERROR); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testWriteGenerationMismatch() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL; + wp.generation = 999; + + try { + client.put(wp, intKey, new Bin(binName, 2)); + } + catch (AerospikeException ae) { + assertSubcodeAbsent(ae, ResultCode.GENERATION_ERROR, "generation"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + @Test + public void testOperateFilteredOut() { + WritePolicy wp = new WritePolicy(); + wp.errorDetailVerbosity = 2; + wp.filterExp = Exp.build(Exp.eq(Exp.intBin(binName), Exp.val(99))); + wp.failOnFilteredOut = true; + + try { + client.operate(wp, intKey, Operation.get(binName)); + } + catch (AerospikeException ae) { + // AS_SUB_FILTERED_BINS = 2 + assertErrorDetails(ae, ResultCode.FILTERED_OUT, "subcode=2"); + return; + } + assertTrue("Expected AerospikeException", false); + } + + // --------------------------------------------------------------------- + // Happy path: verbosity set on a successful command must not break. + // --------------------------------------------------------------------- + @Test public void testSuccessNoErrorDetails() { WritePolicy wp = new WritePolicy(); wp.errorDetailVerbosity = 2; - // A successful write with verbosity=2 should not cause issues. Key key = new Key(args.namespace, args.set, "edv-success-key"); client.put(wp, key, new Bin(binName, 42)); @@ -230,4 +729,22 @@ private void assertErrorDetails(AerospikeException ae, int expectedResultCode, S assertTrue("Expected '" + expected + "' in: " + msg, msg.contains(expected)); } } + + /** + * Assert that the server surfaced a contextual message but NO subcode + * (AS_SUB_NONE): the "(subcode=...)" suffix must never appear. Any + * expectedSubstrings are required in the message; pass none to skip the + * message-text check (mirrors a NULL expected_msg_substr in the C example). + */ + private void assertSubcodeAbsent(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { + assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + + String msg = ae.getBaseMessage(); + assertNotNull("Expected server error message", msg); + + for (String expected : expectedSubstrings) { + assertTrue("Expected '" + expected + "' in: " + msg, msg.contains(expected)); + } + assertFalse("Expected NO subcode suffix in: " + msg, msg.contains("subcode=")); + } } From fa81f09caecb0af43252af829cc67dc18a4165fa Mon Sep 17 00:00:00 2001 From: Mirza Karacic Date: Thu, 11 Jun 2026 15:24:52 -0700 Subject: [PATCH 5/6] Explicit subcode addition --- .../aerospike/client/AerospikeException.java | 28 +++ client/src/com/aerospike/client/SubCode.java | 218 ++++++++++++++++++ .../aerospike/client/async/AsyncDelete.java | 4 +- .../aerospike/client/async/AsyncExecute.java | 4 +- .../aerospike/client/async/AsyncExists.java | 4 +- .../client/async/AsyncOperateWrite.java | 4 +- .../com/aerospike/client/async/AsyncRead.java | 4 +- .../client/async/AsyncReadHeader.java | 4 +- .../aerospike/client/async/AsyncTouch.java | 6 +- .../aerospike/client/async/AsyncWrite.java | 4 +- .../client/async/AsyncWriteBase.java | 2 + .../client/command/DeleteCommand.java | 4 +- .../client/command/ExecuteCommand.java | 4 +- .../client/command/ExistsCommand.java | 4 +- .../client/command/OperateCommandWrite.java | 4 +- .../aerospike/client/command/ReadCommand.java | 4 +- .../client/command/ReadHeaderCommand.java | 4 +- .../client/command/RecordParser.java | 21 +- .../client/command/SyncWriteCommand.java | 2 + .../client/command/TouchCommand.java | 6 +- .../client/command/WriteCommand.java | 4 +- .../async/TestAsyncErrorDetailVerbosity.java | 20 +- .../sync/basic/TestErrorDetailVerbosity.java | 46 ++-- 23 files changed, 345 insertions(+), 60 deletions(-) create mode 100644 client/src/com/aerospike/client/SubCode.java diff --git a/client/src/com/aerospike/client/AerospikeException.java b/client/src/com/aerospike/client/AerospikeException.java index 8d356593c..9b77475fd 100644 --- a/client/src/com/aerospike/client/AerospikeException.java +++ b/client/src/com/aerospike/client/AerospikeException.java @@ -32,6 +32,7 @@ public class AerospikeException extends RuntimeException { protected transient Policy policy; protected List subExceptions; protected int resultCode = ResultCode.CLIENT_ERROR; + protected int subcode = SubCode.NONE; protected int iteration = -1; protected boolean inDoubt; @@ -40,6 +41,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; @@ -184,6 +191,27 @@ 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). + *

+ * 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 number of attempts before failing. */ diff --git a/client/src/com/aerospike/client/SubCode.java b/client/src/com/aerospike/client/SubCode.java new file mode 100644 index 000000000..f6ae89d83 --- /dev/null +++ b/client/src/com/aerospike/client/SubCode.java @@ -0,0 +1,218 @@ +/* + * 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; + +/** + * Server error detail subcodes. + *

+ * When extended error detail is requested (see + * {@link com.aerospike.client.policy.Policy#errorDetailVerbosity}), the server may + * attach a numeric subcode to a failure response. The subcode is surfaced on + * {@link AerospikeException#getSubcode()}. + *

+ * Match on the {@code (resultCode, subcode)} pair. Subcode integer values are + * scoped to their parent {@link ResultCode} and are not globally unique — the + * value {@code 1}, for example, recurs under every parent status. A subcode is only + * meaningful when interpreted together with the result code. Always check the result + * code first: + *

{@code
+ * catch (AerospikeException ae) {
+ *     if (ae.getResultCode() == ResultCode.OP_NOT_APPLICABLE &&
+ *         ae.getSubcode() == SubCode.OPNOT_CDT_BOUNDED_LIST_OVERFLOW) {
+ *         // roll to a fresh partition / apply backpressure
+ *     }
+ * }
+ * }
+ *

+ * {@link #NONE} (0) means "no subcode" — it is reserved universally and is the value + * returned when the server did not send a subcode (verbosity disabled, or the failing + * branch had no dispatchable subcode). + *

+ * This catalogue mirrors the server's per-status enums in + * {@code as/include/base/proto.h} and is server-version-specific. It is append-only: + * published values are immutable and are never renumbered or reused. New failure modes + * get new values appended to their group. Treat any subcode value not declared here as + * an opaque integer rather than assuming it is absent. + */ +public final class SubCode { + /** + * No subcode (universal). Returned when the server did not supply a subcode. + */ + public static final int NONE = 0; + + //------------------------------------------------------- + // Pairs with ResultCode.PARAMETER_ERROR (4) [AS_ERR_PARAMETER] + //------------------------------------------------------- + + /** Per-record TTL exceeds the namespace's max-ttl. */ + public static final int PARAM_TTL_INVALID = 1; + + /** Bit op offset lands past the blob (or above the proto cap). */ + public static final int PARAM_BITS_OFFSET_OUT_OF_RANGE = 2; + + /** Bit op size is out of range (e.g. zero, or too large). */ + public static final int PARAM_BITS_SIZE_OUT_OF_RANGE = 3; + + /** Blob resize would exceed the maximum blob size. */ + public static final int PARAM_BITS_RESIZE_EXCEEDED = 4; + + /** Write would exceed the per-record bin-count limit (write path). */ + public static final int PARAM_BIN_COUNT_TOO_LARGE = 5; + + /** String op wire/expression args malformed or out of range. */ + public static final int PARAM_STRING_OP_PARAMS_INVALID = 6; + + /** String op code or modifier/read class mismatch on the wire path. */ + public static final int PARAM_STRING_OP_INVALID = 7; + + /** String context-eval path malformed. */ + public static final int PARAM_STRING_CTX_NOT_APPLICABLE = 8; + + /** String modify/read index or code-point range out of bounds. */ + public static final int PARAM_STRING_INDEX_OUT_OF_BOUNDS = 9; + + /** String regex pattern invalid (compile / ICU failure). */ + public static final int PARAM_STRING_REGEX_INVALID = 10; + + /** String or string op argument is not valid UTF-8. */ + public static final int PARAM_STRING_UTF8_INVALID = 11; + + //------------------------------------------------------- + // Pairs with ResultCode.PARTITION_UNAVAILABLE (11) [AS_ERR_UNAVAILABLE] + //------------------------------------------------------- + + /** Cluster is still resolving initial partition balance at startup. */ + public static final int UNAVAIL_INITIAL_BALANCE_UNRESOLVED = 1; + + /** A needed replica is unavailable (likely a partition split). */ + public static final int UNAVAIL_REPLICA_UNAVAILABLE = 2; + + //------------------------------------------------------- + // Pairs with ResultCode.UNSUPPORTED_FEATURE (16) [AS_ERR_UNSUPPORTED_FEATURE] + //------------------------------------------------------- + + /** MRT attempted against a non-SC (AP) namespace. */ + public static final int UNSUPP_FEAT_MRT_REQUIRES_STRONG_CONSISTENCY = 1; + + /** Requested feature is unsupported in this context (generic). */ + public static final int UNSUPP_FEAT_GENERIC = 2; + + //------------------------------------------------------- + // Pairs with ResultCode.BIN_NOT_FOUND (17) [AS_ERR_BIN_NOT_FOUND] + //------------------------------------------------------- + + /** HLL op needs an existing bin and can't auto-create one. */ + public static final int BIN_NOT_FOUND_HLL_CANNOT_CREATE_WITH_OP = 1; + + /** String modify on a missing bin (non-NO_FAIL path). */ + public static final int BIN_NOT_FOUND_STRING_VALUE_NOT_FOUND = 2; + + //------------------------------------------------------- + // Pairs with ResultCode.BIN_NAME_TOO_LONG (21) [AS_ERR_BIN_NAME] + //------------------------------------------------------- + + /** Write would exceed the per-record bin-count limit (UDF path). */ + public static final int BIN_NAME_COUNT_TOO_LARGE = 1; + + //------------------------------------------------------- + // Pairs with ResultCode.FAIL_FORBIDDEN (22) [AS_ERR_FORBIDDEN] + //------------------------------------------------------- + + /** Write bounced by an XDR ship filter at the destination. */ + public static final int FORBID_XDR_FILTER_BLOCKED = 1; + + /** Set-level record-count stop-writes limit reached. */ + public static final int FORBID_SET_COUNT_STOP_WRITES = 2; + + /** Set-level size stop-writes limit reached. */ + public static final int FORBID_SET_SIZE_STOP_WRITES = 3; + + /** Writes stopped due to cluster clock skew. */ + public static final int FORBID_CLOCK_SKEW_STOP_WRITES = 4; + + /** REPLACE / CREATE_OR_REPLACE forbidden while resolving conflicts. */ + public static final int FORBID_REPLACE_CONFLICT_RESOLVING = 5; + + /** Write forbidden because the set/namespace is mid-truncate. */ + public static final int FORBID_TRUNCATED = 6; + + // Note: server subcodes 7 and 9 in this family are retired (masking violations + // return ROLE_VIOLATION, not FORBIDDEN) and are intentionally not declared. + + /** Non-durable delete forbidden (would violate durability). */ + public static final int FORBID_DURABILITY_VIOLATION = 8; + + //------------------------------------------------------- + // Pairs with ResultCode.OP_NOT_APPLICABLE (26) [AS_ERR_OP_NOT_APPLICABLE] + //------------------------------------------------------- + + /** List index is outside the current element range. */ + public static final int OPNOT_CDT_INDEX_OUT_OF_BOUNDS = 1; + + /** Requested rank is past the current population. */ + public static final int OPNOT_CDT_RANK_OUT_OF_BOUNDS = 2; + + /** Insert would exceed an ordered+bounded list's cap. */ + public static final int OPNOT_CDT_BOUNDED_LIST_OVERFLOW = 3; + + /** HLL op needs index_bits but the sketch has none set. */ + public static final int OPNOT_HLL_INDEX_BITS_UNSET = 4; + + /** Union needs to reduce index_bits but folding isn't allowed. */ + public static final int OPNOT_HLL_CANNOT_REDUCE_INDEX_BITS = 5; + + /** As above, for the minhash dimension. */ + public static final int OPNOT_HLL_CANNOT_REDUCE_MINHASH_BITS = 6; + + /** Fold blocked because the sketch carries minhash bits. */ + public static final int OPNOT_HLL_CANNOT_FOLD_MINHASH = 7; + + /** Fold target index_bits >= current (fold can only reduce). */ + public static final int OPNOT_HLL_FOLD_INDEX_BITS_TOO_LARGE = 8; + + /** Intersect inputs have mismatched minhash parameters. */ + public static final int OPNOT_HLL_INTERSECT_MINHASH_MISMATCH = 9; + + /** String to numeric conversion failed (strtoll/strtod). */ + public static final int OPNOT_STRING_CONVERSION_FAILED = 10; + + /** Source blob/string is not valid UTF-8 for an OP_NOT_APPLICABLE path. */ + public static final int OPNOT_STRING_UTF8_INVALID = 11; + + //------------------------------------------------------- + // Pairs with ResultCode.FILTERED_OUT (27) [AS_ERR_FILTERED_OUT] + //------------------------------------------------------- + + /** Record filtered out by a metadata-only filter expression. */ + public static final int FILTERED_META = 1; + + /** Record filtered out by a bin-reading filter expression. */ + public static final int FILTERED_BINS = 2; + + //------------------------------------------------------- + // Pairs with ResultCode.MRT_BLOCKED (120) [AS_ERR_MRT_BLOCKED] + //------------------------------------------------------- + + /** Record is provisionally locked by another MRT. */ + public static final int MRT_BLOCKED_RECORD_LOCKED = 1; + + /** Op belongs to a different MRT than the one holding the lock. */ + public static final int MRT_BLOCKED_ID_MISMATCH = 2; + + private SubCode() { + } +} diff --git a/client/src/com/aerospike/client/async/AsyncDelete.java b/client/src/com/aerospike/client/async/AsyncDelete.java index a5903b40e..c2817112a 100644 --- a/client/src/com/aerospike/client/async/AsyncDelete.java +++ b/client/src/com/aerospike/client/async/AsyncDelete.java @@ -54,7 +54,7 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } existed = true; @@ -62,7 +62,7 @@ protected boolean parseResult() { } throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncExecute.java b/client/src/com/aerospike/client/async/AsyncExecute.java index 0f8754e4f..db6f0722b 100644 --- a/client/src/com/aerospike/client/async/AsyncExecute.java +++ b/client/src/com/aerospike/client/async/AsyncExecute.java @@ -75,14 +75,14 @@ record = rp.parseRecord(false); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } return true; } throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncExists.java b/client/src/com/aerospike/client/async/AsyncExists.java index ffa6f4694..82d5cc95b 100644 --- a/client/src/com/aerospike/client/async/AsyncExists.java +++ b/client/src/com/aerospike/client/async/AsyncExists.java @@ -56,7 +56,7 @@ protected boolean parseResult() { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } exists = true; @@ -64,7 +64,7 @@ protected boolean parseResult() { } throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncOperateWrite.java b/client/src/com/aerospike/client/async/AsyncOperateWrite.java index dc9936644..c93ed399a 100644 --- a/client/src/com/aerospike/client/async/AsyncOperateWrite.java +++ b/client/src/com/aerospike/client/async/AsyncOperateWrite.java @@ -54,14 +54,14 @@ record = rp.parseRecord(true); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } return true; } throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncRead.java b/client/src/com/aerospike/client/async/AsyncRead.java index 9ad15c030..dddcf31a7 100644 --- a/client/src/com/aerospike/client/async/AsyncRead.java +++ b/client/src/com/aerospike/client/async/AsyncRead.java @@ -67,14 +67,14 @@ protected final boolean parseResult() { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } return true; } throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncReadHeader.java b/client/src/com/aerospike/client/async/AsyncReadHeader.java index 393c1d1f8..ef6c7b80b 100644 --- a/client/src/com/aerospike/client/async/AsyncReadHeader.java +++ b/client/src/com/aerospike/client/async/AsyncReadHeader.java @@ -56,14 +56,14 @@ record = new Record(null, rp.generation, rp.expiration); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } return true; } throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage) : + new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : new AerospikeException(rp.resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncTouch.java b/client/src/com/aerospike/client/async/AsyncTouch.java index 6632521bc..17337abe3 100644 --- a/client/src/com/aerospike/client/async/AsyncTouch.java +++ b/client/src/com/aerospike/client/async/AsyncTouch.java @@ -58,7 +58,7 @@ protected boolean parseResult() { if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) { if (existsListener == null) { throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } touched = false; @@ -68,7 +68,7 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } touched = false; @@ -76,7 +76,7 @@ protected boolean parseResult() { } throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncWrite.java b/client/src/com/aerospike/client/async/AsyncWrite.java index 58760c856..0255f6f7d 100644 --- a/client/src/com/aerospike/client/async/AsyncWrite.java +++ b/client/src/com/aerospike/client/async/AsyncWrite.java @@ -60,14 +60,14 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } return true; } throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage) : + new AerospikeException(resultCode, serverMessage, serverSubcode) : new AerospikeException(resultCode); } diff --git a/client/src/com/aerospike/client/async/AsyncWriteBase.java b/client/src/com/aerospike/client/async/AsyncWriteBase.java index b8b3e9eab..892a98057 100644 --- a/client/src/com/aerospike/client/async/AsyncWriteBase.java +++ b/client/src/com/aerospike/client/async/AsyncWriteBase.java @@ -66,11 +66,13 @@ void onInDoubt() { } protected String serverMessage; + protected int serverSubcode; // SubCode.NONE (0) when absent protected int parseHeader() { RecordParser rp = new RecordParser(dataBuffer, dataOffset, receiveSize); rp.parseFields(policy.txn, key, true); this.serverMessage = rp.serverMessage; + this.serverSubcode = rp.serverSubcode; return rp.resultCode; } } diff --git a/client/src/com/aerospike/client/command/DeleteCommand.java b/client/src/com/aerospike/client/command/DeleteCommand.java index a10175858..99152f207 100644 --- a/client/src/com/aerospike/client/command/DeleteCommand.java +++ b/client/src/com/aerospike/client/command/DeleteCommand.java @@ -53,13 +53,13 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.FILTERED_OUT) { if (writePolicy.failOnFilteredOut) { - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } existed = true; return; } - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } public boolean existed() { diff --git a/client/src/com/aerospike/client/command/ExecuteCommand.java b/client/src/com/aerospike/client/command/ExecuteCommand.java index 7d4bb3438..961efb24e 100644 --- a/client/src/com/aerospike/client/command/ExecuteCommand.java +++ b/client/src/com/aerospike/client/command/ExecuteCommand.java @@ -74,12 +74,12 @@ record = rp.parseRecord(false); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return; } - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } private void handleUdfError(int resultCode) { diff --git a/client/src/com/aerospike/client/command/ExistsCommand.java b/client/src/com/aerospike/client/command/ExistsCommand.java index 9aa6e3fe8..50c3f616f 100644 --- a/client/src/com/aerospike/client/command/ExistsCommand.java +++ b/client/src/com/aerospike/client/command/ExistsCommand.java @@ -58,13 +58,13 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } exists = true; return; } - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } public boolean exists() { diff --git a/client/src/com/aerospike/client/command/OperateCommandWrite.java b/client/src/com/aerospike/client/command/OperateCommandWrite.java index e6ae99c4a..3dfbfcfcd 100644 --- a/client/src/com/aerospike/client/command/OperateCommandWrite.java +++ b/client/src/com/aerospike/client/command/OperateCommandWrite.java @@ -56,12 +56,12 @@ record = rp.parseRecord(true); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return; } - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/ReadCommand.java b/client/src/com/aerospike/client/command/ReadCommand.java index fe10f70fe..bbb1a1258 100644 --- a/client/src/com/aerospike/client/command/ReadCommand.java +++ b/client/src/com/aerospike/client/command/ReadCommand.java @@ -73,12 +73,12 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return; } - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/ReadHeaderCommand.java b/client/src/com/aerospike/client/command/ReadHeaderCommand.java index 04eb59e42..69c7dc82a 100644 --- a/client/src/com/aerospike/client/command/ReadHeaderCommand.java +++ b/client/src/com/aerospike/client/command/ReadHeaderCommand.java @@ -59,12 +59,12 @@ record = new Record(null, rp.generation, rp.expiration); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return; } - throw RecordParser.toException(rp.resultCode, rp.serverMessage); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } public Record getRecord() { diff --git a/client/src/com/aerospike/client/command/RecordParser.java b/client/src/com/aerospike/client/command/RecordParser.java index a23a18249..cb12defdf 100644 --- a/client/src/com/aerospike/client/command/RecordParser.java +++ b/client/src/com/aerospike/client/command/RecordParser.java @@ -25,6 +25,7 @@ import com.aerospike.client.AerospikeException; import com.aerospike.client.Key; import com.aerospike.client.Record; +import com.aerospike.client.SubCode; import com.aerospike.client.Txn; import com.aerospike.client.cluster.Connection; import com.aerospike.client.command.Command.OpResults; @@ -39,6 +40,7 @@ public final class RecordParser { public int dataOffset; public long bytesIn; public String serverMessage; + public int serverSubcode = SubCode.NONE; /** * Build a failure exception that includes the server's extended-error @@ -47,9 +49,19 @@ public final class RecordParser { * as FILTERED_OUT or KEY_NOT_FOUND_ERROR. */ public static AerospikeException toException(int resultCode, String serverMessage) { - return (serverMessage != null) ? + return toException(resultCode, serverMessage, SubCode.NONE); + } + + /** + * Build a failure exception that carries the server's extended-error detail — + * the human-readable message and the numeric subcode — when present. + */ + public static AerospikeException toException(int resultCode, String serverMessage, int subcode) { + AerospikeException ae = (serverMessage != null) ? new AerospikeException(resultCode, serverMessage) : new AerospikeException(resultCode); + ae.setSubcode(subcode); + return ae; } /** @@ -313,6 +325,13 @@ else if (b == 0xCC && offset < end) { } } + // Retain the numeric subcode as a first-class value. The server only + // serializes subcodes >= 1 (AS_SUB_NONE = 0 is never sent), so a parsed + // subcode always overrides the SubCode.NONE default. + if (subcode >= 0) { + serverSubcode = (int)subcode; + } + if (message != null && subcode >= 0) { return message + " (subcode=" + subcode + ")"; } diff --git a/client/src/com/aerospike/client/command/SyncWriteCommand.java b/client/src/com/aerospike/client/command/SyncWriteCommand.java index 0c38692bc..5bbcbb784 100644 --- a/client/src/com/aerospike/client/command/SyncWriteCommand.java +++ b/client/src/com/aerospike/client/command/SyncWriteCommand.java @@ -76,8 +76,10 @@ protected int parseHeader(Node node, Connection conn) throws IOException { if (rp.serverMessage != null) { this.serverMessage = rp.serverMessage; } + this.serverSubcode = rp.serverSubcode; return rp.resultCode; } protected String serverMessage; + protected int serverSubcode = SubCode.NONE; } diff --git a/client/src/com/aerospike/client/command/TouchCommand.java b/client/src/com/aerospike/client/command/TouchCommand.java index c877546ea..6bb87804c 100644 --- a/client/src/com/aerospike/client/command/TouchCommand.java +++ b/client/src/com/aerospike/client/command/TouchCommand.java @@ -51,7 +51,7 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) { if (failOnNotFound) { - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } touched = false; return; @@ -59,13 +59,13 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.FILTERED_OUT) { if (writePolicy.failOnFilteredOut) { - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } touched = false; return; } - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } public boolean getTouched() { diff --git a/client/src/com/aerospike/client/command/WriteCommand.java b/client/src/com/aerospike/client/command/WriteCommand.java index 42f00a86f..0f0428193 100644 --- a/client/src/com/aerospike/client/command/WriteCommand.java +++ b/client/src/com/aerospike/client/command/WriteCommand.java @@ -52,11 +52,11 @@ protected void parseResult(Node node, Connection conn) throws IOException { if (resultCode == ResultCode.FILTERED_OUT) { if (writePolicy.failOnFilteredOut) { - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } return; } - throw RecordParser.toException(resultCode, serverMessage); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } } diff --git a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java index 85c9febdc..3cc384bff 100644 --- a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java +++ b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java @@ -29,6 +29,7 @@ import com.aerospike.client.Key; import com.aerospike.client.Record; import com.aerospike.client.ResultCode; +import com.aerospike.client.SubCode; import com.aerospike.client.Value; import com.aerospike.client.cdt.ListOperation; import com.aerospike.client.cdt.ListOrder; @@ -87,7 +88,7 @@ public void onFailure(AerospikeException e) { }, wp, listKey, ListOperation.insert(bounded, binName, 10, Value.get(5))); waitTillComplete(); - assertDetail(caught.get(), ResultCode.OP_NOT_APPLICABLE, "subcode=3"); + assertSubcode(caught.get(), ResultCode.OP_NOT_APPLICABLE, SubCode.OPNOT_CDT_BOUNDED_LIST_OVERFLOW); } // AsyncDelete — generation mismatch surfaces the detail message. The status @@ -233,25 +234,30 @@ public void onFailure(AerospikeException e) { waitTillComplete(); } - private static void assertDetail(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { + /** + * Assert the server-supplied {@code (resultCode, subcode)} pair reached the + * async exception, including the first-class numeric subcode. + */ + private static void assertSubcode(AerospikeException ae, int expectedResultCode, int expectedSubcode) { org.junit.Assert.assertNotNull("Expected AerospikeException to be captured", ae); org.junit.Assert.assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + org.junit.Assert.assertEquals("Unexpected subcode", expectedSubcode, ae.getSubcode()); String msg = ae.getBaseMessage(); org.junit.Assert.assertNotNull("Expected server error message, got null. ae=" + ae, msg); - - for (String expected : expectedSubstrings) { - org.junit.Assert.assertTrue("Expected '" + expected + "' in: " + msg, msg.contains(expected)); - } + org.junit.Assert.assertTrue("Expected 'subcode=" + expectedSubcode + "' in: " + msg, + msg.contains("subcode=" + expectedSubcode)); } /** * Assert that the server surfaced a contextual message but NO subcode - * (AS_SUB_NONE): the "(subcode=...)" suffix must never appear. + * (AS_SUB_NONE): {@link AerospikeException#getSubcode()} is {@link SubCode#NONE} + * and the "(subcode=...)" suffix must never appear. */ private static void assertSubcodeAbsent(AerospikeException ae, int expectedResultCode, String expectedSubstring) { org.junit.Assert.assertNotNull("Expected AerospikeException to be captured", ae); org.junit.Assert.assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + org.junit.Assert.assertEquals("Expected no subcode", SubCode.NONE, ae.getSubcode()); String msg = ae.getBaseMessage(); org.junit.Assert.assertNotNull("Expected server error message, got null. ae=" + ae, msg); diff --git a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java index 554b49e1d..d64d51998 100644 --- a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java +++ b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java @@ -27,6 +27,7 @@ import com.aerospike.client.Operation; import com.aerospike.client.Record; import com.aerospike.client.ResultCode; +import com.aerospike.client.SubCode; import com.aerospike.client.Value; import com.aerospike.client.cdt.CTX; import com.aerospike.client.cdt.ListOperation; @@ -136,6 +137,7 @@ public void testVerbositySubcodeOnly() { } catch (AerospikeException ae) { assertEquals(ResultCode.BIN_NOT_FOUND, ae.getResultCode()); + assertEquals(SubCode.BIN_NOT_FOUND_HLL_CANNOT_CREATE_WITH_OP, ae.getSubcode()); String msg = ae.getBaseMessage(); assertNotNull(msg); assertTrue("Expected subcode in: " + msg, msg.contains("subcode=1")); @@ -159,6 +161,7 @@ public void testVerbositySubcodeAndMessage() { } catch (AerospikeException ae) { assertEquals(ResultCode.BIN_NOT_FOUND, ae.getResultCode()); + assertEquals(SubCode.BIN_NOT_FOUND_HLL_CANNOT_CREATE_WITH_OP, ae.getSubcode()); String msg = ae.getBaseMessage(); assertNotNull(msg); assertTrue("Expected message text in: " + msg, msg.contains("count op")); @@ -257,7 +260,7 @@ public void testHllRefreshCountMissingBin() { } catch (AerospikeException ae) { // AS_SUB_BIN_NOT_FOUND_HLL_CANNOT_CREATE_WITH_OP = 1 - assertErrorDetails(ae, ResultCode.BIN_NOT_FOUND, "subcode=1"); + assertSubcode(ae, ResultCode.BIN_NOT_FOUND, SubCode.BIN_NOT_FOUND_HLL_CANNOT_CREATE_WITH_OP); return; } assertTrue("Expected AerospikeException", false); @@ -273,7 +276,7 @@ public void testListGetIndexOutOfBounds() { } catch (AerospikeException ae) { // AS_SUB_OPNOT_CDT_INDEX_OUT_OF_BOUNDS = 1 - assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=1"); + assertSubcode(ae, ResultCode.OP_NOT_APPLICABLE, SubCode.OPNOT_CDT_INDEX_OUT_OF_BOUNDS); return; } assertTrue("Expected AerospikeException", false); @@ -289,7 +292,7 @@ public void testListGetByRankOutOfBounds() { } catch (AerospikeException ae) { // AS_SUB_OPNOT_CDT_RANK_OUT_OF_BOUNDS = 2 - assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=2"); + assertSubcode(ae, ResultCode.OP_NOT_APPLICABLE, SubCode.OPNOT_CDT_RANK_OUT_OF_BOUNDS); return; } assertTrue("Expected AerospikeException", false); @@ -307,7 +310,7 @@ public void testListBoundedOverflow() { } catch (AerospikeException ae) { // AS_SUB_OPNOT_CDT_BOUNDED_LIST_OVERFLOW = 3 - assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=3"); + assertSubcode(ae, ResultCode.OP_NOT_APPLICABLE, SubCode.OPNOT_CDT_BOUNDED_LIST_OVERFLOW); return; } assertTrue("Expected AerospikeException", false); @@ -327,7 +330,7 @@ public void testHllFoldTargetTooLarge() { } catch (AerospikeException ae) { // AS_SUB_OPNOT_HLL_FOLD_INDEX_BITS_TOO_LARGE = 8 - assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=8"); + assertSubcode(ae, ResultCode.OP_NOT_APPLICABLE, SubCode.OPNOT_HLL_FOLD_INDEX_BITS_TOO_LARGE); return; } assertTrue("Expected AerospikeException", false); @@ -346,7 +349,7 @@ public void testBitGetOffsetOutOfRange() { } catch (AerospikeException ae) { // AS_SUB_PARAM_BITS_OFFSET_OUT_OF_RANGE = 2 - assertErrorDetails(ae, ResultCode.PARAMETER_ERROR, "subcode=2"); + assertSubcode(ae, ResultCode.PARAMETER_ERROR, SubCode.PARAM_BITS_OFFSET_OUT_OF_RANGE); return; } assertTrue("Expected AerospikeException", false); @@ -365,7 +368,7 @@ public void testBitGetSizeZero() { } catch (AerospikeException ae) { // AS_SUB_PARAM_BITS_SIZE_OUT_OF_RANGE = 3 - assertErrorDetails(ae, ResultCode.PARAMETER_ERROR, "subcode=3"); + assertSubcode(ae, ResultCode.PARAMETER_ERROR, SubCode.PARAM_BITS_SIZE_OUT_OF_RANGE); return; } assertTrue("Expected AerospikeException", false); @@ -383,7 +386,7 @@ public void testReadFilteredOut() { } catch (AerospikeException ae) { // AS_SUB_FILTERED_BINS = 2 - assertErrorDetails(ae, ResultCode.FILTERED_OUT, "subcode=2"); + assertSubcode(ae, ResultCode.FILTERED_OUT, SubCode.FILTERED_BINS); return; } assertTrue("Expected AerospikeException", false); @@ -437,7 +440,7 @@ public void testListPopIndexOutOfBounds() { } catch (AerospikeException ae) { // AS_SUB_OPNOT_CDT_INDEX_OUT_OF_BOUNDS = 1 - assertErrorDetails(ae, ResultCode.OP_NOT_APPLICABLE, "subcode=1"); + assertSubcode(ae, ResultCode.OP_NOT_APPLICABLE, SubCode.OPNOT_CDT_INDEX_OUT_OF_BOUNDS); return; } assertTrue("Expected AerospikeException", false); @@ -693,7 +696,7 @@ public void testOperateFilteredOut() { } catch (AerospikeException ae) { // AS_SUB_FILTERED_BINS = 2 - assertErrorDetails(ae, ResultCode.FILTERED_OUT, "subcode=2"); + assertSubcode(ae, ResultCode.FILTERED_OUT, SubCode.FILTERED_BINS); return; } assertTrue("Expected AerospikeException", false); @@ -719,25 +722,32 @@ public void testSuccessNoErrorDetails() { assertEquals(42, record.getInt(binName)); } - private void assertErrorDetails(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { + /** + * Assert the server-supplied {@code (resultCode, subcode)} pair. The numeric + * subcode must be exposed first-class via {@link AerospikeException#getSubcode()} + * (not merely embedded in the message string), and the "subcode=N" suffix must + * still appear in the message for parity with the C client. + */ + private void assertSubcode(AerospikeException ae, int expectedResultCode, int expectedSubcode) { assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + assertEquals("Unexpected subcode", expectedSubcode, ae.getSubcode()); String msg = ae.getBaseMessage(); assertNotNull("Expected server error message", msg); - - for (String expected : expectedSubstrings) { - assertTrue("Expected '" + expected + "' in: " + msg, msg.contains(expected)); - } + assertTrue("Expected 'subcode=" + expectedSubcode + "' in: " + msg, + msg.contains("subcode=" + expectedSubcode)); } /** * Assert that the server surfaced a contextual message but NO subcode - * (AS_SUB_NONE): the "(subcode=...)" suffix must never appear. Any - * expectedSubstrings are required in the message; pass none to skip the - * message-text check (mirrors a NULL expected_msg_substr in the C example). + * (AS_SUB_NONE): {@link AerospikeException#getSubcode()} is {@link SubCode#NONE} + * and the "(subcode=...)" suffix must never appear. Any expectedSubstrings are + * required in the message; pass none to skip the message-text check (mirrors a + * NULL expected_msg_substr in the C example). */ private void assertSubcodeAbsent(AerospikeException ae, int expectedResultCode, String... expectedSubstrings) { assertEquals("Unexpected result code", expectedResultCode, ae.getResultCode()); + assertEquals("Expected no subcode", SubCode.NONE, ae.getSubcode()); String msg = ae.getBaseMessage(); assertNotNull("Expected server error message", msg); From c3ed9cc7dd10c89149e4eb04b36a8f1aa81e8870 Mon Sep 17 00:00:00 2001 From: Mirza Karacic Date: Thu, 11 Jun 2026 16:46:24 -0700 Subject: [PATCH 6/6] Added 8.1.3 server gates --- .../src/com/aerospike/client/async/AsyncDelete.java | 9 +++------ .../com/aerospike/client/async/AsyncExecute.java | 8 ++------ .../src/com/aerospike/client/async/AsyncExists.java | 8 ++------ .../aerospike/client/async/AsyncOperateWrite.java | 8 ++------ .../src/com/aerospike/client/async/AsyncRead.java | 8 ++------ .../com/aerospike/client/async/AsyncReadHeader.java | 8 ++------ .../src/com/aerospike/client/async/AsyncTouch.java | 13 ++++--------- .../src/com/aerospike/client/async/AsyncWrite.java | 9 +++------ .../test/async/TestAsyncErrorDetailVerbosity.java | 3 +++ .../test/sync/basic/TestErrorDetailVerbosity.java | 3 +++ 10 files changed, 26 insertions(+), 51 deletions(-) diff --git a/client/src/com/aerospike/client/async/AsyncDelete.java b/client/src/com/aerospike/client/async/AsyncDelete.java index c2817112a..55fd111a5 100644 --- a/client/src/com/aerospike/client/async/AsyncDelete.java +++ b/client/src/com/aerospike/client/async/AsyncDelete.java @@ -20,6 +20,7 @@ import com.aerospike.client.Key; import com.aerospike.client.ResultCode; import com.aerospike.client.cluster.Cluster; +import com.aerospike.client.command.RecordParser; import com.aerospike.client.listener.DeleteListener; import com.aerospike.client.policy.WritePolicy; @@ -53,17 +54,13 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } existed = true; return true; } - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncExecute.java b/client/src/com/aerospike/client/async/AsyncExecute.java index db6f0722b..7893379af 100644 --- a/client/src/com/aerospike/client/async/AsyncExecute.java +++ b/client/src/com/aerospike/client/async/AsyncExecute.java @@ -74,16 +74,12 @@ record = rp.parseRecord(false); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return true; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } private void handleUdfError(int resultCode) { diff --git a/client/src/com/aerospike/client/async/AsyncExists.java b/client/src/com/aerospike/client/async/AsyncExists.java index 82d5cc95b..a15116a0d 100644 --- a/client/src/com/aerospike/client/async/AsyncExists.java +++ b/client/src/com/aerospike/client/async/AsyncExists.java @@ -55,17 +55,13 @@ protected boolean parseResult() { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } exists = true; return true; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncOperateWrite.java b/client/src/com/aerospike/client/async/AsyncOperateWrite.java index c93ed399a..c5958e83c 100644 --- a/client/src/com/aerospike/client/async/AsyncOperateWrite.java +++ b/client/src/com/aerospike/client/async/AsyncOperateWrite.java @@ -53,16 +53,12 @@ record = rp.parseRecord(true); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return true; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncRead.java b/client/src/com/aerospike/client/async/AsyncRead.java index dddcf31a7..260d95209 100644 --- a/client/src/com/aerospike/client/async/AsyncRead.java +++ b/client/src/com/aerospike/client/async/AsyncRead.java @@ -66,16 +66,12 @@ protected final boolean parseResult() { if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return true; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncReadHeader.java b/client/src/com/aerospike/client/async/AsyncReadHeader.java index ef6c7b80b..4a8a0b3cf 100644 --- a/client/src/com/aerospike/client/async/AsyncReadHeader.java +++ b/client/src/com/aerospike/client/async/AsyncReadHeader.java @@ -55,16 +55,12 @@ record = new Record(null, rp.generation, rp.expiration); if (rp.resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } return true; } - throw (rp.serverMessage != null) ? - new AerospikeException(rp.resultCode, rp.serverMessage, rp.serverSubcode) : - new AerospikeException(rp.resultCode); + throw RecordParser.toException(rp.resultCode, rp.serverMessage, rp.serverSubcode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncTouch.java b/client/src/com/aerospike/client/async/AsyncTouch.java index 17337abe3..c8aa4c982 100644 --- a/client/src/com/aerospike/client/async/AsyncTouch.java +++ b/client/src/com/aerospike/client/async/AsyncTouch.java @@ -20,6 +20,7 @@ import com.aerospike.client.Key; import com.aerospike.client.ResultCode; import com.aerospike.client.cluster.Cluster; +import com.aerospike.client.command.RecordParser; import com.aerospike.client.listener.ExistsListener; import com.aerospike.client.listener.WriteListener; import com.aerospike.client.policy.WritePolicy; @@ -57,9 +58,7 @@ protected boolean parseResult() { if (resultCode == ResultCode.KEY_NOT_FOUND_ERROR) { if (existsListener == null) { - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } touched = false; return true; @@ -67,17 +66,13 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } touched = false; return true; } - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } @Override diff --git a/client/src/com/aerospike/client/async/AsyncWrite.java b/client/src/com/aerospike/client/async/AsyncWrite.java index 0255f6f7d..6f61d7da7 100644 --- a/client/src/com/aerospike/client/async/AsyncWrite.java +++ b/client/src/com/aerospike/client/async/AsyncWrite.java @@ -22,6 +22,7 @@ import com.aerospike.client.Operation; import com.aerospike.client.ResultCode; import com.aerospike.client.cluster.Cluster; +import com.aerospike.client.command.RecordParser; import com.aerospike.client.listener.WriteListener; import com.aerospike.client.policy.WritePolicy; @@ -59,16 +60,12 @@ protected boolean parseResult() { if (resultCode == ResultCode.FILTERED_OUT) { if (policy.failOnFilteredOut) { - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } return true; } - throw (serverMessage != null) ? - new AerospikeException(resultCode, serverMessage, serverSubcode) : - new AerospikeException(resultCode); + throw RecordParser.toException(resultCode, serverMessage, serverSubcode); } @Override diff --git a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java index 3cc384bff..7cdaa3f2c 100644 --- a/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java +++ b/test/src/com/aerospike/test/async/TestAsyncErrorDetailVerbosity.java @@ -54,6 +54,9 @@ public class TestAsyncErrorDetailVerbosity extends TestAsync { @BeforeClass public static void setup() { + org.junit.Assume.assumeTrue("Extended error-detail requires server version 8.1.3 or later", + args.serverVersion.isGreaterOrEqual(8, 1, 3, 0)); + WritePolicy wp = new WritePolicy(); intKey = new Key(args.namespace, args.set, "edv-async-int-key"); client.put(wp, intKey, new Bin(binName, 1)); diff --git a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java index d64d51998..929327355 100644 --- a/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java +++ b/test/src/com/aerospike/test/sync/basic/TestErrorDetailVerbosity.java @@ -75,6 +75,9 @@ public class TestErrorDetailVerbosity extends TestSync { @BeforeClass public static void setup() { + org.junit.Assume.assumeTrue("Extended error-detail requires server version 8.1.3 or later", + args.serverVersion.isGreaterOrEqual(8, 1, 3, 0)); + WritePolicy wp = new WritePolicy(); intKey = new Key(args.namespace, args.set, "edv-int-key"); strKey = new Key(args.namespace, args.set, "edv-str-key");