Skip to content

Commit

Permalink
Update AMQP Error Context and adding more AMQP error codes (#22060)
Browse files Browse the repository at this point in the history
* Adding documentation to potential Amqp errors.

* Making valueMap for AmqpResponseCode final.

* Update AmqpErrorContext to contain ErrorInfo.

* Add revapi.json suppression. The serialization itself is compatible based on "Compatible changes" in https://docs.oracle.com/javase/6/docs/platform/serialization/spec/version.html#6678

* Adding test case.

* Adding more error conditions.
  • Loading branch information
conniey authored Jun 4, 2021
1 parent 19b1dde commit 13ca8d1
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,12 @@
"code": "java.annotation.attributeValueChanged",
"new": "@interface com.azure.core.annotation.JsonFlatten",
"justification": "Changes made the annotation more permissive on the target types."
},
{
"code": "java.field.serialVersionUIDUnchanged",
"old": "field com.azure.core.amqp.exception.AmqpErrorContext.serialVersionUID",
"new": "field com.azure.core.amqp.exception.AmqpErrorContext.serialVersionUID",
"justification": "The field ErrorInfo was added to AmqpErrorContext, but no existing fields were removed or changed."
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public enum AmqpErrorCondition {
*/
PROTON_IO("proton:io"),
/**
* A connection error occurred.
* A connection error occurred. A valid frame header cannot be formed from the incoming byte stream.
*/
CONNECTION_FRAMING_ERROR("amqp:connection:framing-error"),
/**
Expand Down Expand Up @@ -139,7 +139,50 @@ public enum AmqpErrorCondition {
/**
* Error condition when a subscription client tries to create a rule with the name of an already existing rule.
*/
ENTITY_ALREADY_EXISTS("com.microsoft:entity-already-exists");
ENTITY_ALREADY_EXISTS("com.microsoft:entity-already-exists"),

/**
* The container is no longer available on the current connection. The peer SHOULD attempt reconnection to the
* container using the details provided in the info map.
*
* The address provided cannot be resolved to a terminus at the current container. The info map MAY contain the
* following information to allow the client to locate the attach to the terminus.
*
* hostname:
* the hostname of the container. This is the value that SHOULD be supplied in the hostname field of the open frame,
* and during the SASL and TLS negotiation (if used).
*
* network-host:
* the DNS hostname or IP address of the machine hosting the container.
*
* port:
* the port number on the machine hosting the container.
*/
CONNECTION_REDIRECT("amqp:connection:redirect"),

/**
* The address provided cannot be resolved to a terminus at the current container. The info map MAY contain the
* following information to allow the client to locate the attach to the terminus.
*
* hostname:
* the hostname of the container hosting the terminus. This is the value that SHOULD be supplied in the hostname
* field of the open frame, and during SASL and TLS negotiation (if used).
*
* network-host:
* the DNS hostname or IP address of the machine hosting the container.
*
* port:
* the port number on the machine hosting the container.
*
* address:
* the address of the terminus at the container.
*/
LINK_REDIRECT("amqp:link:redirect"),

/**
* The peer sent more message transfers than currently allowed on the link.
*/
TRANSFER_LIMIT_EXCEEDED("amqp:link:transfer-limit-exceeded");

private static final Map<String, AmqpErrorCondition> ERROR_CONSTANT_MAP = new HashMap<>();
private final String errorCondition;
Expand All @@ -150,6 +193,11 @@ public enum AmqpErrorCondition {
}
}

/**
* Creates an instance with the error condition header.
*
* @param errorCondition Error condition header value.
*/
AmqpErrorCondition(String errorCondition) {
this.errorCondition = errorCondition;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import com.azure.core.util.CoreUtils;

import java.io.Serializable;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

/**
* Provides context for an {@link AmqpException} that occurs in an {@link AmqpConnection}, {@link AmqpSession},
* or {@link AmqpLink}.
* Provides context for an {@link AmqpException} that occurs in an {@link AmqpConnection}, {@link AmqpSession}, or
* {@link AmqpLink}.
*
* @see AmqpException
* @see SessionErrorContext
Expand All @@ -24,11 +27,13 @@ public class AmqpErrorContext implements Serializable {
private static final long serialVersionUID = -2819764407122954922L;

private final String namespace;
private final Map<String, Object> errorInfo;

/**
* Creates a new instance with the provided {@code namespace}.
*
* @param namespace The service namespace of the error.
*
* @throws IllegalArgumentException when {@code namespace} is {@code null} or empty.
*/
public AmqpErrorContext(String namespace) {
Expand All @@ -37,6 +42,24 @@ public AmqpErrorContext(String namespace) {
}

this.namespace = namespace;
this.errorInfo = null;
}

/**
* Creates a new instance with the provided {@code namespace}.
*
* @param namespace The service namespace of the error.
* @param errorInfo Additional information associated with the error.
*
* @throws IllegalArgumentException when {@code namespace} is {@code null} or empty.
*/
public AmqpErrorContext(String namespace, Map<String, Object> errorInfo) {
if (CoreUtils.isNullOrEmpty(namespace)) {
throw new IllegalArgumentException("'namespace' cannot be null or empty");
}

this.namespace = namespace;
this.errorInfo = Objects.requireNonNull(errorInfo, "'errorInfo' cannot be null.");
}

/**
Expand All @@ -48,13 +71,31 @@ public String getNamespace() {
return namespace;
}

/**
* Gets the map carrying information about the error condition.
*
* @return Map carrying additional information about the error.
*/
public Map<String, Object> getErrorInfo() {
return errorInfo != null ? Collections.unmodifiableMap(errorInfo) : Collections.emptyMap();
}

/**
* Creates a string representation of this ErrorContext.
*
* @return A string representation of this ErrorContext.
*/
@Override
public String toString() {
return String.format(Locale.US, "NAMESPACE: %s", getNamespace());
final String formatString = "NAMESPACE: %s. ERROR CONTEXT: %s";

if (errorInfo == null) {
return String.format(Locale.ROOT, formatString, getNamespace(), "N/A");
}

final StringBuilder builder = new StringBuilder();
errorInfo.forEach((key, value) -> builder.append(String.format("[%s: %s], ", key, value)));

return String.format(Locale.ROOT, formatString, getNamespace(), builder.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* General exception for AMQP related failures.
*
* @see AmqpErrorCondition
* @see <a href="http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-error">Amqp
* Error</a>
* @see <a href="https://docs.microsoft.com/azure/event-hubs/event-hubs-messaging-exceptions">Azure Messaging
* Exceptions</a>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ public enum AmqpResponseCode {
GATEWAY_TIMEOUT(504),
HTTP_VERSION_NOT_SUPPORTED(505);

private static Map<Integer, AmqpResponseCode> valueMap = new HashMap<>();
private static final Map<Integer, AmqpResponseCode> VALUE_MAP = new HashMap<>();

static {
for (AmqpResponseCode code : AmqpResponseCode.values()) {
valueMap.put(code.value, code);
VALUE_MAP.put(code.value, code);
}
}

Expand All @@ -81,7 +81,7 @@ public enum AmqpResponseCode {
* is found.
*/
public static AmqpResponseCode fromValue(final int value) {
return valueMap.get(value);
return VALUE_MAP.get(value);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertThrows;

public class AmqpErrorContextTest {
Expand All @@ -22,6 +25,37 @@ public void constructor() {

// Assert
Assertions.assertEquals(namespace, context.getNamespace());

final Map<String, Object> actual = context.getErrorInfo();
Assertions.assertNotNull(actual);
Assertions.assertTrue(actual.isEmpty());
}

/**
* Verifies properties set correctly.
*/
@Test
public void constructorErrorInfo() {
// Arrange
String namespace = "an-namespace-test";
Map<String, Object> expected = new HashMap<>();
expected.put("foo", 1);
expected.put("bar", 11);

// Act
AmqpErrorContext context = new AmqpErrorContext(namespace, expected);

// Assert
Assertions.assertEquals(namespace, context.getNamespace());

final Map<String, Object> actual = context.getErrorInfo();
Assertions.assertNotNull(actual);
Assertions.assertEquals(expected.size(), actual.size());

expected.forEach((key, value) -> {
Assertions.assertTrue(actual.containsKey(key));
Assertions.assertEquals(value, actual.get(key));
});
}

/**
Expand Down

0 comments on commit 13ca8d1

Please sign in to comment.