From 34228ddeb9b1d9077d70c28ef88ca699759ff0f2 Mon Sep 17 00:00:00 2001 From: annie-mac Date: Thu, 4 Aug 2022 10:53:59 -0700 Subject: [PATCH 01/18] add replica validation for openConnectionAndInitCaches --- .../GatewayAddressCache.java | 21 ++++- .../directconnectivity/Uri.java | 8 +- .../GatewayAddressCacheTest.java | 77 ++++++++++++++----- .../directconnectivity/ReflectionUtils.java | 5 ++ 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java index 4d5bf6f89c109..f8fc95edc6929 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java @@ -50,7 +50,6 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import java.net.MalformedURLException; import java.net.URI; @@ -61,6 +60,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -106,6 +106,7 @@ public class GatewayAddressCache implements IAddressCache { private IOpenConnectionsHandler openConnectionsHandler; private final ConnectionPolicy connectionPolicy; private final boolean replicaAddressValidationEnabled; + private final List replicaValidationScopes; public GatewayAddressCache( DiagnosticsClientContext clientContext, @@ -164,6 +165,10 @@ public GatewayAddressCache( this.openConnectionsHandler = openConnectionsHandler; this.connectionPolicy = connectionPolicy; this.replicaAddressValidationEnabled = Configs.isReplicaAddressValidationEnabled(); + this.replicaValidationScopes = new ArrayList<>(); + if (this.replicaAddressValidationEnabled) { + this.replicaValidationScopes.add(Uri.HealthStatus.UnhealthyPending); + } } public GatewayAddressCache( @@ -898,7 +903,15 @@ private void validateReplicaAddresses(AddressInformation[] addresses) { Arrays .stream(addresses) .map(address -> address.getPhysicalUri()) - .filter(addressUri -> addressUri.getHealthStatus() == Uri.HealthStatus.UnhealthyPending) + .filter(addressUri -> this.replicaValidationScopes.contains(addressUri.getHealthStatus())) + .sorted(new Comparator() { + @Override + public int compare(Uri o1, Uri o2) { + // Generally, an unhealthyPending replica has more chances to fail the request compared to unknown replica + // and we will want to validate replicas with the highest chance to fail the request first + return o2.getHealthStatus().getPriority() - o1.getHealthStatus().getPriority(); + } + }) .collect(Collectors.toList()); if (addressesNeedToValidation.size() > 0) { @@ -965,6 +978,10 @@ public Flux openConnectionsAndInitCaches( JavaStreamUtils.toString(partitionKeyRangeIdentities, ",")); } + if (this.replicaAddressValidationEnabled) { + this.replicaValidationScopes.add(Uri.HealthStatus.Unknown); + } + List>> tasks = new ArrayList<>(); int batchSize = GatewayAddressCache.DefaultBatchSize; diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java index 86813b94abc75..d201e3dbaeef8 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java @@ -195,10 +195,10 @@ public String toString() { *

*/ public enum HealthStatus { - Connected(0), - Unknown(1), - UnhealthyPending(2), - Unhealthy(3); + Connected(100), + Unknown(200), + UnhealthyPending(300), + Unhealthy(400); private int priority; HealthStatus(int priority) { diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java index b6f7009aa2c87..8f94cf89f1823 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java @@ -64,6 +64,7 @@ import static com.azure.cosmos.implementation.TestUtils.mockDiagnosticsClientContext; import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Connected; import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.UnhealthyPending; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Unknown; import static org.assertj.core.api.Assertions.assertThat; public class GatewayAddressCacheTest extends TestSuiteBase { @@ -105,9 +106,11 @@ public Object[][] protocolProvider() { @DataProvider(name = "replicaValidationArgsProvider") public Object[][] replicaValidationArgsProvider() { return new Object[][]{ - // replica validation is enabled - { false }, - { true }, + // replicaValidationIsEnabled, openConnectionsAndInitCaches + { false, false }, + { false, true }, + { true, false }, + { true, true }, }; } @@ -928,7 +931,7 @@ public Mono> answer(InvocationOnMock invocationOnMock) throws Thro @SuppressWarnings("unchecked") @Test(groups = { "direct" }, dataProvider = "replicaValidationArgsProvider", timeOut = TIMEOUT) - public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnabled) throws Exception { + public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnabled, boolean openConnectionAndInitCaches) throws Exception { Configs configs = ConfigsBuilder.instance().withProtocol(Protocol.TCP).build(); URI serviceEndpoint = new URI(TestConfigurations.HOST); IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; @@ -966,8 +969,15 @@ public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnable new Database(), new HashMap<>()); + if (openConnectionAndInitCaches) { + List pkriList = Arrays.asList(new PartitionKeyRangeIdentity("0")); + cache.openConnectionsAndInitCaches(createdCollection, pkriList).blockLast(); + Mockito.clearInvocations(openConnectionsHandlerMock); + httpClientWrapper.capturedRequests.clear(); + } + PartitionKeyRangeIdentity partitionKeyRangeIdentity = new PartitionKeyRangeIdentity(createdCollection.getResourceId(), "0"); - boolean forceRefreshPartitionAddresses = false; + boolean forceRefreshPartitionAddresses = true; Mono> addressesInfosFromCacheObs = cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses); @@ -982,21 +992,38 @@ public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnable if (replicaValidationEnabled) { ArgumentCaptor> openConnectionArguments = ArgumentCaptor.forClass(List.class); - // Open connection will only be called for unhealthyPending status address - Mockito.verify(openConnectionsHandlerMock, Mockito.times(0)).openConnections(openConnectionArguments.capture()); + if (openConnectionAndInitCaches) { + // If openConnectionAndInitCaches is called, then replica validation will also include for unknown status + Mockito.verify(openConnectionsHandlerMock, Mockito.times(1)).openConnections(openConnectionArguments.capture()); + assertThat(openConnectionArguments.getValue()).hasSize(addressInfosFromCache.size()); + } else { + // Open connection will only be called for unhealthyPending status address + Mockito.verify(openConnectionsHandlerMock, Mockito.times(0)).openConnections(openConnectionArguments.capture()); + } } else { Mockito.verify(openConnectionsHandlerMock, Mockito.never()).openConnections(Mockito.any()); } - // Mark one of the uri as unhealthy, others as connected - // and then force refresh the addresses again, make sure the health status of the uri is reserved httpClientWrapper.capturedRequests.clear(); Mockito.clearInvocations(openConnectionsHandlerMock); - for (AddressInformation address : addressInfosFromCache) { - address.getPhysicalUri().setConnected(); + + // Mark one of the uri as unhealthy, one as unknown, others as connected + // and then force refresh the addresses again, make sure the health status of the uri is reserved + assertThat(addressInfosFromCache.size()).isGreaterThan(2); + Uri unknownAddressUri = null; + Uri unhealthyAddressUri = null; + for (int i = 0; i < addressInfosFromCache.size(); i++) { + if (i == 0) { + unknownAddressUri = addressInfosFromCache.get(0).getPhysicalUri(); + continue; + } + if (i == 1) { + unhealthyAddressUri = addressInfosFromCache.get(1).getPhysicalUri(); + unhealthyAddressUri.setUnhealthy(); + } else { + addressInfosFromCache.get(i).getPhysicalUri().setConnected(); + } } - Uri unhealthyAddressUri = addressInfosFromCache.get(0).getPhysicalUri(); - unhealthyAddressUri.setUnhealthy(); ArrayList refreshedAddresses = Lists.newArrayList(getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, true), TIMEOUT).v); @@ -1007,9 +1034,12 @@ public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnable // validate connected status will be reserved // validate unhealthy status will change into unhealthyPending status - // validate openConnection will only be called for addresses not in connected status + // Validate openConnection will be called for addresses in unhealthyPending status + // Validate openConnection will be called for addresses in unknown status if openConnectionAndInitCaches is called for (AddressInformation addressInformation : refreshedAddresses) { - if (addressInformation.getPhysicalUri().equals(unhealthyAddressUri)) { + if (addressInformation.getPhysicalUri().equals(unknownAddressUri)) { + assertThat(addressInformation.getPhysicalUri().getHealthStatus()).isEqualTo(Unknown); + } else if (addressInformation.getPhysicalUri().equals(unhealthyAddressUri)) { assertThat(addressInformation.getPhysicalUri().getHealthStatus()).isEqualTo(UnhealthyPending); } else { assertThat(addressInformation.getPhysicalUri().getHealthStatus()).isEqualTo(Connected); @@ -1019,8 +1049,12 @@ public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnable if (replicaValidationEnabled) { ArgumentCaptor> openConnectionArguments = ArgumentCaptor.forClass(List.class); Mockito.verify(openConnectionsHandlerMock, Mockito.times(1)).openConnections(openConnectionArguments.capture()); + if (openConnectionAndInitCaches) { + assertThat(openConnectionArguments.getValue()).containsExactlyElementsOf(Arrays.asList(unhealthyAddressUri, unknownAddressUri)); + } else { + assertThat(openConnectionArguments.getValue()).containsExactly(unhealthyAddressUri); + } - assertThat(openConnectionArguments.getValue()).hasSize(1).containsExactly(unhealthyAddressUri); } else { Mockito.verify(openConnectionsHandlerMock, Mockito.never()).openConnections(Mockito.any()); } @@ -1125,7 +1159,7 @@ public void validateReplicaAddressesTests() throws URISyntaxException, NoSuchMet AddressInformation address1 = new AddressInformation(true, true, "rntbd://127.0.0.1:1", Protocol.TCP); address1.getPhysicalUri().setConnected(); - // remain in unknwon status + // remain in unknown status AddressInformation address2 = new AddressInformation(true, false, "rntbd://127.0.0.1:2", Protocol.TCP); // unhealthy status @@ -1137,14 +1171,19 @@ public void validateReplicaAddressesTests() throws URISyntaxException, NoSuchMet AtomicReference healthStatus = ReflectionUtils.getHealthStatus(address4.getPhysicalUri()); healthStatus.set(UnhealthyPending); + // Set the replica validation scope + List replicaValidationScopes = ReflectionUtils.getReplicaValidationScopes(cache); + replicaValidationScopes.add(Unknown); + replicaValidationScopes.add(UnhealthyPending); + validateReplicaAddressesMethod.invoke(cache, new Object[]{ new AddressInformation[]{ address1, address2, address3, address4 }}) ; // Validate openConnection will only be called for address in unhealthyPending status ArgumentCaptor> openConnectionArguments = ArgumentCaptor.forClass(List.class); Mockito.verify(openConnectionsHandlerMock, Mockito.times(1)).openConnections(openConnectionArguments.capture()); - assertThat(openConnectionArguments.getValue()).hasSize(1).containsExactlyElementsOf( - Arrays.asList(address4) + assertThat(openConnectionArguments.getValue()).containsExactlyElementsOf( + Arrays.asList(address4, address2) .stream() .map(addressInformation -> addressInformation.getPhysicalUri()) .collect(Collectors.toList())); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java index 4a517e341848d..0bde5781cf4af 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java @@ -384,4 +384,9 @@ public static void setClientTelemetryMetadataHttpClient(ClientTelemetry clientTe public static AtomicReference getHealthStatus(Uri uri) { return get(AtomicReference.class, uri, "healthStatus"); } + + @SuppressWarnings("unchecked") + public static List getReplicaValidationScopes(GatewayAddressCache gatewayAddressCache) { + return get(List.class, gatewayAddressCache, "replicaValidationScopes"); + } } From 1ee4dd0abeb59474a07ec29e78d95700078d2133 Mon Sep 17 00:00:00 2001 From: Alan Zimmer <48699787+alzimmermsft@users.noreply.github.com> Date: Thu, 1 Sep 2022 12:26:19 -0400 Subject: [PATCH 02/18] Add ServiceProvider Interface to azure-json and azure-xml (#30635) Add ServiceProvider Interface to azure-json and azure-xml --- .../resources/spotbugs/spotbugs-exclude.xml | 4 +- .../com/azure/json/gson/GsonJsonProvider.java | 49 ++ .../com/azure/json/gson/GsonJsonReader.java | 94 ++-- .../com/azure/json/gson/GsonJsonWriter.java | 27 +- .../src/main/java/module-info.java | 2 + .../services/com.azure.json.JsonProvider | 1 + .../json/gson/GsonJsonInstantiationTests.java | 10 +- .../gson/GsonJsonReaderContractTests.java | 3 +- .../gson/GsonJsonWriterContractTests.java | 3 +- .../main/java/com/azure/json/JsonOptions.java | 37 ++ .../java/com/azure/json/JsonProvider.java | 141 +++++ .../java/com/azure/json/JsonProviders.java | 269 +++++++++ .../main/java/com/azure/json/JsonReader.java | 9 +- .../main/java/com/azure/json/JsonToken.java | 7 +- .../DefaultJsonReader.java | 114 ++-- .../DefaultJsonWriter.java | 37 +- .../json/implementation/package-info.java | 7 + .../azure-json/src/main/java/module-info.java | 3 + .../contract/JsonReaderContractTests.java | 532 +++++++++++------- .../DefaultJsonReaderContractTests.java | 6 +- .../DefaultJsonWriterContractTests.java | 6 +- .../main/java/com/azure/xml/XmlProvider.java | 69 +++ .../main/java/com/azure/xml/XmlProviders.java | 260 +++++++++ .../DefaultXmlReader.java | 10 +- .../DefaultXmlWriter.java | 22 +- .../azure-xml/src/main/java/module-info.java | 3 + .../xml/DefaultXmlReaderContractTests.java | 1 + .../xml/DefaultXmlWriterContractTests.java | 3 +- .../java/com/azure/xml/PlaygroundTests.java | 6 +- .../storage/DeserializeListBlobsTests.java | 2 +- 30 files changed, 1438 insertions(+), 299 deletions(-) create mode 100644 sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonProvider.java create mode 100644 sdk/core/azure-json-gson/src/main/resources/META-INF/services/com.azure.json.JsonProvider create mode 100644 sdk/core/azure-json/src/main/java/com/azure/json/JsonOptions.java create mode 100644 sdk/core/azure-json/src/main/java/com/azure/json/JsonProvider.java create mode 100644 sdk/core/azure-json/src/main/java/com/azure/json/JsonProviders.java rename sdk/core/azure-json/src/main/java/com/azure/json/{ => implementation}/DefaultJsonReader.java (62%) rename sdk/core/azure-json/src/main/java/com/azure/json/{ => implementation}/DefaultJsonWriter.java (82%) create mode 100644 sdk/core/azure-json/src/main/java/com/azure/json/implementation/package-info.java rename sdk/core/azure-json/src/test/java/com/azure/json/{ => implementation}/DefaultJsonReaderContractTests.java (69%) rename sdk/core/azure-json/src/test/java/com/azure/json/{ => implementation}/DefaultJsonWriterContractTests.java (84%) create mode 100644 sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProvider.java create mode 100644 sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProviders.java rename sdk/core/azure-xml/src/main/java/com/azure/xml/{ => implementation}/DefaultXmlReader.java (96%) rename sdk/core/azure-xml/src/main/java/com/azure/xml/{ => implementation}/DefaultXmlWriter.java (86%) diff --git a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml index 885e270e1ba4c..7df72d00e293d 100755 --- a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml +++ b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml @@ -150,7 +150,7 @@ - + @@ -2732,7 +2732,7 @@ - + diff --git a/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonProvider.java b/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonProvider.java new file mode 100644 index 0000000000000..6ae9888c87969 --- /dev/null +++ b/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonProvider.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.json.gson; + +import com.azure.json.JsonOptions; +import com.azure.json.JsonProvider; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/** + * Implementation of {@link JsonProvider} that creates instances using GSON. + */ +public class GsonJsonProvider implements JsonProvider { + @Override + public JsonReader createReader(byte[] json, JsonOptions options) { + return GsonJsonReader.fromBytes(json, options); + } + + @Override + public JsonReader createReader(String json, JsonOptions options) { + return GsonJsonReader.fromString(json, options); + } + + @Override + public JsonReader createReader(InputStream json, JsonOptions options) { + return GsonJsonReader.fromStream(json, options); + } + + @Override + public JsonReader createReader(Reader json, JsonOptions options) { + return GsonJsonReader.fromReader(json, options); + } + + @Override + public JsonWriter createWriter(OutputStream json, JsonOptions options) { + return GsonJsonWriter.toStream(json, options); + } + + @Override + public JsonWriter createWriter(Writer json, JsonOptions options) { + return GsonJsonWriter.toWriter(json, options); + } +} diff --git a/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonReader.java b/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonReader.java index 4087f6b15454a..7a9f8260c4014 100644 --- a/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonReader.java +++ b/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonReader.java @@ -3,6 +3,7 @@ package com.azure.json.gson; +import com.azure.json.JsonOptions; import com.azure.json.JsonReader; import com.azure.json.JsonToken; @@ -25,46 +26,71 @@ public final class GsonJsonReader extends JsonReader { private final byte[] jsonBytes; private final String jsonString; private final boolean resetSupported; + private final boolean nonNumericNumbersSupported; private JsonToken currentToken; private boolean consumed = false; + private boolean complete = false; /** * Constructs an instance of {@link GsonJsonReader} from a {@code byte[]}. * * @param json JSON {@code byte[]}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. * @return An instance of {@link GsonJsonReader}. */ - public static JsonReader fromBytes(byte[] json) { + static JsonReader fromBytes(byte[] json, JsonOptions options) { return new GsonJsonReader(new InputStreamReader(new ByteArrayInputStream(json), StandardCharsets.UTF_8), - true, json, null); + true, json, null, options); } /** * Constructs an instance of {@link GsonJsonReader} from a String. * * @param json JSON String. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. * @return An instance of {@link GsonJsonReader}. */ - public static JsonReader fromString(String json) { - return new GsonJsonReader(new StringReader(json), true, null, json); + static JsonReader fromString(String json, JsonOptions options) { + return new GsonJsonReader(new StringReader(json), true, null, json, options); } /** * Constructs an instance of {@link GsonJsonReader} from an {@link InputStream}. * * @param json JSON {@link InputStream}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. * @return An instance of {@link GsonJsonReader}. */ - public static JsonReader fromStream(InputStream json) { - return new GsonJsonReader(new InputStreamReader(json, StandardCharsets.UTF_8), false, null, null); + static JsonReader fromStream(InputStream json, JsonOptions options) { + return new GsonJsonReader(new InputStreamReader(json, StandardCharsets.UTF_8), json.markSupported(), null, null, + options); } - private GsonJsonReader(Reader reader, boolean resetSupported, byte[] jsonBytes, String jsonString) { + /** + * Constructs an instance of {@link GsonJsonReader} from a {@link Reader}. + * + * @param json JSON {@link Reader}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @return An instance of {@link GsonJsonReader}. + */ + static JsonReader fromReader(Reader json, JsonOptions options) { + return new GsonJsonReader(json, json.markSupported(), null, null, options); + } + + private GsonJsonReader(Reader reader, boolean resetSupported, byte[] jsonBytes, String jsonString, + JsonOptions options) { + this(reader, resetSupported, jsonBytes, jsonString, options.isNonNumericNumbersSupported()); + } + + private GsonJsonReader(Reader reader, boolean resetSupported, byte[] jsonBytes, String jsonString, + boolean nonNumericNumbersSupported) { this.reader = new com.google.gson.stream.JsonReader(reader); + this.reader.setLenient(nonNumericNumbersSupported); this.resetSupported = resetSupported; this.jsonBytes = jsonBytes; this.jsonString = jsonString; + this.nonNumericNumbersSupported = nonNumericNumbersSupported; } @Override @@ -74,6 +100,10 @@ public JsonToken currentToken() { @Override public JsonToken nextToken() { + if (complete) { + return currentToken; + } + // GSON requires explicitly beginning and ending arrays and objects and consuming null values. // The contract of JsonReader implicitly overlooks these properties. try { @@ -112,7 +142,12 @@ public JsonToken nextToken() { } } - currentToken = mapToken(reader.peek()); + com.google.gson.stream.JsonToken gsonToken = reader.peek(); + if (gsonToken == com.google.gson.stream.JsonToken.END_DOCUMENT) { + complete = true; + } + + currentToken = mapToken(gsonToken); consumed = false; return currentToken; } catch (IOException e) { @@ -235,7 +270,8 @@ public JsonReader bufferObject() { consumed = true; StringBuilder bufferedObject = new StringBuilder(); readChildren(bufferedObject); - return GsonJsonReader.fromString(bufferedObject.toString()); + String json = bufferedObject.toString(); + return new GsonJsonReader(new StringReader(json), true, null, json, nonNumericNumbersSupported); } else { throw new IllegalStateException("Cannot buffer a JSON object from a non-object, non-field name " + "starting location. Starting location: " + currentToken()); @@ -254,9 +290,11 @@ public JsonReader reset() { } if (jsonBytes != null) { - return GsonJsonReader.fromBytes(jsonBytes); + return new GsonJsonReader( + new InputStreamReader(new ByteArrayInputStream(jsonBytes), StandardCharsets.UTF_8), true, jsonBytes, + null, nonNumericNumbersSupported); } else { - return GsonJsonReader.fromString(jsonString); + return new GsonJsonReader(new StringReader(jsonString), true, null, jsonString, nonNumericNumbersSupported); } } @@ -275,33 +313,19 @@ private static JsonToken mapToken(com.google.gson.stream.JsonToken token) { } switch (token) { - case BEGIN_OBJECT: - return JsonToken.START_OBJECT; - - case END_OBJECT: - case END_DOCUMENT: - return JsonToken.END_OBJECT; - - case BEGIN_ARRAY: - return JsonToken.START_ARRAY; - - case END_ARRAY: - return JsonToken.END_ARRAY; - - case NAME: - return JsonToken.FIELD_NAME; - - case STRING: - return JsonToken.STRING; + case BEGIN_OBJECT: return JsonToken.START_OBJECT; + case END_OBJECT: return JsonToken.END_OBJECT; - case NUMBER: - return JsonToken.NUMBER; + case BEGIN_ARRAY: return JsonToken.START_ARRAY; + case END_ARRAY: return JsonToken.END_ARRAY; - case BOOLEAN: - return JsonToken.BOOLEAN; + case NAME: return JsonToken.FIELD_NAME; + case STRING: return JsonToken.STRING; + case NUMBER: return JsonToken.NUMBER; + case BOOLEAN: return JsonToken.BOOLEAN; + case NULL: return JsonToken.NULL; - case NULL: - return JsonToken.NULL; + case END_DOCUMENT: return JsonToken.END_DOCUMENT; default: throw new IllegalStateException("Unsupported token type: '" + token + "'."); diff --git a/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonWriter.java b/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonWriter.java index 2818bb42cb905..0e5b382d935d1 100644 --- a/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonWriter.java +++ b/sdk/core/azure-json-gson/src/main/java/com/azure/json/gson/GsonJsonWriter.java @@ -3,6 +3,7 @@ package com.azure.json.gson; +import com.azure.json.JsonOptions; import com.azure.json.JsonToken; import com.azure.json.JsonWriteContext; import com.azure.json.JsonWriter; @@ -11,6 +12,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UncheckedIOException; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; @@ -31,15 +33,30 @@ public final class GsonJsonWriter extends JsonWriter { * isn't the owner of the stream. * * @param stream The {@link OutputStream} that will be written. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. * @return An instance of {@link GsonJsonWriter}. */ - public static JsonWriter toStream(OutputStream stream) { - return new GsonJsonWriter(new com.google.gson.stream.JsonWriter( - new OutputStreamWriter(stream, StandardCharsets.UTF_8))); + static JsonWriter toStream(OutputStream stream, JsonOptions options) { + return new GsonJsonWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8), options); } - private GsonJsonWriter(com.google.gson.stream.JsonWriter writer) { - this.writer = writer; + /** + * Creates a {@link GsonJsonWriter} that writes the given {@link Writer}. + *

+ * The passed {@link Writer} won't be closed when {@link #close()} is called as the {@link GsonJsonWriter} + * isn't the owner of the stream. + * + * @param writer The {@link Writer} that will be written. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @return An instance of {@link GsonJsonWriter}. + */ + static JsonWriter toWriter(Writer writer, JsonOptions options) { + return new GsonJsonWriter(writer, options); + } + + private GsonJsonWriter(Writer writer, JsonOptions options) { + this.writer = new com.google.gson.stream.JsonWriter(writer); + this.writer.setLenient(options.isNonNumericNumbersSupported()); } @Override diff --git a/sdk/core/azure-json-gson/src/main/java/module-info.java b/sdk/core/azure-json-gson/src/main/java/module-info.java index eaa4376caf735..9ef2e6e8dc8d1 100644 --- a/sdk/core/azure-json-gson/src/main/java/module-info.java +++ b/sdk/core/azure-json-gson/src/main/java/module-info.java @@ -7,4 +7,6 @@ requires com.google.gson; exports com.azure.json.gson; + + provides com.azure.json.JsonProvider with com.azure.json.gson.GsonJsonProvider; } diff --git a/sdk/core/azure-json-gson/src/main/resources/META-INF/services/com.azure.json.JsonProvider b/sdk/core/azure-json-gson/src/main/resources/META-INF/services/com.azure.json.JsonProvider new file mode 100644 index 0000000000000..2c52ef3599b3f --- /dev/null +++ b/sdk/core/azure-json-gson/src/main/resources/META-INF/services/com.azure.json.JsonProvider @@ -0,0 +1 @@ +com.azure.json.gson.GsonJsonProvider diff --git a/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonInstantiationTests.java b/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonInstantiationTests.java index 236a88e99f243..03b96f5f6e07a 100644 --- a/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonInstantiationTests.java +++ b/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonInstantiationTests.java @@ -24,11 +24,13 @@ public void throwsNullPointerException(Executable executable) { @SuppressWarnings("resource") private static Stream throwsNullPointerExceptionSupplier() { return Stream.of( - () -> GsonJsonReader.fromBytes(null), - () -> GsonJsonReader.fromString(null), - () -> GsonJsonReader.fromStream(null), + () -> GsonJsonReader.fromBytes(null, null), + () -> GsonJsonReader.fromReader(null, null), + () -> GsonJsonReader.fromString(null, null), + () -> GsonJsonReader.fromStream(null, null), - () -> GsonJsonWriter.toStream(null) + () -> GsonJsonWriter.toStream(null, null), + () -> GsonJsonWriter.toWriter(null, null) ); } } diff --git a/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonReaderContractTests.java b/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonReaderContractTests.java index e03a1d1280329..71abd8e5bdf78 100644 --- a/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonReaderContractTests.java +++ b/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonReaderContractTests.java @@ -3,6 +3,7 @@ package com.azure.json.gson; +import com.azure.json.JsonOptions; import com.azure.json.JsonReader; import com.azure.json.contract.JsonReaderContractTests; @@ -12,6 +13,6 @@ public class GsonJsonReaderContractTests extends JsonReaderContractTests { @Override public JsonReader getJsonReader(String json) { - return GsonJsonReader.fromString(json); + return GsonJsonReader.fromString(json, new JsonOptions()); } } diff --git a/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonWriterContractTests.java b/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonWriterContractTests.java index 88b2a0828fcd8..34d0aeaf18c99 100644 --- a/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonWriterContractTests.java +++ b/sdk/core/azure-json-gson/src/test/java/com/azure/json/gson/GsonJsonWriterContractTests.java @@ -3,6 +3,7 @@ package com.azure.json.gson; +import com.azure.json.JsonOptions; import com.azure.json.JsonWriter; import com.azure.json.contract.JsonWriterContractTests; import org.junit.jupiter.api.BeforeEach; @@ -21,7 +22,7 @@ public class GsonJsonWriterContractTests extends JsonWriterContractTests { @BeforeEach public void beforeEach() { this.outputStream = new ByteArrayOutputStream(); - this.writer = GsonJsonWriter.toStream(outputStream); + this.writer = GsonJsonWriter.toStream(outputStream, new JsonOptions()); } @Override diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/JsonOptions.java b/sdk/core/azure-json/src/main/java/com/azure/json/JsonOptions.java new file mode 100644 index 0000000000000..deb7c0f21f403 --- /dev/null +++ b/sdk/core/azure-json/src/main/java/com/azure/json/JsonOptions.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.json; + +/** + * Contains configuration options for creating a {@link JsonReader} or {@link JsonWriter}. + */ +public final class JsonOptions { + static final JsonOptions DEFAULT_OPTIONS = new JsonOptions(); + + private boolean nonNumericNumbersSupported = true; + + /** + * Whether non-numeric numbers such as {@code NaN} and {@code INF} and {@code -INF} are supported. + *

+ * By default, this is configured to true. + * + * @return Whether non-numeric numbers are supported. + */ + public boolean isNonNumericNumbersSupported() { + return nonNumericNumbersSupported; + } + + /** + * Sets whether non-numeric numbers such as {@code NaN} and {@code INF} and {@code -INF} are supported. + *

+ * By default, this is configured to true. + * + * @param nonNumericNumbersSupported Whether non-numeric numbers are supported. + * @return The updated JsonOptions object. + */ + public JsonOptions setNonNumericNumbersSupported(boolean nonNumericNumbersSupported) { + this.nonNumericNumbersSupported = nonNumericNumbersSupported; + return this; + } +} diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/JsonProvider.java b/sdk/core/azure-json/src/main/java/com/azure/json/JsonProvider.java new file mode 100644 index 0000000000000..699bf67dafc18 --- /dev/null +++ b/sdk/core/azure-json/src/main/java/com/azure/json/JsonProvider.java @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.json; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/** + * An interface to be implemented by any azure-json plugin that wishes to provide an alternate {@link JsonReader} or + * {@link JsonWriter} implementation. + */ +public interface JsonProvider { + /** + * Creates an instance of {@link JsonReader} that reads a {@code byte[]}. + * + * @param json The JSON represented as a {@code byte[]}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + default JsonReader createReader(byte[] json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@code byte[]}. + * + * @param json The JSON represented as a {@code byte[]}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + JsonReader createReader(byte[] json, JsonOptions options); + + /** + * Creates an instance of {@link JsonReader} that reads a {@link String}. + * + * @param json The JSON represented as a {@link String}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + default JsonReader createReader(String json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link String}. + * + * @param json The JSON represented as a {@link String}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + JsonReader createReader(String json, JsonOptions options); + + /** + * Creates an instance of {@link JsonReader} that reads a {@link InputStream}. + * + * @param json The JSON represented as a {@link InputStream}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + default JsonReader createReader(InputStream json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link InputStream}. + * + * @param json The JSON represented as a {@link InputStream}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + JsonReader createReader(InputStream json, JsonOptions options); + + /** + * Creates an instance of {@link JsonReader} that reads a {@link Reader}. + * + * @param json The JSON represented as a {@link Reader}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + default JsonReader createReader(Reader json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link Reader}. + * + * @param json The JSON represented as a {@link Reader}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + JsonReader createReader(Reader json, JsonOptions options); + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link OutputStream}. + * + * @param json The JSON represented as an {@link OutputStream}. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + */ + default JsonWriter createWriter(OutputStream json) { + return createWriter(json, JsonOptions.DEFAULT_OPTIONS); + } + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link OutputStream}. + * + * @param json The JSON represented as an {@link OutputStream}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + */ + JsonWriter createWriter(OutputStream json, JsonOptions options); + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link Writer}. + * + * @param json The JSON represented as an {@link Writer}. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + */ + default JsonWriter createWriter(Writer json) { + return createWriter(json, JsonOptions.DEFAULT_OPTIONS); + } + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link Writer}. + * + * @param json The JSON represented as an {@link Writer}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + */ + JsonWriter createWriter(Writer json, JsonOptions options); +} diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/JsonProviders.java b/sdk/core/azure-json/src/main/java/com/azure/json/JsonProviders.java new file mode 100644 index 0000000000000..e995663167f3a --- /dev/null +++ b/sdk/core/azure-json/src/main/java/com/azure/json/JsonProviders.java @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.json; + +import com.azure.json.implementation.DefaultJsonReader; +import com.azure.json.implementation.DefaultJsonWriter; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Handles loading an instance of {@link JsonProvider} found on the classpath. + */ +public final class JsonProviders { + private static final String CANNOT_FIND_JSON = "A request was made to load a JsonReader and JsonWriter provider " + + "but one could not be found on the classpath. If you are using a dependency manager, consider including a " + + "dependency on azure-json-gson or azure-json-reflect or indicate to the loader to fallback to the default " + + "implementation. Depending on your existing dependencies, you have the choice of Jackson or JSON " + + "implementations. Additionally, refer to https://aka.ms/azsdk/java/docs/custom-json to learn about writing " + + "your own implementation."; + + private static JsonProvider defaultProvider; + + static { + // Use as classloader to load provider-configuration files and provider classes the classloader + // that loaded this class. In most cases this will be the System classloader. + // But this choice here provides additional flexibility in managed environments that control + // classloading differently (OSGi, Spring and others) and don't/ depend on the + // System classloader to load HttpClientProvider classes. + ServiceLoader serviceLoader = ServiceLoader.load(JsonProvider.class, + JsonProvider.class.getClassLoader()); + // Use the first provider found in the service loader iterator. + Iterator it = serviceLoader.iterator(); + if (it.hasNext()) { + defaultProvider = it.next(); + } + + while (it.hasNext()) { + it.next(); + } + } + + private JsonProviders() { + // no-op + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@code byte[]}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(byte[], JsonOptions, boolean) createReader(json, new JsonOptions(), true)}. + * + * @param json The JSON represented as a {@code byte[]}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + public static JsonReader createReader(byte[] json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS, true); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@code byte[]}. + * + * @param json The JSON represented as a {@code byte[]}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static JsonReader createReader(byte[] json, JsonOptions options, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultJsonReader.fromBytes(json, options); + } else { + throw new IllegalStateException(CANNOT_FIND_JSON); + } + } else { + return defaultProvider.createReader(json, options); + } + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link String}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(String, JsonOptions, boolean) createReader(json, new JsonOptions(), true)}. + * + * @param json The JSON represented as a {@link String}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + public static JsonReader createReader(String json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS, true); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link String}. + * + * @param json The JSON represented as a {@link String}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static JsonReader createReader(String json, JsonOptions options, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultJsonReader.fromString(json, options); + } else { + throw new IllegalStateException(CANNOT_FIND_JSON); + } + } else { + return defaultProvider.createReader(json, options); + } + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link InputStream}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to + * {@link #createReader(InputStream, JsonOptions, boolean) createReader(json, new JsonOptions(), true)}. + * + * @param json The JSON represented as a {@link InputStream}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + public static JsonReader createReader(InputStream json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS, true); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link InputStream}. + * + * @param json The JSON represented as a {@link InputStream}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static JsonReader createReader(InputStream json, JsonOptions options, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultJsonReader.fromStream(json, options); + } else { + throw new IllegalStateException(CANNOT_FIND_JSON); + } + } else { + return defaultProvider.createReader(json, options); + } + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link Reader}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(Reader, JsonOptions, boolean) createReader(json, new JsonOptions(), true)}. + * + * @param json The JSON represented as a {@link Reader}. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + */ + public static JsonReader createReader(Reader json) { + return createReader(json, JsonOptions.DEFAULT_OPTIONS, true); + } + + /** + * Creates an instance of {@link JsonReader} that reads a {@link Reader}. + * + * @param json The JSON represented as a {@link Reader}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link JsonReader}. + * @throws NullPointerException If {@code json} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static JsonReader createReader(Reader json, JsonOptions options, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultJsonReader.fromReader(json, options); + } else { + throw new IllegalStateException(CANNOT_FIND_JSON); + } + } else { + return defaultProvider.createReader(json, options); + } + } + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link OutputStream}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to + * {@link #createWriter(OutputStream, JsonOptions, boolean) createWriter(json, new JsonOptions(), true)}. + * + * @param json The JSON represented as an {@link OutputStream}. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + */ + public static JsonWriter createWriter(OutputStream json) { + return createWriter(json, JsonOptions.DEFAULT_OPTIONS, true); + } + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link OutputStream}. + * + * @param json The JSON represented as an {@link OutputStream}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static JsonWriter createWriter(OutputStream json, JsonOptions options, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultJsonWriter.toStream(json, options); + } else { + throw new IllegalStateException(CANNOT_FIND_JSON); + } + } else { + return defaultProvider.createWriter(json, options); + } + } + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link Writer}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createWriter(Writer, JsonOptions, boolean) createWriter(json, new JsonOptions(), true)}. + * + * @param json The JSON represented as an {@link Writer}. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + */ + public static JsonWriter createWriter(Writer json) { + return createWriter(json, JsonOptions.DEFAULT_OPTIONS, true); + } + + /** + * Creates an instance of {@link JsonWriter} that writes to an {@link Writer}. + * + * @param json The JSON represented as an {@link Writer}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link JsonWriter}. + * @throws NullPointerException If {@code json} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static JsonWriter createWriter(Writer json, JsonOptions options, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultJsonWriter.toWriter(json, options); + } else { + throw new IllegalStateException(CANNOT_FIND_JSON); + } + } else { + return defaultProvider.createWriter(json, options); + } + } +} diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/JsonReader.java b/sdk/core/azure-json/src/main/java/com/azure/json/JsonReader.java index 04b6c0adcb9c8..7edf214626f0e 100644 --- a/sdk/core/azure-json/src/main/java/com/azure/json/JsonReader.java +++ b/sdk/core/azure-json/src/main/java/com/azure/json/JsonReader.java @@ -491,7 +491,14 @@ private Object readUntypedHelper(int depth) { return getBoolean(); } else if (token == JsonToken.NUMBER) { String numberText = getText(); - if (numberText.contains(".")) { + + if ("INF".equals(numberText) || "Infinity".equals(numberText) + || "-INF".equals(numberText) || "-Infinity".equals(numberText) + || "NaN".equals(numberText)) { + // Return special Double values as text as not all implementations of JsonReader may be able to handle + // them as Doubles when parsing generically. + return numberText; + } else if (numberText.contains(".")) { // Unlike integers always use Double to prevent floating point rounding issues. return Double.parseDouble(numberText); } else { diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/JsonToken.java b/sdk/core/azure-json/src/main/java/com/azure/json/JsonToken.java index f87889278f47e..f33f3594647f0 100644 --- a/sdk/core/azure-json/src/main/java/com/azure/json/JsonToken.java +++ b/sdk/core/azure-json/src/main/java/com/azure/json/JsonToken.java @@ -50,5 +50,10 @@ public enum JsonToken { /** * String, in value context. */ - STRING + STRING, + + /** + * JSON document has completed. + */ + END_DOCUMENT } diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/DefaultJsonReader.java b/sdk/core/azure-json/src/main/java/com/azure/json/implementation/DefaultJsonReader.java similarity index 62% rename from sdk/core/azure-json/src/main/java/com/azure/json/DefaultJsonReader.java rename to sdk/core/azure-json/src/main/java/com/azure/json/implementation/DefaultJsonReader.java index f62e87e4aec36..dcc197af19dbc 100644 --- a/sdk/core/azure-json/src/main/java/com/azure/json/DefaultJsonReader.java +++ b/sdk/core/azure-json/src/main/java/com/azure/json/implementation/DefaultJsonReader.java @@ -1,13 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.json; +package com.azure.json.implementation; +import com.azure.json.JsonOptions; +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; import com.azure.json.implementation.jackson.core.JsonFactory; import com.azure.json.implementation.jackson.core.JsonParser; +import com.azure.json.implementation.jackson.core.json.JsonReadFeature; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.io.UncheckedIOException; /** @@ -20,18 +26,22 @@ public final class DefaultJsonReader extends JsonReader { private final byte[] jsonBytes; private final String jsonString; private final boolean resetSupported; + private final boolean nonNumericNumbersSupported; + + private JsonToken currentToken; /** * Constructs an instance of {@link DefaultJsonReader} from a {@code byte[]}. * * @param json JSON {@code byte[]}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonReader}. * @return An instance of {@link DefaultJsonReader}. * @throws UncheckedIOException If a {@link DefaultJsonReader} wasn't able to be constructed from the JSON * {@code byte[]}. */ - public static JsonReader fromBytes(byte[] json) { + public static JsonReader fromBytes(byte[] json, JsonOptions options) { try { - return new DefaultJsonReader(FACTORY.createParser(json), true, json, null); + return new DefaultJsonReader(FACTORY.createParser(json), true, json, null, options); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -41,12 +51,13 @@ public static JsonReader fromBytes(byte[] json) { * Constructs an instance of {@link DefaultJsonReader} from a String. * * @param json JSON String. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. * @return An instance of {@link DefaultJsonReader}. * @throws UncheckedIOException If a {@link DefaultJsonReader} wasn't able to be constructed from the JSON String. */ - public static JsonReader fromString(String json) { + public static JsonReader fromString(String json, JsonOptions options) { try { - return new DefaultJsonReader(FACTORY.createParser(json), true, null, json); + return new DefaultJsonReader(FACTORY.createParser(json), true, null, json, options); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -56,34 +67,61 @@ public static JsonReader fromString(String json) { * Constructs an instance of {@link DefaultJsonReader} from an {@link InputStream}. * * @param json JSON {@link InputStream}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. * @return An instance of {@link DefaultJsonReader}. * @throws UncheckedIOException If a {@link DefaultJsonReader} wasn't able to be constructed from the JSON * {@link InputStream}. */ - public static JsonReader fromStream(InputStream json) { + public static JsonReader fromStream(InputStream json, JsonOptions options) { + try { + return new DefaultJsonReader(FACTORY.createParser(json), json.markSupported(), null, null, options); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Constructs an instance of {@link DefaultJsonReader} from a {@link Reader}. + * + * @param reader JSON {@link Reader}. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @return An instance of {@link DefaultJsonReader}. + * @throws UncheckedIOException If a {@link DefaultJsonReader} wasn't able to be constructed from the JSON + * {@link Reader}. + */ + public static JsonReader fromReader(Reader reader, JsonOptions options) { try { - return new DefaultJsonReader(FACTORY.createParser(json), true, null, null); + return new DefaultJsonReader(FACTORY.createParser(reader), reader.markSupported(), null, null, options); } catch (IOException e) { throw new UncheckedIOException(e); } } - private DefaultJsonReader(JsonParser parser, boolean resetSupported, byte[] jsonBytes, String jsonString) { + private DefaultJsonReader(JsonParser parser, boolean resetSupported, byte[] jsonBytes, String jsonString, + JsonOptions options) { + this(parser, resetSupported, jsonBytes, jsonString, options.isNonNumericNumbersSupported()); + } + + private DefaultJsonReader(JsonParser parser, boolean resetSupported, byte[] jsonBytes, String jsonString, + boolean nonNumericNumbersSupported) { this.parser = parser; + this.parser.configure(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS.mappedFeature(), nonNumericNumbersSupported); this.resetSupported = resetSupported; this.jsonBytes = jsonBytes; this.jsonString = jsonString; + this.nonNumericNumbersSupported = nonNumericNumbersSupported; } @Override public JsonToken currentToken() { - return mapToken(parser.currentToken()); + return currentToken; } @Override public JsonToken nextToken() { try { - return mapToken(parser.nextToken()); + currentToken = mapToken(parser.nextToken(), currentToken); + return currentToken; } catch (IOException e) { throw new UncheckedIOException(e); } @@ -181,7 +219,12 @@ public JsonReader bufferObject() { || (currentToken == JsonToken.FIELD_NAME && nextToken() == JsonToken.START_OBJECT)) { StringBuilder bufferedObject = new StringBuilder(); readChildren(bufferedObject); - return DefaultJsonReader.fromString(bufferedObject.toString()); + String json = bufferedObject.toString(); + try { + return new DefaultJsonReader(FACTORY.createParser(json), true, null, json, nonNumericNumbersSupported); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } } else { throw new IllegalStateException("Cannot buffer a JSON object from a non-object, non-field name " + "starting location. Starting location: " + currentToken()); @@ -199,10 +242,16 @@ public JsonReader reset() { throw new IllegalStateException("'reset' isn't supported by this JsonReader."); } - if (jsonBytes != null) { - return DefaultJsonReader.fromBytes(jsonBytes); - } else { - return DefaultJsonReader.fromString(jsonString); + try { + if (jsonBytes != null) { + return new DefaultJsonReader(FACTORY.createParser(jsonBytes), true, jsonBytes, null, + nonNumericNumbersSupported); + } else { + return new DefaultJsonReader(FACTORY.createParser(jsonString), true, null, jsonString, + nonNumericNumbersSupported); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); } } @@ -217,30 +266,24 @@ public void close() throws IOException { * azure-json doesn't support the EMBEDDED_OBJECT or NOT_AVAILABLE Jackson Core JsonTokens, but those should only * be returned by specialty implementations that aren't used. */ - private static JsonToken mapToken(com.azure.json.implementation.jackson.core.JsonToken token) { + private static JsonToken mapToken(com.azure.json.implementation.jackson.core.JsonToken nextToken, + JsonToken currentToken) { // Special case for when currentToken is called after instantiating the JsonReader. - if (token == null) { + if (nextToken == null && currentToken == null) { return null; + } else if (nextToken == null) { + return JsonToken.END_DOCUMENT; } - switch (token) { - case START_OBJECT: - return JsonToken.START_OBJECT; - - case END_OBJECT: - return JsonToken.END_OBJECT; - - case START_ARRAY: - return JsonToken.START_ARRAY; - - case END_ARRAY: - return JsonToken.END_ARRAY; + switch (nextToken) { + case START_OBJECT: return JsonToken.START_OBJECT; + case END_OBJECT: return JsonToken.END_OBJECT; - case FIELD_NAME: - return JsonToken.FIELD_NAME; + case START_ARRAY: return JsonToken.START_ARRAY; + case END_ARRAY: return JsonToken.END_ARRAY; - case VALUE_STRING: - return JsonToken.STRING; + case FIELD_NAME: return JsonToken.FIELD_NAME; + case VALUE_STRING: return JsonToken.STRING; case VALUE_NUMBER_INT: case VALUE_NUMBER_FLOAT: @@ -250,11 +293,10 @@ private static JsonToken mapToken(com.azure.json.implementation.jackson.core.Jso case VALUE_FALSE: return JsonToken.BOOLEAN; - case VALUE_NULL: - return JsonToken.NULL; + case VALUE_NULL: return JsonToken.NULL; default: - throw new IllegalStateException("Unsupported token type: '" + token + "'."); + throw new IllegalStateException("Unsupported token type: '" + nextToken + "'."); } } } diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/DefaultJsonWriter.java b/sdk/core/azure-json/src/main/java/com/azure/json/implementation/DefaultJsonWriter.java similarity index 82% rename from sdk/core/azure-json/src/main/java/com/azure/json/DefaultJsonWriter.java rename to sdk/core/azure-json/src/main/java/com/azure/json/implementation/DefaultJsonWriter.java index e45830914ae2a..5ce38df8786a0 100644 --- a/sdk/core/azure-json/src/main/java/com/azure/json/DefaultJsonWriter.java +++ b/sdk/core/azure-json/src/main/java/com/azure/json/implementation/DefaultJsonWriter.java @@ -1,14 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.json; +package com.azure.json.implementation; +import com.azure.json.JsonOptions; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriteContext; +import com.azure.json.JsonWriter; import com.azure.json.implementation.jackson.core.JsonFactory; import com.azure.json.implementation.jackson.core.JsonGenerator; +import com.azure.json.implementation.jackson.core.json.JsonWriteFeature; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; +import java.io.Writer; import java.util.Objects; /** @@ -29,20 +35,43 @@ public final class DefaultJsonWriter extends JsonWriter { * isn't the owner of the stream. * * @param stream The {@link OutputStream} that will be written. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. * @return An instance of {@link DefaultJsonWriter}. * @throws UncheckedIOException If a {@link DefaultJsonWriter} wasn't able to be constructed from the * {@link OutputStream}. */ - public static JsonWriter toStream(OutputStream stream) { + public static JsonWriter toStream(OutputStream stream, JsonOptions options) { try { - return new DefaultJsonWriter(FACTORY.createGenerator(stream)); + return new DefaultJsonWriter(FACTORY.createGenerator(stream), options); } catch (IOException e) { throw new UncheckedIOException(e); } } - private DefaultJsonWriter(JsonGenerator generator) { + /** + * Creates a {@link DefaultJsonWriter} that writes the given {@link Writer}. + *

+ * The passed {@link Writer} won't be closed when {@link #close()} is called as the {@link DefaultJsonWriter} + * isn't the owner of the stream. + * + * @param writer The {@link Writer} that will be written. + * @param options {@link JsonOptions} to configure the creation of the {@link JsonWriter}. + * @return An instance of {@link DefaultJsonWriter}. + * @throws UncheckedIOException If a {@link DefaultJsonWriter} wasn't able to be constructed from the + * {@link Writer}. + */ + public static JsonWriter toWriter(Writer writer, JsonOptions options) { + try { + return new DefaultJsonWriter(FACTORY.createGenerator(writer), options); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private DefaultJsonWriter(JsonGenerator generator, JsonOptions options) { this.generator = generator; + this.generator.configure(JsonWriteFeature.WRITE_NAN_AS_STRINGS.mappedFeature(), + options.isNonNumericNumbersSupported()); } @Override diff --git a/sdk/core/azure-json/src/main/java/com/azure/json/implementation/package-info.java b/sdk/core/azure-json/src/main/java/com/azure/json/implementation/package-info.java new file mode 100644 index 0000000000000..46016bb266715 --- /dev/null +++ b/sdk/core/azure-json/src/main/java/com/azure/json/implementation/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Contains general implementation classes for handling JSON. + */ +package com.azure.json.implementation; diff --git a/sdk/core/azure-json/src/main/java/module-info.java b/sdk/core/azure-json/src/main/java/module-info.java index 614f137bfebec..2ee0b1b8bac4e 100644 --- a/sdk/core/azure-json/src/main/java/module-info.java +++ b/sdk/core/azure-json/src/main/java/module-info.java @@ -3,4 +3,7 @@ module com.azure.json { exports com.azure.json; + exports com.azure.json.implementation; + + uses com.azure.json.JsonProvider; } diff --git a/sdk/core/azure-json/src/test/java/com/azure/json/contract/JsonReaderContractTests.java b/sdk/core/azure-json/src/test/java/com/azure/json/contract/JsonReaderContractTests.java index 035a05cbf315d..61b968d59e6d3 100644 --- a/sdk/core/azure-json/src/test/java/com/azure/json/contract/JsonReaderContractTests.java +++ b/sdk/core/azure-json/src/test/java/com/azure/json/contract/JsonReaderContractTests.java @@ -13,14 +13,21 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -46,13 +53,14 @@ public abstract class JsonReaderContractTests { @ParameterizedTest @MethodSource("basicOperationsSupplier") - public void basicOperations(String json, T expectedValue, Function function) { - JsonReader reader = getJsonReader(json); - reader.nextToken(); // Initialize the JsonReader for reading. + public void basicOperations(String json, T expectedValue, Function function) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + reader.nextToken(); // Initialize the JsonReader for reading. - T actualValue = assertDoesNotThrow(() -> function.apply(reader)); + T actualValue = assertDoesNotThrow(() -> function.apply(reader)); - assertEquals(expectedValue, actualValue); + assertEquals(expectedValue, actualValue); + } } private static Stream basicOperationsSupplier() { @@ -98,13 +106,14 @@ private static Stream basicOperationsSupplier() { // Byte arrays can't use Object.equals as they'll be compared by memory location instead of value equality. @ParameterizedTest @MethodSource("binaryOperationsSupplier") - public void binaryOperations(String json, byte[] expectedValue, Function function) { - JsonReader reader = getJsonReader(json); - reader.nextToken(); // Initialize the JsonReader for reading. + public void binaryOperations(String json, byte[] expectedValue, Function function) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + reader.nextToken(); // Initialize the JsonReader for reading. - byte[] actualValue = assertDoesNotThrow(() -> function.apply(reader)); + byte[] actualValue = assertDoesNotThrow(() -> function.apply(reader)); - assertArrayEquals(expectedValue, actualValue); + assertArrayEquals(expectedValue, actualValue); + } } private static Stream binaryOperationsSupplier() { @@ -118,137 +127,142 @@ private static Stream binaryOperationsSupplier() { } @Test - public void emptyObject() { + public void emptyObject() throws IOException { String json = "{}"; - JsonReader reader = getJsonReader(json); + try (JsonReader reader = getJsonReader(json)) { - assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); + assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); - while (reader.nextToken() != JsonToken.END_OBJECT) { - fail("Empty object shouldn't have any non-END_OBJECT JsonTokens but found: " + reader.currentToken()); + while (reader.nextToken() != JsonToken.END_OBJECT) { + fail("Empty object shouldn't have any non-END_OBJECT JsonTokens but found: " + reader.currentToken()); + } } } @Test - public void emptyArray() { + public void emptyArray() throws IOException { String json = "[]"; - JsonReader reader = getJsonReader(json); + try (JsonReader reader = getJsonReader(json)) { - assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); + assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); - while (reader.nextToken() != JsonToken.END_ARRAY) { - fail("Empty array shouldn't have any non-END_ARRAY JsonTokens but found: " + reader.currentToken()); + while (reader.nextToken() != JsonToken.END_ARRAY) { + fail("Empty array shouldn't have any non-END_ARRAY JsonTokens but found: " + reader.currentToken()); + } } } @Test - public void simpleObject() { + public void simpleObject() throws IOException { String json = "{\"stringProperty\":\"string\",\"nullProperty\":null,\"integerProperty\":10,\"floatProperty\":10.0,\"booleanProperty\":true}"; - JsonReader reader = getJsonReader(json); - - assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); - - String stringProperty = null; - boolean hasNullProperty = false; - int integerProperty = 0; - float floatProperty = 0.0F; - boolean booleanProperty = false; - while (reader.nextToken() != JsonToken.END_OBJECT) { - String fieldName = reader.getFieldName(); - reader.nextToken(); - - if ("stringProperty".equals(fieldName)) { - stringProperty = reader.getString(); - } else if ("nullProperty".equals(fieldName)) { - hasNullProperty = true; - } else if ("integerProperty".equals(fieldName)) { - integerProperty = reader.getInt(); - } else if ("floatProperty".equals(fieldName)) { - floatProperty = reader.getFloat(); - } else if ("booleanProperty".equals(fieldName)) { - booleanProperty = reader.getBoolean(); - } else { - fail("Unknown property name: '" + fieldName + "'"); + try (JsonReader reader = getJsonReader(json)) { + + assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); + + String stringProperty = null; + boolean hasNullProperty = false; + int integerProperty = 0; + float floatProperty = 0.0F; + boolean booleanProperty = false; + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("stringProperty".equals(fieldName)) { + stringProperty = reader.getString(); + } else if ("nullProperty".equals(fieldName)) { + hasNullProperty = true; + } else if ("integerProperty".equals(fieldName)) { + integerProperty = reader.getInt(); + } else if ("floatProperty".equals(fieldName)) { + floatProperty = reader.getFloat(); + } else if ("booleanProperty".equals(fieldName)) { + booleanProperty = reader.getBoolean(); + } else { + fail("Unknown property name: '" + fieldName + "'"); + } } - } - assertEquals("string", stringProperty); - assertTrue(hasNullProperty, "Didn't find the expected 'nullProperty'."); - assertEquals(10, integerProperty); - assertEquals(10.0F, floatProperty); - assertEquals(true, booleanProperty); + assertEquals("string", stringProperty); + assertTrue(hasNullProperty, "Didn't find the expected 'nullProperty'."); + assertEquals(10, integerProperty); + assertEquals(10.0F, floatProperty); + assertEquals(true, booleanProperty); + } } @Test - public void arrayOfBasicTypesInJsonRoot() { + public void arrayOfBasicTypesInJsonRoot() throws IOException { String json = "[\"string\",null,10,10.0,true]"; - JsonReader reader = getJsonReader(json); + try (JsonReader reader = getJsonReader(json)) { - assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); + assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); - Object[] jsonArray = new Object[5]; - int jsonArrayIndex = 0; - while (reader.nextToken() != JsonToken.END_ARRAY) { - jsonArray[jsonArrayIndex++] = ContractUtils.readUntypedField(reader); - } + Object[] jsonArray = new Object[5]; + int jsonArrayIndex = 0; + while (reader.nextToken() != JsonToken.END_ARRAY) { + jsonArray[jsonArrayIndex++] = ContractUtils.readUntypedField(reader); + } - assertEquals("string", jsonArray[0]); - assertNull(jsonArray[1]); - assertEquals(10, jsonArray[2]); - assertEquals(10.0F, jsonArray[3]); - assertEquals(true, jsonArray[4]); + assertEquals("string", jsonArray[0]); + assertNull(jsonArray[1]); + assertEquals(10, jsonArray[2]); + assertEquals(10.0F, jsonArray[3]); + assertEquals(true, jsonArray[4]); + } } @ParameterizedTest @MethodSource("objectWithInnerObjectSupplier") - public void objectWithInnerObject(String json) { - JsonReader reader = getJsonReader(json); - - assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); - - String stringProperty = null; - boolean hasNullProperty = false; - int integerProperty = 0; - float floatProperty = 0.0F; - boolean booleanProperty = false; - String innerStringProperty = null; - while (reader.nextToken() != JsonToken.END_OBJECT) { - String fieldName = reader.getFieldName(); - reader.nextToken(); - - if ("stringProperty".equals(fieldName)) { - stringProperty = reader.getString(); - } else if ("nullProperty".equals(fieldName)) { - hasNullProperty = true; - } else if ("integerProperty".equals(fieldName)) { - integerProperty = reader.getInt(); - } else if ("floatProperty".equals(fieldName)) { - floatProperty = reader.getFloat(); - } else if ("booleanProperty".equals(fieldName)) { - booleanProperty = reader.getBoolean(); - } else if ("innerObject".equals(fieldName)) { - assertEquals(JsonToken.START_OBJECT, reader.currentToken()); - while (reader.nextToken() != JsonToken.END_OBJECT) { - fieldName = reader.getFieldName(); - reader.nextToken(); - - if ("innerStringProperty".equals(fieldName)) { - innerStringProperty = reader.getString(); - } else { - fail("Unknown property name: '" + fieldName + "'"); + public void objectWithInnerObject(String json) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + + assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); + + String stringProperty = null; + boolean hasNullProperty = false; + int integerProperty = 0; + float floatProperty = 0.0F; + boolean booleanProperty = false; + String innerStringProperty = null; + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("stringProperty".equals(fieldName)) { + stringProperty = reader.getString(); + } else if ("nullProperty".equals(fieldName)) { + hasNullProperty = true; + } else if ("integerProperty".equals(fieldName)) { + integerProperty = reader.getInt(); + } else if ("floatProperty".equals(fieldName)) { + floatProperty = reader.getFloat(); + } else if ("booleanProperty".equals(fieldName)) { + booleanProperty = reader.getBoolean(); + } else if ("innerObject".equals(fieldName)) { + assertEquals(JsonToken.START_OBJECT, reader.currentToken()); + while (reader.nextToken() != JsonToken.END_OBJECT) { + fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("innerStringProperty".equals(fieldName)) { + innerStringProperty = reader.getString(); + } else { + fail("Unknown property name: '" + fieldName + "'"); + } } + } else { + fail("Unknown property name: '" + fieldName + "'"); } - } else { - fail("Unknown property name: '" + fieldName + "'"); } - } - assertEquals("string", stringProperty); - assertTrue(hasNullProperty, "Didn't find the expected 'nullProperty'."); - assertEquals(10, integerProperty); - assertEquals(10.0F, floatProperty); - assertEquals(true, booleanProperty); - assertEquals("innerString", innerStringProperty); + assertEquals("string", stringProperty); + assertTrue(hasNullProperty, "Didn't find the expected 'nullProperty'."); + assertEquals(10, integerProperty); + assertEquals(10.0F, floatProperty); + assertEquals(true, booleanProperty); + assertEquals("innerString", innerStringProperty); + } } private static Stream objectWithInnerObjectSupplier() { @@ -270,50 +284,51 @@ private static Stream objectWithInnerObjectSupplier() { @ParameterizedTest @MethodSource("objectWithInnerArraySupplier") - public void objectWithInnerArray(String json) { - JsonReader reader = getJsonReader(json); - - assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); - - String stringProperty = null; - boolean hasNullProperty = false; - int integerProperty = 0; - float floatProperty = 0.0F; - boolean booleanProperty = false; - String innerStringProperty = null; - while (reader.nextToken() != JsonToken.END_OBJECT) { - String fieldName = reader.getFieldName(); - reader.nextToken(); - - if ("stringProperty".equals(fieldName)) { - stringProperty = reader.getString(); - } else if ("nullProperty".equals(fieldName)) { - hasNullProperty = true; - } else if ("integerProperty".equals(fieldName)) { - integerProperty = reader.getInt(); - } else if ("floatProperty".equals(fieldName)) { - floatProperty = reader.getFloat(); - } else if ("booleanProperty".equals(fieldName)) { - booleanProperty = reader.getBoolean(); - } else if ("innerArray".equals(fieldName)) { - assertEquals(JsonToken.START_ARRAY, reader.currentToken()); - while (reader.nextToken() != JsonToken.END_ARRAY) { - if (innerStringProperty != null) { - fail("Only expected one value in the inner array but found more."); + public void objectWithInnerArray(String json) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + + assertJsonReaderStructInitialization(reader, JsonToken.START_OBJECT); + + String stringProperty = null; + boolean hasNullProperty = false; + int integerProperty = 0; + float floatProperty = 0.0F; + boolean booleanProperty = false; + String innerStringProperty = null; + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("stringProperty".equals(fieldName)) { + stringProperty = reader.getString(); + } else if ("nullProperty".equals(fieldName)) { + hasNullProperty = true; + } else if ("integerProperty".equals(fieldName)) { + integerProperty = reader.getInt(); + } else if ("floatProperty".equals(fieldName)) { + floatProperty = reader.getFloat(); + } else if ("booleanProperty".equals(fieldName)) { + booleanProperty = reader.getBoolean(); + } else if ("innerArray".equals(fieldName)) { + assertEquals(JsonToken.START_ARRAY, reader.currentToken()); + while (reader.nextToken() != JsonToken.END_ARRAY) { + if (innerStringProperty != null) { + fail("Only expected one value in the inner array but found more."); + } + innerStringProperty = reader.getString(); } - innerStringProperty = reader.getString(); + } else { + fail("Unknown property name: '" + fieldName + "'"); } - } else { - fail("Unknown property name: '" + fieldName + "'"); } - } - assertEquals("string", stringProperty); - assertTrue(hasNullProperty, "Didn't find the expected 'nullProperty'."); - assertEquals(10, integerProperty); - assertEquals(10.0F, floatProperty); - assertEquals(true, booleanProperty); - assertEquals("innerString", innerStringProperty); + assertEquals("string", stringProperty); + assertTrue(hasNullProperty, "Didn't find the expected 'nullProperty'."); + assertEquals(10, integerProperty); + assertEquals(10.0F, floatProperty); + assertEquals(true, booleanProperty); + assertEquals("innerString", innerStringProperty); + } } private static Stream objectWithInnerArraySupplier() { @@ -334,33 +349,34 @@ private static Stream objectWithInnerArraySupplier() { @ParameterizedTest @MethodSource("arrayWithInnerArraySupplier") - public void arrayWithInnerArray(String json) { - JsonReader reader = getJsonReader(json); + public void arrayWithInnerArray(String json) throws IOException { + try (JsonReader reader = getJsonReader(json)) { - assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); + assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); - Object[] jsonArray = new Object[6]; - int jsonArrayIndex = 0; - while (reader.nextToken() != JsonToken.END_ARRAY) { - if (reader.currentToken() == JsonToken.START_ARRAY) { - while (reader.nextToken() != JsonToken.END_ARRAY) { - if (jsonArray[5] != null) { - fail("Only expected one value in the inner array but found more."); - } + Object[] jsonArray = new Object[6]; + int jsonArrayIndex = 0; + while (reader.nextToken() != JsonToken.END_ARRAY) { + if (reader.currentToken() == JsonToken.START_ARRAY) { + while (reader.nextToken() != JsonToken.END_ARRAY) { + if (jsonArray[5] != null) { + fail("Only expected one value in the inner array but found more."); + } - jsonArray[5] = reader.getString(); + jsonArray[5] = reader.getString(); + } + } else { + jsonArray[jsonArrayIndex++] = ContractUtils.readUntypedField(reader); } - } else { - jsonArray[jsonArrayIndex++] = ContractUtils.readUntypedField(reader); } - } - assertEquals("string", jsonArray[0]); - assertNull(jsonArray[1]); - assertEquals(10, jsonArray[2]); - assertEquals(10.0F, jsonArray[3]); - assertEquals(true, jsonArray[4]); - assertEquals("innerString", jsonArray[5]); + assertEquals("string", jsonArray[0]); + assertNull(jsonArray[1]); + assertEquals(10, jsonArray[2]); + assertEquals(10.0F, jsonArray[3]); + assertEquals(true, jsonArray[4]); + assertEquals("innerString", jsonArray[5]); + } } private static Stream arrayWithInnerArraySupplier() { @@ -378,36 +394,37 @@ private static Stream arrayWithInnerArraySupplier() { @ParameterizedTest @MethodSource("arrayWithInnerObjectSupplier") - public void arrayWithInnerObject(String json) { - JsonReader reader = getJsonReader(json); - - assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); - - Object[] jsonArray = new Object[6]; - int jsonArrayIndex = 0; - while (reader.nextToken() != JsonToken.END_ARRAY) { - if (reader.currentToken() == JsonToken.START_OBJECT) { - while (reader.nextToken() != JsonToken.END_OBJECT) { - String fieldName = reader.getFieldName(); - reader.nextToken(); - - if ("innerStringProperty".equals(fieldName)) { - jsonArray[5] = reader.getString(); - } else { - fail("Unknown property name: '" + fieldName + "'"); + public void arrayWithInnerObject(String json) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + + assertJsonReaderStructInitialization(reader, JsonToken.START_ARRAY); + + Object[] jsonArray = new Object[6]; + int jsonArrayIndex = 0; + while (reader.nextToken() != JsonToken.END_ARRAY) { + if (reader.currentToken() == JsonToken.START_OBJECT) { + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("innerStringProperty".equals(fieldName)) { + jsonArray[5] = reader.getString(); + } else { + fail("Unknown property name: '" + fieldName + "'"); + } } + } else { + jsonArray[jsonArrayIndex++] = ContractUtils.readUntypedField(reader); } - } else { - jsonArray[jsonArrayIndex++] = ContractUtils.readUntypedField(reader); } - } - assertEquals("string", jsonArray[0]); - assertNull(jsonArray[1]); - assertEquals(10, jsonArray[2]); - assertEquals(10.0F, jsonArray[3]); - assertEquals(true, jsonArray[4]); - assertEquals("innerString", jsonArray[5]); + assertEquals("string", jsonArray[0]); + assertNull(jsonArray[1]); + assertEquals(10, jsonArray[2]); + assertEquals(10.0F, jsonArray[3]); + assertEquals(true, jsonArray[4]); + assertEquals("innerString", jsonArray[5]); + } } private static Stream arrayWithInnerObjectSupplier() { @@ -423,6 +440,129 @@ private static Stream arrayWithInnerObjectSupplier() { ); } + @ParameterizedTest + @MethodSource("readUntypedSimpleSupplier") + public void readUntypedSimple(String json, int nextCount, Object expected) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + for (int i = 0; i < nextCount; i++) { + reader.nextToken(); + } + + Object actual = reader.readUntyped(); + assertEquals(expected, actual); + } + + } + + private static Stream readUntypedSimpleSupplier() { + return Stream.of( + Arguments.of("null", 1, null), + Arguments.of("true", 1, true), + Arguments.of("false", 1, false), + Arguments.of("3.14", 1, 3.14), + Arguments.of("NaN", 1, String.valueOf(Double.NaN)), + Arguments.of("-Infinity", 1, String.valueOf(Double.NEGATIVE_INFINITY)), + Arguments.of("Infinity", 1, String.valueOf(Double.POSITIVE_INFINITY)), + Arguments.of("42", 1, 42), + Arguments.of("420000000000", 1, 420000000000L), + Arguments.of("\"hello\"", 1, "hello") + ); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @MethodSource("readUntypedArraySupplier") + public void readUntypedArray(String json, int nextCount, List expected) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + for (int i = 0; i < nextCount; i++) { + reader.nextToken(); + } + + List actual = (List) reader.readUntyped(); + assertIterableEquals(expected, actual); + } + + } + + private static Stream readUntypedArraySupplier() { + return Stream.of( + Arguments.of("[]", 1, new ArrayList<>()), + Arguments.of("[42,true,\"hello\"]", 1, Arrays.asList(42, true, "hello")) + ); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @MethodSource("readUntypedObjectSupplier") + public void readUntypedObject(String json, int nextCount, Map expected) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + for (int i = 0; i < nextCount; i++) { + reader.nextToken(); + } + + Map actual = (Map) reader.readUntyped(); + assertEquals(expected.size(), actual.size()); + for (Map.Entry expectedKvp : expected.entrySet()) { + assertTrue(actual.containsKey(expectedKvp.getKey())); + assertEquals(expectedKvp.getValue(), actual.get(expectedKvp.getKey())); + } + } + } + + private static Stream readUntypedObjectSupplier() { + Map complexExpected = new LinkedHashMap<>(); + complexExpected.put("field1", 42); + complexExpected.put("field2", true); + complexExpected.put("field3", "hello"); + + return Stream.of( + Arguments.of("{}", 1, new LinkedHashMap<>()), + Arguments.of("{\"field1\":42,\"field2\":true,\"field3\":\"hello\"}", 1, complexExpected) + ); + } + + @ParameterizedTest + @MethodSource("readUntypedIllegalStartSupplier") + public void readUntypedIllegalStart(String json, int nextCount) throws IOException { + try (JsonReader reader = getJsonReader(json)) { + for (int i = 0; i < nextCount; i++) { + reader.nextToken(); + } + + assertThrows(IllegalStateException.class, reader::readUntyped); + } + } + + private static Stream readUntypedIllegalStartSupplier() { + return Stream.of( + Arguments.of("{}", 2), + Arguments.of("[]", 2), + Arguments.of("{\"field\":\"value\"}", 2) + ); + } + + @Test + public void readUntypedPreventsStackOverflow() throws IOException { + // At 1000 levels of nesting readUntyped will throw an exception. + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 1001; i++) { + builder.append("{\"field\":"); + } + + builder.append("null"); + + for (int i = 0; i < 1001; i++) { + builder.append('}'); + } + + String deeplyNestJson = builder.toString(); + + try (JsonReader reader = getJsonReader(deeplyNestJson)) { + reader.nextToken(); + assertThrows(IllegalStateException.class, reader::readUntyped); + } + } + @ParameterizedTest @MethodSource("bufferObjectSupplier") public void bufferObject(String json, int nextCount) { diff --git a/sdk/core/azure-json/src/test/java/com/azure/json/DefaultJsonReaderContractTests.java b/sdk/core/azure-json/src/test/java/com/azure/json/implementation/DefaultJsonReaderContractTests.java similarity index 69% rename from sdk/core/azure-json/src/test/java/com/azure/json/DefaultJsonReaderContractTests.java rename to sdk/core/azure-json/src/test/java/com/azure/json/implementation/DefaultJsonReaderContractTests.java index 7a957a2cc1301..171ac70a956a2 100644 --- a/sdk/core/azure-json/src/test/java/com/azure/json/DefaultJsonReaderContractTests.java +++ b/sdk/core/azure-json/src/test/java/com/azure/json/implementation/DefaultJsonReaderContractTests.java @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.json; +package com.azure.json.implementation; +import com.azure.json.JsonOptions; +import com.azure.json.JsonReader; import com.azure.json.contract.JsonReaderContractTests; /** @@ -11,6 +13,6 @@ public class DefaultJsonReaderContractTests extends JsonReaderContractTests { @Override public JsonReader getJsonReader(String json) { - return DefaultJsonReader.fromString(json); + return DefaultJsonReader.fromString(json, new JsonOptions()); } } diff --git a/sdk/core/azure-json/src/test/java/com/azure/json/DefaultJsonWriterContractTests.java b/sdk/core/azure-json/src/test/java/com/azure/json/implementation/DefaultJsonWriterContractTests.java similarity index 84% rename from sdk/core/azure-json/src/test/java/com/azure/json/DefaultJsonWriterContractTests.java rename to sdk/core/azure-json/src/test/java/com/azure/json/implementation/DefaultJsonWriterContractTests.java index 1c69d7f352a63..c1117545c5f69 100644 --- a/sdk/core/azure-json/src/test/java/com/azure/json/DefaultJsonWriterContractTests.java +++ b/sdk/core/azure-json/src/test/java/com/azure/json/implementation/DefaultJsonWriterContractTests.java @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.json; +package com.azure.json.implementation; +import com.azure.json.JsonOptions; +import com.azure.json.JsonWriter; import com.azure.json.contract.JsonWriterContractTests; import org.junit.jupiter.api.BeforeEach; @@ -20,7 +22,7 @@ public class DefaultJsonWriterContractTests extends JsonWriterContractTests { @BeforeEach public void beforeEach() { this.outputStream = new ByteArrayOutputStream(); - this.writer = DefaultJsonWriter.toStream(outputStream); + this.writer = DefaultJsonWriter.toStream(outputStream, new JsonOptions()); } @Override diff --git a/sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProvider.java b/sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProvider.java new file mode 100644 index 0000000000000..79c40e12b6f61 --- /dev/null +++ b/sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProvider.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.xml; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/** + * An interface to be implemented by any azure-xml plugin that wishes to provide an alternate {@link XmlReader} or + * {@link XmlWriter} implementation. + */ +public interface XmlProvider { + /** + * Creates an instance of {@link XmlReader} that reads a {@code byte[]}. + * + * @param json The JSON represented as a {@code byte[]}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code json} is null. + */ + XmlReader createReader(byte[] json); + + /** + * Creates an instance of {@link XmlReader} that reads a {@link String}. + * + * @param json The JSON represented as a {@link String}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code json} is null. + */ + XmlReader createReader(String json); + + /** + * Creates an instance of {@link XmlReader} that reads a {@link InputStream}. + * + * @param json The JSON represented as a {@link InputStream}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code json} is null. + */ + XmlReader createReader(InputStream json); + + /** + * Creates an instance of {@link XmlReader} that reads a {@link Reader}. + * + * @param json The JSON represented as a {@link Reader}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code json} is null. + */ + XmlReader createReader(Reader json); + + /** + * Creates an instance of {@link XmlWriter} that writes to an {@link OutputStream}. + * + * @param json The JSON represented as an {@link OutputStream}. + * @return A new instance of {@link XmlWriter}. + * @throws NullPointerException If {@code json} is null. + */ + XmlWriter createWriter(OutputStream json); + + /** + * Creates an instance of {@link XmlWriter} that writes to an {@link Writer}. + * + * @param json The JSON represented as an {@link Writer}. + * @return A new instance of {@link XmlWriter}. + * @throws NullPointerException If {@code json} is null. + */ + XmlWriter createWriter(Writer json); +} diff --git a/sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProviders.java b/sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProviders.java new file mode 100644 index 0000000000000..243be901851f0 --- /dev/null +++ b/sdk/core/azure-xml/src/main/java/com/azure/xml/XmlProviders.java @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.xml; + +import com.azure.xml.implementation.DefaultXmlReader; +import com.azure.xml.implementation.DefaultXmlWriter; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Handles loading an instance of {@link XmlProvider} found on the classpath. + */ +public final class XmlProviders { + private static final String CANNOT_FIND_XML = "A request was made to load an XmlReader and XmlWriter provider " + + "but one could not be found on the classpath. If you are using a dependency manager, consider including a " + + "dependency that supplies a provider for XmlProvider or indicate to the loader to fallback to the default " + + "implementation. Additionally, refer to https://aka.ms/azsdk/java/docs/custom-xml to learn about writing " + + "your own implementation."; + + private static XmlProvider defaultProvider; + + static { + // Use as classloader to load provider-configuration files and provider classes the classloader + // that loaded this class. In most cases this will be the System classloader. + // But this choice here provides additional flexibility in managed environments that control + // classloading differently (OSGi, Spring and others) and don't/ depend on the + // System classloader to load HttpClientProvider classes. + ServiceLoader serviceLoader = ServiceLoader.load(XmlProvider.class, + XmlProvider.class.getClassLoader()); + // Use the first provider found in the service loader iterator. + Iterator it = serviceLoader.iterator(); + if (it.hasNext()) { + defaultProvider = it.next(); + } + + while (it.hasNext()) { + it.next(); + } + } + + private XmlProviders() { + // no-op + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@code byte[]}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(byte[], boolean) createReader(xml, true)}. + * + * @param xml The XML represented as a {@code byte[]}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + */ + public static XmlReader createReader(byte[] xml) { + return createReader(xml, true); + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@code byte[]}. + * + * @param xml The XML represented as a {@code byte[]}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static XmlReader createReader(byte[] xml, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultXmlReader.fromBytes(xml); + } else { + throw new IllegalStateException(CANNOT_FIND_XML); + } + } else { + return defaultProvider.createReader(xml); + } + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@link String}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(String, boolean) createReader(xml, true)}. + * + * @param xml The XML represented as a {@link String}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + */ + public static XmlReader createReader(String xml) { + return createReader(xml, true); + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@link String}. + * + * @param xml The XML represented as a {@link String}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static XmlReader createReader(String xml, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultXmlReader.fromString(xml); + } else { + throw new IllegalStateException(CANNOT_FIND_XML); + } + } else { + return defaultProvider.createReader(xml); + } + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@link InputStream}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(InputStream, boolean) createReader(xml, true)}. + * + * @param xml The XML represented as a {@link InputStream}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + */ + public static XmlReader createReader(InputStream xml) { + return createReader(xml, true); + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@link InputStream}. + * + * @param xml The XML represented as a {@link InputStream}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static XmlReader createReader(InputStream xml, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultXmlReader.fromStream(xml); + } else { + throw new IllegalStateException(CANNOT_FIND_XML); + } + } else { + return defaultProvider.createReader(xml); + } + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@link Reader}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createReader(Reader, boolean) createReader(xml, true)}. + * + * @param xml The XML represented as a {@link Reader}. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + */ + public static XmlReader createReader(Reader xml) { + return createReader(xml, true); + } + + /** + * Creates an instance of {@link XmlReader} that reads a {@link Reader}. + * + * @param xml The XML represented as a {@link Reader}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link XmlReader}. + * @throws NullPointerException If {@code xml} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static XmlReader createReader(Reader xml, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultXmlReader.fromReader(xml); + } else { + throw new IllegalStateException(CANNOT_FIND_XML); + } + } else { + return defaultProvider.createReader(xml); + } + } + + /** + * Creates an instance of {@link XmlWriter} that writes to an {@link OutputStream}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createWriter(OutputStream, boolean) createWriter(xml, true)}. + * + * @param xml The XML represented as an {@link OutputStream}. + * @return A new instance of {@link XmlWriter}. + * @throws NullPointerException If {@code xml} is null. + */ + public static XmlWriter createWriter(OutputStream xml) { + return createWriter(xml, true); + } + + /** + * Creates an instance of {@link XmlWriter} that writes to an {@link OutputStream}. + * + * @param xml The XML represented as an {@link OutputStream}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link XmlWriter}. + * @throws NullPointerException If {@code xml} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static XmlWriter createWriter(OutputStream xml, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultXmlWriter.toStream(xml); + } else { + throw new IllegalStateException(CANNOT_FIND_XML); + } + } else { + return defaultProvider.createWriter(xml); + } + } + + /** + * Creates an instance of {@link XmlWriter} that writes to an {@link Writer}. + *

+ * If a provider could not be found on the classpath this will use the default implementation, effectively the + * equivalent to {@link #createWriter(Writer, boolean) createWriter(xml, true)}. + * + * @param xml The XML represented as an {@link Writer}. + * @return A new instance of {@link XmlWriter}. + * @throws NullPointerException If {@code xml} is null. + */ + public static XmlWriter createWriter(Writer xml) { + return createWriter(xml, true); + } + + /** + * Creates an instance of {@link XmlWriter} that writes to an {@link Writer}. + * + * @param xml The XML represented as an {@link Writer}. + * @param useDefault Whether the default implementation should be used if one could not be found on the classpath. + * @return A new instance of {@link XmlWriter}. + * @throws NullPointerException If {@code xml} is null. + * @throws IllegalStateException If a provider could not be found on the classpath and {@code useDefault} is false. + */ + public static XmlWriter createWriter(Writer xml, boolean useDefault) { + if (defaultProvider == null) { + if (useDefault) { + return DefaultXmlWriter.toWriter(xml); + } else { + throw new IllegalStateException(CANNOT_FIND_XML); + } + } else { + return defaultProvider.createWriter(xml); + } + } +} diff --git a/sdk/core/azure-xml/src/main/java/com/azure/xml/DefaultXmlReader.java b/sdk/core/azure-xml/src/main/java/com/azure/xml/implementation/DefaultXmlReader.java similarity index 96% rename from sdk/core/azure-xml/src/main/java/com/azure/xml/DefaultXmlReader.java rename to sdk/core/azure-xml/src/main/java/com/azure/xml/implementation/DefaultXmlReader.java index d6d136b0f9cf6..8109472ef22ec 100644 --- a/sdk/core/azure-xml/src/main/java/com/azure/xml/DefaultXmlReader.java +++ b/sdk/core/azure-xml/src/main/java/com/azure/xml/implementation/DefaultXmlReader.java @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.xml; +package com.azure.xml.implementation; + +import com.azure.xml.XmlReader; +import com.azure.xml.XmlToken; +import com.azure.xml.XmlWriter; import javax.xml.namespace.QName; import javax.xml.stream.XMLInputFactory; @@ -37,7 +41,7 @@ public final class DefaultXmlReader extends XmlReader { * @return A new {@link XmlReader} instance. */ public static XmlReader fromBytes(byte[] xml) { - return fromInputStream(new ByteArrayInputStream(xml)); + return fromStream(new ByteArrayInputStream(xml)); } /** @@ -57,7 +61,7 @@ public static XmlReader fromString(String xml) { * @return A new {@link XmlReader} instance. * @throws RuntimeException If an {@link XmlReader} cannot be instantiated. */ - public static XmlReader fromInputStream(InputStream xml) { + public static XmlReader fromStream(InputStream xml) { try { return new DefaultXmlReader(XML_INPUT_FACTORY.createXMLStreamReader(xml)); } catch (XMLStreamException e) { diff --git a/sdk/core/azure-xml/src/main/java/com/azure/xml/DefaultXmlWriter.java b/sdk/core/azure-xml/src/main/java/com/azure/xml/implementation/DefaultXmlWriter.java similarity index 86% rename from sdk/core/azure-xml/src/main/java/com/azure/xml/DefaultXmlWriter.java rename to sdk/core/azure-xml/src/main/java/com/azure/xml/implementation/DefaultXmlWriter.java index 019eaef65f670..662bd523d5fd4 100644 --- a/sdk/core/azure-xml/src/main/java/com/azure/xml/DefaultXmlWriter.java +++ b/sdk/core/azure-xml/src/main/java/com/azure/xml/implementation/DefaultXmlWriter.java @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.xml; +package com.azure.xml.implementation; + +import com.azure.xml.XmlWriter; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; @@ -9,6 +11,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.Objects; @@ -29,7 +32,7 @@ public final class DefaultXmlWriter extends XmlWriter { * @return A new instance of {@link XmlWriter}. * @throws RuntimeException If an {@link XmlWriter} cannot be instantiated. */ - public static XmlWriter toOutputStream(OutputStream outputStream) { + public static XmlWriter toStream(OutputStream outputStream) { try { return new DefaultXmlWriter(XML_OUTPUT_FACTORY.createXMLStreamWriter( new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))); @@ -38,6 +41,21 @@ public static XmlWriter toOutputStream(OutputStream outputStream) { } } + /** + * Creates an instance of {@link XmlWriter} that writes to the provided {@link Writer}. + * + * @param writer The {@link Writer} where content will be written. + * @return A new instance of {@link XmlWriter}. + * @throws RuntimeException If an {@link XmlWriter} cannot be instantiated. + */ + public static XmlWriter toWriter(Writer writer) { + try { + return new DefaultXmlWriter(XML_OUTPUT_FACTORY.createXMLStreamWriter(writer)); + } catch (XMLStreamException ex) { + throw new RuntimeException(ex); + } + } + private DefaultXmlWriter(XMLStreamWriter writer) { this.writer = writer; } diff --git a/sdk/core/azure-xml/src/main/java/module-info.java b/sdk/core/azure-xml/src/main/java/module-info.java index 9336476b78c5f..1a38d62f075d5 100644 --- a/sdk/core/azure-xml/src/main/java/module-info.java +++ b/sdk/core/azure-xml/src/main/java/module-info.java @@ -5,4 +5,7 @@ requires java.xml; exports com.azure.xml; + exports com.azure.xml.implementation; + + uses com.azure.xml.XmlProvider; } diff --git a/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlReaderContractTests.java b/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlReaderContractTests.java index 1525610f9ef0a..0fe58723e5a22 100644 --- a/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlReaderContractTests.java +++ b/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlReaderContractTests.java @@ -4,6 +4,7 @@ package com.azure.xml; import com.azure.xml.contract.XmlReaderContractTests; +import com.azure.xml.implementation.DefaultXmlReader; /** * Tests {@link DefaultXmlReader} against the contract required by {@link XmlReader}. diff --git a/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlWriterContractTests.java b/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlWriterContractTests.java index 1ce7ca0098715..1090b77641ff2 100644 --- a/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlWriterContractTests.java +++ b/sdk/core/azure-xml/src/test/java/com/azure/xml/DefaultXmlWriterContractTests.java @@ -4,6 +4,7 @@ package com.azure.xml; import com.azure.xml.contract.XmlWriterContractTests; +import com.azure.xml.implementation.DefaultXmlWriter; import org.junit.jupiter.api.BeforeEach; import java.io.ByteArrayOutputStream; @@ -20,7 +21,7 @@ public final class DefaultXmlWriterContractTests extends XmlWriterContractTests @BeforeEach public void beforeEach() { this.outputStream = new ByteArrayOutputStream(); - this.writer = DefaultXmlWriter.toOutputStream(outputStream); + this.writer = DefaultXmlWriter.toStream(outputStream); } @Override diff --git a/sdk/core/azure-xml/src/test/java/com/azure/xml/PlaygroundTests.java b/sdk/core/azure-xml/src/test/java/com/azure/xml/PlaygroundTests.java index 3ed25abc3ff09..d4afc07f0ce38 100644 --- a/sdk/core/azure-xml/src/test/java/com/azure/xml/PlaygroundTests.java +++ b/sdk/core/azure-xml/src/test/java/com/azure/xml/PlaygroundTests.java @@ -3,6 +3,8 @@ package com.azure.xml; +import com.azure.xml.implementation.DefaultXmlReader; +import com.azure.xml.implementation.DefaultXmlWriter; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; @@ -63,7 +65,7 @@ public void toXmlSimple() throws IOException { SignedIdentifiersWrapper wrapper = new SignedIdentifiersWrapper(Collections.singletonList(signedIdentifier)); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (XmlWriter xmlWriter = DefaultXmlWriter.toOutputStream(byteArrayOutputStream)) { + try (XmlWriter xmlWriter = DefaultXmlWriter.toStream(byteArrayOutputStream)) { xmlWriter.writeStartDocument(); wrapper.toXml(xmlWriter); } @@ -137,7 +139,7 @@ public void toXmlComplex() throws IOException { .setContent(namespacePropertiesEntryContent); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (XmlWriter xmlWriter = DefaultXmlWriter.toOutputStream(byteArrayOutputStream)) { + try (XmlWriter xmlWriter = DefaultXmlWriter.toStream(byteArrayOutputStream)) { xmlWriter.writeStartDocument(); namespacePropertiesEntry.toXml(xmlWriter); } diff --git a/sdk/core/azure-xml/src/test/java/com/azure/xml/storage/DeserializeListBlobsTests.java b/sdk/core/azure-xml/src/test/java/com/azure/xml/storage/DeserializeListBlobsTests.java index 5307ad073c3bf..ce07e4356be3b 100644 --- a/sdk/core/azure-xml/src/test/java/com/azure/xml/storage/DeserializeListBlobsTests.java +++ b/sdk/core/azure-xml/src/test/java/com/azure/xml/storage/DeserializeListBlobsTests.java @@ -3,7 +3,7 @@ package com.azure.xml.storage; -import com.azure.xml.DefaultXmlReader; +import com.azure.xml.implementation.DefaultXmlReader; import org.junit.jupiter.api.Test; import java.util.ArrayList; From 21573f5a374e9e0a71abdd98fed47f428c548b94 Mon Sep 17 00:00:00 2001 From: Rabab Ibrahim <88855721+ibrahimrabab@users.noreply.github.com> Date: Thu, 1 Sep 2022 11:50:50 -0700 Subject: [PATCH 03/18] Removing Rename for Containers and File Systems due to no service supportability (#30729) --- .../blob/BlobContainerAsyncClient.java | 127 ++++------ .../storage/blob/BlobContainerClient.java | 66 +---- ...ntainerAsyncClientJavaDocCodeSnippets.java | 45 ++-- ...lobContainerClientJavaDocCodeSnippets.java | 49 ++-- .../storage/blob/ContainerAPITest.groovy | 229 +++++++++--------- .../ContainerAPITestRename.json | 91 ------- .../ContainerAPITestRenameACFail[0].json | 47 ---- .../ContainerAPITestRenameACIllegal[0].json | 25 -- .../ContainerAPITestRenameACIllegal[1].json | 25 -- .../ContainerAPITestRenameACIllegal[2].json | 25 -- .../ContainerAPITestRenameACIllegal[3].json | 25 -- .../ContainerAPITestRenameACIllegal[4].json | 25 -- .../ContainerAPITestRenameAC[0].json | 44 ---- .../ContainerAPITestRenameAC[1].json | 66 ----- .../ContainerAPITestRenameError.json | 47 ---- .../ContainerAPITestRenameSas.json | 91 ------- .../DataLakeFileSystemAsyncClient.java | 107 +++----- .../datalake/DataLakeFileSystemClient.java | 77 ++---- ...leSystemAsyncClientJavaDocCodeSamples.java | 45 ++-- .../FileSystemClientJavaDocCodeSamples.java | 49 ++-- .../file/datalake/FileSystemAPITest.groovy | 224 +++++++++-------- .../FileSystemAPITestRename.json | 91 ------- .../FileSystemAPITestRenameACFail[0].json | 47 ---- .../FileSystemAPITestRenameACIllegal[0].json | 25 -- .../FileSystemAPITestRenameACIllegal[1].json | 25 -- .../FileSystemAPITestRenameACIllegal[2].json | 25 -- .../FileSystemAPITestRenameACIllegal[3].json | 25 -- .../FileSystemAPITestRenameAC[0].json | 44 ---- .../FileSystemAPITestRenameAC[1].json | 66 ----- .../FileSystemAPITestRenameError.json | 47 ---- .../FileSystemAPITestRenameSas.json | 91 ------- 31 files changed, 427 insertions(+), 1588 deletions(-) delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRename.json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACFail[0].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[0].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[1].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[2].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[3].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[4].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[0].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[1].json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameError.json delete mode 100644 sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameSas.json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRename.json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACFail[0].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[0].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[1].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[2].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[3].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[0].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[1].json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameError.json delete mode 100644 sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameSas.json diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java index 0f3b5221ae664..a87c44140b1d1 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java @@ -44,7 +44,6 @@ import com.azure.storage.blob.models.TaggedBlobItem; import com.azure.storage.blob.models.UserDelegationKey; import com.azure.storage.blob.options.BlobContainerCreateOptions; -import com.azure.storage.blob.options.BlobContainerRenameOptions; import com.azure.storage.blob.options.FindBlobsOptions; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.StorageSharedKeyCredential; @@ -1538,80 +1537,44 @@ Mono> getAccountInfoWithResponse(Context context) { }); } - /** - * Renames an existing blob container. - * - *

Code Samples

- * - * - *
-     * BlobContainerAsyncClient blobContainerAsyncClient =
-     *     client.rename("newContainerName")
-     *         .block();
-     * 
- * - * - * @param destinationContainerName The new name of the container. - * @return A {@link Mono} containing a {@link BlobContainerAsyncClient} used to interact with the renamed container. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public Mono rename(String destinationContainerName) { - return renameWithResponse(new BlobContainerRenameOptions(destinationContainerName)).flatMap(FluxUtil::toMono); - } - - /** - * Renames an existing blob container. - * - *

Code Samples

- * - * - *
-     * BlobRequestConditions requestConditions = new BlobRequestConditions().setLeaseId("lease-id");
-     * BlobContainerAsyncClient containerClient =
-     *     client.renameWithResponse(new BlobContainerRenameOptions("newContainerName")
-     *         .setRequestConditions(requestConditions)).block().getValue();
-     * 
- * - * - * @param options {@link BlobContainerRenameOptions} - * @return A {@link Mono} containing a {@link Response} whose {@link Response#getValue() value} contains a - * {@link BlobContainerAsyncClient} used to interact with the renamed container. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public Mono> renameWithResponse(BlobContainerRenameOptions options) { - try { - return withContext(context -> this.renameWithResponse(options, context)); - } catch (RuntimeException ex) { - return monoError(LOGGER, ex); - } - } - - Mono> renameWithResponse(BlobContainerRenameOptions options, Context context) { - // TODO (gapra) : Change this when we have migrated to new generator. There will be a cleaner way to do this by - // calling the container constructor directly instead of needing to do URI surgery - BlobContainerAsyncClient destinationContainerClient = getServiceAsyncClient() - .getBlobContainerAsyncClient(options.getDestinationContainerName()); - return destinationContainerClient.renameWithResponseHelper(this.getBlobContainerName(), options, context); - } - - Mono> renameWithResponseHelper(String sourceContainerName, - BlobContainerRenameOptions options, Context context) { - StorageImplUtils.assertNotNull("options", options); - BlobRequestConditions requestConditions = options.getRequestConditions() == null ? new BlobRequestConditions() - : options.getRequestConditions(); - context = context == null ? Context.NONE : context; - - if (!validateNoETag(requestConditions) || !validateNoTime(requestConditions) - || requestConditions.getTagsConditions() != null) { - throw LOGGER.logExceptionAsError(new UnsupportedOperationException( - "Lease-Id is the only HTTP access condition supported for this API")); - } - - return this.azureBlobStorage.getContainers().renameWithResponseAsync(containerName, - sourceContainerName, null, null, requestConditions.getLeaseId(), - context.addData(AZ_TRACING_NAMESPACE_KEY, STORAGE_TRACING_NAMESPACE_VALUE)) - .map(response -> new SimpleResponse<>(response, this)); - } + // TODO: Reintroduce this API once service starts supporting it. +// Mono rename(String destinationContainerName) { +// return renameWithResponse(new BlobContainerRenameOptions(destinationContainerName)).flatMap(FluxUtil::toMono); +// } + + // TODO: Reintroduce this API once service starts supporting it. +// Mono> renameWithResponse(BlobContainerRenameOptions options) { +// try { +// return withContext(context -> this.renameWithResponse(options, context)); +// } catch (RuntimeException ex) { +// return monoError(LOGGER, ex); +// } +// } + +// Mono> renameWithResponse(BlobContainerRenameOptions options, Context context) { +// BlobContainerAsyncClient destinationContainerClient = getServiceAsyncClient() +// .getBlobContainerAsyncClient(options.getDestinationContainerName()); +// return destinationContainerClient.renameWithResponseHelper(this.getBlobContainerName(), options, context); +// } + +// Mono> renameWithResponseHelper(String sourceContainerName, +// BlobContainerRenameOptions options, Context context) { +// StorageImplUtils.assertNotNull("options", options); +// BlobRequestConditions requestConditions = options.getRequestConditions() == null ? new BlobRequestConditions() +// : options.getRequestConditions(); +// context = context == null ? Context.NONE : context; +// +// if (!validateNoETag(requestConditions) || !validateNoTime(requestConditions) +// || requestConditions.getTagsConditions() != null) { +// throw LOGGER.logExceptionAsError(new UnsupportedOperationException( +// "Lease-Id is the only HTTP access condition supported for this API")); +// } +// +// return this.azureBlobStorage.getContainers().renameWithResponseAsync(containerName, +// sourceContainerName, null, null, requestConditions.getLeaseId(), +// context.addData(AZ_TRACING_NAMESPACE_KEY, STORAGE_TRACING_NAMESPACE_VALUE)) +// .map(response -> new SimpleResponse<>(response, this)); +// } /** * Generates a user delegation SAS for the container using the specified {@link BlobServiceSasSignatureValues}. @@ -1741,11 +1704,11 @@ private static boolean validateNoETag(BlobRequestConditions modifiedRequestCondi return modifiedRequestConditions.getIfMatch() == null && modifiedRequestConditions.getIfNoneMatch() == null; } - private boolean validateNoTime(BlobRequestConditions modifiedRequestConditions) { - if (modifiedRequestConditions == null) { - return true; - } - return modifiedRequestConditions.getIfModifiedSince() == null - && modifiedRequestConditions.getIfUnmodifiedSince() == null; - } +// private boolean validateNoTime(BlobRequestConditions modifiedRequestConditions) { +// if (modifiedRequestConditions == null) { +// return true; +// } +// return modifiedRequestConditions.getIfModifiedSince() == null +// && modifiedRequestConditions.getIfUnmodifiedSince() == null; +// } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java index 1e602eb290fcf..711ccdd6f7cb9 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java @@ -9,7 +9,6 @@ import com.azure.core.http.HttpPipeline; import com.azure.core.http.rest.PagedIterable; import com.azure.core.http.rest.Response; -import com.azure.core.http.rest.SimpleResponse; import com.azure.core.util.Context; import com.azure.storage.blob.models.BlobContainerAccessPolicies; import com.azure.storage.blob.models.BlobContainerProperties; @@ -23,7 +22,6 @@ import com.azure.storage.blob.models.TaggedBlobItem; import com.azure.storage.blob.models.UserDelegationKey; import com.azure.storage.blob.options.BlobContainerCreateOptions; -import com.azure.storage.blob.options.BlobContainerRenameOptions; import com.azure.storage.blob.options.FindBlobsOptions; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.StorageSharedKeyCredential; @@ -1053,58 +1051,20 @@ public Response getAccountInfoWithResponse(Duration timeout, return blockWithOptionalTimeout(response, timeout); } - /** - * Renames an existing blob container. - * - *

Code Samples

- * - * - *
-     * BlobContainerClient blobContainerClient = client.rename("newContainerName");
-     * 
- * - * - * @param destinationContainerName The new name of the container. - * @return A {@link BlobContainerClient} used to interact with the renamed container. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public BlobContainerClient rename(String destinationContainerName) { - return renameWithResponse(new BlobContainerRenameOptions(destinationContainerName - ), null, Context.NONE).getValue(); - } - - /** - * Renames an existing blob container. - * - *

Code Samples

- * - * - *
-     * BlobRequestConditions requestConditions = new BlobRequestConditions().setLeaseId("lease-id");
-     * Context context = new Context("Key", "Value");
-     *
-     * BlobContainerClient blobContainerClient = client.renameWithResponse(
-     *     new BlobContainerRenameOptions("newContainerName")
-     *         .setRequestConditions(requestConditions),
-     *     Duration.ofSeconds(1),
-     *     context).getValue();
-     * 
- * - * - * @param options {@link BlobContainerRenameOptions} - * @param timeout An optional timeout value beyond which a {@link RuntimeException} will be raised. - * @param context Additional context that is passed through the Http pipeline during the service call. - * @return A {@link Response} whose {@link Response#getValue() value} contains a - * {@link BlobContainerClient} used to interact with the renamed container. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public Response renameWithResponse(BlobContainerRenameOptions options, Duration timeout, - Context context) { - Mono> response = this.client.renameWithResponse(options, context) - .map(r -> new SimpleResponse<>(r, new BlobContainerClient(r.getValue()))); + // TODO: Reintroduce this API once service starts supporting it. +// BlobContainerClient rename(String destinationContainerName) { +// return renameWithResponse(new BlobContainerRenameOptions(destinationContainerName +// ), null, Context.NONE).getValue(); +// } - return StorageImplUtils.blockWithOptionalTimeout(response, timeout); - } + // TODO: Reintroduce this API once service starts supporting it. +// Response renameWithResponse(BlobContainerRenameOptions options, Duration timeout, +// Context context) { +// Mono> response = this.client.renameWithResponse(options, context) +// .map(r -> new SimpleResponse<>(r, new BlobContainerClient(r.getValue()))); +// +// return StorageImplUtils.blockWithOptionalTimeout(response, timeout); +// } /** * Generates a user delegation SAS for the container using the specified {@link BlobServiceSasSignatureValues}. diff --git a/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerAsyncClientJavaDocCodeSnippets.java b/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerAsyncClientJavaDocCodeSnippets.java index c6f2644a4ab96..07fab9945be48 100644 --- a/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerAsyncClientJavaDocCodeSnippets.java +++ b/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerAsyncClientJavaDocCodeSnippets.java @@ -12,7 +12,6 @@ import com.azure.storage.blob.models.PublicAccessType; import com.azure.storage.blob.models.UserDelegationKey; import com.azure.storage.blob.options.BlobContainerCreateOptions; -import com.azure.storage.blob.options.BlobContainerRenameOptions; import com.azure.storage.blob.options.FindBlobsOptions; import com.azure.storage.blob.sas.BlobContainerSasPermission; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; @@ -492,26 +491,26 @@ public void deleteIfExistsCodeSnippets() { // END: com.azure.storage.blob.BlobContainerAsyncClient.deleteIfExistsWithResponse#BlobRequestConditions } - /** - * Code snippet for {@link BlobContainerAsyncClient#rename(String)} - */ - public void renameContainer() { - // BEGIN: com.azure.storage.blob.BlobContainerAsyncClient.rename#String - BlobContainerAsyncClient blobContainerAsyncClient = - client.rename("newContainerName") - .block(); - // END: com.azure.storage.blob.BlobContainerAsyncClient.rename#String - } - - /** - * Code snippet for {@link BlobContainerAsyncClient#renameWithResponse(BlobContainerRenameOptions)} - */ - public void renameContainerWithResponse() { - // BEGIN: com.azure.storage.blob.BlobContainerAsyncClient.renameWithResponse#BlobContainerRenameOptions - BlobRequestConditions requestConditions = new BlobRequestConditions().setLeaseId("lease-id"); - BlobContainerAsyncClient containerClient = - client.renameWithResponse(new BlobContainerRenameOptions("newContainerName") - .setRequestConditions(requestConditions)).block().getValue(); - // END: com.azure.storage.blob.BlobContainerAsyncClient.renameWithResponse#BlobContainerRenameOptions - } +// /** +// * Code snippet for {@link BlobContainerAsyncClient#rename(String)} +// */ +// public void renameContainer() { +// // BEGIN: com.azure.storage.blob.BlobContainerAsyncClient.rename#String +// BlobContainerAsyncClient blobContainerAsyncClient = +// client.rename("newContainerName") +// .block(); +// // END: com.azure.storage.blob.BlobContainerAsyncClient.rename#String +// } +// +// /** +// * Code snippet for {@link BlobContainerAsyncClient#renameWithResponse(BlobContainerRenameOptions)} +// */ +// public void renameContainerWithResponse() { +// // BEGIN: com.azure.storage.blob.BlobContainerAsyncClient.renameWithResponse#BlobContainerRenameOptions +// BlobRequestConditions requestConditions = new BlobRequestConditions().setLeaseId("lease-id"); +// BlobContainerAsyncClient containerClient = +// client.renameWithResponse(new BlobContainerRenameOptions("newContainerName") +// .setRequestConditions(requestConditions)).block().getValue(); +// // END: com.azure.storage.blob.BlobContainerAsyncClient.renameWithResponse#BlobContainerRenameOptions +// } } diff --git a/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerClientJavaDocCodeSnippets.java b/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerClientJavaDocCodeSnippets.java index 5974a696d31c3..874bb9b6d688d 100644 --- a/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerClientJavaDocCodeSnippets.java +++ b/sdk/storage/azure-storage-blob/src/samples/java/com/azure/storage/blob/BlobContainerClientJavaDocCodeSnippets.java @@ -18,7 +18,6 @@ import com.azure.storage.blob.models.StorageAccountInfo; import com.azure.storage.blob.models.UserDelegationKey; import com.azure.storage.blob.options.BlobContainerCreateOptions; -import com.azure.storage.blob.options.BlobContainerRenameOptions; import com.azure.storage.blob.options.FindBlobsOptions; import com.azure.storage.blob.sas.BlobContainerSasPermission; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; @@ -511,28 +510,28 @@ public void deleteIfExistsCodeSnippets() { // END: com.azure.storage.blob.BlobContainerClient.deleteIfExistsWithResponse#BlobRequestConditions-Duration-Context } - /** - * Code snippet for {@link BlobContainerClient#rename(String)} - */ - public void renameContainer() { - // BEGIN: com.azure.storage.blob.BlobContainerClient.rename#String - BlobContainerClient blobContainerClient = client.rename("newContainerName"); - // END: com.azure.storage.blob.BlobContainerClient.rename#String - } - - /** - * Code snippet for {@link BlobContainerClient#renameWithResponse(BlobContainerRenameOptions, Duration, Context)} - */ - public void renameContainerWithResponse() { - // BEGIN: com.azure.storage.blob.BlobContainerClient.renameWithResponse#BlobContainerRenameOptions-Duration-Context - BlobRequestConditions requestConditions = new BlobRequestConditions().setLeaseId("lease-id"); - Context context = new Context("Key", "Value"); - - BlobContainerClient blobContainerClient = client.renameWithResponse( - new BlobContainerRenameOptions("newContainerName") - .setRequestConditions(requestConditions), - Duration.ofSeconds(1), - context).getValue(); - // END: com.azure.storage.blob.BlobContainerClient.renameWithResponse#BlobContainerRenameOptions-Duration-Context - } +// /** +// * Code snippet for {@link BlobContainerClient#rename(String)} +// */ +// public void renameContainer() { +// // BEGIN: com.azure.storage.blob.BlobContainerClient.rename#String +// BlobContainerClient blobContainerClient = client.rename("newContainerName"); +// // END: com.azure.storage.blob.BlobContainerClient.rename#String +// } +// +// /** +// * Code snippet for {@link BlobContainerClient#renameWithResponse(BlobContainerRenameOptions, Duration, Context)} +// */ +// public void renameContainerWithResponse() { +// // BEGIN: com.azure.storage.blob.BlobContainerClient.renameWithResponse#BlobContainerRenameOptions-Duration-Context +// BlobRequestConditions requestConditions = new BlobRequestConditions().setLeaseId("lease-id"); +// Context context = new Context("Key", "Value"); +// +// BlobContainerClient blobContainerClient = client.renameWithResponse( +// new BlobContainerRenameOptions("newContainerName") +// .setRequestConditions(requestConditions), +// Duration.ofSeconds(1), +// context).getValue(); +// // END: com.azure.storage.blob.BlobContainerClient.renameWithResponse#BlobContainerRenameOptions-Duration-Context +// } } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAPITest.groovy b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAPITest.groovy index 1e9822fbe83fd..e47710166b7b5 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAPITest.groovy +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAPITest.groovy @@ -28,7 +28,6 @@ import com.azure.storage.blob.models.ObjectReplicationStatus import com.azure.storage.blob.models.PublicAccessType import com.azure.storage.blob.models.RehydratePriority import com.azure.storage.blob.options.BlobContainerCreateOptions -import com.azure.storage.blob.options.BlobContainerRenameOptions import com.azure.storage.blob.options.BlobParallelUploadOptions import com.azure.storage.blob.options.BlobSetAccessTierOptions import com.azure.storage.blob.options.FindBlobsOptions @@ -36,10 +35,6 @@ import com.azure.storage.blob.options.PageBlobCreateOptions import com.azure.storage.blob.specialized.AppendBlobClient import com.azure.storage.blob.specialized.BlobClientBase import com.azure.storage.common.Utility -import com.azure.storage.common.sas.AccountSasPermission -import com.azure.storage.common.sas.AccountSasResourceType -import com.azure.storage.common.sas.AccountSasService -import com.azure.storage.common.sas.AccountSasSignatureValues import com.azure.storage.common.test.shared.extensions.PlaybackOnly import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion import reactor.test.StepVerifier @@ -2213,115 +2208,117 @@ class ContainerAPITest extends APISpec { response.getHeaders().getValue("x-ms-version") == "2017-11-09" } - def "Rename"() { - setup: - def newName = generateContainerName() - - when: - def renamedContainer = cc.rename(newName) - - then: - renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 - - cleanup: - renamedContainer.delete() - } - - def "Rename sas"() { - setup: - def newName = generateContainerName() - def service = new AccountSasService() - .setBlobAccess(true) - def resourceType = new AccountSasResourceType() - .setContainer(true) - .setService(true) - .setObject(true) - def expiryTime = namer.getUtcNow().plusDays(1) - def permissions = new AccountSasPermission() - .setReadPermission(true) - .setWritePermission(true) - .setCreatePermission(true) - .setDeletePermission(true) - - def sasValues = new AccountSasSignatureValues(expiryTime, permissions, service, resourceType) - def sas = primaryBlobServiceClient.generateAccountSas(sasValues) - def sasClient = getContainerClient(sas, cc.getBlobContainerUrl()) - - when: - def renamedContainer = sasClient.rename(newName) - - then: - renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 - - cleanup: - renamedContainer.delete() - } - - @Unroll - def "Rename AC"() { - setup: - leaseID = setupContainerLeaseCondition(cc, leaseID) - def cac = new BlobRequestConditions() - .setLeaseId(leaseID) - - expect: - cc.renameWithResponse(new BlobContainerRenameOptions(generateContainerName()).setRequestConditions(cac), - null, null).getStatusCode() == 200 - - where: - leaseID || _ - null || _ - receivedLeaseID || _ - } - - @Unroll - def "Rename AC fail"() { - setup: - def cac = new BlobRequestConditions() - .setLeaseId(leaseID) - - when: - cc.renameWithResponse(new BlobContainerRenameOptions(generateContainerName()).setRequestConditions(cac), - null, null) - - then: - thrown(BlobStorageException) - - where: - leaseID || _ - garbageLeaseID || _ - } - - @Unroll - def "Rename AC illegal"() { - setup: - def ac = new BlobRequestConditions().setIfMatch(match).setIfNoneMatch(noneMatch).setIfModifiedSince(modified).setIfUnmodifiedSince(unmodified).setTagsConditions(tags) - - when: - cc.renameWithResponse(new BlobContainerRenameOptions(generateContainerName()).setRequestConditions(ac), - null, null) - - then: - thrown(UnsupportedOperationException) - - where: - modified | unmodified | match | noneMatch | tags - oldDate | null | null | null | null - null | newDate | null | null | null - null | null | receivedEtag | null | null - null | null | null | garbageEtag | null - null | null | null | null | "tags" - } - - def "Rename error"() { - setup: - cc = primaryBlobServiceClient.getBlobContainerClient(generateContainerName()) - def newName = generateContainerName() - - when: - cc.rename(newName) - - then: - thrown(BlobStorageException) - } +// TODO: Reintroduce these tests once service starts supporting it. + +// def "Rename"() { +// setup: +// def newName = generateContainerName() +// +// when: +// def renamedContainer = cc.rename(newName) +// +// then: +// renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 +// +// cleanup: +// renamedContainer.delete() +// } + +// def "Rename sas"() { +// setup: +// def newName = generateContainerName() +// def service = new AccountSasService() +// .setBlobAccess(true) +// def resourceType = new AccountSasResourceType() +// .setContainer(true) +// .setService(true) +// .setObject(true) +// def expiryTime = namer.getUtcNow().plusDays(1) +// def permissions = new AccountSasPermission() +// .setReadPermission(true) +// .setWritePermission(true) +// .setCreatePermission(true) +// .setDeletePermission(true) +// +// def sasValues = new AccountSasSignatureValues(expiryTime, permissions, service, resourceType) +// def sas = primaryBlobServiceClient.generateAccountSas(sasValues) +// def sasClient = getContainerClient(sas, cc.getBlobContainerUrl()) +// +// when: +// def renamedContainer = sasClient.rename(newName) +// +// then: +// renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 +// +// cleanup: +// renamedContainer.delete() +// } + +// @Unroll +// def "Rename AC"() { +// setup: +// leaseID = setupContainerLeaseCondition(cc, leaseID) +// def cac = new BlobRequestConditions() +// .setLeaseId(leaseID) +// +// expect: +// cc.renameWithResponse(new BlobContainerRenameOptions(generateContainerName()).setRequestConditions(cac), +// null, null).getStatusCode() == 200 +// +// where: +// leaseID || _ +// null || _ +// receivedLeaseID || _ +// } + +// @Unroll +// def "Rename AC fail"() { +// setup: +// def cac = new BlobRequestConditions() +// .setLeaseId(leaseID) +// +// when: +// cc.renameWithResponse(new BlobContainerRenameOptions(generateContainerName()).setRequestConditions(cac), +// null, null) +// +// then: +// thrown(BlobStorageException) +// +// where: +// leaseID || _ +// garbageLeaseID || _ +// } + +// @Unroll +// def "Rename AC illegal"() { +// setup: +// def ac = new BlobRequestConditions().setIfMatch(match).setIfNoneMatch(noneMatch).setIfModifiedSince(modified).setIfUnmodifiedSince(unmodified).setTagsConditions(tags) +// +// when: +// cc.renameWithResponse(new BlobContainerRenameOptions(generateContainerName()).setRequestConditions(ac), +// null, null) +// +// then: +// thrown(UnsupportedOperationException) +// +// where: +// modified | unmodified | match | noneMatch | tags +// oldDate | null | null | null | null +// null | newDate | null | null | null +// null | null | receivedEtag | null | null +// null | null | null | garbageEtag | null +// null | null | null | null | "tags" +// } + +// def "Rename error"() { +// setup: +// cc = primaryBlobServiceClient.getBlobContainerClient(generateContainerName()) +// def newName = generateContainerName() +// +// when: +// cc.rename(newName) +// +// then: +// thrown(BlobStorageException) +// } } diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRename.json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRename.json deleted file mode 100644 index 8c5bf40ace576..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRename.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a8add0010a8add001465681847902c7aef6aa4e9485f?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "da39f7eb-1379-43cc-92f4-6ea363fd9817" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F78B793EFC", - "Last-Modified" : "Mon, 29 Aug 2022 19:49:11 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "40eba2c6-901e-0054-39e0-bb9595000000", - "x-ms-client-request-id" : "da39f7eb-1379-43cc-92f4-6ea363fd9817", - "Date" : "Mon, 29 Aug 2022 19:49:11 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a8add0011a8add00146583374b15a2496fafa4095ac0?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "d07b77cb-a0d4-4b40-850c-ac3e7848c68c" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "40eba385-901e-0054-69e0-bb9595000000", - "x-ms-client-request-id" : "d07b77cb-a0d4-4b40-850c-ac3e7848c68c", - "Date" : "Mon, 29 Aug 2022 19:49:12 GMT" - }, - "Exception" : null - }, { - "Method" : "GET", - "Uri" : "https://REDACTED.blob.core.windows.net/a8add0011a8add00146583374b15a2496fafa4095ac0?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "84775690-4b2a-4f5e-8d78-fa1d3ccde52a" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "x-ms-lease-status" : "unlocked", - "x-ms-immutable-storage-with-versioning-enabled" : "false", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-lease-state" : "available", - "x-ms-deny-encryption-scope-override" : "false", - "Last-Modified" : "Mon, 29 Aug 2022 19:49:12 GMT", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-has-legal-hold" : "false", - "Date" : "Mon, 29 Aug 2022 19:49:13 GMT", - "x-ms-default-encryption-scope" : "$account-encryption-key", - "x-ms-has-immutability-policy" : "false", - "eTag" : "0x8DA89F78C30FDBF", - "x-ms-request-id" : "40eba74f-901e-0054-50e0-bb9595000000", - "x-ms-client-request-id" : "84775690-4b2a-4f5e-8d78-fa1d3ccde52a" - }, - "Exception" : null - }, { - "Method" : "DELETE", - "Uri" : "https://REDACTED.blob.core.windows.net/a8add0011a8add00146583374b15a2496fafa4095ac0?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "4a14c824-34f2-4dde-bd11-3321667ef171" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "202", - "x-ms-request-id" : "40eba781-901e-0054-7fe0-bb9595000000", - "x-ms-client-request-id" : "4a14c824-34f2-4dde-bd11-3321667ef171", - "Date" : "Mon, 29 Aug 2022 19:49:13 GMT" - }, - "Exception" : null - } ], - "variables" : [ "a8add0010a8add001465681847902c7aef6aa4e9485f", "a8add00196773d1a", "a8add0018736921c", "a8add0011a8add00146583374b15a2496fafa4095ac0" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACFail[0].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACFail[0].json deleted file mode 100644 index 1b20c271967cd..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACFail[0].json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/e1739e460e1739e46f6a20124f2a009559ba24c118e3?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "f1afb546-0129-4556-aede-65e8542c9427" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C4F21C", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "49584001-301e-003f-16e0-bb1261000000", - "x-ms-client-request-id" : "f1afb546-0129-4556-aede-65e8542c9427", - "Date" : "Mon, 29 Aug 2022 19:51:15 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/e1739e461e1739e46f6a68928b9f5802ee2a94ef3884?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "47f3b163-0709-4b5e-91fd-dde35c31ef65" - }, - "Response" : { - "content-length" : "252", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-error-code" : "LeaseNotPresentWithContainerOperation", - "retry-after" : "0", - "StatusCode" : "412", - "x-ms-request-id" : "495840cb-301e-003f-36e0-bb1261000000", - "Body" : "\nLeaseNotPresentWithContainerOperationThere is currently no lease on the container.\nRequestId:495840cb-301e-003f-36e0-bb1261000000\nTime:2022-08-29T19:51:17.4254504Z", - "x-ms-client-request-id" : "47f3b163-0709-4b5e-91fd-dde35c31ef65", - "Date" : "Mon, 29 Aug 2022 19:51:17 GMT", - "Content-Type" : "application/xml" - }, - "Exception" : null - } ], - "variables" : [ "e1739e460e1739e46f6a20124f2a009559ba24c118e3", "e1739e46687434be", "e1739e46019199c3", "e1739e461e1739e46f6a68928b9f5802ee2a94ef3884" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[0].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[0].json deleted file mode 100644 index f446d35fb924e..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[0].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/e48d3c7c0e48d3c7c54034732b9742ec71b5b446dad3?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "5f92eb6c-227a-456d-9531-1dd3d2fda3e6" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C3C64F", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "c77786da-a01e-0002-07e0-bb647a000000", - "x-ms-client-request-id" : "5f92eb6c-227a-456d-9531-1dd3d2fda3e6", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - } ], - "variables" : [ "e48d3c7c0e48d3c7c54034732b9742ec71b5b446dad3", "e48d3c7c29513451", "e48d3c7c12559103", "e48d3c7c1e48d3c7c54021240e228b8d81f16446eaf7" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[1].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[1].json deleted file mode 100644 index e3b2ace2dc900..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[1].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/fd960d3d0fd960d3dab945527178fc6c447424ecabe9?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "74f7f5b7-6a1e-4ca5-bc4e-ebc56c9006bd" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C3ED62", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "eccdd303-801e-0067-80e0-bbca3e000000", - "x-ms-client-request-id" : "74f7f5b7-6a1e-4ca5-bc4e-ebc56c9006bd", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - } ], - "variables" : [ "fd960d3d0fd960d3dab945527178fc6c447424ecabe9", "fd960d3d308567c6", "fd960d3d44482204", "fd960d3d1fd960d3dab98453818432faf67164e3f80e" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[2].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[2].json deleted file mode 100644 index dcc4ce1980d9e..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[2].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/d6bb5efe0d6bb5efe94992631adbb430a9d2c4683a55?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "f6d97dcb-c0bb-40b5-80d1-65faa0f85cd0" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C2F424", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "1a3b0a3f-f01e-000f-08e0-bbacae000000", - "x-ms-client-request-id" : "f6d97dcb-c0bb-40b5-80d1-65faa0f85cd0", - "Date" : "Mon, 29 Aug 2022 19:51:13 GMT" - }, - "Exception" : null - } ], - "variables" : [ "d6bb5efe0d6bb5efe94992631adbb430a9d2c4683a55", "d6bb5efe14150977", "d6bb5efe35806eb5", "d6bb5efe1d6bb5efe949391467cc9c61778064d3cb3a" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[3].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[3].json deleted file mode 100644 index 62d672a0cce0e..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[3].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/cfa06fbf0cfa06fbfd8277294c66bf650caef46918d7?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "545215e2-1b5f-4e82-aa30-51c218392d26" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C4102E", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "4579b2c7-601e-008b-62e0-bbdeaf000000", - "x-ms-client-request-id" : "545215e2-1b5f-4e82-aa30-51c218392d26", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - } ], - "variables" : [ "cfa06fbf0cfa06fbfd8277294c66bf650caef46918d7", "cfa06fbf691968c8", "cfa06fbf04998792", "cfa06fbf1cfa06fbfd8258540e2de39e7674042b9826" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[4].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[4].json deleted file mode 100644 index df6a7598b76a9..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameACIllegal[4].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/80e1f978080e1f97806056264cad307a9de8d4f0e99a?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "bbd62cbe-6055-4b6a-987a-91091676999b" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C413FD", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "c54c4322-a01e-003d-2de0-bbacd9000000", - "x-ms-client-request-id" : "bbd62cbe-6055-4b6a-987a-91091676999b", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - } ], - "variables" : [ "80e1f978080e1f97806056264cad307a9de8d4f0e99a", "80e1f978099053bf", "80e1f978722964a2", "80e1f978180e1f97806096226deecfc84ca99461d915" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[0].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[0].json deleted file mode 100644 index 01b00d6bb7998..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[0].json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/2c2fc32702c2fc327dc736183d62b4440e1c54b439de?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "8bb42b43-cc26-4f42-a53e-de0a0f8ba584" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C3A47F", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "58f34824-301e-0000-1fe0-bbdac2000000", - "x-ms-client-request-id" : "8bb42b43-cc26-4f42-a53e-de0a0f8ba584", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/2c2fc32712c2fc327dc784778337cb2624df2465c8f5?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "610228cd-6a3a-44f0-b710-3cb4dce63439" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "c7778896-a01e-0002-0ee0-bb647a000000", - "x-ms-client-request-id" : "610228cd-6a3a-44f0-b710-3cb4dce63439", - "Date" : "Mon, 29 Aug 2022 19:51:15 GMT" - }, - "Exception" : null - } ], - "variables" : [ "2c2fc32702c2fc327dc736183d62b4440e1c54b439de", "2c2fc32772671c9f", "2c2fc327877896e6", "2c2fc32712c2fc327dc784778337cb2624df2465c8f5" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[1].json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[1].json deleted file mode 100644 index 40fb43af719fd..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameAC[1].json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/3534f26603534f266c6435788e51b653de38046a3afa?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "d6ef9566-f2df-449b-bde7-51f097b7eba7" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C3A118", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "ce33008c-f01e-0042-13e0-bb6342000000", - "x-ms-client-request-id" : "d6ef9566-f2df-449b-bde7-51f097b7eba7", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/3534f26603534f266c6435788e51b653de38046a3afa?comp=lease&restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "d787e447-7b49-4958-acd0-d94c2cefc9be" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-lease-id" : "908812c1-aaea-4748-9833-27de801c96e7", - "eTag" : "0x8DA89F7D4C3A118", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "eccdd3ef-801e-0067-4ce0-bbca3e000000", - "x-ms-client-request-id" : "d787e447-7b49-4958-acd0-d94c2cefc9be", - "Date" : "Mon, 29 Aug 2022 19:51:15 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/3534f26613534f266c64412191a2c6f3a22714879a01?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "84da3438-db6c-4bcc-9bd3-ecfb07a012fe" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "eccdd44d-801e-0067-1ee0-bbca3e000000", - "x-ms-client-request-id" : "84da3438-db6c-4bcc-9bd3-ecfb07a012fe", - "Date" : "Mon, 29 Aug 2022 19:51:16 GMT" - }, - "Exception" : null - } ], - "variables" : [ "3534f26603534f266c6435788e51b653de38046a3afa", "3534f26699302dfd", "3534f2660645320f", "3534f26613534f266c64412191a2c6f3a22714879a01" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameError.json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameError.json deleted file mode 100644 index f0fc5cfff2f29..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameError.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/3056fb8e03056fb8e56136005aff852f09ea14c0fa29?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "fe124576-4647-4e5c-b363-a4bb3773d8bd" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F7D4C39C7F", - "Last-Modified" : "Mon, 29 Aug 2022 19:51:14 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "620b392b-201e-0023-45e0-bb4001000000", - "x-ms-client-request-id" : "fe124576-4647-4e5c-b363-a4bb3773d8bd", - "Date" : "Mon, 29 Aug 2022 19:51:14 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/3056fb8e23056fb8e5618743307b87fa6e18540c7982?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "55a2d59c-1a5f-4247-83ab-d9fcadd0aa60" - }, - "Response" : { - "content-length" : "226", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-error-code" : "ContainerNotFound", - "retry-after" : "0", - "StatusCode" : "404", - "x-ms-request-id" : "844526a0-501e-0074-5de0-bbee32000000", - "Body" : "\nContainerNotFoundThe specified container does not exist.\nRequestId:844526a0-501e-0074-5de0-bbee32000000\nTime:2022-08-29T19:51:18.7662387Z", - "x-ms-client-request-id" : "55a2d59c-1a5f-4247-83ab-d9fcadd0aa60", - "Date" : "Mon, 29 Aug 2022 19:51:18 GMT", - "Content-Type" : "application/xml" - }, - "Exception" : null - } ], - "variables" : [ "3056fb8e03056fb8e56136005aff852f09ea14c0fa29", "3056fb8e075034fd", "3056fb8e3479411b", "3056fb8e13056fb8e5617510951a330f78dec4a569da", "3056fb8e23056fb8e5618743307b87fa6e18540c7982" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameSas.json b/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameSas.json deleted file mode 100644 index 6f0644478d3ef..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/resources/session-records/ContainerAPITestRenameSas.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/0ccc434e00ccc434ea0d50026d7c4613270524eb49ea?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "35d978a5-4fb9-4603-8098-866ded17cc5c" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A18272D604A", - "Last-Modified" : "Mon, 29 Aug 2022 23:42:36 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "dd0da0d3-401e-0035-0201-bcb6d6000000", - "x-ms-client-request-id" : "35d978a5-4fb9-4603-8098-866ded17cc5c", - "Date" : "Mon, 29 Aug 2022 23:42:35 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/0ccc434e10ccc434ea0d78544028d98c6ad5e489ba9a?restype=container&comp=rename&sv=2021-10-04&ss=b&srt=sco&se=2022-08-30T23%3A42%3A36Z&sp=rwdc&sig=REDACTED", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "04c30745-a7c3-49ab-82ac-d8cf7c3a2749" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "dd0da10c-401e-0035-3001-bcb6d6000000", - "x-ms-client-request-id" : "04c30745-a7c3-49ab-82ac-d8cf7c3a2749", - "Date" : "Mon, 29 Aug 2022 23:42:37 GMT" - }, - "Exception" : null - }, { - "Method" : "GET", - "Uri" : "https://REDACTED.blob.core.windows.net/0ccc434e10ccc434ea0d78544028d98c6ad5e489ba9a?restype=container&sv=2021-10-04&ss=b&srt=sco&se=2022-08-30T23%3A42%3A36Z&sp=rwdc&sig=REDACTED", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "d83058c5-c2dd-4b0c-be49-d6bdf3b979b3" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "x-ms-lease-status" : "unlocked", - "x-ms-immutable-storage-with-versioning-enabled" : "false", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-lease-state" : "available", - "x-ms-deny-encryption-scope-override" : "false", - "Last-Modified" : "Mon, 29 Aug 2022 23:42:38 GMT", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-has-legal-hold" : "false", - "Date" : "Mon, 29 Aug 2022 23:42:37 GMT", - "x-ms-default-encryption-scope" : "$account-encryption-key", - "x-ms-has-immutability-policy" : "false", - "eTag" : "0x8DA8A1827F7DED2", - "x-ms-request-id" : "dd0da303-401e-0035-6d01-bcb6d6000000", - "x-ms-client-request-id" : "d83058c5-c2dd-4b0c-be49-d6bdf3b979b3" - }, - "Exception" : null - }, { - "Method" : "DELETE", - "Uri" : "https://REDACTED.blob.core.windows.net/0ccc434e10ccc434ea0d78544028d98c6ad5e489ba9a?restype=container&sv=2021-10-04&ss=b&srt=sco&se=2022-08-30T23%3A42%3A36Z&sp=rwdc&sig=REDACTED", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "82d23a08-26cc-479a-8601-cbc24c81ecbb" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "202", - "x-ms-request-id" : "dd0da32c-401e-0035-1401-bcb6d6000000", - "x-ms-client-request-id" : "82d23a08-26cc-479a-8601-cbc24c81ecbb", - "Date" : "Mon, 29 Aug 2022 23:42:37 GMT" - }, - "Exception" : null - } ], - "variables" : [ "0ccc434e00ccc434ea0d50026d7c4613270524eb49ea", "0ccc434e9681811d", "0ccc434e24942fc5", "0ccc434e10ccc434ea0d78544028d98c6ad5e489ba9a", "2022-08-29T23:42:36.790068500Z" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemAsyncClient.java b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemAsyncClient.java index 9bb6799af106d..7362a727c98c8 100644 --- a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemAsyncClient.java +++ b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemAsyncClient.java @@ -53,7 +53,6 @@ import com.azure.storage.file.datalake.models.UserDelegationKey; import com.azure.storage.file.datalake.options.DataLakePathCreateOptions; import com.azure.storage.file.datalake.options.DataLakePathDeleteOptions; -import com.azure.storage.file.datalake.options.FileSystemRenameOptions; import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues; import reactor.core.publisher.Mono; @@ -1825,73 +1824,39 @@ public Mono> getAccessPolicyWithResponse(Stri Transforms.toFileSystemAccessPolicies(response.getValue()))); } - /** - * Renames an existing file system. - * - *

Code Samples

- * - * - *
-     * DataLakeFileSystemAsyncClient fileSystemAsyncClient =
-     *     client.rename("newFileSystemName")
-     *         .block();
-     * 
- * - * - * @param destinationContainerName The new name of the file system. - * @return A {@link Mono} containing a {@link DataLakeFileSystemAsyncClient} used to interact with the renamed file system. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public Mono rename(String destinationContainerName) { - return renameWithResponse(new FileSystemRenameOptions(destinationContainerName)).flatMap(FluxUtil::toMono); - } - - /** - * Renames an existing file system. - * - *

Code Samples

- * - * - *
-     * DataLakeRequestConditions requestConditions = new DataLakeRequestConditions().setLeaseId("lease-id");
-     * DataLakeFileSystemAsyncClient fileSystemAsyncClient = client
-     *     .renameWithResponse(new FileSystemRenameOptions( "newFileSystemName")
-     *         .setRequestConditions(requestConditions)).block().getValue();
-     * 
- * - * - * @param options {@link FileSystemRenameOptions} - * @return A {@link Mono} containing a {@link Response} whose {@link Response#getValue() value} contains a - * {@link DataLakeFileSystemAsyncClient} used to interact with the renamed file system. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public Mono> renameWithResponse(FileSystemRenameOptions options) { - try { - return blobContainerAsyncClient.renameWithResponse(Transforms.toBlobContainerRenameOptions(options)) - .onErrorMap(DataLakeImplUtils::transformBlobStorageException) - .map(response -> new SimpleResponse<>(response, - this.getFileSystemAsyncClient(options.getDestinationFileSystemName()))); - } catch (RuntimeException ex) { - return monoError(LOGGER, ex); - } - } - - /** - * Takes in a destination and creates a DataLakeFileSystemAsyncClient with a new path - * @param destinationFileSystem The destination file system - * @return A DataLakeFileSystemAsyncClient - */ - DataLakeFileSystemAsyncClient getFileSystemAsyncClient(String destinationFileSystem) { - if (CoreUtils.isNullOrEmpty(destinationFileSystem)) { - throw LOGGER.logExceptionAsError(new IllegalArgumentException("'destinationFileSystem' can not be set to null")); - } - // Get current Datalake URL and replace current filesystem with user provided filesystem - String newDfsEndpoint = BlobUrlParts.parse(getFileSystemUrl()) - .setContainerName(destinationFileSystem).toUrl().toString(); - - return new DataLakeFileSystemAsyncClient(getHttpPipeline(), newDfsEndpoint, serviceVersion, accountName, - destinationFileSystem, prepareBuilderReplacePath(destinationFileSystem).buildAsyncClient(), sasToken); - } + // TODO: Reintroduce this API once service starts supporting it. +// Mono rename(String destinationContainerName) { +// return renameWithResponse(new FileSystemRenameOptions(destinationContainerName)).flatMap(FluxUtil::toMono); +// } + + // TODO: Reintroduce this API once service starts supporting it. +// Mono> renameWithResponse(FileSystemRenameOptions options) { +// try { +// return blobContainerAsyncClient.renameWithResponse(Transforms.toBlobContainerRenameOptions(options)) +// .onErrorMap(DataLakeImplUtils::transformBlobStorageException) +// .map(response -> new SimpleResponse<>(response, +// this.getFileSystemAsyncClient(options.getDestinationFileSystemName()))); +// } catch (RuntimeException ex) { +// return monoError(LOGGER, ex); +// } +// } + +// /** +// * Takes in a destination and creates a DataLakeFileSystemAsyncClient with a new path +// * @param destinationFileSystem The destination file system +// * @return A DataLakeFileSystemAsyncClient +// */ +// DataLakeFileSystemAsyncClient getFileSystemAsyncClient(String destinationFileSystem) { +// if (CoreUtils.isNullOrEmpty(destinationFileSystem)) { +// throw LOGGER.logExceptionAsError(new IllegalArgumentException("'destinationFileSystem' can not be set to null")); +// } +// // Get current Datalake URL and replace current filesystem with user provided filesystem +// String newDfsEndpoint = BlobUrlParts.parse(getFileSystemUrl()) +// .setContainerName(destinationFileSystem).toUrl().toString(); +// +// return new DataLakeFileSystemAsyncClient(getHttpPipeline(), newDfsEndpoint, serviceVersion, accountName, +// destinationFileSystem, prepareBuilderReplacePath(destinationFileSystem).buildAsyncClient(), sasToken); +// } /** * Takes in a destination path and creates a ContainerClientBuilder with a new path name @@ -1912,9 +1877,9 @@ BlobContainerClientBuilder prepareBuilderReplacePath(String destinationFileSyste .serviceVersion(TransformUtils.toBlobServiceVersion(getServiceVersion())); } - BlobContainerAsyncClient getBlobContainerAsyncClient() { - return blobContainerAsyncClient; - } +// BlobContainerAsyncClient getBlobContainerAsyncClient() { +// return blobContainerAsyncClient; +// } /** * Generates a user delegation SAS for the file system using the specified diff --git a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemClient.java b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemClient.java index 8e7931b44b5db..a854c28ed4ad5 100644 --- a/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemClient.java +++ b/sdk/storage/azure-storage-file-datalake/src/main/java/com/azure/storage/file/datalake/DataLakeFileSystemClient.java @@ -34,7 +34,6 @@ import com.azure.storage.file.datalake.models.UserDelegationKey; import com.azure.storage.file.datalake.options.DataLakePathCreateOptions; import com.azure.storage.file.datalake.options.DataLakePathDeleteOptions; -import com.azure.storage.file.datalake.options.FileSystemRenameOptions; import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues; import reactor.core.publisher.Mono; @@ -1617,64 +1616,24 @@ public Response setAccessPolicyWithResponse(PublicAccessType accessType, timeout, context), LOGGER); } - /** - * Renames an existing file system. - * - *

Code Samples

- * - * - *
-     * DataLakeFileSystemClient fileSystemClient = client.rename("newFileSystemName");
-     * 
- * - * - * @param destinationFileSystemName The new name of the file system. - * @return A {@link DataLakeFileSystemClient} used to interact with the renamed file system. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public DataLakeFileSystemClient rename(String destinationFileSystemName) { - return this.renameWithResponse(new FileSystemRenameOptions(destinationFileSystemName), null, Context.NONE).getValue(); - } - - /** - * Renames an existing file system. - * - *

Code Samples

- * - * - *
-     * DataLakeRequestConditions requestConditions = new DataLakeRequestConditions().setLeaseId("lease-id");
-     * Context context = new Context("Key", "Value");
-     *
-     * DataLakeFileSystemClient fileSystemClient = client.renameWithResponse(
-     *     new FileSystemRenameOptions("newFileSystemName")
-     *         .setRequestConditions(requestConditions),
-     *     Duration.ofSeconds(1),
-     *     context).getValue();
-     * 
- * - * - * @param options {@link FileSystemRenameOptions} - * @param timeout An optional timeout value beyond which a {@link RuntimeException} will be raised. - * @param context Additional context that is passed through the Http pipeline during the service call. - * @return A {@link Response} whose {@link Response#getValue() value} contains a - * {@link DataLakeFileSystemClient} used to interact with the renamed file system. - */ - @ServiceMethod(returns = ReturnType.SINGLE) - public Response renameWithResponse(FileSystemRenameOptions options, - Duration timeout, Context context) { - return DataLakeImplUtils.returnOrConvertException(() -> { - Response response = blobContainerClient - .renameWithResponse(Transforms.toBlobContainerRenameOptions(options), timeout, context); - return new SimpleResponse<>(response, getFileSystemClient(options.getDestinationFileSystemName())); - }, LOGGER); - } - - private DataLakeFileSystemClient getFileSystemClient(String destinationFileSystem) { - return new DataLakeFileSystemClient( - dataLakeFileSystemAsyncClient.getFileSystemAsyncClient(destinationFileSystem), - dataLakeFileSystemAsyncClient.prepareBuilderReplacePath(destinationFileSystem).buildClient()); - } +// DataLakeFileSystemClient rename(String destinationFileSystemName) { +// return this.renameWithResponse(new FileSystemRenameOptions(destinationFileSystemName), null, Context.NONE).getValue(); +// } + +// Response renameWithResponse(FileSystemRenameOptions options, +// Duration timeout, Context context) { +// return DataLakeImplUtils.returnOrConvertException(() -> { +// Response response = blobContainerClient +// .renameWithResponse(Transforms.toBlobContainerRenameOptions(options), timeout, context); +// return new SimpleResponse<>(response, getFileSystemClient(options.getDestinationFileSystemName())); +// }, LOGGER); +// } + +// private DataLakeFileSystemClient getFileSystemClient(String destinationFileSystem) { +// return new DataLakeFileSystemClient( +// dataLakeFileSystemAsyncClient.getFileSystemAsyncClient(destinationFileSystem), +// dataLakeFileSystemAsyncClient.prepareBuilderReplacePath(destinationFileSystem).buildClient()); +// } BlobContainerClient getBlobContainerClient() { return blobContainerClient; diff --git a/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemAsyncClientJavaDocCodeSamples.java b/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemAsyncClientJavaDocCodeSamples.java index 21bd00816a1a7..ae8d456ee7772 100644 --- a/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemAsyncClientJavaDocCodeSamples.java +++ b/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemAsyncClientJavaDocCodeSamples.java @@ -14,7 +14,6 @@ import com.azure.storage.file.datalake.models.UserDelegationKey; import com.azure.storage.file.datalake.options.DataLakePathCreateOptions; import com.azure.storage.file.datalake.options.DataLakePathDeleteOptions; -import com.azure.storage.file.datalake.options.FileSystemRenameOptions; import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues; import com.azure.storage.file.datalake.sas.FileSystemSasPermission; import reactor.core.publisher.Mono; @@ -724,27 +723,27 @@ public void deleteDirectoryIfExistsCodeSnippets() { // END: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.deleteDirectoryIfExistsWithResponse#String-DataLakePathDeleteOptions } - /** - * Code snippet for {@link DataLakeFileSystemAsyncClient#rename(String)} - */ - public void renameFileSystem() { - // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.rename#String - DataLakeFileSystemAsyncClient fileSystemAsyncClient = - client.rename("newFileSystemName") - .block(); - // END: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.rename#String - } - - /** - * Code snippet for {@link DataLakeFileSystemAsyncClient#renameWithResponse(FileSystemRenameOptions)} - */ - public void renameFileSystemWithResponse() { - // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.renameWithResponse#FileSystemRenameOptions - DataLakeRequestConditions requestConditions = new DataLakeRequestConditions().setLeaseId("lease-id"); - DataLakeFileSystemAsyncClient fileSystemAsyncClient = client - .renameWithResponse(new FileSystemRenameOptions("newFileSystemName") - .setRequestConditions(requestConditions)).block().getValue(); - // END: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.renameWithResponse#FileSystemRenameOptions - } +// /** +// * Code snippet for {@link DataLakeFileSystemAsyncClient#rename(String)} +// */ +// public void renameFileSystem() { +// // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.rename#String +// DataLakeFileSystemAsyncClient fileSystemAsyncClient = +// client.rename("newFileSystemName") +// .block(); +// // END: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.rename#String +// } + +// /** +// * Code snippet for {@link DataLakeFileSystemAsyncClient#renameWithResponse(FileSystemRenameOptions)} +// */ +// public void renameFileSystemWithResponse() { +// // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.renameWithResponse#FileSystemRenameOptions +// DataLakeRequestConditions requestConditions = new DataLakeRequestConditions().setLeaseId("lease-id"); +// DataLakeFileSystemAsyncClient fileSystemAsyncClient = client +// .renameWithResponse(new FileSystemRenameOptions("newFileSystemName") +// .setRequestConditions(requestConditions)).block().getValue(); +// // END: com.azure.storage.file.datalake.DataLakeFileSystemAsyncClient.renameWithResponse#FileSystemRenameOptions +// } } diff --git a/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemClientJavaDocCodeSamples.java b/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemClientJavaDocCodeSamples.java index e02dfdc9a270e..508cf5d2ebb2b 100644 --- a/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemClientJavaDocCodeSamples.java +++ b/sdk/storage/azure-storage-file-datalake/src/samples/java/com/azure/storage/file/datalake/FileSystemClientJavaDocCodeSamples.java @@ -18,7 +18,6 @@ import com.azure.storage.file.datalake.models.UserDelegationKey; import com.azure.storage.file.datalake.options.DataLakePathCreateOptions; import com.azure.storage.file.datalake.options.DataLakePathDeleteOptions; -import com.azure.storage.file.datalake.options.FileSystemRenameOptions; import com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues; import com.azure.storage.file.datalake.sas.FileSystemSasPermission; @@ -742,29 +741,29 @@ public void deleteDirectoryIfExistsCodeSnippets() { // END: com.azure.storage.file.datalake.DataLakeFileSystemClient.deleteDirectoryIfExistsWithResponse#String-DataLakePathDeleteOptions-Duration-Context } - /** - * Code snippet for {@link DataLakeFileSystemClient#rename(String)} - */ - public void renameContainer() { - // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemClient.rename#String - DataLakeFileSystemClient fileSystemClient = client.rename("newFileSystemName"); - // END: com.azure.storage.file.datalake.DataLakeFileSystemClient.rename#String - } - - /** - * Code snippet for {@link DataLakeFileSystemClient#renameWithResponse(FileSystemRenameOptions, Duration, Context)} - */ - public void renameContainerWithResponse() { - // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemClient.renameWithResponse#FileSystemRenameOptions-Duration-Context - DataLakeRequestConditions requestConditions = new DataLakeRequestConditions().setLeaseId("lease-id"); - Context context = new Context("Key", "Value"); - - DataLakeFileSystemClient fileSystemClient = client.renameWithResponse( - new FileSystemRenameOptions("newFileSystemName") - .setRequestConditions(requestConditions), - Duration.ofSeconds(1), - context).getValue(); - // END: com.azure.storage.file.datalake.DataLakeFileSystemClient.renameWithResponse#FileSystemRenameOptions-Duration-Context - } +// /** +// * Code snippet for {@link DataLakeFileSystemClient#rename(String)} +// */ +// public void renameContainer() { +// // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemClient.rename#String +// DataLakeFileSystemClient fileSystemClient = client.rename("newFileSystemName"); +// // END: com.azure.storage.file.datalake.DataLakeFileSystemClient.rename#String +// } + +// /** +// * Code snippet for {@link DataLakeFileSystemClient#renameWithResponse(FileSystemRenameOptions, Duration, Context)} +// */ +// public void renameContainerWithResponse() { +// // BEGIN: com.azure.storage.file.datalake.DataLakeFileSystemClient.renameWithResponse#FileSystemRenameOptions-Duration-Context +// DataLakeRequestConditions requestConditions = new DataLakeRequestConditions().setLeaseId("lease-id"); +// Context context = new Context("Key", "Value"); +// +// DataLakeFileSystemClient fileSystemClient = client.renameWithResponse( +// new FileSystemRenameOptions("newFileSystemName") +// .setRequestConditions(requestConditions), +// Duration.ofSeconds(1), +// context).getValue(); +// // END: com.azure.storage.file.datalake.DataLakeFileSystemClient.renameWithResponse#FileSystemRenameOptions-Duration-Context +// } } diff --git a/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/FileSystemAPITest.groovy b/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/FileSystemAPITest.groovy index 186c9eefbd832..534751906fa25 100644 --- a/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/FileSystemAPITest.groovy +++ b/sdk/storage/azure-storage-file-datalake/src/test/java/com/azure/storage/file/datalake/FileSystemAPITest.groovy @@ -5,10 +5,6 @@ import com.azure.identity.DefaultAzureCredentialBuilder import com.azure.storage.blob.BlobUrlParts import com.azure.storage.blob.models.BlobErrorCode import com.azure.storage.common.Utility -import com.azure.storage.common.sas.AccountSasPermission -import com.azure.storage.common.sas.AccountSasResourceType -import com.azure.storage.common.sas.AccountSasService -import com.azure.storage.common.sas.AccountSasSignatureValues import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion import com.azure.storage.common.test.shared.extensions.PlaybackOnly import com.azure.storage.file.datalake.models.DataLakeAccessPolicy @@ -2759,115 +2755,115 @@ class FileSystemAPITest extends APISpec { response.getHeaders().getValue("x-ms-version") == "2019-02-02" } - def "Rename"() { - setup: - def newName = generateFileSystemName() - - when: - def renamedContainer = fsc.rename(newName) - - then: - renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 - - cleanup: - renamedContainer.delete() - } - - def "Rename sas"() { - setup: - def service = new AccountSasService() - .setBlobAccess(true) - def resourceType = new AccountSasResourceType() - .setContainer(true) - .setService(true) - .setObject(true) - def permissions = new AccountSasPermission() - .setReadPermission(true) - .setCreatePermission(true) - .setWritePermission(true) - .setDeletePermission(true) - def expiryTime = namer.getUtcNow().plusDays(1) - - def newName = generateFileSystemName() - def sasValues = new AccountSasSignatureValues(expiryTime, permissions, service, resourceType) - def sas = primaryDataLakeServiceClient.generateAccountSas(sasValues) - def sasClient = getFileSystemClient(sas, fsc.getFileSystemUrl()) - - when: - def renamedContainer = sasClient.rename(newName) - - then: - renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 - - cleanup: - renamedContainer.delete() - } - - @Unroll - def "Rename AC"() { - setup: - leaseID = setupFileSystemLeaseCondition(fsc, leaseID) - def cac = new DataLakeRequestConditions() - .setLeaseId(leaseID) - - expect: - fsc.renameWithResponse(new FileSystemRenameOptions(generateFileSystemName()).setRequestConditions(cac), - null, null).getStatusCode() == 200 - - where: - leaseID || _ - null || _ - receivedLeaseID || _ - } - - @Unroll - def "Rename AC fail"() { - setup: - def cac = new DataLakeRequestConditions() - .setLeaseId(leaseID) - - when: - fsc.renameWithResponse(new FileSystemRenameOptions(generateFileSystemName()).setRequestConditions(cac), - null, null) - - then: - thrown(DataLakeStorageException) - - where: - leaseID || _ - garbageLeaseID || _ - } - - @Unroll - def "Rename AC illegal"() { - setup: - def ac = new DataLakeRequestConditions().setIfMatch(match).setIfNoneMatch(noneMatch).setIfModifiedSince(modified).setIfUnmodifiedSince(unmodified) - - when: - fsc.renameWithResponse(new FileSystemRenameOptions(generateFileSystemName()).setRequestConditions(ac), - null, null) - - then: - thrown(UnsupportedOperationException) - - where: - modified | unmodified | match | noneMatch - oldDate | null | null | null - null | newDate | null | null - null | null | receivedEtag | null - null | null | null | garbageEtag - } - - def "Rename error"() { - setup: - fsc = primaryDataLakeServiceClient.getFileSystemClient(generateFileSystemName()) - def newName = generateFileSystemName() - - when: - fsc.rename(newName) - - then: - thrown(DataLakeStorageException) - } +// def "Rename"() { +// setup: +// def newName = generateFileSystemName() +// +// when: +// def renamedContainer = fsc.rename(newName) +// +// then: +// renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 +// +// cleanup: +// renamedContainer.delete() +// } + +// def "Rename sas"() { +// setup: +// def service = new AccountSasService() +// .setBlobAccess(true) +// def resourceType = new AccountSasResourceType() +// .setContainer(true) +// .setService(true) +// .setObject(true) +// def permissions = new AccountSasPermission() +// .setReadPermission(true) +// .setCreatePermission(true) +// .setWritePermission(true) +// .setDeletePermission(true) +// def expiryTime = namer.getUtcNow().plusDays(1) +// +// def newName = generateFileSystemName() +// def sasValues = new AccountSasSignatureValues(expiryTime, permissions, service, resourceType) +// def sas = primaryDataLakeServiceClient.generateAccountSas(sasValues) +// def sasClient = getFileSystemClient(sas, fsc.getFileSystemUrl()) +// +// when: +// def renamedContainer = sasClient.rename(newName) +// +// then: +// renamedContainer.getPropertiesWithResponse(null, null, null).getStatusCode() == 200 +// +// cleanup: +// renamedContainer.delete() +// } + +// @Unroll +// def "Rename AC"() { +// setup: +// leaseID = setupFileSystemLeaseCondition(fsc, leaseID) +// def cac = new DataLakeRequestConditions() +// .setLeaseId(leaseID) +// +// expect: +// fsc.renameWithResponse(new FileSystemRenameOptions(generateFileSystemName()).setRequestConditions(cac), +// null, null).getStatusCode() == 200 +// +// where: +// leaseID || _ +// null || _ +// receivedLeaseID || _ +// } + +// @Unroll +// def "Rename AC fail"() { +// setup: +// def cac = new DataLakeRequestConditions() +// .setLeaseId(leaseID) +// +// when: +// fsc.renameWithResponse(new FileSystemRenameOptions(generateFileSystemName()).setRequestConditions(cac), +// null, null) +// +// then: +// thrown(DataLakeStorageException) +// +// where: +// leaseID || _ +// garbageLeaseID || _ +// } + +// @Unroll +// def "Rename AC illegal"() { +// setup: +// def ac = new DataLakeRequestConditions().setIfMatch(match).setIfNoneMatch(noneMatch).setIfModifiedSince(modified).setIfUnmodifiedSince(unmodified) +// +// when: +// fsc.renameWithResponse(new FileSystemRenameOptions(generateFileSystemName()).setRequestConditions(ac), +// null, null) +// +// then: +// thrown(UnsupportedOperationException) +// +// where: +// modified | unmodified | match | noneMatch +// oldDate | null | null | null +// null | newDate | null | null +// null | null | receivedEtag | null +// null | null | null | garbageEtag +// } + +// def "Rename error"() { +// setup: +// fsc = primaryDataLakeServiceClient.getFileSystemClient(generateFileSystemName()) +// def newName = generateFileSystemName() +// +// when: +// fsc.rename(newName) +// +// then: +// thrown(DataLakeStorageException) +// } } diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRename.json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRename.json deleted file mode 100644 index 52608581eab0c..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRename.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/9e430f7009e430f7062c13438e2b13aaba587429a97a?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "d7e6dddb-b64a-4336-9b3c-31e9c304eac2" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F8AE8027B5", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:19 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "8ae425fc-701e-0011-4be1-bb4076000000", - "x-ms-client-request-id" : "d7e6dddb-b64a-4336-9b3c-31e9c304eac2", - "Date" : "Mon, 29 Aug 2022 19:57:19 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/9e430f7019e430f7062c05162298f0d55004b47fdba3?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "bb07a887-9bb8-4984-a12f-ab46c757d68c" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "6a71dfd4-001e-0079-49e1-bb26e6000000", - "x-ms-client-request-id" : "bb07a887-9bb8-4984-a12f-ab46c757d68c", - "Date" : "Mon, 29 Aug 2022 19:57:21 GMT" - }, - "Exception" : null - }, { - "Method" : "GET", - "Uri" : "https://REDACTED.blob.core.windows.net/9e430f7019e430f7062c05162298f0d55004b47fdba3?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "88b42515-ce1b-4c48-9e9d-9d08d5c05a13" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "x-ms-lease-status" : "unlocked", - "x-ms-immutable-storage-with-versioning-enabled" : "false", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-lease-state" : "available", - "x-ms-deny-encryption-scope-override" : "false", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:21 GMT", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-has-legal-hold" : "false", - "Date" : "Mon, 29 Aug 2022 19:57:21 GMT", - "x-ms-default-encryption-scope" : "$account-encryption-key", - "x-ms-has-immutability-policy" : "false", - "eTag" : "0x8DA89F8AF358579", - "x-ms-request-id" : "6a71e309-001e-0079-79e1-bb26e6000000", - "x-ms-client-request-id" : "88b42515-ce1b-4c48-9e9d-9d08d5c05a13" - }, - "Exception" : null - }, { - "Method" : "DELETE", - "Uri" : "https://REDACTED.blob.core.windows.net/9e430f7019e430f7062c05162298f0d55004b47fdba3?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "e2c8a93a-b503-478a-bab8-2a4c491acad0" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "202", - "x-ms-request-id" : "6a71e337-001e-0079-1be1-bb26e6000000", - "x-ms-client-request-id" : "e2c8a93a-b503-478a-bab8-2a4c491acad0", - "Date" : "Mon, 29 Aug 2022 19:57:21 GMT" - }, - "Exception" : null - } ], - "variables" : [ "9e430f7009e430f7062c13438e2b13aaba587429a97a", "9e430f7019e430f7062c05162298f0d55004b47fdba3" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACFail[0].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACFail[0].json deleted file mode 100644 index 3203dc7d6d1c6..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACFail[0].json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/582a149a0582a149ae9a09141ec1291250ed549048ed?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "064aec13-cd7f-4430-8968-4ea551494c29" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A1C4D90F554", - "Last-Modified" : "Tue, 30 Aug 2022 00:12:19 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "33a2b645-301e-0096-7105-bcd313000000", - "x-ms-client-request-id" : "064aec13-cd7f-4430-8968-4ea551494c29", - "Date" : "Tue, 30 Aug 2022 00:12:18 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/582a149a1582a149ae9a9787432ca1a15b6eb44348bb?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.2 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "878a8ef4-2add-4e7b-bcef-625d7b2d480e" - }, - "Response" : { - "content-length" : "252", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-error-code" : "LeaseNotPresentWithContainerOperation", - "retry-after" : "0", - "StatusCode" : "412", - "x-ms-request-id" : "33a2b6b1-301e-0096-4905-bcd313000000", - "Body" : "\nLeaseNotPresentWithContainerOperationThere is currently no lease on the container.\nRequestId:33a2b6b1-301e-0096-4905-bcd313000000\nTime:2022-08-30T00:12:20.6460978Z", - "x-ms-client-request-id" : "878a8ef4-2add-4e7b-bcef-625d7b2d480e", - "Date" : "Tue, 30 Aug 2022 00:12:19 GMT", - "Content-Type" : "application/xml" - }, - "Exception" : null - } ], - "variables" : [ "582a149a0582a149ae9a09141ec1291250ed549048ed", "582a149a1582a149ae9a9787432ca1a15b6eb44348bb" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[0].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[0].json deleted file mode 100644 index 49b483c3b266f..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[0].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/b3c569000b3c569009b71630094cc6c53ccfd4b2a887?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "b6cdb4d7-8aac-407c-8e0c-f62e61798eb8" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A04E18C3184", - "Last-Modified" : "Mon, 29 Aug 2022 21:24:39 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "80c97e31-101e-0091-06ed-bbbf70000000", - "x-ms-client-request-id" : "b6cdb4d7-8aac-407c-8e0c-f62e61798eb8", - "Date" : "Mon, 29 Aug 2022 21:24:39 GMT" - }, - "Exception" : null - } ], - "variables" : [ "b3c569000b3c569009b71630094cc6c53ccfd4b2a887", "b3c569001b3c569009b766375bc10d7920b9f4c4daff" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[1].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[1].json deleted file mode 100644 index a62a5a0f4b831..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[1].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/aade58410aade5841bad13201c3238934911f4b43a64?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "ebd06352-6172-4afa-ba40-944bc54cd8ee" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A04E18CB241", - "Last-Modified" : "Mon, 29 Aug 2022 21:24:39 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "4edfc84e-801e-0048-32ed-bbc7f5000000", - "x-ms-client-request-id" : "ebd06352-6172-4afa-ba40-944bc54cd8ee", - "Date" : "Mon, 29 Aug 2022 21:24:39 GMT" - }, - "Exception" : null - } ], - "variables" : [ "aade58410aade5841bad13201c3238934911f4b43a64", "aade58411aade5841bad822999176775eb90446079ff" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[2].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[2].json deleted file mode 100644 index 97edd1ed4e827..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[2].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/81f30b82081f30b82e0853448a1e20400c02c403585d?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "a76d8114-dc79-41e5-a08c-bc1c18620e69" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A04E18C8B09", - "Last-Modified" : "Mon, 29 Aug 2022 21:24:39 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "01ba6878-b01e-007c-63ed-bbf43d000000", - "x-ms-client-request-id" : "a76d8114-dc79-41e5-a08c-bc1c18620e69", - "Date" : "Mon, 29 Aug 2022 21:24:39 GMT" - }, - "Exception" : null - } ], - "variables" : [ "81f30b82081f30b82e0853448a1e20400c02c403585d", "81f30b82181f30b82e08106528c67c05289b747dd8b6" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[3].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[3].json deleted file mode 100644 index 4810fae19a298..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameACIllegal[3].json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/98e83ac3098e83ac311134268f5d73c90094b45d7ad8?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "5d4bbcce-fc3d-4d9b-bf23-7534b3a21a19" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A04E18C9FD2", - "Last-Modified" : "Mon, 29 Aug 2022 21:24:39 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "c7a6e2c4-a01e-0002-50ed-bb647a000000", - "x-ms-client-request-id" : "5d4bbcce-fc3d-4d9b-bf23-7534b3a21a19", - "Date" : "Mon, 29 Aug 2022 21:24:38 GMT" - }, - "Exception" : null - } ], - "variables" : [ "98e83ac3098e83ac311134268f5d73c90094b45d7ad8", "98e83ac3198e83ac3111104925f2c3c4c4a9d403aaa7" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[0].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[0].json deleted file mode 100644 index 24d6099f34ed8..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[0].json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/bccacf480bccacf48c0e7029900d3ce1aea314eccb54?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "b5536b22-7ffc-4903-9d15-19239202371b" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F8AE8019A5", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:19 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "25c29c90-401e-0078-1ce1-bb793a000000", - "x-ms-client-request-id" : "b5536b22-7ffc-4903-9d15-19239202371b", - "Date" : "Mon, 29 Aug 2022 19:57:19 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/bccacf481bccacf48c0e66951a2e9942b11af46909bf?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "9e0ff522-7300-45bb-afe0-4c2531affd9b" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "8ae42624-701e-0011-6de1-bb4076000000", - "x-ms-client-request-id" : "9e0ff522-7300-45bb-afe0-4c2531affd9b", - "Date" : "Mon, 29 Aug 2022 19:57:22 GMT" - }, - "Exception" : null - } ], - "variables" : [ "bccacf480bccacf48c0e7029900d3ce1aea314eccb54", "bccacf481bccacf48c0e66951a2e9942b11af46909bf" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[1].json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[1].json deleted file mode 100644 index 34710ac1eb0be..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameAC[1].json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a5d1fe090a5d1fe092eb94985f0f908a9a3e341dab89?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "4904ca6b-88e7-4786-bf29-71ad24f68c07" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F8AE801998", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:19 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "e7c5ceb7-001e-0024-17e1-bb2c62000000", - "x-ms-client-request-id" : "4904ca6b-88e7-4786-bf29-71ad24f68c07", - "Date" : "Mon, 29 Aug 2022 19:57:19 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a5d1fe090a5d1fe092eb94985f0f908a9a3e341dab89?comp=lease&restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "7f245f37-4ee1-48ed-bc12-428b0e8584a1" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-lease-id" : "30d82a3e-9c3e-450a-80b0-de27fa647c9e", - "eTag" : "0x8DA89F8AE801998", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:19 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "25c29d47-401e-0078-43e1-bb793a000000", - "x-ms-client-request-id" : "7f245f37-4ee1-48ed-bc12-428b0e8584a1", - "Date" : "Mon, 29 Aug 2022 19:57:19 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a5d1fe091a5d1fe092eb62165abedde9166b0495793f?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "76838022-df18-4ecc-85a1-dc9ac8014645" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "25c29d84-401e-0078-7ee1-bb793a000000", - "x-ms-client-request-id" : "76838022-df18-4ecc-85a1-dc9ac8014645", - "Date" : "Mon, 29 Aug 2022 19:57:21 GMT" - }, - "Exception" : null - } ], - "variables" : [ "a5d1fe090a5d1fe092eb94985f0f908a9a3e341dab89", "a5d1fe091a5d1fe092eb62165abedde9166b0495793f" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameError.json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameError.json deleted file mode 100644 index c829cc5621499..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameError.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a0b3f7e10a0b3f7e16fc3973528e758338af7497abe3?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "1727a741-3f26-4daa-8c71-eacc4c2c1280" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA8A00390B9876", - "Last-Modified" : "Mon, 29 Aug 2022 20:51:18 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "77b6774d-901e-006b-24e9-bb5d36000000", - "x-ms-client-request-id" : "1727a741-3f26-4daa-8c71-eacc4c2c1280", - "Date" : "Mon, 29 Aug 2022 20:51:18 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/a0b3f7e12a0b3f7e16fc3454491209e95887d4124829?restype=container&comp=rename", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "ecb76597-553c-4b06-a32d-1c1cbecaf40a" - }, - "Response" : { - "content-length" : "226", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-error-code" : "ContainerNotFound", - "retry-after" : "0", - "StatusCode" : "404", - "x-ms-request-id" : "77b677b7-901e-006b-01e9-bb5d36000000", - "Body" : "\nContainerNotFoundThe specified container does not exist.\nRequestId:77b677b7-901e-006b-01e9-bb5d36000000\nTime:2022-08-29T20:51:21.5794976Z", - "x-ms-client-request-id" : "ecb76597-553c-4b06-a32d-1c1cbecaf40a", - "Date" : "Mon, 29 Aug 2022 20:51:20 GMT", - "Content-Type" : "application/xml" - }, - "Exception" : null - } ], - "variables" : [ "a0b3f7e10a0b3f7e16fc3973528e758338af7497abe3", "a0b3f7e11a0b3f7e16fc78352d6b2504480e14e35b4c", "a0b3f7e12a0b3f7e16fc3454491209e95887d4124829" ] -} \ No newline at end of file diff --git a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameSas.json b/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameSas.json deleted file mode 100644 index 2fd92f0082cf3..0000000000000 --- a/sdk/storage/azure-storage-file-datalake/src/test/resources/session-records/FileSystemAPITestRenameSas.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "networkCallRecords" : [ { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/18ed57b0018ed57b0ae592810fa6a3ff132ea4d68a50?restype=container", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "1f494353-a756-4a73-a78f-17104a9e5c5e" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "eTag" : "0x8DA89F8AE82713C", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:19 GMT", - "retry-after" : "0", - "StatusCode" : "201", - "x-ms-request-id" : "6a71df75-001e-0079-7ee1-bb26e6000000", - "x-ms-client-request-id" : "1f494353-a756-4a73-a78f-17104a9e5c5e", - "Date" : "Mon, 29 Aug 2022 19:57:19 GMT" - }, - "Exception" : null - }, { - "Method" : "PUT", - "Uri" : "https://REDACTED.blob.core.windows.net/18ed57b0118ed57b0ae595216958861d123b24f579cb?restype=container&comp=rename&sv=2021-10-04&ss=b&srt=sco&se=2022-08-30T19%3A57%3A19Z&sp=rwdc&sig=REDACTED", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-blob/12.20.0-beta.2 azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "0f1780e2-ff86-4006-8e74-fa0b20feba2d" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-request-id" : "e7c5cf02-001e-0024-55e1-bb2c62000000", - "x-ms-client-request-id" : "0f1780e2-ff86-4006-8e74-fa0b20feba2d", - "Date" : "Mon, 29 Aug 2022 19:57:20 GMT" - }, - "Exception" : null - }, { - "Method" : "GET", - "Uri" : "https://REDACTED.blob.core.windows.net/18ed57b0118ed57b0ae595216958861d123b24f579cb?restype=container&sv=2021-10-04&ss=b&srt=sco&se=2022-08-30T19%3A57%3A19Z&sp=rwdc&sig=REDACTED", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "46ca0b66-7c1d-4bc8-88e8-7f8e0e252c40" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "x-ms-lease-status" : "unlocked", - "x-ms-immutable-storage-with-versioning-enabled" : "false", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "x-ms-lease-state" : "available", - "x-ms-deny-encryption-scope-override" : "false", - "Last-Modified" : "Mon, 29 Aug 2022 19:57:20 GMT", - "retry-after" : "0", - "StatusCode" : "200", - "x-ms-has-legal-hold" : "false", - "Date" : "Mon, 29 Aug 2022 19:57:20 GMT", - "x-ms-default-encryption-scope" : "$account-encryption-key", - "x-ms-has-immutability-policy" : "false", - "eTag" : "0x8DA89F8AEB89E96", - "x-ms-request-id" : "e7c5cfa2-001e-0024-63e1-bb2c62000000", - "x-ms-client-request-id" : "46ca0b66-7c1d-4bc8-88e8-7f8e0e252c40" - }, - "Exception" : null - }, { - "Method" : "DELETE", - "Uri" : "https://REDACTED.blob.core.windows.net/18ed57b0118ed57b0ae595216958861d123b24f579cb?restype=container&sv=2021-10-04&ss=b&srt=sco&se=2022-08-30T19%3A57%3A19Z&sp=rwdc&sig=REDACTED", - "Headers" : { - "x-ms-version" : "2021-10-04", - "User-Agent" : "azsdk-java-azure-storage-file-datalake/12.13.0-beta.1 (11.0.14; Windows 10; 10.0)", - "x-ms-client-request-id" : "34939e24-622d-4c61-91e7-bacb34f20cf5" - }, - "Response" : { - "content-length" : "0", - "x-ms-version" : "2021-10-04", - "Server" : "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0", - "retry-after" : "0", - "StatusCode" : "202", - "x-ms-request-id" : "e7c5cfc2-001e-0024-7ee1-bb2c62000000", - "x-ms-client-request-id" : "34939e24-622d-4c61-91e7-bacb34f20cf5", - "Date" : "Mon, 29 Aug 2022 19:57:20 GMT" - }, - "Exception" : null - } ], - "variables" : [ "18ed57b0018ed57b0ae592810fa6a3ff132ea4d68a50", "2022-08-29T19:57:19.965086800Z", "18ed57b0118ed57b0ae595216958861d123b24f579cb" ] -} \ No newline at end of file From 66ff586ddde84aa328884bc79e9014a1039424b6 Mon Sep 17 00:00:00 2001 From: Alan Zimmer <48699787+alzimmermsft@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:20:11 -0400 Subject: [PATCH 04/18] Prepare Core Libraries for September 2022 Release (#30745) Prepare Core Libraries for September 2022 Release --- eng/jacoco-test-coverage/pom.xml | 14 +++++++------- eng/versioning/version_client.txt | 18 +++++++++--------- .../azure-data-appconfiguration/pom.xml | 2 +- sdk/core/azure-core-amqp-experimental/pom.xml | 2 +- sdk/core/azure-core-amqp/CHANGELOG.md | 8 ++++---- sdk/core/azure-core-amqp/README.md | 2 +- sdk/core/azure-core-amqp/pom.xml | 6 +++--- sdk/core/azure-core-experimental/CHANGELOG.md | 10 ++++------ sdk/core/azure-core-experimental/README.md | 2 +- sdk/core/azure-core-experimental/pom.xml | 2 +- .../azure-core-http-jdk-httpclient/pom.xml | 8 ++++---- sdk/core/azure-core-http-netty/CHANGELOG.md | 13 ++++++++----- sdk/core/azure-core-http-netty/README.md | 2 +- sdk/core/azure-core-http-netty/pom.xml | 10 +++++----- sdk/core/azure-core-http-okhttp/CHANGELOG.md | 10 ++++------ sdk/core/azure-core-http-okhttp/README.md | 2 +- sdk/core/azure-core-http-okhttp/pom.xml | 10 +++++----- sdk/core/azure-core-http-vertx/pom.xml | 8 ++++---- sdk/core/azure-core-jackson-tests/pom.xml | 4 ++-- sdk/core/azure-core-management/CHANGELOG.md | 8 ++++---- sdk/core/azure-core-management/README.md | 2 +- sdk/core/azure-core-management/pom.xml | 6 +++--- .../azure-core-metrics-opentelemetry/pom.xml | 6 +++--- sdk/core/azure-core-perf/pom.xml | 6 +++--- .../CHANGELOG.md | 10 ++++------ .../README.md | 2 +- .../azure-core-serializer-avro-apache/pom.xml | 2 +- .../azure-core-serializer-avro-jackson/pom.xml | 2 +- .../CHANGELOG.md | 11 +++++------ .../azure-core-serializer-json-gson/README.md | 2 +- .../azure-core-serializer-json-gson/pom.xml | 4 ++-- .../CHANGELOG.md | 10 ++++------ .../README.md | 2 +- .../azure-core-serializer-json-jackson/pom.xml | 4 ++-- sdk/core/azure-core-test/CHANGELOG.md | 10 ++++++---- sdk/core/azure-core-test/README.md | 2 +- sdk/core/azure-core-test/pom.xml | 4 ++-- .../CHANGELOG.md | 10 ++++------ .../azure-core-tracing-opentelemetry/README.md | 2 +- .../azure-core-tracing-opentelemetry/pom.xml | 6 +++--- sdk/core/azure-core/CHANGELOG.md | 14 +++++++++----- sdk/core/azure-core/README.md | 2 +- sdk/core/azure-core/pom.xml | 2 +- sdk/e2e/pom.xml | 6 +++--- 44 files changed, 133 insertions(+), 135 deletions(-) diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 5ea3d5bde7174..79f6594a913f1 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -123,12 +123,12 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure azure-core-amqp - 2.8.0-beta.1 + 2.7.1 com.azure @@ -148,17 +148,17 @@ com.azure azure-core-http-netty - 1.13.0-beta.1 + 1.12.5 com.azure azure-core-http-okhttp - 1.12.0-beta.1 + 1.11.2 com.azure azure-core-management - 1.8.0-beta.1 + 1.8.0 com.azure @@ -168,12 +168,12 @@ com.azure azure-core-serializer-json-gson - 1.2.0-beta.1 + 1.1.20 com.azure azure-core-serializer-json-jackson - 1.3.0-beta.1 + 1.2.21 com.azure diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 750ecf348eef1..e397040b23ac4 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -69,22 +69,22 @@ com.azure:azure-communication-jobrouter;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-communication-rooms;1.0.0-beta.2;1.0.0-beta.3 com.azure:azure-containers-containerregistry;1.0.7;1.1.0-beta.2 com.azure:azure-containers-containerregistry-perf;1.0.0-beta.1;1.0.0-beta.1 -com.azure:azure-core;1.31.0;1.32.0-beta.1 -com.azure:azure-core-amqp;2.7.0;2.8.0-beta.1 +com.azure:azure-core;1.31.0;1.32.0 +com.azure:azure-core-amqp;2.7.0;2.7.1 com.azure:azure-core-amqp-experimental;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-core-experimental;1.0.0-beta.31;1.0.0-beta.32 com.azure:azure-core-http-jdk-httpclient;1.0.0-beta.1;1.0.0-beta.1 -com.azure:azure-core-http-netty;1.12.4;1.13.0-beta.1 -com.azure:azure-core-http-okhttp;1.11.1;1.12.0-beta.1 +com.azure:azure-core-http-netty;1.12.4;1.12.5 +com.azure:azure-core-http-okhttp;1.11.1;1.11.2 com.azure:azure-core-http-vertx;1.0.0-beta.1;1.0.0-beta.1 -com.azure:azure-core-management;1.7.1;1.8.0-beta.1 +com.azure:azure-core-management;1.7.1;1.8.0 com.azure:azure-core-metrics-opentelemetry;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-core-perf;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-core-serializer-avro-apache;1.0.0-beta.27;1.0.0-beta.28 com.azure:azure-core-serializer-avro-jackson;1.0.0-beta.1;1.0.0-beta.2 -com.azure:azure-core-serializer-json-gson;1.1.19;1.2.0-beta.1 -com.azure:azure-core-serializer-json-jackson;1.2.20;1.3.0-beta.1 -com.azure:azure-core-test;1.11.0;1.12.0-beta.1 +com.azure:azure-core-serializer-json-gson;1.1.19;1.1.20 +com.azure:azure-core-serializer-json-jackson;1.2.20;1.2.21 +com.azure:azure-core-test;1.11.0;1.12.0 com.azure:azure-core-tracing-opentelemetry;1.0.0-beta.27;1.0.0-beta.28 com.azure:azure-cosmos;4.35.1;4.36.0-beta.1 com.azure:azure-cosmos-benchmark;4.0.1-beta.1;4.0.1-beta.1 @@ -383,7 +383,7 @@ com.azure.tools:azure-sdk-build-tool;1.0.0-beta.1;1.0.0-beta.2 # In the pom, the version update tag after the version should name the unreleased package and the dependency version: # unreleased_com.azure:azure-ai-formrecognizer;4.0.0-beta.7 -unreleased_com.azure:azure-core;1.32.0-beta.1 +unreleased_com.azure:azure-core;1.32.0 # Released Beta dependencies: Copy the entry from above, prepend "beta_", remove the current # version and set the version to the released beta. Released beta dependencies are only valid diff --git a/sdk/appconfiguration/azure-data-appconfiguration/pom.xml b/sdk/appconfiguration/azure-data-appconfiguration/pom.xml index 96b78ceb22cd9..ecd450784bf3d 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/pom.xml +++ b/sdk/appconfiguration/azure-data-appconfiguration/pom.xml @@ -45,7 +45,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure diff --git a/sdk/core/azure-core-amqp-experimental/pom.xml b/sdk/core/azure-core-amqp-experimental/pom.xml index 1ce99d2d91d6b..112fe69318d40 100644 --- a/sdk/core/azure-core-amqp-experimental/pom.xml +++ b/sdk/core/azure-core-amqp-experimental/pom.xml @@ -61,7 +61,7 @@ com.azure azure-core-amqp - 2.8.0-beta.1 + 2.7.1 diff --git a/sdk/core/azure-core-amqp/CHANGELOG.md b/sdk/core/azure-core-amqp/CHANGELOG.md index 777992aca9c93..8278f7efa7165 100644 --- a/sdk/core/azure-core-amqp/CHANGELOG.md +++ b/sdk/core/azure-core-amqp/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.8.0-beta.1 (Unreleased) +## 2.7.1 (2022-09-01) ### Features Added @@ -8,11 +8,11 @@ error counters. Metrics are off by default and can be enabled with [azure-core-metrics-opentelemetry](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/core/azure-core-metrics-opentelemetry/README.md) plugin. ([#30583](https://github.com/Azure/azure-sdk-for-java/pull/30583)) -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded Reactor from `3.4.21` to `3.4.22`. ## 2.7.0 (2022-08-05) diff --git a/sdk/core/azure-core-amqp/README.md b/sdk/core/azure-core-amqp/README.md index ea42b719663d6..2423823baa2cd 100644 --- a/sdk/core/azure-core-amqp/README.md +++ b/sdk/core/azure-core-amqp/README.md @@ -48,7 +48,7 @@ add the direct dependency to your project as follows. com.azure azure-core-amqp - 2.7.0 + 2.7.1 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-amqp/pom.xml b/sdk/core/azure-core-amqp/pom.xml index 0457cf15f150a..47458934a831d 100644 --- a/sdk/core/azure-core-amqp/pom.xml +++ b/sdk/core/azure-core-amqp/pom.xml @@ -14,7 +14,7 @@ com.azure azure-core-amqp - 2.8.0-beta.1 + 2.7.1 jar Microsoft Azure Java Core AMQP Library @@ -67,7 +67,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.microsoft.azure @@ -114,7 +114,7 @@ com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test diff --git a/sdk/core/azure-core-experimental/CHANGELOG.md b/sdk/core/azure-core-experimental/CHANGELOG.md index 3466a708f1c19..6726982af50eb 100644 --- a/sdk/core/azure-core-experimental/CHANGELOG.md +++ b/sdk/core/azure-core-experimental/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.0.0-beta.32 (Unreleased) +## 1.0.0-beta.32 (2022-09-01) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. ## 1.0.0-beta.31 (2022-08-05) diff --git a/sdk/core/azure-core-experimental/README.md b/sdk/core/azure-core-experimental/README.md index a77a8296ebb40..f189e1578bb2d 100644 --- a/sdk/core/azure-core-experimental/README.md +++ b/sdk/core/azure-core-experimental/README.md @@ -17,7 +17,7 @@ Azure Core Experimental contains types that are being evaluated and might eventu com.azure azure-core-experimental - 1.0.0-beta.31 + 1.0.0-beta.32 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-experimental/pom.xml b/sdk/core/azure-core-experimental/pom.xml index 76d4bf8f2803b..90c72402e6739 100644 --- a/sdk/core/azure-core-experimental/pom.xml +++ b/sdk/core/azure-core-experimental/pom.xml @@ -67,7 +67,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 diff --git a/sdk/core/azure-core-http-jdk-httpclient/pom.xml b/sdk/core/azure-core-http-jdk-httpclient/pom.xml index fe2c8c7171dc5..7b2d5906d0e71 100644 --- a/sdk/core/azure-core-http-jdk-httpclient/pom.xml +++ b/sdk/core/azure-core-http-jdk-httpclient/pom.xml @@ -71,27 +71,27 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure azure-core - 1.32.0-beta.1 + 1.32.0 test-jar test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test-jar test diff --git a/sdk/core/azure-core-http-netty/CHANGELOG.md b/sdk/core/azure-core-http-netty/CHANGELOG.md index f4d0320edeb3c..57b0651bd9b11 100644 --- a/sdk/core/azure-core-http-netty/CHANGELOG.md +++ b/sdk/core/azure-core-http-netty/CHANGELOG.md @@ -1,15 +1,18 @@ # Release History -## 1.13.0-beta.1 (Unreleased) - -### Features Added - -### Breaking Changes +## 1.12.5 (2022-09-01) ### Bugs Fixed +- Fixed a bug where `HttpResponse.writeBodyTo` could leak `ByteBuf`s. ([#30670](https://github.com/Azure/azure-sdk-for-java/pull/30670)) + ### Other Changes +#### Dependency Updates + +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. +- Upgraded Reactor Netty from `1.0.21` to `1.0.22`. + ## 1.12.4 (2022-08-05) ### Other Changes diff --git a/sdk/core/azure-core-http-netty/README.md b/sdk/core/azure-core-http-netty/README.md index 108cd84aa89fb..092e4843af566 100644 --- a/sdk/core/azure-core-http-netty/README.md +++ b/sdk/core/azure-core-http-netty/README.md @@ -47,7 +47,7 @@ add the direct dependency to your project as follows. com.azure azure-core-http-netty - 1.12.4 + 1.12.5 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-http-netty/pom.xml b/sdk/core/azure-core-http-netty/pom.xml index e6b6a81f2664c..75217843ef2bf 100644 --- a/sdk/core/azure-core-http-netty/pom.xml +++ b/sdk/core/azure-core-http-netty/pom.xml @@ -15,7 +15,7 @@ com.azure azure-core-http-netty jar - 1.13.0-beta.1 + 1.12.5 Microsoft Azure Netty HTTP Client Library This package contains the Netty HTTP client plugin for azure-core. @@ -68,7 +68,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 @@ -132,20 +132,20 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 test-jar test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test-jar test diff --git a/sdk/core/azure-core-http-okhttp/CHANGELOG.md b/sdk/core/azure-core-http-okhttp/CHANGELOG.md index 72055ccd7fdec..4481a08e6f58e 100644 --- a/sdk/core/azure-core-http-okhttp/CHANGELOG.md +++ b/sdk/core/azure-core-http-okhttp/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.12.0-beta.1 (Unreleased) +## 1.11.2 (2022-09-01) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. ## 1.11.1 (2022-08-05) diff --git a/sdk/core/azure-core-http-okhttp/README.md b/sdk/core/azure-core-http-okhttp/README.md index 9f1760c9e1b1c..af353cd309cf6 100644 --- a/sdk/core/azure-core-http-okhttp/README.md +++ b/sdk/core/azure-core-http-okhttp/README.md @@ -47,7 +47,7 @@ add the direct dependency to your project as follows. com.azure azure-core-http-okhttp - 1.11.1 + 1.11.2 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-http-okhttp/pom.xml b/sdk/core/azure-core-http-okhttp/pom.xml index c2d29e60f4a09..b6cc46998c90a 100644 --- a/sdk/core/azure-core-http-okhttp/pom.xml +++ b/sdk/core/azure-core-http-okhttp/pom.xml @@ -15,7 +15,7 @@ com.azure azure-core-http-okhttp jar - 1.12.0-beta.1 + 1.11.2 Microsoft Azure OkHttp HTTP Client Library This package contains the OkHttp HTTP client plugin for azure-core. @@ -67,7 +67,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 @@ -80,20 +80,20 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 test-jar test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test-jar test diff --git a/sdk/core/azure-core-http-vertx/pom.xml b/sdk/core/azure-core-http-vertx/pom.xml index 30b40f9bf9243..b60267e91fad1 100644 --- a/sdk/core/azure-core-http-vertx/pom.xml +++ b/sdk/core/azure-core-http-vertx/pom.xml @@ -67,7 +67,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 @@ -88,20 +88,20 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 test-jar test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test-jar test diff --git a/sdk/core/azure-core-jackson-tests/pom.xml b/sdk/core/azure-core-jackson-tests/pom.xml index 4327a8f61da5e..35deaa715d15b 100644 --- a/sdk/core/azure-core-jackson-tests/pom.xml +++ b/sdk/core/azure-core-jackson-tests/pom.xml @@ -51,14 +51,14 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 test com.azure azure-core - 1.32.0-beta.1 + 1.32.0 tests test-jar test diff --git a/sdk/core/azure-core-management/CHANGELOG.md b/sdk/core/azure-core-management/CHANGELOG.md index 7eb58362190fa..ccc86fb5bd41b 100644 --- a/sdk/core/azure-core-management/CHANGELOG.md +++ b/sdk/core/azure-core-management/CHANGELOG.md @@ -1,16 +1,16 @@ # Release History -## 1.8.0-beta.1 (Unreleased) +## 1.8.0 (2022-09-01) ### Features Added - Added new Azure region `Region.QATAR_CENTRAL`. -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. ## 1.7.1 (2022-08-05) diff --git a/sdk/core/azure-core-management/README.md b/sdk/core/azure-core-management/README.md index 7e06b1d6cfa71..6ede85cc687b0 100644 --- a/sdk/core/azure-core-management/README.md +++ b/sdk/core/azure-core-management/README.md @@ -15,7 +15,7 @@ Azure Core Management library is a collection of classes common to the [Azure Re com.azure azure-core-management - 1.7.1 + 1.8.0 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-management/pom.xml b/sdk/core/azure-core-management/pom.xml index 2ac1b492101ce..b6ab8464fe132 100644 --- a/sdk/core/azure-core-management/pom.xml +++ b/sdk/core/azure-core-management/pom.xml @@ -13,7 +13,7 @@ com.azure azure-core-management - 1.8.0-beta.1 + 1.8.0 jar Microsoft Azure Management Java Core Library @@ -65,7 +65,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 @@ -89,7 +89,7 @@ com.azure azure-core-http-netty - 1.13.0-beta.1 + 1.12.5 test diff --git a/sdk/core/azure-core-metrics-opentelemetry/pom.xml b/sdk/core/azure-core-metrics-opentelemetry/pom.xml index 8957fca6d97e7..ce5c51d63ff4a 100644 --- a/sdk/core/azure-core-metrics-opentelemetry/pom.xml +++ b/sdk/core/azure-core-metrics-opentelemetry/pom.xml @@ -40,7 +40,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.google.code.findbugs @@ -67,13 +67,13 @@ com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test com.azure azure-core-http-netty - 1.13.0-beta.1 + 1.12.5 test diff --git a/sdk/core/azure-core-perf/pom.xml b/sdk/core/azure-core-perf/pom.xml index 35bd7e91aadce..ac7b085019c6c 100644 --- a/sdk/core/azure-core-perf/pom.xml +++ b/sdk/core/azure-core-perf/pom.xml @@ -21,17 +21,17 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure azure-core-http-netty - 1.13.0-beta.1 + 1.12.5 com.azure azure-core-http-okhttp - 1.12.0-beta.1 + 1.11.2 com.azure diff --git a/sdk/core/azure-core-serializer-avro-apache/CHANGELOG.md b/sdk/core/azure-core-serializer-avro-apache/CHANGELOG.md index fd6972f4ee0c6..c82a302005b68 100644 --- a/sdk/core/azure-core-serializer-avro-apache/CHANGELOG.md +++ b/sdk/core/azure-core-serializer-avro-apache/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.0.0-beta.28 (Unreleased) +## 1.0.0-beta.28 (2022-09-01) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. ## 1.0.0-beta.27 (2022-08-05) diff --git a/sdk/core/azure-core-serializer-avro-apache/README.md b/sdk/core/azure-core-serializer-avro-apache/README.md index 9769551f6e3f0..29a298c8afe63 100644 --- a/sdk/core/azure-core-serializer-avro-apache/README.md +++ b/sdk/core/azure-core-serializer-avro-apache/README.md @@ -15,7 +15,7 @@ Azure Core Apache Avro Serializer is a plugin for the `azure-core` `AvroSerializ com.azure azure-core-serializer-avro-apache - 1.0.0-beta.27 + 1.0.0-beta.28 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-serializer-avro-apache/pom.xml b/sdk/core/azure-core-serializer-avro-apache/pom.xml index 46d5192f8faea..97fdc1deb5390 100644 --- a/sdk/core/azure-core-serializer-avro-apache/pom.xml +++ b/sdk/core/azure-core-serializer-avro-apache/pom.xml @@ -66,7 +66,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure diff --git a/sdk/core/azure-core-serializer-avro-jackson/pom.xml b/sdk/core/azure-core-serializer-avro-jackson/pom.xml index da34441ba5382..286b6e99a1b42 100644 --- a/sdk/core/azure-core-serializer-avro-jackson/pom.xml +++ b/sdk/core/azure-core-serializer-avro-jackson/pom.xml @@ -63,7 +63,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure diff --git a/sdk/core/azure-core-serializer-json-gson/CHANGELOG.md b/sdk/core/azure-core-serializer-json-gson/CHANGELOG.md index 7171c58edaa6f..7099646327c04 100644 --- a/sdk/core/azure-core-serializer-json-gson/CHANGELOG.md +++ b/sdk/core/azure-core-serializer-json-gson/CHANGELOG.md @@ -1,14 +1,13 @@ # Release History -## 1.2.0-beta.1 (Unreleased) +## 1.1.20 (2022-09-01) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. +- Upgraded GSON from `2.9.0` to `2.9.1`. ## 1.1.19 (2022-08-05) diff --git a/sdk/core/azure-core-serializer-json-gson/README.md b/sdk/core/azure-core-serializer-json-gson/README.md index c87e4533d32d0..936a2dba253d0 100644 --- a/sdk/core/azure-core-serializer-json-gson/README.md +++ b/sdk/core/azure-core-serializer-json-gson/README.md @@ -47,7 +47,7 @@ add the direct dependency to your project as follows. com.azure azure-core-serializer-json-gson - 1.1.19 + 1.1.20 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-serializer-json-gson/pom.xml b/sdk/core/azure-core-serializer-json-gson/pom.xml index efc178fd0ef83..e255f9c4959eb 100644 --- a/sdk/core/azure-core-serializer-json-gson/pom.xml +++ b/sdk/core/azure-core-serializer-json-gson/pom.xml @@ -15,7 +15,7 @@ com.azure azure-core-serializer-json-gson jar - 1.2.0-beta.1 + 1.1.20 Microsoft Azure Gson JSON Serializer Library This package contains the Gson JSON serializer client plugin for azure-core. @@ -65,7 +65,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 diff --git a/sdk/core/azure-core-serializer-json-jackson/CHANGELOG.md b/sdk/core/azure-core-serializer-json-jackson/CHANGELOG.md index 0037e08a3694d..41502793a3d22 100644 --- a/sdk/core/azure-core-serializer-json-jackson/CHANGELOG.md +++ b/sdk/core/azure-core-serializer-json-jackson/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.3.0-beta.1 (Unreleased) +## 1.2.21 (2022-09-01) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. ## 1.2.20 (2022-08-05) diff --git a/sdk/core/azure-core-serializer-json-jackson/README.md b/sdk/core/azure-core-serializer-json-jackson/README.md index 82c4311869b30..cbbc938d7ea4a 100644 --- a/sdk/core/azure-core-serializer-json-jackson/README.md +++ b/sdk/core/azure-core-serializer-json-jackson/README.md @@ -47,7 +47,7 @@ add the direct dependency to your project as follows. com.azure azure-core-serializer-json-jackson - 1.2.20 + 1.2.21 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-serializer-json-jackson/pom.xml b/sdk/core/azure-core-serializer-json-jackson/pom.xml index 038b6d7f08e46..69648dde786f7 100644 --- a/sdk/core/azure-core-serializer-json-jackson/pom.xml +++ b/sdk/core/azure-core-serializer-json-jackson/pom.xml @@ -15,7 +15,7 @@ com.azure azure-core-serializer-json-jackson jar - 1.3.0-beta.1 + 1.2.21 Microsoft Azure Jackson JSON Serializer Library This package contains the Jackson JSON serializer client plugin for azure-core. @@ -66,7 +66,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 diff --git a/sdk/core/azure-core-test/CHANGELOG.md b/sdk/core/azure-core-test/CHANGELOG.md index ade69cb69a741..6e584124c72f9 100644 --- a/sdk/core/azure-core-test/CHANGELOG.md +++ b/sdk/core/azure-core-test/CHANGELOG.md @@ -1,15 +1,17 @@ # Release History -## 1.12.0-beta.1 (Unreleased) +## 1.12.0 (2022-09-01) ### Features Added -### Breaking Changes - -### Bugs Fixed +- Added metrics-based testing utilities. ### Other Changes +#### Dependency Updates + +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. + ## 1.11.0 (2022-08-05) ### Features Added diff --git a/sdk/core/azure-core-test/README.md b/sdk/core/azure-core-test/README.md index 05e7e49a5a27d..5e52a9d146e42 100644 --- a/sdk/core/azure-core-test/README.md +++ b/sdk/core/azure-core-test/README.md @@ -18,7 +18,7 @@ To use this package, add the following to your _pom.xml_. com.azure azure-core-test - 1.11.0 + 1.12.0 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-test/pom.xml b/sdk/core/azure-core-test/pom.xml index 4ece63584f392..aae72097432e6 100644 --- a/sdk/core/azure-core-test/pom.xml +++ b/sdk/core/azure-core-test/pom.xml @@ -13,7 +13,7 @@ com.azure azure-core-test jar - 1.12.0-beta.1 + 1.12.0 Microsoft Azure Java Core Test Library This package contains core test types for Azure Java clients. @@ -53,7 +53,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 diff --git a/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md b/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md index ee58ab12267a0..b639647a86429 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md +++ b/sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.0.0-beta.28 (Unreleased) +## 1.0.0-beta.28 (2022-09-01) -### Features Added - -### Breaking Changes +### Other Changes -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded `azure-core` from `1.31.0` to `1.32.0`. ## 1.0.0-beta.27 (2022-08-05) diff --git a/sdk/core/azure-core-tracing-opentelemetry/README.md b/sdk/core/azure-core-tracing-opentelemetry/README.md index 2d1e40d50e87b..d2ade85ec5495 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/README.md +++ b/sdk/core/azure-core-tracing-opentelemetry/README.md @@ -51,7 +51,7 @@ To enable Azure SDK tracing, add the latest `com.azure:azure-core-tracing-opente com.azure azure-core-tracing-opentelemetry - 1.0.0-beta.27 + 1.0.0-beta.28 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core-tracing-opentelemetry/pom.xml b/sdk/core/azure-core-tracing-opentelemetry/pom.xml index 9aea806264d94..d2010dcbf74cb 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/pom.xml +++ b/sdk/core/azure-core-tracing-opentelemetry/pom.xml @@ -50,7 +50,7 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.google.code.findbugs @@ -69,13 +69,13 @@ com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test com.azure azure-core-http-netty - 1.13.0-beta.1 + 1.12.5 test diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 24b6579649129..3861d123bfdf7 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -1,16 +1,20 @@ # Release History -## 1.32.0-beta.1 (Unreleased) +## 1.32.0 (2022-09-01) +### Features Added + +- Added new constructor overloads to `PagedIterable` and introduced `PageRetrieverSync`. - Added `com.azure.core.util.metrics.LongGauge` instrument support to metrics. +- Added `CoreUtils.stringJoin` which optimizes `String.join` for small `List`s. -### Features Added +### Other Changes -### Breaking Changes +- Miscellaneous performance improvements. -### Bugs Fixed +#### Dependency Updates -### Other Changes +- Upgraded Reactor from `3.4.2` to `3.4.22`. ## 1.31.0 (2022-08-05) diff --git a/sdk/core/azure-core/README.md b/sdk/core/azure-core/README.md index e051caf8b628e..7aa94df9a6249 100644 --- a/sdk/core/azure-core/README.md +++ b/sdk/core/azure-core/README.md @@ -58,7 +58,7 @@ add the direct dependency to your project as follows. com.azure azure-core - 1.31.0 + 1.32.0 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/core/azure-core/pom.xml b/sdk/core/azure-core/pom.xml index 1029a9a43094f..82c3700cea0e5 100644 --- a/sdk/core/azure-core/pom.xml +++ b/sdk/core/azure-core/pom.xml @@ -15,7 +15,7 @@ com.azure azure-core jar - 1.32.0-beta.1 + 1.32.0 Microsoft Azure Java Core Library This package contains core types for Azure Java clients. diff --git a/sdk/e2e/pom.xml b/sdk/e2e/pom.xml index b530f81560a3d..8c8104cf6ed2e 100644 --- a/sdk/e2e/pom.xml +++ b/sdk/e2e/pom.xml @@ -29,12 +29,12 @@ com.azure azure-core - 1.32.0-beta.1 + 1.32.0 com.azure azure-core-http-netty - 1.13.0-beta.1 + 1.12.5 com.azure @@ -70,7 +70,7 @@ com.azure azure-core-test - 1.12.0-beta.1 + 1.12.0 test From c3cceb3f2122de553bd6bf2977c7c87cbe82ce59 Mon Sep 17 00:00:00 2001 From: Shawn Fang <45607042+mssfang@users.noreply.github.com> Date: Thu, 1 Sep 2022 12:28:02 -0700 Subject: [PATCH 05/18] [TA] Client side validation (#30708) --- .../AnalyzeActionsAsyncClient.java | 48 ++- .../AnalyzeHealthcareEntityAsyncClient.java | 19 +- .../AnalyzeSentimentAsyncClient.java | 33 +- .../DetectLanguageAsyncClient.java | 25 +- .../ExtractKeyPhraseAsyncClient.java | 24 +- .../LabelClassifyAsyncClient.java | 23 +- .../RecognizeCustomEntitiesAsyncClient.java | 16 +- .../RecognizeEntityAsyncClient.java | 24 +- .../RecognizeLinkedEntityAsyncClient.java | 25 +- .../RecognizePiiEntityAsyncClient.java | 24 +- .../TextAnalyticsAsyncClient.java | 135 +++++- .../ai/textanalytics/TextAnalyticsClient.java | 93 +++++ .../textanalytics/implementation/Utility.java | 38 +- .../ai/textanalytics/models/LinkedEntity.java | 4 +- .../models/PiiEntityCollection.java | 2 +- .../ClientSideValidationUnitTests.java | 395 ++++++++++++++++++ .../TextAnalyticsClientTest.java | 1 - 17 files changed, 873 insertions(+), 56 deletions(-) create mode 100644 sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeActionsAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeActionsAsyncClient.java index 3717d10565caa..ec8089916f2db 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeActionsAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeActionsAsyncClient.java @@ -134,9 +134,11 @@ import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.DEFAULT_POLL_INTERVAL; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; import static com.azure.ai.textanalytics.implementation.Utility.parseNextLink; import static com.azure.ai.textanalytics.implementation.Utility.parseOperationId; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toAnalyzeHealthcareEntitiesResultCollection; import static com.azure.ai.textanalytics.implementation.Utility.toAnalyzeSentimentResultCollection; import static com.azure.ai.textanalytics.implementation.Utility.toCategoriesFilter; @@ -174,19 +176,24 @@ class AnalyzeActionsAsyncClient { private final ClientLogger logger = new ClientLogger(AnalyzeActionsAsyncClient.class); private final TextAnalyticsClientImpl legacyService; private final AnalyzeTextsImpl service; + + private final TextAnalyticsServiceVersion serviceVersion; + private static final Pattern PATTERN; static { PATTERN = Pattern.compile(REGEX_ACTION_ERROR_TARGET, Pattern.MULTILINE); } - AnalyzeActionsAsyncClient(TextAnalyticsClientImpl legacyService) { + AnalyzeActionsAsyncClient(TextAnalyticsClientImpl legacyService, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - AnalyzeActionsAsyncClient(AnalyzeTextsImpl service) { + AnalyzeActionsAsyncClient(AnalyzeTextsImpl service, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } PollerFlux beginAnalyzeActions( @@ -194,6 +201,9 @@ PollerFlux beginAn Context context) { try { Objects.requireNonNull(actions, "'actions' cannot be null."); + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("beginAnalyzeActions", serviceVersion, + TextAnalyticsServiceVersion.V3_1)); inputDocumentsValidation(documents); options = getNotNullAnalyzeActionsOptions(options); final Context finalContext = getNotNullContext(context) @@ -229,6 +239,8 @@ PollerFlux beginAn ); } + throwIfTargetServiceVersionFoundForActions(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), actions); final AnalyzeBatchInput analyzeBatchInput = new AnalyzeBatchInput() .setAnalysisInput(new MultiLanguageBatchInput().setDocuments(toMultiLanguageInput(documents))) @@ -263,6 +275,9 @@ PollerFlux beg Context context) { try { Objects.requireNonNull(actions, "'actions' cannot be null."); + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("beginAnalyzeActions", serviceVersion, + TextAnalyticsServiceVersion.V3_1)); inputDocumentsValidation(documents); options = getNotNullAnalyzeActionsOptions(options); final Context finalContext = getNotNullContext(context) @@ -301,6 +316,8 @@ PollerFlux beg ); } + throwIfTargetServiceVersionFoundForActions(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), actions); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( @@ -1284,4 +1301,31 @@ private String[] parseActionErrorTarget(String targetReference, String errorMess } return taskNameIdPair; } + + private void throwIfTargetServiceVersionFoundForActions(TextAnalyticsServiceVersion sourceVersion, + List targetVersions, TextAnalyticsActions actions) { + if (actions.getMultiLabelClassifyActions() != null) { + throwIfTargetServiceVersionFound(sourceVersion, targetVersions, + getUnsupportedServiceApiVersionMessage("MultiLabelClassifyAction", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); + } + + if (actions.getSingleLabelClassifyActions() != null) { + throwIfTargetServiceVersionFound(sourceVersion, targetVersions, + getUnsupportedServiceApiVersionMessage("SingleLabelClassifyAction", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); + } + + if (actions.getRecognizeCustomEntitiesActions() != null) { + throwIfTargetServiceVersionFound(sourceVersion, targetVersions, + getUnsupportedServiceApiVersionMessage("RecognizeCustomEntitiesAction", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); + } + + if (actions.getAnalyzeHealthcareEntitiesActions() != null) { + throwIfTargetServiceVersionFound(sourceVersion, targetVersions, + getUnsupportedServiceApiVersionMessage("AnalyzeHealthcareEntitiesAction", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java index 3e216d0172a8a..fc6e78ce93ab0 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java @@ -58,9 +58,11 @@ import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.DEFAULT_POLL_INTERVAL; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; import static com.azure.ai.textanalytics.implementation.Utility.parseNextLink; import static com.azure.ai.textanalytics.implementation.Utility.parseOperationId; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toAnalyzeHealthcareEntitiesResultCollection; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.models.State.CANCELLED; @@ -75,20 +77,29 @@ class AnalyzeHealthcareEntityAsyncClient { private final TextAnalyticsClientImpl legacyService; private final AnalyzeTextsImpl service; - AnalyzeHealthcareEntityAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + AnalyzeHealthcareEntityAsyncClient(TextAnalyticsClientImpl legacyService, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - AnalyzeHealthcareEntityAsyncClient(AnalyzeTextsImpl service) { + AnalyzeHealthcareEntityAsyncClient(AnalyzeTextsImpl service, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } PollerFlux beginAnalyzeHealthcareEntities(Iterable documents, AnalyzeHealthcareEntitiesOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("beginAnalyzeHealthcareEntities", serviceVersion, + TextAnalyticsServiceVersion.V3_1)); inputDocumentsValidation(documents); options = getNotNullAnalyzeHealthcareEntitiesOptions(options); final Context finalContext = getNotNullContext(context) @@ -162,6 +173,10 @@ class AnalyzeHealthcareEntityAsyncClient { beginAnalyzeHealthcarePagedIterable(Iterable documents, AnalyzeHealthcareEntitiesOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("beginAnalyzeHealthcareEntities", serviceVersion, + TextAnalyticsServiceVersion.V3_1)); inputDocumentsValidation(documents); options = getNotNullAnalyzeHealthcareEntitiesOptions(options); final Context finalContext = getNotNullContext(context) diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeSentimentAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeSentimentAsyncClient.java index 9268917eb0a84..499ac274ad4ab 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeSentimentAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeSentimentAsyncClient.java @@ -21,10 +21,14 @@ import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; +import java.util.Arrays; + import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.getDocumentCount; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.core.util.FluxUtil.monoError; import static com.azure.core.util.FluxUtil.withContext; @@ -38,14 +42,19 @@ class AnalyzeSentimentAsyncClient { private final TextAnalyticsClientImpl legacyService; private final MicrosoftCognitiveLanguageServiceTextAnalysisImpl service; - AnalyzeSentimentAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + AnalyzeSentimentAsyncClient(TextAnalyticsClientImpl legacyService, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - AnalyzeSentimentAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service) { + AnalyzeSentimentAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } /** @@ -64,7 +73,6 @@ class AnalyzeSentimentAsyncClient { public Mono> analyzeSentimentBatch( Iterable documents, AnalyzeSentimentOptions options) { try { - inputDocumentsValidation(documents); return withContext(context -> getAnalyzedSentimentResponse(documents, options, context)); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -85,7 +93,6 @@ public Mono> analyzeSentimentBatch( Mono> analyzeSentimentBatchWithContext( Iterable documents, AnalyzeSentimentOptions options, Context context) { try { - inputDocumentsValidation(documents); return getAnalyzedSentimentResponse(documents, options, context); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -105,6 +112,8 @@ Mono> analyzeSentimentBatchWithContex */ private Mono> getAnalyzedSentimentResponse( Iterable documents, AnalyzeSentimentOptions options, Context context) { + throwIfCallingNotAvailableFeatureInOptions(options); + inputDocumentsValidation(documents); options = options == null ? new AnalyzeSentimentOptions() : options; if (service != null) { @@ -146,4 +155,20 @@ private Mono> getAnalyzedSentimentRes .map(Utility::toAnalyzeSentimentResultCollectionResponse) .onErrorMap(Utility::mapToHttpResponseExceptionIfExists); } + + private void throwIfCallingNotAvailableFeatureInOptions(AnalyzeSentimentOptions options) { + if (options == null) { + return; + } + if (options.isIncludeOpinionMining()) { + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("AnalyzeSentimentOptions.includeOpinionMining", + serviceVersion, TextAnalyticsServiceVersion.V3_1)); + } + if (options.isServiceLogsDisabled()) { + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("TextAnalyticsRequestOptions.disableServiceLogs", + serviceVersion, TextAnalyticsServiceVersion.V3_1)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/DetectLanguageAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/DetectLanguageAsyncClient.java index fc4fb4342cb1b..7bd95f8dd4316 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/DetectLanguageAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/DetectLanguageAsyncClient.java @@ -21,10 +21,14 @@ import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; +import java.util.Arrays; + import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.getDocumentCount; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toLanguageInput; import static com.azure.core.util.FluxUtil.monoError; import static com.azure.core.util.FluxUtil.withContext; @@ -38,14 +42,19 @@ class DetectLanguageAsyncClient { private final TextAnalyticsClientImpl legacyService; private final MicrosoftCognitiveLanguageServiceTextAnalysisImpl service; - DetectLanguageAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + DetectLanguageAsyncClient(TextAnalyticsClientImpl legacyService, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - DetectLanguageAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service) { + DetectLanguageAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } /** @@ -59,7 +68,6 @@ class DetectLanguageAsyncClient { Mono> detectLanguageBatch( Iterable documents, TextAnalyticsRequestOptions options) { try { - inputDocumentsValidation(documents); return withContext(context -> getDetectedLanguageResponse(documents, options, context)); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -78,7 +86,6 @@ Mono> detectLanguageBatch( Mono> detectLanguageBatchWithContext( Iterable documents, TextAnalyticsRequestOptions options, Context context) { try { - inputDocumentsValidation(documents); return getDetectedLanguageResponse(documents, options, context); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -97,6 +104,8 @@ Mono> detectLanguageBatchWithContext( */ private Mono> getDetectedLanguageResponse( Iterable documents, TextAnalyticsRequestOptions options, Context context) { + throwIfCallingNotAvailableFeatureInOptions(options); + inputDocumentsValidation(documents); options = options == null ? new TextAnalyticsRequestOptions() : options; if (service != null) { return service @@ -135,4 +144,12 @@ private Mono> getDetectedLanguageRespon .map(Utility::toDetectLanguageResultCollectionResponse) .onErrorMap(Utility::mapToHttpResponseExceptionIfExists); } + + private void throwIfCallingNotAvailableFeatureInOptions(TextAnalyticsRequestOptions options) { + if (options != null && options.isServiceLogsDisabled()) { + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("TextAnalyticsRequestOptions.disableServiceLogs", + serviceVersion, TextAnalyticsServiceVersion.V3_1)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/ExtractKeyPhraseAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/ExtractKeyPhraseAsyncClient.java index 8a90137b95fcd..a84eaf7215c7d 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/ExtractKeyPhraseAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/ExtractKeyPhraseAsyncClient.java @@ -22,13 +22,16 @@ import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.Collections; import java.util.Objects; import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.getDocumentCount; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.Utility.toTextAnalyticsException; import static com.azure.core.util.FluxUtil.monoError; @@ -43,14 +46,19 @@ class ExtractKeyPhraseAsyncClient { private final TextAnalyticsClientImpl legacyService; private final MicrosoftCognitiveLanguageServiceTextAnalysisImpl service; - ExtractKeyPhraseAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + ExtractKeyPhraseAsyncClient(TextAnalyticsClientImpl legacyService, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - ExtractKeyPhraseAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service) { + ExtractKeyPhraseAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } /** @@ -96,7 +104,6 @@ Mono extractKeyPhrasesSingleText(String document, String l Mono> extractKeyPhrasesWithResponse( Iterable documents, TextAnalyticsRequestOptions options) { try { - inputDocumentsValidation(documents); return withContext(context -> getExtractedKeyPhrasesResponse(documents, options, context)); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -116,7 +123,6 @@ Mono> extractKeyPhrasesWithResponse( Mono> extractKeyPhrasesBatchWithContext( Iterable documents, TextAnalyticsRequestOptions options, Context context) { try { - inputDocumentsValidation(documents); return getExtractedKeyPhrasesResponse(documents, options, context); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -135,6 +141,8 @@ Mono> extractKeyPhrasesBatchWithCont */ private Mono> getExtractedKeyPhrasesResponse( Iterable documents, TextAnalyticsRequestOptions options, Context context) { + throwIfCallingNotAvailableFeatureInOptions(options); + inputDocumentsValidation(documents); options = options == null ? new TextAnalyticsRequestOptions() : options; if (service != null) { @@ -171,4 +179,12 @@ private Mono> getExtractedKeyPhrases .map(Utility::toExtractKeyPhrasesResultCollectionResponse) .onErrorMap(Utility::mapToHttpResponseExceptionIfExists); } + + private void throwIfCallingNotAvailableFeatureInOptions(TextAnalyticsRequestOptions options) { + if (options != null && options.isServiceLogsDisabled()) { + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("TextAnalyticsRequestOptions.disableServiceLogs", + serviceVersion, TextAnalyticsServiceVersion.V3_1)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java index f7802827091a2..c68207f0cdcff 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java @@ -58,9 +58,11 @@ import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.DEFAULT_POLL_INTERVAL; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; import static com.azure.ai.textanalytics.implementation.Utility.parseNextLink; import static com.azure.ai.textanalytics.implementation.Utility.parseOperationId; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toLabelClassificationResultCollection; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.models.State.CANCELLED; @@ -74,14 +76,21 @@ class LabelClassifyAsyncClient { private static final ClientLogger LOGGER = new ClientLogger(LabelClassifyAsyncClient.class); private final AnalyzeTextsImpl service; - LabelClassifyAsyncClient(AnalyzeTextsImpl service) { + private final TextAnalyticsServiceVersion serviceVersion; + + LabelClassifyAsyncClient(AnalyzeTextsImpl service, TextAnalyticsServiceVersion serviceVersion) { this.service = service; + this.serviceVersion = serviceVersion; } PollerFlux singleLabelClassify( Iterable documents, String projectName, String deploymentName, SingleLabelClassifyOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("beginSingleLabelClassify", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); inputDocumentsValidation(documents); options = getNotNullSingleLabelClassifyOptions(options); final Context finalContext = getNotNullContext(context) @@ -129,6 +138,10 @@ PollerFlux singl Iterable documents, String projectName, String deploymentName, SingleLabelClassifyOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("beginSingleLabelClassify", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); inputDocumentsValidation(documents); options = getNotNullSingleLabelClassifyOptions(options); final Context finalContext = getNotNullContext(context) @@ -176,6 +189,10 @@ PollerFlux multiLabe Iterable documents, String projectName, String deploymentName, MultiLabelClassifyOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("beginMultiLabelClassify", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); inputDocumentsValidation(documents); options = getNotNullMultiLabelClassifyOptions(options); final Context finalContext = getNotNullContext(context) @@ -223,6 +240,10 @@ PollerFlux multi Iterable documents, String projectName, String deploymentName, MultiLabelClassifyOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("beginMultiLabelClassify", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); inputDocumentsValidation(documents); options = getNotNullMultiLabelClassifyOptions(options); final Context finalContext = getNotNullContext(context) diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java index 6bff6a5ff68fd..84ef23532524f 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java @@ -55,9 +55,11 @@ import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.DEFAULT_POLL_INTERVAL; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; import static com.azure.ai.textanalytics.implementation.Utility.parseNextLink; import static com.azure.ai.textanalytics.implementation.Utility.parseOperationId; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.Utility.toRecognizeCustomEntitiesResultCollection; import static com.azure.ai.textanalytics.implementation.models.State.CANCELLED; @@ -71,14 +73,22 @@ class RecognizeCustomEntitiesAsyncClient { private final ClientLogger logger = new ClientLogger(RecognizeCustomEntitiesAsyncClient.class); private final AnalyzeTextsImpl service; - RecognizeCustomEntitiesAsyncClient(AnalyzeTextsImpl service) { + private final TextAnalyticsServiceVersion serviceVersion; + + RecognizeCustomEntitiesAsyncClient(AnalyzeTextsImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.service = service; + this.serviceVersion = serviceVersion; } PollerFlux recognizeCustomEntities( Iterable documents, String projectName, String deploymentName, RecognizeCustomEntitiesOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("beginRecognizeCustomEntities", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); inputDocumentsValidation(documents); options = getNotNullRecognizeCustomEntitiesOptions(options); final Context finalContext = getNotNullContext(context) @@ -127,6 +137,10 @@ PollerFlux documents, String projectName, String deploymentName, RecognizeCustomEntitiesOptions options, Context context) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("beginRecognizeCustomEntities", serviceVersion, + TextAnalyticsServiceVersion.V2022_05_01)); inputDocumentsValidation(documents); options = getNotNullRecognizeCustomEntitiesOptions(options); final Context finalContext = getNotNullContext(context) diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeEntityAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeEntityAsyncClient.java index d79e88fab8154..11a9010f464af 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeEntityAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeEntityAsyncClient.java @@ -23,13 +23,16 @@ import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.Collections; import java.util.Objects; import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.getDocumentCount; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.Utility.toTextAnalyticsException; import static com.azure.core.util.FluxUtil.monoError; @@ -44,14 +47,19 @@ class RecognizeEntityAsyncClient { private final TextAnalyticsClientImpl legacyService; private final MicrosoftCognitiveLanguageServiceTextAnalysisImpl service; - RecognizeEntityAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + RecognizeEntityAsyncClient(TextAnalyticsClientImpl legacyService, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - RecognizeEntityAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service) { + RecognizeEntityAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } /** @@ -97,7 +105,6 @@ Mono recognizeEntities(String document, String lang Mono> recognizeEntitiesBatch( Iterable documents, TextAnalyticsRequestOptions options) { try { - inputDocumentsValidation(documents); return withContext(context -> getRecognizedEntitiesResponse(documents, options, context)); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -116,7 +123,6 @@ Mono> recognizeEntitiesBatch( Mono> recognizeEntitiesBatchWithContext( Iterable documents, TextAnalyticsRequestOptions options, Context context) { try { - inputDocumentsValidation(documents); return getRecognizedEntitiesResponse(documents, options, context); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -135,6 +141,8 @@ Mono> recognizeEntitiesBatchWithCont */ private Mono> getRecognizedEntitiesResponse( Iterable documents, TextAnalyticsRequestOptions options, Context context) { + throwIfCallingNotAvailableFeatureInOptions(options); + inputDocumentsValidation(documents); options = options == null ? new TextAnalyticsRequestOptions() : options; final Context finalContext = getNotNullContext(context) .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); @@ -180,4 +188,12 @@ private Mono> getRecognizedEntitiesR .map(Utility::toRecognizeEntitiesResultCollection) .onErrorMap(Utility::mapToHttpResponseExceptionIfExists); } + + private void throwIfCallingNotAvailableFeatureInOptions(TextAnalyticsRequestOptions options) { + if (options != null && options.isServiceLogsDisabled()) { + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("TextAnalyticsRequestOptions.disableServiceLogs", + serviceVersion, TextAnalyticsServiceVersion.V3_1)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeLinkedEntityAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeLinkedEntityAsyncClient.java index 3c3d86bb95b46..e0c826c2ebc37 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeLinkedEntityAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeLinkedEntityAsyncClient.java @@ -23,13 +23,16 @@ import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.Collections; import java.util.Objects; import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.getDocumentCount; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.Utility.toTextAnalyticsException; import static com.azure.core.util.FluxUtil.monoError; @@ -44,14 +47,20 @@ class RecognizeLinkedEntityAsyncClient { private final TextAnalyticsClientImpl legacyService; private final MicrosoftCognitiveLanguageServiceTextAnalysisImpl service; - RecognizeLinkedEntityAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + RecognizeLinkedEntityAsyncClient(TextAnalyticsClientImpl legacyService, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - RecognizeLinkedEntityAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service) { + RecognizeLinkedEntityAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } /** @@ -97,7 +106,6 @@ Mono recognizeLinkedEntities(String document, String lan Mono> recognizeLinkedEntitiesBatch( Iterable documents, TextAnalyticsRequestOptions options) { try { - inputDocumentsValidation(documents); return withContext(context -> getRecognizedLinkedEntitiesResponse(documents, options, context)); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -118,7 +126,6 @@ Mono> recognizeLinkedEntitiesB recognizeLinkedEntitiesBatchWithContext(Iterable documents, TextAnalyticsRequestOptions options, Context context) { try { - inputDocumentsValidation(documents); return getRecognizedLinkedEntitiesResponse(documents, options, context); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -136,6 +143,8 @@ Mono> recognizeLinkedEntitiesB */ private Mono> getRecognizedLinkedEntitiesResponse( Iterable documents, TextAnalyticsRequestOptions options, Context context) { + throwIfCallingNotAvailableFeatureInOptions(options); + inputDocumentsValidation(documents); options = options == null ? new TextAnalyticsRequestOptions() : options; final Context finalContext = getNotNullContext(context) .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); @@ -180,4 +189,12 @@ private Mono> getRecognizedLin .map(Utility::toRecognizeLinkedEntitiesResultCollectionResponse) .onErrorMap(Utility::mapToHttpResponseExceptionIfExists); } + + private void throwIfCallingNotAvailableFeatureInOptions(TextAnalyticsRequestOptions options) { + if (options != null && options.isServiceLogsDisabled()) { + throwIfTargetServiceVersionFound(this.serviceVersion, Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("TextAnalyticsRequestOptions.disableServiceLogs", + serviceVersion, TextAnalyticsServiceVersion.V3_1)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizePiiEntityAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizePiiEntityAsyncClient.java index 1d0038585f610..e690c8439239d 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizePiiEntityAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizePiiEntityAsyncClient.java @@ -24,12 +24,15 @@ import com.azure.core.util.logging.ClientLogger; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.Collections; import java.util.Objects; import static com.azure.ai.textanalytics.TextAnalyticsAsyncClient.COGNITIVE_TRACING_NAMESPACE_VALUE; import static com.azure.ai.textanalytics.implementation.Utility.getNotNullContext; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; import static com.azure.ai.textanalytics.implementation.Utility.inputDocumentsValidation; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; import static com.azure.ai.textanalytics.implementation.Utility.toCategoriesFilter; import static com.azure.ai.textanalytics.implementation.Utility.toMultiLanguageInput; import static com.azure.ai.textanalytics.implementation.Utility.toTextAnalyticsException; @@ -45,14 +48,19 @@ class RecognizePiiEntityAsyncClient { private final TextAnalyticsClientImpl legacyService; private final MicrosoftCognitiveLanguageServiceTextAnalysisImpl service; - RecognizePiiEntityAsyncClient(TextAnalyticsClientImpl legacyService) { + private final TextAnalyticsServiceVersion serviceVersion; + + RecognizePiiEntityAsyncClient(TextAnalyticsClientImpl legacyService, TextAnalyticsServiceVersion serviceVersion) { this.legacyService = legacyService; this.service = null; + this.serviceVersion = serviceVersion; } - RecognizePiiEntityAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service) { + RecognizePiiEntityAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, + TextAnalyticsServiceVersion serviceVersion) { this.legacyService = null; this.service = service; + this.serviceVersion = serviceVersion; } /** @@ -69,6 +77,10 @@ class RecognizePiiEntityAsyncClient { Mono recognizePiiEntities(String document, String language, RecognizePiiEntitiesOptions options) { try { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("recognizePiiEntitiesBatch", serviceVersion, + TextAnalyticsServiceVersion.V3_1)); Objects.requireNonNull(document, "'document' cannot be null."); return recognizePiiEntitiesBatch( Collections.singletonList(new TextDocumentInput("0", document).setLanguage(language)), options) @@ -102,7 +114,6 @@ Mono recognizePiiEntities(String document, String language, Mono> recognizePiiEntitiesBatch( Iterable documents, RecognizePiiEntitiesOptions options) { try { - inputDocumentsValidation(documents); return withContext(context -> getRecognizePiiEntitiesResponse(documents, options, context)); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -122,7 +133,6 @@ Mono> recognizePiiEntitiesBatch( Mono> recognizePiiEntitiesBatchWithContext( Iterable documents, RecognizePiiEntitiesOptions options, Context context) { try { - inputDocumentsValidation(documents); return getRecognizePiiEntitiesResponse(documents, options, context); } catch (RuntimeException ex) { return monoError(logger, ex); @@ -139,9 +149,15 @@ Mono> recognizePiiEntitiesBatchWi * @param context Additional context that is passed through the Http pipeline during the service call. * * @return A mono {@link Response} that contains {@link RecognizePiiEntitiesResultCollection}. + * */ private Mono> getRecognizePiiEntitiesResponse( Iterable documents, RecognizePiiEntitiesOptions options, Context context) { + throwIfTargetServiceVersionFound(this.serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_0), + getUnsupportedServiceApiVersionMessage("recognizePiiEntitiesBatch", serviceVersion, + TextAnalyticsServiceVersion.V3_1)); + inputDocumentsValidation(documents); options = options == null ? new RecognizePiiEntitiesOptions() : options; final Context finalContext = getNotNullContext(context) .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClient.java index 07a4ba089f2a6..993bb174860ef 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClient.java @@ -8,6 +8,7 @@ import com.azure.ai.textanalytics.implementation.TextAnalyticsClientImpl; import com.azure.ai.textanalytics.models.AnalyzeActionsOperationDetail; import com.azure.ai.textanalytics.models.AnalyzeActionsOptions; +import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesAction; import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesOperationDetail; import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesOptions; import com.azure.ai.textanalytics.models.AnalyzeSentimentOptions; @@ -20,11 +21,14 @@ import com.azure.ai.textanalytics.models.DocumentSentiment; import com.azure.ai.textanalytics.models.KeyPhrasesCollection; import com.azure.ai.textanalytics.models.LinkedEntityCollection; +import com.azure.ai.textanalytics.models.MultiLabelClassifyAction; import com.azure.ai.textanalytics.models.MultiLabelClassifyOptions; import com.azure.ai.textanalytics.models.PiiEntityCollection; +import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesAction; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOperationDetail; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOptions; import com.azure.ai.textanalytics.models.RecognizePiiEntitiesOptions; +import com.azure.ai.textanalytics.models.SingleLabelClassifyAction; import com.azure.ai.textanalytics.models.SingleLabelClassifyOptions; import com.azure.ai.textanalytics.models.TextAnalyticsActions; import com.azure.ai.textanalytics.models.TextAnalyticsError; @@ -121,16 +125,16 @@ public final class TextAnalyticsAsyncClient { this.serviceVersion = serviceVersion; this.defaultCountryHint = defaultCountryHint; this.defaultLanguage = defaultLanguage; - this.detectLanguageAsyncClient = new DetectLanguageAsyncClient(legacyService); - this.analyzeSentimentAsyncClient = new AnalyzeSentimentAsyncClient(legacyService); - this.extractKeyPhraseAsyncClient = new ExtractKeyPhraseAsyncClient(legacyService); - this.recognizeEntityAsyncClient = new RecognizeEntityAsyncClient(legacyService); - this.recognizePiiEntityAsyncClient = new RecognizePiiEntityAsyncClient(legacyService); - this.recognizeLinkedEntityAsyncClient = new RecognizeLinkedEntityAsyncClient(legacyService); - this.recognizeCustomEntitiesAsyncClient = new RecognizeCustomEntitiesAsyncClient(null); - this.analyzeHealthcareEntityAsyncClient = new AnalyzeHealthcareEntityAsyncClient(legacyService); - this.analyzeActionsAsyncClient = new AnalyzeActionsAsyncClient(legacyService); - this.labelClassifyAsyncClient = new LabelClassifyAsyncClient(null); + this.detectLanguageAsyncClient = new DetectLanguageAsyncClient(legacyService, serviceVersion); + this.analyzeSentimentAsyncClient = new AnalyzeSentimentAsyncClient(legacyService, serviceVersion); + this.extractKeyPhraseAsyncClient = new ExtractKeyPhraseAsyncClient(legacyService, serviceVersion); + this.recognizeEntityAsyncClient = new RecognizeEntityAsyncClient(legacyService, serviceVersion); + this.recognizePiiEntityAsyncClient = new RecognizePiiEntityAsyncClient(legacyService, serviceVersion); + this.recognizeLinkedEntityAsyncClient = new RecognizeLinkedEntityAsyncClient(legacyService, serviceVersion); + this.recognizeCustomEntitiesAsyncClient = new RecognizeCustomEntitiesAsyncClient(null, serviceVersion); + this.analyzeHealthcareEntityAsyncClient = new AnalyzeHealthcareEntityAsyncClient(legacyService, serviceVersion); + this.analyzeActionsAsyncClient = new AnalyzeActionsAsyncClient(legacyService, serviceVersion); + this.labelClassifyAsyncClient = new LabelClassifyAsyncClient(null, serviceVersion); } TextAnalyticsAsyncClient(MicrosoftCognitiveLanguageServiceTextAnalysisImpl service, @@ -140,16 +144,18 @@ public final class TextAnalyticsAsyncClient { this.serviceVersion = serviceVersion; this.defaultCountryHint = defaultCountryHint; this.defaultLanguage = defaultLanguage; - this.detectLanguageAsyncClient = new DetectLanguageAsyncClient(service); - this.analyzeSentimentAsyncClient = new AnalyzeSentimentAsyncClient(service); - this.extractKeyPhraseAsyncClient = new ExtractKeyPhraseAsyncClient(service); - this.recognizeEntityAsyncClient = new RecognizeEntityAsyncClient(service); - this.recognizePiiEntityAsyncClient = new RecognizePiiEntityAsyncClient(service); - this.recognizeLinkedEntityAsyncClient = new RecognizeLinkedEntityAsyncClient(service); - this.recognizeCustomEntitiesAsyncClient = new RecognizeCustomEntitiesAsyncClient(new AnalyzeTextsImpl(service)); - this.analyzeHealthcareEntityAsyncClient = new AnalyzeHealthcareEntityAsyncClient(new AnalyzeTextsImpl(service)); - this.analyzeActionsAsyncClient = new AnalyzeActionsAsyncClient(new AnalyzeTextsImpl(service)); - this.labelClassifyAsyncClient = new LabelClassifyAsyncClient(new AnalyzeTextsImpl(service)); + this.detectLanguageAsyncClient = new DetectLanguageAsyncClient(service, serviceVersion); + this.analyzeSentimentAsyncClient = new AnalyzeSentimentAsyncClient(service, serviceVersion); + this.extractKeyPhraseAsyncClient = new ExtractKeyPhraseAsyncClient(service, serviceVersion); + this.recognizeEntityAsyncClient = new RecognizeEntityAsyncClient(service, serviceVersion); + this.recognizePiiEntityAsyncClient = new RecognizePiiEntityAsyncClient(service, serviceVersion); + this.recognizeLinkedEntityAsyncClient = new RecognizeLinkedEntityAsyncClient(service, serviceVersion); + this.recognizeCustomEntitiesAsyncClient = new RecognizeCustomEntitiesAsyncClient( + new AnalyzeTextsImpl(service), serviceVersion); + this.analyzeHealthcareEntityAsyncClient = new AnalyzeHealthcareEntityAsyncClient(new AnalyzeTextsImpl(service), + serviceVersion); + this.analyzeActionsAsyncClient = new AnalyzeActionsAsyncClient(new AnalyzeTextsImpl(service), serviceVersion); + this.labelClassifyAsyncClient = new LabelClassifyAsyncClient(new AnalyzeTextsImpl(service), serviceVersion); } /** @@ -300,6 +306,9 @@ public Mono detectLanguage(String document, String countryHint * @return A {@link Mono} contains a {@link DetectLanguageResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono detectLanguageBatch( @@ -364,6 +373,9 @@ public Mono detectLanguageBatch( * @return A {@link Mono} contains a {@link Response} which contains a {@link DetectLanguageResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono> detectLanguageBatchWithResponse( @@ -490,6 +502,9 @@ public Mono recognizeEntities(String document, Stri * @return A {@link Mono} contains a {@link RecognizeEntitiesResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono recognizeEntitiesBatch( @@ -552,6 +567,9 @@ public Mono recognizeEntitiesBatch( * @return A {@link Mono} contains a {@link Response} which contains a {@link RecognizeEntitiesResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono> recognizeEntitiesBatchWithResponse( @@ -595,6 +613,9 @@ public Mono> recognizeEntitiesBatchW * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@code recognizePiiEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntities} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono recognizePiiEntities(String document) { @@ -635,6 +656,9 @@ public Mono recognizePiiEntities(String document) { * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@code recognizePiiEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntities} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono recognizePiiEntities(String document, String language) { @@ -679,6 +703,9 @@ public Mono recognizePiiEntities(String document, String la * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@code recognizePiiEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntities} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono recognizePiiEntities(String document, String language, @@ -735,6 +762,9 @@ public Mono recognizePiiEntities(String document, String la * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code recognizePiiEntitiesBatch} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntitiesBatch} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono recognizePiiEntitiesBatch( @@ -801,6 +831,9 @@ public Mono recognizePiiEntitiesBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code recognizePiiEntitiesBatchWithResponse} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntitiesBatchWithResponse} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono> recognizePiiEntitiesBatchWithResponse( @@ -938,6 +971,9 @@ public Mono recognizeLinkedEntities(String document, Str * @return A {@link Mono} contains a {@link RecognizeLinkedEntitiesResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono recognizeLinkedEntitiesBatch( @@ -1006,6 +1042,9 @@ public Mono recognizeLinkedEntitiesBatc * {@link RecognizeLinkedEntitiesResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono> recognizeLinkedEntitiesBatchWithResponse( @@ -1120,6 +1159,9 @@ public Mono extractKeyPhrases(String document, String lang * @return A {@link Mono} contains a {@link ExtractKeyPhrasesResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono extractKeyPhrasesBatch( @@ -1182,6 +1224,9 @@ public Mono extractKeyPhrasesBatch( * @return A {@link Mono} contains a {@link Response} that contains a {@link ExtractKeyPhrasesResultCollection}. * * @throws NullPointerException if {@code documents} is null. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono> extractKeyPhrasesBatchWithResponse( @@ -1324,6 +1369,10 @@ public Mono analyzeSentiment(String document, String language * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@link AnalyzeSentimentOptions#isServiceLogsDisabled()} or + * {@link AnalyzeSentimentOptions#isIncludeOpinionMining()} is true in service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} and {@code includeOpinionMining} are only + * available for API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono analyzeSentiment(String document, String language, AnalyzeSentimentOptions options) { @@ -1479,6 +1528,10 @@ public Mono analyzeSentimentBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link AnalyzeSentimentOptions#isServiceLogsDisabled()} or + * {@link AnalyzeSentimentOptions#isIncludeOpinionMining()} is true in service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} and {@code includeOpinionMining} are only + * available for API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono analyzeSentimentBatch(Iterable documents, @@ -1624,6 +1677,10 @@ public Mono> analyzeSentimentBatchWit * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link AnalyzeSentimentOptions#isServiceLogsDisabled()} or + * {@link AnalyzeSentimentOptions#isIncludeOpinionMining()} is true in service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} and {@code includeOpinionMining} are only + * available for API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Mono> analyzeSentimentBatchWithResponse( @@ -1651,6 +1708,9 @@ public Mono> analyzeSentimentBatchWit * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeHealthcareEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeHealthcareEntities} is only available for API + * version v3.1 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1752,6 +1812,9 @@ public Mono> analyzeSentimentBatchWit * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeHealthcareEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeHealthcareEntities} is only available for API + * version v3.1 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1829,6 +1892,9 @@ public Mono> analyzeSentimentBatchWit * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginRecognizeCustomEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginRecognizeCustomEntities} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1906,6 +1972,9 @@ public Mono> analyzeSentimentBatchWit * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginRecognizeCustomEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginRecognizeCustomEntities} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1987,6 +2056,9 @@ public Mono> analyzeSentimentBatchWit * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginSingleLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginSingleLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2065,6 +2137,9 @@ public PollerFlux be * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginSingleLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginSingleLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2141,6 +2216,9 @@ public PollerFlux be * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginMultiLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginMultiLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2215,6 +2293,9 @@ public PollerFlux be * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginMultiLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginMultiLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2291,6 +2372,13 @@ public PollerFlux be * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeActions} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeActions} is only available for API version + * v3.1 and newer. + * @throws IllegalStateException if request {@link AnalyzeHealthcareEntitiesAction}, + * {@link RecognizeCustomEntitiesAction}, {@link SingleLabelClassifyAction}, or {@link MultiLabelClassifyAction} + * in service API version {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * Those actions are only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2367,6 +2455,13 @@ public PollerFlux * * @throws NullPointerException if {@code documents} or {@code actions} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeActions} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeActions} is only available for API version + * v3.1 and newer. + * @throws IllegalStateException if request {@link AnalyzeHealthcareEntitiesAction}, + * {@link RecognizeCustomEntitiesAction}, {@link SingleLabelClassifyAction}, or {@link MultiLabelClassifyAction} + * in service API version {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * Those actions are only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsClient.java index 1fbd69dc6935e..d41e1600ca031 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/TextAnalyticsClient.java @@ -5,6 +5,7 @@ import com.azure.ai.textanalytics.models.AnalyzeActionsOperationDetail; import com.azure.ai.textanalytics.models.AnalyzeActionsOptions; +import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesAction; import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesOperationDetail; import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesOptions; import com.azure.ai.textanalytics.models.AnalyzeSentimentOptions; @@ -17,11 +18,14 @@ import com.azure.ai.textanalytics.models.KeyPhrasesCollection; import com.azure.ai.textanalytics.models.LinkedEntity; import com.azure.ai.textanalytics.models.LinkedEntityCollection; +import com.azure.ai.textanalytics.models.MultiLabelClassifyAction; import com.azure.ai.textanalytics.models.MultiLabelClassifyOptions; import com.azure.ai.textanalytics.models.PiiEntityCollection; +import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesAction; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOperationDetail; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOptions; import com.azure.ai.textanalytics.models.RecognizePiiEntitiesOptions; +import com.azure.ai.textanalytics.models.SingleLabelClassifyAction; import com.azure.ai.textanalytics.models.SingleLabelClassifyOptions; import com.azure.ai.textanalytics.models.TextAnalyticsActions; import com.azure.ai.textanalytics.models.TextAnalyticsError; @@ -247,6 +251,9 @@ public DetectedLanguage detectLanguage(String document, String countryHint) { * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public DetectLanguageResultCollection detectLanguageBatch( @@ -306,6 +313,9 @@ public DetectLanguageResultCollection detectLanguageBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Response detectLanguageBatchWithResponse( @@ -427,6 +437,9 @@ public CategorizedEntityCollection recognizeEntities(String document, String lan * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public RecognizeEntitiesResultCollection recognizeEntitiesBatch( @@ -481,6 +494,9 @@ public RecognizeEntitiesResultCollection recognizeEntitiesBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Response recognizeEntitiesBatchWithResponse( @@ -522,6 +538,9 @@ public Response recognizeEntitiesBatchWithRes * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@code recognizePiiEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntities} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public PiiEntityCollection recognizePiiEntities(String document) { @@ -559,6 +578,9 @@ public PiiEntityCollection recognizePiiEntities(String document) { * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@code recognizePiiEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntities} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public PiiEntityCollection recognizePiiEntities(String document, String language) { @@ -600,6 +622,9 @@ public PiiEntityCollection recognizePiiEntities(String document, String language * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@code recognizePiiEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntities} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public PiiEntityCollection recognizePiiEntities(String document, String language, @@ -652,6 +677,9 @@ public PiiEntityCollection recognizePiiEntities(String document, String language * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code recognizePiiEntitiesBatch} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntitiesBatch} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public RecognizePiiEntitiesResultCollection recognizePiiEntitiesBatch( @@ -707,6 +735,9 @@ public RecognizePiiEntitiesResultCollection recognizePiiEntitiesBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code recognizePiiEntitiesBatchWithResponse} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code recognizePiiEntitiesBatchWithResponse} is only available for + * API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Response recognizePiiEntitiesBatchWithResponse( @@ -843,6 +874,9 @@ public LinkedEntityCollection recognizeLinkedEntities(String document, String la * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public RecognizeLinkedEntitiesResultCollection recognizeLinkedEntitiesBatch( @@ -905,6 +939,9 @@ public RecognizeLinkedEntitiesResultCollection recognizeLinkedEntitiesBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Response recognizeLinkedEntitiesBatchWithResponse( @@ -1025,6 +1062,9 @@ public KeyPhrasesCollection extractKeyPhrases(String document, String language) * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public ExtractKeyPhrasesResultCollection extractKeyPhrasesBatch( @@ -1086,6 +1126,9 @@ public ExtractKeyPhrasesResultCollection extractKeyPhrasesBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link TextAnalyticsRequestOptions#isServiceLogsDisabled()} is true in service + * API version {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} is only available for API + * version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Response extractKeyPhrasesBatchWithResponse( @@ -1233,6 +1276,10 @@ public DocumentSentiment analyzeSentiment(String document, String language) { * * @throws NullPointerException if {@code document} is null. * @throws TextAnalyticsException if the response returned with an {@link TextAnalyticsError error}. + * @throws IllegalStateException if {@link AnalyzeSentimentOptions#isServiceLogsDisabled()} or + * {@link AnalyzeSentimentOptions#isIncludeOpinionMining()} is true in service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} and {@code includeOpinionMining} are only + * available for API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public DocumentSentiment analyzeSentiment(String document, String language, AnalyzeSentimentOptions options) { @@ -1360,6 +1407,10 @@ public AnalyzeSentimentResultCollection analyzeSentimentBatch( * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link AnalyzeSentimentOptions#isServiceLogsDisabled()} or + * {@link AnalyzeSentimentOptions#isIncludeOpinionMining()} is true in service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} and {@code includeOpinionMining} are only + * available for API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public AnalyzeSentimentResultCollection analyzeSentimentBatch(Iterable documents, @@ -1515,6 +1566,10 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@link AnalyzeSentimentOptions#isServiceLogsDisabled()} or + * {@link AnalyzeSentimentOptions#isIncludeOpinionMining()} is true in service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code disableServiceLogs} and {@code includeOpinionMining} are only + * available for API version v3.1 and newer. */ @ServiceMethod(returns = ReturnType.SINGLE) public Response analyzeSentimentBatchWithResponse( @@ -1541,6 +1596,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeHealthcareEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeHealthcareEntities} is only available for API + * version v3.1 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1642,6 +1700,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeHealthcareEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeHealthcareEntities} is only available for API + * version v3.1 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1706,6 +1767,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginRecognizeCustomEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginRecognizeCustomEntities} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1774,6 +1838,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginRecognizeCustomEntities} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginRecognizeCustomEntities} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1841,6 +1908,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginSingleLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginSingleLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1907,6 +1977,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginSingleLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginSingleLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -1968,6 +2041,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginMultiLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginMultiLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2030,6 +2106,9 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginMultiLabelClassify} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * {@code beginMultiLabelClassify} is only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2109,6 +2188,13 @@ public Response analyzeSentimentBatchWithRespo * * @throws NullPointerException if {@code documents} is null. * @throws IllegalArgumentException if {@code documents} is empty. + * @throws IllegalStateException if {@code beginAnalyzeActions} is called with service API version + * {@link TextAnalyticsServiceVersion#V3_0}. {@code beginAnalyzeActions} is only available for API version + * v3.1 and newer. + * @throws IllegalStateException if request {@link AnalyzeHealthcareEntitiesAction}, + * {@link RecognizeCustomEntitiesAction}, {@link SingleLabelClassifyAction}, or {@link MultiLabelClassifyAction} + * in service API version {@link TextAnalyticsServiceVersion#V3_0} or {@link TextAnalyticsServiceVersion#V3_1}. + * Those actions are only available for API version 2022-05-01 and newer. * @throws TextAnalyticsException If analyze operation fails. */ @ServiceMethod(returns = ReturnType.COLLECTION) @@ -2188,6 +2274,13 @@ public SyncPoller targetVersions, + String errorMessage) { + for (TextAnalyticsServiceVersion targetVersion : targetVersions) { + if (targetVersion != null && sourceVersion != null + && targetVersion.getVersion().equals(sourceVersion.getVersion())) { + throw LOGGER.logExceptionAsError(new IllegalStateException(errorMessage)); + } + } + } + + /** + * Retrieve custom unsupported Service API version error message. + * + * @param unsupportedName The unsupported API or property name that the not available in 'minSupportedVersion'. + * @param sourceVersion The source service API version that client is using. + * @param minSupportedVersion The minimum supported Service API version. + * @return The error message. + */ + public static String getUnsupportedServiceApiVersionMessage(String unsupportedName, + TextAnalyticsServiceVersion sourceVersion, TextAnalyticsServiceVersion minSupportedVersion) { + return String.format("'%s' is not available in API version %s. Use service API version '%s' or newer.", + unsupportedName, sourceVersion, minSupportedVersion); + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/LinkedEntity.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/LinkedEntity.java index b25317c8e4ae3..73d15084243cb 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/LinkedEntity.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/LinkedEntity.java @@ -45,7 +45,7 @@ public final class LinkedEntity { /* * Bing Entity Search unique identifier of the recognized entity. Use in conjunction with * the Bing Entity Search API to fetch additional relevant information. Only available for API version - * v3.1 and up. + * v3.1 and newer. */ private String bingEntitySearchApiId; @@ -131,7 +131,7 @@ public String getDataSource() { /** * Gets the bingEntitySearchApiId property: Bing Entity Search unique identifier of the recognized entity. * Use in conjunction with the Bing Entity Search SDK to fetch additional relevant information. Only available - * for API version v3.1 and up. + * for API version v3.1 and newer. * * @return The bingEntitySearchApiId value. */ diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/PiiEntityCollection.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/PiiEntityCollection.java index 9a4f9cc735d69..4b815103e94d6 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/PiiEntityCollection.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/PiiEntityCollection.java @@ -41,7 +41,7 @@ public IterableStream getWarnings() { /** * Get the property redactedText value. The text of the input document with all of the PII information redacted out. - * Only returned for API version v3.1 and up. + * Only returned for API version v3.1 and newer. * * @return The text of the input document with all of the PII information redacted out. */ diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java new file mode 100644 index 0000000000000..71948af084d74 --- /dev/null +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.ai.textanalytics; + +import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesAction; +import com.azure.ai.textanalytics.models.AnalyzeSentimentOptions; +import com.azure.ai.textanalytics.models.MultiLabelClassifyAction; +import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesAction; +import com.azure.ai.textanalytics.models.SingleLabelClassifyAction; +import com.azure.ai.textanalytics.models.TextAnalyticsActions; +import com.azure.ai.textanalytics.models.TextAnalyticsRequestOptions; +import com.azure.core.credential.AzureKeyCredential; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.List; + +import static com.azure.ai.textanalytics.TestUtils.VALID_HTTPS_LOCALHOST; +import static com.azure.ai.textanalytics.implementation.Utility.getUnsupportedServiceApiVersionMessage; +import static com.azure.ai.textanalytics.implementation.Utility.throwIfTargetServiceVersionFound; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for Text Analytics client side validation + */ +public class ClientSideValidationUnitTests { + static TextAnalyticsClient clientV30; + static TextAnalyticsClient clientV31; + static TextAnalyticsAsyncClient asyncClientV30; + static TextAnalyticsAsyncClient asyncClientV31; + static List dummyDocument = Arrays.asList("A tree", "Be good"); + static final String DISABLE_SERVICE_LOGS_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("TextAnalyticsRequestOptions.disableServiceLogs", + TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1); + static final String RECOGNIZE_PII_ENTITIES_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("recognizePiiEntitiesBatch", TextAnalyticsServiceVersion.V3_0, + TextAnalyticsServiceVersion.V3_1); + static final String OPINION_MINING_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("AnalyzeSentimentOptions.includeOpinionMining", + TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1); + static final String ANALYZE_ACTIONS_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("beginAnalyzeActions", TextAnalyticsServiceVersion.V3_0, + TextAnalyticsServiceVersion.V3_1); + static final String HEALTHCARE_ENTITIES_ACTION_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("AnalyzeHealthcareEntitiesAction", TextAnalyticsServiceVersion.V3_1, + TextAnalyticsServiceVersion.V2022_05_01); + static final String CUSTOM_ENTITIES_ACTION_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("RecognizeCustomEntitiesAction", TextAnalyticsServiceVersion.V3_1, + TextAnalyticsServiceVersion.V2022_05_01); + static final String SINGLE_LABEL_ACTION_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("SingleLabelClassifyAction", TextAnalyticsServiceVersion.V3_1, + TextAnalyticsServiceVersion.V2022_05_01); + static final String MULTI_LABEL_ACTION_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("MultiLabelClassifyAction", TextAnalyticsServiceVersion.V3_1, + TextAnalyticsServiceVersion.V2022_05_01); + static final String ANALYZE_HEALTHCARE_ENTITIES_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("beginAnalyzeHealthcareEntities", + TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1); + static final String RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_30 = + getUnsupportedServiceApiVersionMessage("beginRecognizeCustomEntities", + TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V2022_05_01); + static final String RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_31 = + getUnsupportedServiceApiVersionMessage("beginRecognizeCustomEntities", + TextAnalyticsServiceVersion.V3_1, TextAnalyticsServiceVersion.V2022_05_01); + static final String SINGLE_LABEL_CLASSIFY_ERROR_MESSAGE_30 = + getUnsupportedServiceApiVersionMessage("beginSingleLabelClassify", TextAnalyticsServiceVersion.V3_0, + TextAnalyticsServiceVersion.V2022_05_01); + static final String SINGLE_LABEL_CLASSIFY_ERROR_MESSAGE_31 = + getUnsupportedServiceApiVersionMessage("beginSingleLabelClassify", TextAnalyticsServiceVersion.V3_1, + TextAnalyticsServiceVersion.V2022_05_01); + static final String MULTI_LABEL_CLASSIFY_ERROR_MESSAGE_30 = + getUnsupportedServiceApiVersionMessage("beginMultiLabelClassify", TextAnalyticsServiceVersion.V3_0, + TextAnalyticsServiceVersion.V2022_05_01); + static final String MULTI_LABEL_CLASSIFY_ERROR_MESSAGE_31 = + getUnsupportedServiceApiVersionMessage("beginMultiLabelClassify", TextAnalyticsServiceVersion.V3_1, + TextAnalyticsServiceVersion.V2022_05_01); + static final String PROJECT_NAME = "project-name"; + static final String DEPLOYMENT_NAME = "deployment-name"; + static final String LANGUAGE_EN = "en"; + + @BeforeAll + protected static void beforeTest() { + TextAnalyticsClientBuilder builder = new TextAnalyticsClientBuilder() + .endpoint(VALID_HTTPS_LOCALHOST) + .credential(new AzureKeyCredential("fakeKey")); + clientV30 = builder.serviceVersion(TextAnalyticsServiceVersion.V3_0).buildClient(); + asyncClientV30 = builder.serviceVersion(TextAnalyticsServiceVersion.V3_0).buildAsyncClient(); + clientV31 = builder.serviceVersion(TextAnalyticsServiceVersion.V3_1).buildClient(); + asyncClientV31 = builder.serviceVersion(TextAnalyticsServiceVersion.V3_1).buildAsyncClient(); + } + + @AfterAll + protected static void afterTest() { + clientV30 = null; + asyncClientV30 = null; + clientV31 = null; + asyncClientV31 = null; + } + + @Test + public void detectLanguageClientSideValidation() { + TextAnalyticsRequestOptions enableServiceLogsOption = new TextAnalyticsRequestOptions().setServiceLogsDisabled(true); + // Async + StepVerifier.create(asyncClientV30.detectLanguageBatch(dummyDocument, null, enableServiceLogsOption)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + }); + + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.detectLanguageBatch(dummyDocument, null, enableServiceLogsOption)); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + } + + + @Test + public void recognizeEntitiesClientSideValidation() { + TextAnalyticsRequestOptions enableServiceLogsOption = new TextAnalyticsRequestOptions().setServiceLogsDisabled(true); + + // Async + StepVerifier.create(asyncClientV30.recognizeEntitiesBatch(dummyDocument, null, enableServiceLogsOption)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertTrue(DISABLE_SERVICE_LOGS_ERROR_MESSAGE.equals(exception.getMessage())); + }); + + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.recognizeEntitiesBatch(dummyDocument, null, enableServiceLogsOption)); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void recognizePiiEntitiesClientSideValidation() { + // Async + StepVerifier.create(asyncClientV30.recognizePiiEntitiesBatch(dummyDocument, null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertTrue(RECOGNIZE_PII_ENTITIES_ERROR_MESSAGE.equals(exception.getMessage())); + }); + + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.recognizePiiEntitiesBatch(dummyDocument, null, null)); + assertEquals(RECOGNIZE_PII_ENTITIES_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void recognizeLinkedEntitiesClientSideValidation() { + TextAnalyticsRequestOptions enableServiceLogsOption = new TextAnalyticsRequestOptions().setServiceLogsDisabled(true); + + // Async + StepVerifier.create(asyncClientV30.recognizeLinkedEntitiesBatch(dummyDocument, null, enableServiceLogsOption)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + }); + + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.recognizeLinkedEntitiesBatch(dummyDocument, null, enableServiceLogsOption)); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void extractKeyPhrasesClientSideValidation() { + TextAnalyticsRequestOptions enableServiceLogsOption = new TextAnalyticsRequestOptions().setServiceLogsDisabled(true); + + // Async + StepVerifier.create(asyncClientV30.extractKeyPhrasesBatch(dummyDocument, null, enableServiceLogsOption)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + }); + + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.extractKeyPhrasesBatch(dummyDocument, null, enableServiceLogsOption)); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void analyzeSentimentClientSideValidation() { + AnalyzeSentimentOptions enableServiceLogsOption = new AnalyzeSentimentOptions().setServiceLogsDisabled(true); + + // Async + StepVerifier.create(asyncClientV30.analyzeSentimentBatch(dummyDocument, null, enableServiceLogsOption)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + }); + + AnalyzeSentimentOptions includeOpinionMiningOption = new AnalyzeSentimentOptions().setIncludeOpinionMining(true); + StepVerifier.create(asyncClientV30.analyzeSentimentBatch(dummyDocument, null, includeOpinionMiningOption)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(OPINION_MINING_ERROR_MESSAGE, exception.getMessage()); + }); + + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.analyzeSentimentBatch(dummyDocument, null, enableServiceLogsOption)); + assertEquals(DISABLE_SERVICE_LOGS_ERROR_MESSAGE, exception.getMessage()); + + IllegalStateException includeOpinionMiningException = assertThrows(IllegalStateException.class, + () -> clientV30.analyzeSentimentBatch(dummyDocument, null, includeOpinionMiningOption)); + assertEquals(OPINION_MINING_ERROR_MESSAGE, includeOpinionMiningException.getMessage()); + } + + @Test + public void analyzeActionsClientSideValidation() { + TextAnalyticsActions actions = new TextAnalyticsActions(); + // Async + // beginAnalyzeActions is only supported in 3.1 and newer + StepVerifier.create(asyncClientV30.beginAnalyzeActions(dummyDocument, actions, LANGUAGE_EN, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(ANALYZE_ACTIONS_ERROR_MESSAGE, exception.getMessage()); + }); + // AnalyzeHealthcareEntitiesAction is only supported in 2022-05-01 and newer + TextAnalyticsActions healthcareEntitiesActions = + new TextAnalyticsActions() + .setAnalyzeHealthcareEntitiesActions(new AnalyzeHealthcareEntitiesAction()); + StepVerifier.create(asyncClientV31.beginAnalyzeActions(dummyDocument, healthcareEntitiesActions, LANGUAGE_EN, + null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(HEALTHCARE_ENTITIES_ACTION_ERROR_MESSAGE, exception.getMessage()); + }); + // RecognizeCustomEntitiesAction is only supported in 2022-05-01 and newer + TextAnalyticsActions customEntitiesActions = + new TextAnalyticsActions() + .setRecognizeCustomEntitiesActions(new RecognizeCustomEntitiesAction(PROJECT_NAME, DEPLOYMENT_NAME)); + StepVerifier.create(asyncClientV31.beginAnalyzeActions(dummyDocument, customEntitiesActions, LANGUAGE_EN, + null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(CUSTOM_ENTITIES_ACTION_ERROR_MESSAGE, exception.getMessage()); + }); + // SingleLabelClassifyAction is only supported in 2022-05-01 and newer + TextAnalyticsActions singleLabelClassifyActions = + new TextAnalyticsActions() + .setSingleLabelClassifyActions(new SingleLabelClassifyAction(PROJECT_NAME, DEPLOYMENT_NAME)); + StepVerifier.create(asyncClientV31.beginAnalyzeActions(dummyDocument, singleLabelClassifyActions, LANGUAGE_EN, + null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(SINGLE_LABEL_ACTION_ERROR_MESSAGE, exception.getMessage()); + }); + // MultiLabelClassifyAction is only supported in 2022-05-01 and newer + TextAnalyticsActions multiLabelClassifyActions = + new TextAnalyticsActions() + .setMultiLabelClassifyActions(new MultiLabelClassifyAction(PROJECT_NAME, DEPLOYMENT_NAME)); + StepVerifier.create(asyncClientV31.beginAnalyzeActions(dummyDocument, multiLabelClassifyActions, LANGUAGE_EN, + null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(MULTI_LABEL_ACTION_ERROR_MESSAGE, exception.getMessage()); + }); + + // Sync + // beginAnalyzeActions is only supported in 3.1 and newer + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.beginAnalyzeActions(dummyDocument, actions, LANGUAGE_EN, null)); + assertEquals(ANALYZE_ACTIONS_ERROR_MESSAGE, exception.getMessage()); + // AnalyzeHealthcareEntitiesAction is only supported in 2022-05-01 and newer + IllegalStateException healthcareEntitiesActionsException = assertThrows(IllegalStateException.class, + () -> clientV31.beginAnalyzeActions(dummyDocument, healthcareEntitiesActions, LANGUAGE_EN, null)); + assertEquals(HEALTHCARE_ENTITIES_ACTION_ERROR_MESSAGE, healthcareEntitiesActionsException.getMessage()); + // RecognizeCustomEntitiesAction is only supported in 2022-05-01 and newer + IllegalStateException customEntitiesActionsException = assertThrows(IllegalStateException.class, + () -> clientV31.beginAnalyzeActions(dummyDocument, customEntitiesActions, LANGUAGE_EN, null)); + assertEquals(CUSTOM_ENTITIES_ACTION_ERROR_MESSAGE, customEntitiesActionsException.getMessage()); + // SingleLabelClassifyAction is only supported in 2022-05-01 and newer + IllegalStateException singleLabelClassifyActionsException = assertThrows(IllegalStateException.class, + () -> clientV31.beginAnalyzeActions(dummyDocument, singleLabelClassifyActions, LANGUAGE_EN, null)); + assertEquals(SINGLE_LABEL_ACTION_ERROR_MESSAGE, singleLabelClassifyActionsException.getMessage()); + // MultiLabelClassifyAction is only supported in 2022-05-01 and newer + IllegalStateException multiLabelClassifyActionsException = assertThrows(IllegalStateException.class, + () -> clientV31.beginAnalyzeActions(dummyDocument, multiLabelClassifyActions, LANGUAGE_EN, null)); + assertEquals(MULTI_LABEL_ACTION_ERROR_MESSAGE, multiLabelClassifyActionsException.getMessage()); + } + + @Test + public void analyzeHealthcareEntitiesClientSideValidation() { + // Async + StepVerifier.create(asyncClientV30.beginAnalyzeHealthcareEntities(dummyDocument, null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(ANALYZE_HEALTHCARE_ENTITIES_ERROR_MESSAGE, exception.getMessage()); + }); + // Sync + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> clientV30.beginAnalyzeHealthcareEntities(dummyDocument, null, null)); + assertEquals(ANALYZE_HEALTHCARE_ENTITIES_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void recognizeCustomEntitiesClientSideValidation() { + // Async + StepVerifier.create(asyncClientV30.beginRecognizeCustomEntities(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, + null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_30, exception.getMessage()); + }); + StepVerifier.create(asyncClientV31.beginRecognizeCustomEntities(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, + null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_31, exception.getMessage()); + }); + // Sync + IllegalStateException exception30 = assertThrows(IllegalStateException.class, + () -> clientV30.beginRecognizeCustomEntities(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, null, null)); + assertEquals(RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_30, exception30.getMessage()); + + IllegalStateException exception31 = assertThrows(IllegalStateException.class, + () -> clientV31.beginRecognizeCustomEntities(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, null, null)); + assertEquals(RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_31, exception31.getMessage()); + } + + @Test + public void singleLabelClassificationClientSideValidation() { + // Async + StepVerifier.create(asyncClientV30.beginSingleLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, + null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(SINGLE_LABEL_CLASSIFY_ERROR_MESSAGE_30, exception.getMessage()); + }); + StepVerifier.create(asyncClientV31.beginSingleLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, + null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(SINGLE_LABEL_CLASSIFY_ERROR_MESSAGE_31, exception.getMessage()); + }); + + // Sync + IllegalStateException exception30 = assertThrows(IllegalStateException.class, + () -> clientV30.beginSingleLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, null, null)); + assertEquals(SINGLE_LABEL_CLASSIFY_ERROR_MESSAGE_30, exception30.getMessage()); + + IllegalStateException exception31 = assertThrows(IllegalStateException.class, + () -> clientV31.beginSingleLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, null, null)); + assertEquals(SINGLE_LABEL_CLASSIFY_ERROR_MESSAGE_31, exception31.getMessage()); + } + + @Test + public void multiLabelClassificationClientSideValidation() { + // Async + StepVerifier.create(asyncClientV30.beginMultiLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, + null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(MULTI_LABEL_CLASSIFY_ERROR_MESSAGE_30, exception.getMessage()); + }); + StepVerifier.create(asyncClientV31.beginMultiLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, + null, null)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(MULTI_LABEL_CLASSIFY_ERROR_MESSAGE_31, exception.getMessage()); + }); + + // Sync + IllegalStateException exception30 = assertThrows(IllegalStateException.class, + () -> clientV30.beginMultiLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, null, null)); + assertEquals(MULTI_LABEL_CLASSIFY_ERROR_MESSAGE_30, exception30.getMessage()); + + IllegalStateException exception31 = assertThrows(IllegalStateException.class, + () -> clientV31.beginMultiLabelClassify(dummyDocument, PROJECT_NAME, DEPLOYMENT_NAME, null, null)); + assertEquals(MULTI_LABEL_CLASSIFY_ERROR_MESSAGE_31, exception31.getMessage()); + } + + + @Test + public void test() { + for (int i = 0; i < 5; i++) { + try { + throwIfTargetServiceVersionFound(TextAnalyticsServiceVersion.V3_0, + Arrays.asList(TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_0), + "a message"); + } catch (IllegalStateException ex) { + System.out.println("i = " + i); + } + } + } +} diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java index e47b94f4170da..b9d83b71c4222 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java @@ -438,7 +438,6 @@ public void recognizeEntitiesEmojiFamily(HttpClient httpClient, TextAnalyticsSer CATEGORIZED_ENTITY_INPUTS.get(1) ); } - @ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS) @MethodSource("com.azure.ai.textanalytics.TestUtils#getTestParameters") public void recognizeEntitiesEmojiFamilyWIthSkinToneModifier(HttpClient httpClient, From 7d84719ae831a6b0f5c5d5811a4f9f10b2694411 Mon Sep 17 00:00:00 2001 From: Shawn Fang <45607042+mssfang@users.noreply.github.com> Date: Thu, 1 Sep 2022 12:28:58 -0700 Subject: [PATCH 06/18] [TA] Added displayName property (#30739) --- .../azure-ai-textanalytics/CHANGELOG.md | 10 +++++ .../AnalyzeHealthcareEntityAsyncClient.java | 7 ++- .../LabelClassifyAsyncClient.java | 11 ++++- .../RecognizeCustomEntitiesAsyncClient.java | 7 ++- ...titiesOperationDetailPropertiesHelper.java | 5 +++ ...cumentOperationDetailPropertiesHelper.java | 5 +++ ...titiesOperationDetailPropertiesHelper.java | 5 +++ ...lyzeHealthcareEntitiesOperationDetail.java | 19 ++++++++ .../AnalyzeHealthcareEntitiesOptions.java | 22 ++++++++++ .../ClassifyDocumentOperationDetail.java | 19 ++++++++ .../models/MultiLabelClassifyOptions.java | 22 ++++++++++ ...ecognizeCustomEntitiesOperationDetail.java | 19 ++++++++ .../RecognizeCustomEntitiesOptions.java | 22 ++++++++++ .../models/SingleLabelClassifyOptions.java | 22 ++++++++++ .../com/azure/ai/textanalytics/TestUtils.java | 29 +++--------- .../TextAnalyticsAsyncClientTest.java | 39 +++++++++++----- .../TextAnalyticsClientTest.java | 33 +++++++++++--- ...lientTest.multiLabelClassification[1].json | 44 +++++++++---------- ...ClientTest.recognizeCustomEntities[1].json | 44 +++++++++---------- ...ientTest.singleLabelClassification[1].json | 44 +++++++++---------- ...lientTest.multiLabelClassification[1].json | 44 +++++++++---------- ...ClientTest.recognizeCustomEntities[1].json | 44 +++++++++---------- ...ientTest.singleLabelClassification[1].json | 44 +++++++++---------- 23 files changed, 384 insertions(+), 176 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md index 5ddeb0ec44496..d6e511c23e679 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md +++ b/sdk/textanalytics/azure-ai-textanalytics/CHANGELOG.md @@ -3,6 +3,16 @@ ## 5.2.0-beta.5 (Unreleased) ### Features Added +- Added `displayName` property which is the name of long-running operation, to the following classes to + set the optional display name: + - `AnalyzeHealthcareEntitiesOptions` + - `MultiLabelClassifyOptions` + - `RecognizeCustomEntitiesOptions` + - `SingleLabelClassifyOptions` +- Added `displayName` property to the following operations to read the optional display name set on options classes above: + - `AnalyzeHealthcareEntitiesOperationDetail` from `AnalyzeHealthcareEntitiesOptions` + - `ClassifyDocumentOperationDetail` from `MultiLabelClassifyOptions` and `SingleLabelClassifyOptions` + - `RecognizeCustomEntitiesOperationDetail` from `RecognizeCustomEntitiesOptions` ### Breaking Changes diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java index fc6e78ce93ab0..a0b222409555e 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java @@ -110,11 +110,13 @@ class AnalyzeHealthcareEntityAsyncClient { final boolean finalLoggingOptOut = options.isServiceLogsDisabled(); if (service != null) { + final String displayName = options.getDisplayName(); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -187,11 +189,13 @@ class AnalyzeHealthcareEntityAsyncClient { final boolean finalLoggingOptOut = options.isServiceLogsDisabled(); if (service != null) { + final String displayName = options.getDisplayName(); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -541,7 +545,8 @@ private Mono> processAnal status = LongRunningOperationStatus.fromString( analyzeOperationResultResponse.getValue().getStatus().toString(), true); } - + AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.setDisplayName(operationResultPollResponse.getValue(), + analyzeOperationResultResponse.getValue().getDisplayName()); AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.setCreatedAt(operationResultPollResponse.getValue(), analyzeOperationResultResponse.getValue().getCreatedDateTime()); AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.setLastModifiedAt( diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java index c68207f0cdcff..79e91024900ad 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/LabelClassifyAsyncClient.java @@ -97,12 +97,14 @@ PollerFlux singleLab .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); final boolean finalLoggingOptOut = options.isServiceLogsDisabled(); final boolean finalIncludeStatistics = options.isIncludeStatistics(); + final String displayName = options.getDisplayName(); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -148,12 +150,14 @@ PollerFlux singl .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); final boolean finalIncludeStatistics = options.isIncludeStatistics(); final boolean finalLoggingOptOut = options.isServiceLogsDisabled(); + final String displayName = options.getDisplayName(); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -199,12 +203,14 @@ PollerFlux multiLabe .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); final boolean finalLoggingOptOut = options.isServiceLogsDisabled(); final boolean finalIncludeStatistics = options.isIncludeStatistics(); + final String displayName = options.getDisplayName(); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -250,12 +256,14 @@ PollerFlux multi .addData(AZ_TRACING_NAMESPACE_KEY, COGNITIVE_TRACING_NAMESPACE_VALUE); final boolean finalIncludeStatistics = options.isIncludeStatistics(); final boolean finalLoggingOptOut = options.isServiceLogsDisabled(); + final String displayName = options.getDisplayName(); return new PollerFlux<>( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -464,7 +472,8 @@ private Mono> processAnalyzeTextMo status = LongRunningOperationStatus.fromString( analyzeOperationResultResponse.getValue().getStatus().toString(), true); } - + ClassifyDocumentOperationDetailPropertiesHelper.setDisplayName(operationResultPollResponse.getValue(), + analyzeOperationResultResponse.getValue().getDisplayName()); ClassifyDocumentOperationDetailPropertiesHelper.setCreatedAt(operationResultPollResponse.getValue(), analyzeOperationResultResponse.getValue().getCreatedDateTime()); ClassifyDocumentOperationDetailPropertiesHelper.setLastModifiedAt( diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java index 84ef23532524f..b1c398db5e4ac 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/RecognizeCustomEntitiesAsyncClient.java @@ -96,12 +96,14 @@ PollerFlux( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -148,12 +150,14 @@ PollerFlux( DEFAULT_POLL_INTERVAL, activationOperation( service.submitJobWithResponseAsync( new AnalyzeTextJobsInput() + .setDisplayName(displayName) .setAnalysisInput( new MultiLanguageAnalysisInput().setDocuments(toMultiLanguageInput(documents))) .setTasks(Arrays.asList( @@ -352,7 +356,8 @@ private Mono> processAnalyz status = LongRunningOperationStatus.fromString( analyzeOperationResultResponse.getValue().getStatus().toString(), true); } - + RecognizeCustomEntitiesOperationDetailPropertiesHelper.setDisplayName(operationResultPollResponse.getValue(), + analyzeOperationResultResponse.getValue().getDisplayName()); RecognizeCustomEntitiesOperationDetailPropertiesHelper.setCreatedAt(operationResultPollResponse.getValue(), analyzeOperationResultResponse.getValue().getCreatedDateTime()); RecognizeCustomEntitiesOperationDetailPropertiesHelper.setLastModifiedAt( diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.java index a5bb2bf016007..4749858711d04 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper.java @@ -19,6 +19,7 @@ private AnalyzeHealthcareEntitiesOperationDetailPropertiesHelper() { } */ public interface AnalyzeHealthcareEntitiesOperationDetailAccessor { void setOperationId(AnalyzeHealthcareEntitiesOperationDetail operationDetail, String operationId); + void setDisplayName(AnalyzeHealthcareEntitiesOperationDetail operationDetail, String name); void setCreatedAt(AnalyzeHealthcareEntitiesOperationDetail operationDetail, OffsetDateTime createdAt); void setExpiresAt(AnalyzeHealthcareEntitiesOperationDetail operationDetail, OffsetDateTime expiresAt); void setLastModifiedAt(AnalyzeHealthcareEntitiesOperationDetail operationDetail, @@ -39,6 +40,10 @@ public static void setOperationId(AnalyzeHealthcareEntitiesOperationDetail opera accessor.setOperationId(operationDetail, operationId); } + public static void setDisplayName(AnalyzeHealthcareEntitiesOperationDetail operationDetail, String name) { + accessor.setDisplayName(operationDetail, name); + } + public static void setCreatedAt(AnalyzeHealthcareEntitiesOperationDetail operationDetail, OffsetDateTime createdAt) { accessor.setCreatedAt(operationDetail, createdAt); diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/ClassifyDocumentOperationDetailPropertiesHelper.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/ClassifyDocumentOperationDetailPropertiesHelper.java index 9f1880760a523..53b4b2b57a809 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/ClassifyDocumentOperationDetailPropertiesHelper.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/ClassifyDocumentOperationDetailPropertiesHelper.java @@ -19,6 +19,7 @@ private ClassifyDocumentOperationDetailPropertiesHelper() { } */ public interface ClassifyDocumentOperationDetailAccessor { void setOperationId(ClassifyDocumentOperationDetail operationDetail, String operationId); + void setDisplayName(ClassifyDocumentOperationDetail operationDetail, String name); void setCreatedAt(ClassifyDocumentOperationDetail operationDetail, OffsetDateTime createdAt); void setExpiresAt(ClassifyDocumentOperationDetail operationDetail, OffsetDateTime expiresAt); void setLastModifiedAt(ClassifyDocumentOperationDetail operationDetail, @@ -39,6 +40,10 @@ public static void setOperationId(ClassifyDocumentOperationDetail operationDetai accessor.setOperationId(operationDetail, operationId); } + public static void setDisplayName(ClassifyDocumentOperationDetail operationDetail, String name) { + accessor.setDisplayName(operationDetail, name); + } + public static void setCreatedAt(ClassifyDocumentOperationDetail operationDetail, OffsetDateTime createdAt) { accessor.setCreatedAt(operationDetail, createdAt); diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/RecognizeCustomEntitiesOperationDetailPropertiesHelper.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/RecognizeCustomEntitiesOperationDetailPropertiesHelper.java index 4e8c4ad50ece6..470b94cba3be3 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/RecognizeCustomEntitiesOperationDetailPropertiesHelper.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/RecognizeCustomEntitiesOperationDetailPropertiesHelper.java @@ -20,6 +20,7 @@ private RecognizeCustomEntitiesOperationDetailPropertiesHelper() { } */ public interface RecognizeCustomEntitiesOperationDetailAccessor { void setOperationId(RecognizeCustomEntitiesOperationDetail operationDetail, String operationId); + void setDisplayName(RecognizeCustomEntitiesOperationDetail operationDetail, String name); void setCreatedAt(RecognizeCustomEntitiesOperationDetail operationDetail, OffsetDateTime createdAt); void setExpiresAt(RecognizeCustomEntitiesOperationDetail operationDetail, OffsetDateTime expiresAt); void setLastModifiedAt(RecognizeCustomEntitiesOperationDetail operationDetail, @@ -40,6 +41,10 @@ public static void setOperationId(RecognizeCustomEntitiesOperationDetail operati accessor.setOperationId(operationDetail, operationId); } + public static void setDisplayName(RecognizeCustomEntitiesOperationDetail operationDetail, String name) { + accessor.setDisplayName(operationDetail, name); + } + public static void setCreatedAt(RecognizeCustomEntitiesOperationDetail operationDetail, OffsetDateTime createdAt) { accessor.setCreatedAt(operationDetail, createdAt); diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOperationDetail.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOperationDetail.java index b792b888ef28e..ac90fcf9e1f91 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOperationDetail.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOperationDetail.java @@ -14,6 +14,7 @@ @Immutable public final class AnalyzeHealthcareEntitiesOperationDetail { private String operationId; + private String displayName; private OffsetDateTime createdAt; private OffsetDateTime expiresAt; private OffsetDateTime lastModifiedAt; @@ -28,6 +29,11 @@ public void setOperationId(AnalyzeHealthcareEntitiesOperationDetail operationRes operationResult.setOperationId(operationId); } + @Override + public void setDisplayName(AnalyzeHealthcareEntitiesOperationDetail operationDetail, String name) { + operationDetail.setDisplayName(name); + } + @Override public void setExpiresAt(AnalyzeHealthcareEntitiesOperationDetail operationDetail, OffsetDateTime expiresAt) { @@ -58,6 +64,15 @@ public String getOperationId() { return operationId; } + /** + * Gets the displayName property of the {@link AnalyzeHealthcareEntitiesOperationDetail}. + * + * @return The displayName property of the {@link AnalyzeHealthcareEntitiesOperationDetail}. + */ + public String getDisplayName() { + return displayName; + } + /** * Gets the created time of an action. * @@ -89,6 +104,10 @@ private void setOperationId(String operationId) { this.operationId = operationId; } + private void setDisplayName(String displayName) { + this.displayName = displayName; + } + private void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOptions.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOptions.java index fe14b97b08d7e..8d3726497a51f 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOptions.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesOptions.java @@ -10,8 +10,30 @@ */ @Fluent public final class AnalyzeHealthcareEntitiesOptions extends TextAnalyticsRequestOptions { + private String displayName; private Boolean disableServiceLogs; + /** + * Gets display name of the operation. + * + * @return Display name of the operation. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets display name of the operation. + * + * @param displayName Display name of the operation. + * + * @return The {@link AnalyzeHealthcareEntitiesOptions} object itself. + */ + public AnalyzeHealthcareEntitiesOptions setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + /** * Sets the model version. This value indicates which model will be used for scoring, e.g. "latest", "2019-10-01". * If a model-version is not specified, the API will default to the latest, non-preview version. diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/ClassifyDocumentOperationDetail.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/ClassifyDocumentOperationDetail.java index 84b144dc9558e..9956fa3af812e 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/ClassifyDocumentOperationDetail.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/ClassifyDocumentOperationDetail.java @@ -14,6 +14,7 @@ @Immutable public final class ClassifyDocumentOperationDetail { private String operationId; + private String displayName; private OffsetDateTime createdAt; private OffsetDateTime expiresAt; private OffsetDateTime lastModifiedAt; @@ -27,6 +28,11 @@ public void setOperationId(ClassifyDocumentOperationDetail operationResult, operationResult.setOperationId(operationId); } + @Override + public void setDisplayName(ClassifyDocumentOperationDetail operationDetail, String name) { + operationDetail.setDisplayName(name); + } + @Override public void setExpiresAt(ClassifyDocumentOperationDetail operationDetail, OffsetDateTime expiresAt) { @@ -57,6 +63,15 @@ public String getOperationId() { return operationId; } + /** + * Gets the displayName property of the {@link ClassifyDocumentOperationDetail}. + * + * @return The displayName property of the {@link ClassifyDocumentOperationDetail}. + */ + public String getDisplayName() { + return displayName; + } + /** * Gets the created time of an action. * @@ -88,6 +103,10 @@ private void setOperationId(String operationId) { this.operationId = operationId; } + private void setDisplayName(String displayName) { + this.displayName = displayName; + } + private void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/MultiLabelClassifyOptions.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/MultiLabelClassifyOptions.java index e957c5dc37ed6..6011650072fe2 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/MultiLabelClassifyOptions.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/MultiLabelClassifyOptions.java @@ -10,9 +10,31 @@ */ @Fluent public final class MultiLabelClassifyOptions { + private String displayName; private boolean disableServiceLogs; private boolean includeStatistics; + /** + * Gets display name of the operation. + * + * @return Display name of the operation. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets display name of the operation. + * + * @param displayName Display name of the operation. + * + * @return The {@link MultiLabelClassifyOptions} object itself. + */ + public MultiLabelClassifyOptions setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + /** * Get the value of {@code includeStatistics}. * diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOperationDetail.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOperationDetail.java index e31b068230a53..f8e7fa41e3dfe 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOperationDetail.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOperationDetail.java @@ -14,6 +14,7 @@ @Immutable public final class RecognizeCustomEntitiesOperationDetail { private String operationId; + private String displayName; private OffsetDateTime createdAt; private OffsetDateTime expiresAt; private OffsetDateTime lastModifiedAt; @@ -28,6 +29,11 @@ public void setOperationId(RecognizeCustomEntitiesOperationDetail operationResul operationResult.setOperationId(operationId); } + @Override + public void setDisplayName(RecognizeCustomEntitiesOperationDetail operationDetail, String name) { + operationDetail.setDisplayName(name); + } + @Override public void setExpiresAt(RecognizeCustomEntitiesOperationDetail operationDetail, OffsetDateTime expiresAt) { @@ -58,6 +64,15 @@ public String getOperationId() { return operationId; } + /** + * Gets the displayName property of the {@link RecognizeCustomEntitiesOperationDetail}. + * + * @return The displayName property of the {@link RecognizeCustomEntitiesOperationDetail}. + */ + public String getDisplayName() { + return displayName; + } + /** * Gets the created time of an action. * @@ -89,6 +104,10 @@ private void setOperationId(String operationId) { this.operationId = operationId; } + private void setDisplayName(String displayName) { + this.displayName = displayName; + } + private void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOptions.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOptions.java index b7c0c166b99af..e0ba43882c073 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOptions.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/RecognizeCustomEntitiesOptions.java @@ -10,9 +10,31 @@ */ @Fluent public final class RecognizeCustomEntitiesOptions { + private String displayName; private boolean includeStatistics; private boolean disableServiceLogs; + /** + * Gets display name of the operation. + * + * @return Display name of the operation. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets display name of the operation. + * + * @param displayName Display name of the operation. + * + * @return The {@link RecognizeCustomEntitiesOptions} object itself. + */ + public RecognizeCustomEntitiesOptions setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + /** * Get the value of {@code includeStatistics}. * diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/SingleLabelClassifyOptions.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/SingleLabelClassifyOptions.java index 2ebb0c360e5f5..d2dca22272fba 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/SingleLabelClassifyOptions.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/SingleLabelClassifyOptions.java @@ -10,9 +10,31 @@ */ @Fluent public final class SingleLabelClassifyOptions { + private String displayName; private boolean disableServiceLogs; private boolean includeStatistics; + /** + * Gets display name of the operation. + * + * @return Display name of the operation. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets display name of the operation. + * + * @param displayName Display name of the operation. + * + * @return The {@link SingleLabelClassifyOptions} object itself. + */ + public SingleLabelClassifyOptions setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + /** * Get the value of {@code includeStatistics}. * diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TestUtils.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TestUtils.java index 65c02552a4b7e..a1b1f2d3c325b 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TestUtils.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TestUtils.java @@ -899,24 +899,15 @@ static AnalyzeHealthcareEntitiesResult getRecognizeHealthcareEntitiesResult2() { HealthcareEntityPropertiesHelper.setDataSources(healthcareEntity2, IterableStream.of(Collections.emptyList())); final HealthcareEntity healthcareEntity3 = new HealthcareEntity(); - HealthcareEntityPropertiesHelper.setText(healthcareEntity3, "ST depressions"); - HealthcareEntityPropertiesHelper.setNormalizedText(healthcareEntity3, "ST segment depression (finding)"); + HealthcareEntityPropertiesHelper.setText(healthcareEntity3, "ST depressions in the anterior lateral leads"); +// HealthcareEntityPropertiesHelper.setNormalizedText(healthcareEntity3, null); HealthcareEntityPropertiesHelper.setCategory(healthcareEntity3, HealthcareEntityCategory.SYMPTOM_OR_SIGN); HealthcareEntityPropertiesHelper.setConfidenceScore(healthcareEntity3, 1.0); HealthcareEntityPropertiesHelper.setOffset(healthcareEntity3, 46); - HealthcareEntityPropertiesHelper.setLength(healthcareEntity3, 14); + HealthcareEntityPropertiesHelper.setLength(healthcareEntity3, 44); // there are too many healthcare entity data sources, we can just assert it is not null. HealthcareEntityPropertiesHelper.setDataSources(healthcareEntity3, IterableStream.of(Collections.emptyList())); - final HealthcareEntity healthcareEntity4 = new HealthcareEntity(); - HealthcareEntityPropertiesHelper.setText(healthcareEntity4, "anterior lateral"); - HealthcareEntityPropertiesHelper.setCategory(healthcareEntity4, HealthcareEntityCategory.DIRECTION); - HealthcareEntityPropertiesHelper.setConfidenceScore(healthcareEntity4, 0.6); - HealthcareEntityPropertiesHelper.setOffset(healthcareEntity4, 68); - HealthcareEntityPropertiesHelper.setLength(healthcareEntity4, 16); - // there are too many healthcare entity data sources, we can just assert it is not null. - HealthcareEntityPropertiesHelper.setDataSources(healthcareEntity4, - IterableStream.of(Collections.emptyList())); final HealthcareEntity healthcareEntity5 = new HealthcareEntity(); HealthcareEntityPropertiesHelper.setText(healthcareEntity5, "fatigue"); HealthcareEntityPropertiesHelper.setNormalizedText(healthcareEntity5, "Fatigue"); @@ -952,7 +943,7 @@ static AnalyzeHealthcareEntitiesResult getRecognizeHealthcareEntitiesResult2() { final AnalyzeHealthcareEntitiesResult healthcareEntitiesResult = new AnalyzeHealthcareEntitiesResult("1", textDocumentStatistics, null); AnalyzeHealthcareEntitiesResultPropertiesHelper.setEntities(healthcareEntitiesResult, - new IterableStream<>(asList(healthcareEntity1, healthcareEntity2, healthcareEntity3, healthcareEntity4, + new IterableStream<>(asList(healthcareEntity1, healthcareEntity2, healthcareEntity3, healthcareEntity5, healthcareEntity6, healthcareEntity7))); // HealthcareEntityRelations @@ -976,18 +967,8 @@ static AnalyzeHealthcareEntitiesResult getRecognizeHealthcareEntitiesResult2() { HealthcareEntityRelationType.QUALIFIER_OF_CONDITION); HealthcareEntityRelationPropertiesHelper.setRoles(healthcareEntityRelation2, IterableStream.of(asList(role3, role2))); - - final HealthcareEntityRelation healthcareEntityRelation3 = new HealthcareEntityRelation(); - final HealthcareEntityRelationRole role4 = new HealthcareEntityRelationRole(); - HealthcareEntityRelationRolePropertiesHelper.setName(role4, "Direction"); - HealthcareEntityRelationRolePropertiesHelper.setEntity(role4, healthcareEntity4); - HealthcareEntityRelationPropertiesHelper.setRelationType(healthcareEntityRelation3, - HealthcareEntityRelationType.DIRECTION_OF_CONDITION); - HealthcareEntityRelationPropertiesHelper.setRoles(healthcareEntityRelation3, - IterableStream.of(asList(role2, role4))); - AnalyzeHealthcareEntitiesResultPropertiesHelper.setEntityRelations(healthcareEntitiesResult, - IterableStream.of(asList(healthcareEntityRelation1, healthcareEntityRelation2, healthcareEntityRelation3))); + IterableStream.of(asList(healthcareEntityRelation1, healthcareEntityRelation2))); return healthcareEntitiesResult; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClientTest.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClientTest.java index 7b0a158153657..38c0145d728db 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClientTest.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsAsyncClientTest.java @@ -11,11 +11,14 @@ import com.azure.ai.textanalytics.models.ClassifyDocumentOperationDetail; import com.azure.ai.textanalytics.models.EntityConditionality; import com.azure.ai.textanalytics.models.HealthcareEntityAssertion; +import com.azure.ai.textanalytics.models.MultiLabelClassifyOptions; import com.azure.ai.textanalytics.models.PiiEntityCategory; import com.azure.ai.textanalytics.models.PiiEntityDomain; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOperationDetail; +import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOptions; import com.azure.ai.textanalytics.models.RecognizeEntitiesAction; import com.azure.ai.textanalytics.models.RecognizePiiEntitiesOptions; +import com.azure.ai.textanalytics.models.SingleLabelClassifyOptions; import com.azure.ai.textanalytics.models.TargetSentiment; import com.azure.ai.textanalytics.models.TextAnalyticsActions; import com.azure.ai.textanalytics.models.TextAnalyticsError; @@ -30,6 +33,7 @@ import com.azure.core.http.HttpClient; import com.azure.core.util.IterableStream; import com.azure.core.util.polling.LongRunningOperationStatus; +import com.azure.core.util.polling.PollResponse; import com.azure.core.util.polling.SyncPoller; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -1899,10 +1903,18 @@ public void healthcareLroPagination(HttpClient httpClient, TextAnalyticsServiceV public void healthcareLroWithOptions(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsAsyncClient(httpClient, serviceVersion); healthcareLroRunner((documents, options) -> { + boolean isValidApiVersionForDisplayName = serviceVersion != TextAnalyticsServiceVersion.V3_0 + && serviceVersion != TextAnalyticsServiceVersion.V3_1; + if (isValidApiVersionForDisplayName) { + options.setDisplayName("operationName"); + } SyncPoller syncPoller = client.beginAnalyzeHealthcareEntities(documents, options).getSyncPoller(); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + if (isValidApiVersionForDisplayName) { + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); + } AnalyzeHealthcareEntitiesPagedFlux analyzeHealthcareEntitiesPagedFlux = syncPoller.getFinalResult(); validateAnalyzeHealthcareEntitiesResultCollectionList( options.isIncludeStatistics(), @@ -2531,13 +2543,15 @@ public void multiCategoryClassifyAction(HttpClient httpClient, TextAnalyticsServ public void recognizeCustomEntities(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsAsyncClient(httpClient, serviceVersion); recognizeCustomEntitiesRunner((documents, parameters) -> { + RecognizeCustomEntitiesOptions options = new RecognizeCustomEntitiesOptions() + .setDisplayName("operationName"); SyncPoller syncPoller = - client.beginRecognizeCustomEntities(documents, parameters.get(0), parameters.get(1), "en", - null).getSyncPoller(); + client.beginRecognizeCustomEntities(documents, parameters.get(0), parameters.get(1), "en", options) + .getSyncPoller(); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); RecognizeCustomEntitiesPagedFlux pagedFlux = syncPoller.getFinalResult(); - pagedFlux.toStream().collect(Collectors.toList()).forEach(resultCollection -> resultCollection.forEach(documentResult -> validateCategorizedEntities(documentResult.getEntities().stream().collect(Collectors.toList())))); @@ -2549,11 +2563,13 @@ public void recognizeCustomEntities(HttpClient httpClient, TextAnalyticsServiceV public void singleLabelClassification(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsAsyncClient(httpClient, serviceVersion); classifyCustomSingleLabelRunner((documents, parameters) -> { + SingleLabelClassifyOptions options = new SingleLabelClassifyOptions().setDisplayName("operationName"); SyncPoller syncPoller = - client.beginSingleLabelClassify(documents, parameters.get(0), parameters.get(1), "en", - null).getSyncPoller(); + client.beginSingleLabelClassify(documents, parameters.get(0), parameters.get(1), "en", options) + .getSyncPoller(); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); ClassifyDocumentPagedFlux pagedFlux = syncPoller.getFinalResult(); pagedFlux.toStream().collect(Collectors.toList()).forEach(resultCollection -> resultCollection.forEach(documentResult -> validateLabelClassificationResult(documentResult))); @@ -2565,10 +2581,13 @@ public void singleLabelClassification(HttpClient httpClient, TextAnalyticsServic public void multiLabelClassification(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsAsyncClient(httpClient, serviceVersion); classifyCustomMultiLabelRunner((documents, parameters) -> { + MultiLabelClassifyOptions options = new MultiLabelClassifyOptions().setDisplayName("operationName"); SyncPoller syncPoller = - client.beginMultiLabelClassify(documents, parameters.get(0), parameters.get(1), "en", null).getSyncPoller(); + client.beginMultiLabelClassify(documents, parameters.get(0), parameters.get(1), "en", options) + .getSyncPoller(); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); ClassifyDocumentPagedFlux pagedFlux = syncPoller.getFinalResult(); pagedFlux.toStream().collect(Collectors.toList()).forEach(resultCollection -> resultCollection.forEach(documentResult -> validateLabelClassificationResult(documentResult))); diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java index b9d83b71c4222..4ef89a965ede5 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTest.java @@ -15,12 +15,15 @@ import com.azure.ai.textanalytics.models.HealthcareEntityAssertion; import com.azure.ai.textanalytics.models.KeyPhrasesCollection; import com.azure.ai.textanalytics.models.LinkedEntity; +import com.azure.ai.textanalytics.models.MultiLabelClassifyOptions; import com.azure.ai.textanalytics.models.PiiEntityCategory; import com.azure.ai.textanalytics.models.PiiEntityCollection; import com.azure.ai.textanalytics.models.PiiEntityDomain; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOperationDetail; +import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesOptions; import com.azure.ai.textanalytics.models.RecognizeEntitiesAction; import com.azure.ai.textanalytics.models.RecognizePiiEntitiesOptions; +import com.azure.ai.textanalytics.models.SingleLabelClassifyOptions; import com.azure.ai.textanalytics.models.TargetSentiment; import com.azure.ai.textanalytics.models.TextAnalyticsActions; import com.azure.ai.textanalytics.models.TextAnalyticsError; @@ -39,6 +42,7 @@ import com.azure.core.util.Context; import com.azure.core.util.IterableStream; import com.azure.core.util.polling.LongRunningOperationStatus; +import com.azure.core.util.polling.PollResponse; import com.azure.core.util.polling.SyncPoller; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -1814,10 +1818,18 @@ public void analyzeSentimentZalgoText(HttpClient httpClient, TextAnalyticsServic public void healthcareLroWithOptions(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsClient(httpClient, serviceVersion); healthcareLroRunner((documents, options) -> { + boolean isValidApiVersionForDisplayName = serviceVersion != TextAnalyticsServiceVersion.V3_0 + && serviceVersion != TextAnalyticsServiceVersion.V3_1; + if (isValidApiVersionForDisplayName) { + options.setDisplayName("operationName"); + } SyncPoller syncPoller = client.beginAnalyzeHealthcareEntities(documents, options, Context.NONE); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + if (isValidApiVersionForDisplayName) { + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); + } AnalyzeHealthcareEntitiesPagedIterable analyzeHealthcareEntitiesPagedIterable = syncPoller.getFinalResult(); validateAnalyzeHealthcareEntitiesResultCollectionList( options.isIncludeStatistics(), @@ -2469,10 +2481,13 @@ public void multiCategoryClassifyAction(HttpClient httpClient, public void recognizeCustomEntities(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsClient(httpClient, serviceVersion); recognizeCustomEntitiesRunner((documents, parameters) -> { + RecognizeCustomEntitiesOptions options = new RecognizeCustomEntitiesOptions() + .setDisplayName("operationName"); SyncPoller syncPoller = - client.beginRecognizeCustomEntities(documents, parameters.get(0), parameters.get(1), "en", null); + client.beginRecognizeCustomEntities(documents, parameters.get(0), parameters.get(1), "en", options); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); RecognizeCustomEntitiesPagedIterable pagedIterable = syncPoller.getFinalResult(); pagedIterable.forEach(resultCollection -> resultCollection.forEach(documentResult -> @@ -2485,10 +2500,12 @@ public void recognizeCustomEntities(HttpClient httpClient, TextAnalyticsServiceV public void singleLabelClassification(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsClient(httpClient, serviceVersion); classifyCustomSingleLabelRunner((documents, parameters) -> { + SingleLabelClassifyOptions options = new SingleLabelClassifyOptions().setDisplayName("operationName"); SyncPoller syncPoller = - client.beginSingleLabelClassify(documents, parameters.get(0), parameters.get(1), "en", null); + client.beginSingleLabelClassify(documents, parameters.get(0), parameters.get(1), "en", options); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); ClassifyDocumentPagedIterable pagedIterable = syncPoller.getFinalResult(); pagedIterable.forEach(resultCollection -> resultCollection.forEach(documentResult -> validateLabelClassificationResult(documentResult))); @@ -2500,10 +2517,12 @@ public void singleLabelClassification(HttpClient httpClient, TextAnalyticsServic public void multiLabelClassification(HttpClient httpClient, TextAnalyticsServiceVersion serviceVersion) { client = getTextAnalyticsClient(httpClient, serviceVersion); classifyCustomMultiLabelRunner((documents, parameters) -> { + MultiLabelClassifyOptions options = new MultiLabelClassifyOptions().setDisplayName("operationName"); SyncPoller syncPoller = - client.beginMultiLabelClassify(documents, parameters.get(0), parameters.get(1), "en", null); + client.beginMultiLabelClassify(documents, parameters.get(0), parameters.get(1), "en", options); syncPoller = setPollInterval(syncPoller); - syncPoller.waitForCompletion(); + PollResponse pollResponse = syncPoller.waitForCompletion(); + assertEquals(options.getDisplayName(), pollResponse.getValue().getDisplayName()); ClassifyDocumentPagedIterable pagedIterable = syncPoller.getFinalResult(); pagedIterable.forEach(resultCollection -> resultCollection.forEach(documentResult -> validateLabelClassificationResult(documentResult))); diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.multiLabelClassification[1].json b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.multiLabelClassification[1].json index a9de5e2387d56..4a3bfa3561de3 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.multiLabelClassification[1].json +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.multiLabelClassification[1].json @@ -3,59 +3,59 @@ "Method" : "POST", "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-05-01", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "f53ab57a-2ef5-449f-a2c2-43f772352bc5", + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "916e5787-979c-493c-8bb2-ae719e79aab7", "Content-Type" : "application/json" }, "Response" : { "content-length" : "0", - "x-envoy-upstream-service-time" : "509", + "x-envoy-upstream-service-time" : "515", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", - "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/fb41649a-4f2a-4a79-b2b6-3f10c8c89407?api-version=2022-05-01", + "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/b8ae4b11-f490-4467-8acd-405e3e52e595?api-version=2022-05-01", "x-content-type-options" : "nosniff", - "apim-request-id" : "079e274c-ee49-4842-8f60-232060e6b851", + "apim-request-id" : "8e94997d-ba80-4fb7-a47c-d1985b8eb03b", "retry-after" : "0", "StatusCode" : "202", - "Date" : "Thu, 11 Aug 2022 18:08:59 GMT" + "Date" : "Thu, 01 Sep 2022 07:36:52 GMT" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/fb41649a-4f2a-4a79-b2b6-3f10c8c89407?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/b8ae4b11-f490-4467-8acd-405e3e52e595?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "5a38509b-1269-4005-8aa6-e1733abb2492" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "4b32e681-f544-4b7b-8aaa-4e410fb31ad5" }, "Response" : { - "content-length" : "641", - "x-envoy-upstream-service-time" : "32", + "content-length" : "671", + "x-envoy-upstream-service-time" : "93", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "1009ca9a-cce1-473b-b364-b77fbf4b35aa", + "apim-request-id" : "8f11db43-d55b-4a76-9eca-3ac9cb61d1e2", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"fb41649a-4f2a-4a79-b2b6-3f10c8c89407\",\"lastUpdatedDateTime\":\"2022-08-11T18:09:01Z\",\"createdDateTime\":\"2022-08-11T18:09:00Z\",\"expirationDateTime\":\"2022-08-12T18:09:00Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:09:01.1086171Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:09:30 GMT", + "Body" : "{\"jobId\":\"b8ae4b11-f490-4467-8acd-405e3e52e595\",\"lastUpdatedDateTime\":\"2022-09-01T07:36:54Z\",\"createdDateTime\":\"2022-09-01T07:36:53Z\",\"expirationDateTime\":\"2022-09-02T07:36:53Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:36:54.2430088Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:37:23 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/fb41649a-4f2a-4a79-b2b6-3f10c8c89407?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/b8ae4b11-f490-4467-8acd-405e3e52e595?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "f357d703-486b-494b-979c-9225be8da9fb" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "0164272d-5e0f-4ef6-8311-9f3640dd582e" }, "Response" : { - "content-length" : "641", - "x-envoy-upstream-service-time" : "29", + "content-length" : "671", + "x-envoy-upstream-service-time" : "36", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "638fcb1e-628f-4004-896b-88f379547935", + "apim-request-id" : "bec6b3ec-6099-4a44-a30d-af73da6f8d02", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"fb41649a-4f2a-4a79-b2b6-3f10c8c89407\",\"lastUpdatedDateTime\":\"2022-08-11T18:09:01Z\",\"createdDateTime\":\"2022-08-11T18:09:00Z\",\"expirationDateTime\":\"2022-08-12T18:09:00Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:09:01.1086171Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:09:30 GMT", + "Body" : "{\"jobId\":\"b8ae4b11-f490-4467-8acd-405e3e52e595\",\"lastUpdatedDateTime\":\"2022-09-01T07:36:54Z\",\"createdDateTime\":\"2022-09-01T07:36:53Z\",\"expirationDateTime\":\"2022-09-02T07:36:53Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:36:54.2430088Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:37:23 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.recognizeCustomEntities[1].json b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.recognizeCustomEntities[1].json index 1b798e2cc5162..030248c645e7e 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.recognizeCustomEntities[1].json +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.recognizeCustomEntities[1].json @@ -3,59 +3,59 @@ "Method" : "POST", "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-05-01", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "8d7803d1-85bc-487f-a60e-0c3913b25be3", + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "fbc1851f-4d88-405c-aeb1-f6efb9e96043", "Content-Type" : "application/json" }, "Response" : { "content-length" : "0", - "x-envoy-upstream-service-time" : "170", + "x-envoy-upstream-service-time" : "268", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", - "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/6afcaf9a-b0b7-49ec-aad6-05d24abb6b36?api-version=2022-05-01", + "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/17082e50-4823-4f0f-83ae-a2a7a52e689d?api-version=2022-05-01", "x-content-type-options" : "nosniff", - "apim-request-id" : "4e76b7ca-ed4d-4dae-a5b9-be205126539f", + "apim-request-id" : "39461fae-319a-4306-b33a-d5d8945ef513", "retry-after" : "0", "StatusCode" : "202", - "Date" : "Thu, 11 Aug 2022 18:05:55 GMT" + "Date" : "Thu, 01 Sep 2022 07:40:18 GMT" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/6afcaf9a-b0b7-49ec-aad6-05d24abb6b36?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/17082e50-4823-4f0f-83ae-a2a7a52e689d?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "7402d5ee-370b-4acf-8284-2f11ca11b44f" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "2feeb6cf-8cef-4b18-99c2-4d11fe5309fd" }, "Response" : { - "content-length" : "1244", - "x-envoy-upstream-service-time" : "29", + "content-length" : "1274", + "x-envoy-upstream-service-time" : "69", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "fd61b8c1-ea70-46b7-a147-47a43896ba15", + "apim-request-id" : "c2689873-a120-4411-999f-5ee6097d1658", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"6afcaf9a-b0b7-49ec-aad6-05d24abb6b36\",\"lastUpdatedDateTime\":\"2022-08-11T18:05:56Z\",\"createdDateTime\":\"2022-08-11T18:05:55Z\",\"expirationDateTime\":\"2022-08-12T18:05:55Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:05:56.8652002Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:06:26 GMT", + "Body" : "{\"jobId\":\"17082e50-4823-4f0f-83ae-a2a7a52e689d\",\"lastUpdatedDateTime\":\"2022-09-01T07:40:19Z\",\"createdDateTime\":\"2022-09-01T07:40:18Z\",\"expirationDateTime\":\"2022-09-02T07:40:18Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:40:19.2774536Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:40:48 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/6afcaf9a-b0b7-49ec-aad6-05d24abb6b36?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/17082e50-4823-4f0f-83ae-a2a7a52e689d?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "754aeb4b-8f66-441c-aeb6-2dedf7a28b3f" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "73d1e59d-2da3-4c64-8c84-b78eb26313ec" }, "Response" : { - "content-length" : "1244", - "x-envoy-upstream-service-time" : "34", + "content-length" : "1274", + "x-envoy-upstream-service-time" : "26", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "30705a54-2950-4769-820f-faa508086405", + "apim-request-id" : "ab0cfd06-99ef-4f14-b171-d2a495de1966", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"6afcaf9a-b0b7-49ec-aad6-05d24abb6b36\",\"lastUpdatedDateTime\":\"2022-08-11T18:05:56Z\",\"createdDateTime\":\"2022-08-11T18:05:55Z\",\"expirationDateTime\":\"2022-08-12T18:05:55Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:05:56.8652002Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:06:26 GMT", + "Body" : "{\"jobId\":\"17082e50-4823-4f0f-83ae-a2a7a52e689d\",\"lastUpdatedDateTime\":\"2022-09-01T07:40:19Z\",\"createdDateTime\":\"2022-09-01T07:40:18Z\",\"expirationDateTime\":\"2022-09-02T07:40:18Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:40:19.2774536Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:40:48 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.singleLabelClassification[1].json b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.singleLabelClassification[1].json index cfcc57c0484d7..5fad6ccbe76bd 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.singleLabelClassification[1].json +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsAsyncClientTest.singleLabelClassification[1].json @@ -3,59 +3,59 @@ "Method" : "POST", "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-05-01", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "a19f8c2d-a218-462d-a230-2bb6e95134a0", + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "e398859f-3d43-4e70-b5ec-ad680b9b7081", "Content-Type" : "application/json" }, "Response" : { "content-length" : "0", - "x-envoy-upstream-service-time" : "207", + "x-envoy-upstream-service-time" : "220", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", - "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/fba6dfad-4ed1-4796-aa7f-93fed9a1b3fc?api-version=2022-05-01", + "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/a48bd328-3e87-4490-b05f-d30e6522bbcd?api-version=2022-05-01", "x-content-type-options" : "nosniff", - "apim-request-id" : "0f7e1458-9544-430b-8abb-e8386e7b1806", + "apim-request-id" : "ee6147b8-64f3-401b-bab6-e29915e1c3e6", "retry-after" : "0", "StatusCode" : "202", - "Date" : "Thu, 11 Aug 2022 18:07:23 GMT" + "Date" : "Thu, 01 Sep 2022 07:38:25 GMT" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/fba6dfad-4ed1-4796-aa7f-93fed9a1b3fc?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/a48bd328-3e87-4490-b05f-d30e6522bbcd?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "321e692d-18fb-4ec8-8ffa-60091b34bd25" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "b36e7a9c-7879-493a-885f-e40d3487fc7b" }, "Response" : { - "content-length" : "636", - "x-envoy-upstream-service-time" : "85", + "content-length" : "666", + "x-envoy-upstream-service-time" : "84", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "44e492ce-4a60-4d48-874c-7cb09c9aa3c0", + "apim-request-id" : "6570b472-1e33-4215-a600-16e6adc377e8", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"fba6dfad-4ed1-4796-aa7f-93fed9a1b3fc\",\"lastUpdatedDateTime\":\"2022-08-11T18:07:24Z\",\"createdDateTime\":\"2022-08-11T18:07:23Z\",\"expirationDateTime\":\"2022-08-12T18:07:23Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:07:24.1054342Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:07:53 GMT", + "Body" : "{\"jobId\":\"a48bd328-3e87-4490-b05f-d30e6522bbcd\",\"lastUpdatedDateTime\":\"2022-09-01T07:38:25Z\",\"createdDateTime\":\"2022-09-01T07:38:25Z\",\"expirationDateTime\":\"2022-09-02T07:38:25Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:38:25.9609802Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:38:55 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/fba6dfad-4ed1-4796-aa7f-93fed9a1b3fc?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/a48bd328-3e87-4490-b05f-d30e6522bbcd?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "d3422882-35d2-4a75-8ea0-3b46eb542140" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "c484eb61-4026-43cf-9993-1b43d1def169" }, "Response" : { - "content-length" : "636", - "x-envoy-upstream-service-time" : "30", + "content-length" : "666", + "x-envoy-upstream-service-time" : "40", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "7855c368-bd58-4d70-b89a-29babc2e5cd5", + "apim-request-id" : "310cf1c1-ddea-49ec-b0f9-de21f1ba0e17", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"fba6dfad-4ed1-4796-aa7f-93fed9a1b3fc\",\"lastUpdatedDateTime\":\"2022-08-11T18:07:24Z\",\"createdDateTime\":\"2022-08-11T18:07:23Z\",\"expirationDateTime\":\"2022-08-12T18:07:23Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:07:24.1054342Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:07:53 GMT", + "Body" : "{\"jobId\":\"a48bd328-3e87-4490-b05f-d30e6522bbcd\",\"lastUpdatedDateTime\":\"2022-09-01T07:38:25Z\",\"createdDateTime\":\"2022-09-01T07:38:25Z\",\"expirationDateTime\":\"2022-09-02T07:38:25Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:38:25.9609802Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:38:55 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.multiLabelClassification[1].json b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.multiLabelClassification[1].json index f06de3005dff9..5524cc06656b1 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.multiLabelClassification[1].json +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.multiLabelClassification[1].json @@ -3,59 +3,59 @@ "Method" : "POST", "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-05-01", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "45d4ee22-71c8-4ce3-b32f-af1243086200", + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "3858227c-5e8e-4cea-8d2a-8f195dbfeb08", "Content-Type" : "application/json" }, "Response" : { "content-length" : "0", - "x-envoy-upstream-service-time" : "160", + "x-envoy-upstream-service-time" : "353", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", - "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/dc7cd7b5-6ed5-4e57-8030-a1d301ba9f5b?api-version=2022-05-01", + "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/2e40913c-90a7-4818-95ed-444f13f80607?api-version=2022-05-01", "x-content-type-options" : "nosniff", - "apim-request-id" : "eb427312-69de-4cb7-8a1d-e846f2a75274", + "apim-request-id" : "67e06ff1-25fa-4d7e-a9db-8c787ee53350", "retry-after" : "0", "StatusCode" : "202", - "Date" : "Thu, 11 Aug 2022 18:09:47 GMT" + "Date" : "Thu, 01 Sep 2022 05:40:33 GMT" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/dc7cd7b5-6ed5-4e57-8030-a1d301ba9f5b?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/2e40913c-90a7-4818-95ed-444f13f80607?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "d714b038-867f-45d8-90f5-0b960002216f" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "0357c5da-a298-4100-a96f-770876605a87" }, "Response" : { - "content-length" : "641", - "x-envoy-upstream-service-time" : "35", + "content-length" : "671", + "x-envoy-upstream-service-time" : "161", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "5076115d-b04a-4526-b70f-701520ec4d24", + "apim-request-id" : "fe90b9f9-0993-4454-bf9e-a527e25665e7", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"dc7cd7b5-6ed5-4e57-8030-a1d301ba9f5b\",\"lastUpdatedDateTime\":\"2022-08-11T18:09:48Z\",\"createdDateTime\":\"2022-08-11T18:09:47Z\",\"expirationDateTime\":\"2022-08-12T18:09:47Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:09:48.5525148Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:10:18 GMT", + "Body" : "{\"jobId\":\"2e40913c-90a7-4818-95ed-444f13f80607\",\"lastUpdatedDateTime\":\"2022-09-01T05:40:34Z\",\"createdDateTime\":\"2022-09-01T05:40:33Z\",\"expirationDateTime\":\"2022-09-02T05:40:33Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T05:40:34.5308547Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", + "Date" : "Thu, 01 Sep 2022 05:41:04 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/dc7cd7b5-6ed5-4e57-8030-a1d301ba9f5b?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/2e40913c-90a7-4818-95ed-444f13f80607?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "c474ab81-52b2-4658-a699-6c65edf7c8e5" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "0fad095e-8b2c-4a20-8d5b-2c34cea728ba" }, "Response" : { - "content-length" : "641", - "x-envoy-upstream-service-time" : "34", + "content-length" : "671", + "x-envoy-upstream-service-time" : "28", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "df0bba47-3684-433f-a53e-f6ec20b43377", + "apim-request-id" : "cd4a0a6b-facd-491e-a287-15ecb6438e62", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"dc7cd7b5-6ed5-4e57-8030-a1d301ba9f5b\",\"lastUpdatedDateTime\":\"2022-08-11T18:09:48Z\",\"createdDateTime\":\"2022-08-11T18:09:47Z\",\"expirationDateTime\":\"2022-08-12T18:09:47Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:09:48.5525148Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:10:18 GMT", + "Body" : "{\"jobId\":\"2e40913c-90a7-4818-95ed-444f13f80607\",\"lastUpdatedDateTime\":\"2022-09-01T05:40:34Z\",\"createdDateTime\":\"2022-09-01T05:40:33Z\",\"expirationDateTime\":\"2022-09-02T05:40:33Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomMultiLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T05:40:34.5308547Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"BookRestaurant\",\"confidenceScore\":0.97}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"7cdace98-537b-494a-b69a-c19754718025\",\"deploymentName\":\"7cdace98-537b-494a-b69a-c19754718025\"}}]}}", + "Date" : "Thu, 01 Sep 2022 05:41:04 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.recognizeCustomEntities[1].json b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.recognizeCustomEntities[1].json index c5123c2226176..3de8ad2aad4f8 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.recognizeCustomEntities[1].json +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.recognizeCustomEntities[1].json @@ -3,59 +3,59 @@ "Method" : "POST", "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-05-01", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "a6433106-5e16-4f88-b459-0af819517ec6", + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "25bbda73-6652-4f2f-9b4d-451a92075f75", "Content-Type" : "application/json" }, "Response" : { "content-length" : "0", - "x-envoy-upstream-service-time" : "165", + "x-envoy-upstream-service-time" : "265", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", - "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/ae7f4e69-f51a-4814-a9ad-0b1c32d7deee?api-version=2022-05-01", + "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/6d988ed7-f490-4b38-a708-53fa8c4aa60b?api-version=2022-05-01", "x-content-type-options" : "nosniff", - "apim-request-id" : "887b110a-e2e6-4dcb-ac3b-292818e862ec", + "apim-request-id" : "8b22955d-693d-4cc5-b6ea-53e96d114d9e", "retry-after" : "0", "StatusCode" : "202", - "Date" : "Thu, 11 Aug 2022 18:06:39 GMT" + "Date" : "Thu, 01 Sep 2022 07:55:46 GMT" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/ae7f4e69-f51a-4814-a9ad-0b1c32d7deee?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/6d988ed7-f490-4b38-a708-53fa8c4aa60b?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "2bb958e6-bf83-4975-aba7-ddfb4ddf6934" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "e74a8441-1a53-4bf3-8147-3ed576031d98" }, "Response" : { - "content-length" : "1243", - "x-envoy-upstream-service-time" : "39", + "content-length" : "1274", + "x-envoy-upstream-service-time" : "98", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "c2f559e2-8ab0-4992-8402-818bd2871653", + "apim-request-id" : "c2339c6f-7427-46a6-bd10-a3e392b84906", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"ae7f4e69-f51a-4814-a9ad-0b1c32d7deee\",\"lastUpdatedDateTime\":\"2022-08-11T18:06:40Z\",\"createdDateTime\":\"2022-08-11T18:06:39Z\",\"expirationDateTime\":\"2022-08-12T18:06:39Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:06:40.779186Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:07:10 GMT", + "Body" : "{\"jobId\":\"6d988ed7-f490-4b38-a708-53fa8c4aa60b\",\"lastUpdatedDateTime\":\"2022-09-01T07:55:48Z\",\"createdDateTime\":\"2022-09-01T07:55:47Z\",\"expirationDateTime\":\"2022-09-02T07:55:47Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:55:48.0894257Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:56:18 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/ae7f4e69-f51a-4814-a9ad-0b1c32d7deee?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/6d988ed7-f490-4b38-a708-53fa8c4aa60b?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "c764b4dd-a6fe-44bf-8659-3985f7bf7889" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "0b5b11a6-fc6e-4f2a-b5e5-45c427506e18" }, "Response" : { - "content-length" : "1243", - "x-envoy-upstream-service-time" : "87", + "content-length" : "1274", + "x-envoy-upstream-service-time" : "90", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "cd472300-b80a-47b3-9541-fcf221672a24", + "apim-request-id" : "70a33c07-da33-4df5-9d3a-560999affb85", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"ae7f4e69-f51a-4814-a9ad-0b1c32d7deee\",\"lastUpdatedDateTime\":\"2022-08-11T18:06:40Z\",\"createdDateTime\":\"2022-08-11T18:06:39Z\",\"expirationDateTime\":\"2022-08-12T18:06:39Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:06:40.779186Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:07:10 GMT", + "Body" : "{\"jobId\":\"6d988ed7-f490-4b38-a708-53fa8c4aa60b\",\"lastUpdatedDateTime\":\"2022-09-01T07:55:48Z\",\"createdDateTime\":\"2022-09-01T07:55:47Z\",\"expirationDateTime\":\"2022-09-02T07:55:47Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomEntityRecognitionLROResults\",\"lastUpdateDateTime\":\"2022-09-01T07:55:48.0894257Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"entities\":[{\"text\":\"David Schmidt\",\"category\":\"artist\",\"offset\":0,\"length\":13,\"confidenceScore\":0.8},{\"text\":\"Food\",\"category\":\"service\",\"offset\":38,\"length\":4,\"confidenceScore\":0.03},{\"text\":\"Safety\",\"category\":\"geographic_poi\",\"offset\":43,\"length\":6,\"confidenceScore\":0.06},{\"text\":\"International Food\",\"category\":\"geographic_poi\",\"offset\":51,\"length\":18,\"confidenceScore\":0.07},{\"text\":\"Information Council\",\"category\":\"restaurant_name\",\"offset\":70,\"length\":19,\"confidenceScore\":0.1},{\"text\":\"IFIC\",\"category\":\"geographic_poi\",\"offset\":91,\"length\":4,\"confidenceScore\":0.05},{\"text\":\"Washington, D.C.\",\"category\":\"state\",\"offset\":98,\"length\":16,\"confidenceScore\":0.49}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\",\"deploymentName\":\"88ee0f78-fbca-444d-98e2-7c4c8631e494\"}}]}}", + "Date" : "Thu, 01 Sep 2022 07:56:18 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.singleLabelClassification[1].json b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.singleLabelClassification[1].json index 59bcd61ca5a16..00325d1c26d1b 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.singleLabelClassification[1].json +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/resources/session-records/TextAnalyticsClientTest.singleLabelClassification[1].json @@ -3,59 +3,59 @@ "Method" : "POST", "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-05-01", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "7e057d0d-d0b2-4e09-9aea-699726e4cc8f", + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "2d9858f6-8247-48ce-9a7d-98540e493dd3", "Content-Type" : "application/json" }, "Response" : { "content-length" : "0", - "x-envoy-upstream-service-time" : "297", + "x-envoy-upstream-service-time" : "255", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", - "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/408b6c7d-81d0-49a5-9a45-96ea6ca0627f?api-version=2022-05-01", + "operation-location" : "https://javatextanalyticstestresources.cognitiveservices.azure.com/language/analyze-text/jobs/fe56e4d1-fb20-43ed-baa3-bfc40cc5df10?api-version=2022-05-01", "x-content-type-options" : "nosniff", - "apim-request-id" : "18d378dd-4652-4273-a9f7-86e2c84049d6", + "apim-request-id" : "305a429a-9a9b-4f3d-87ef-fe45843c57a3", "retry-after" : "0", "StatusCode" : "202", - "Date" : "Thu, 11 Aug 2022 18:08:03 GMT" + "Date" : "Thu, 01 Sep 2022 05:45:15 GMT" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/408b6c7d-81d0-49a5-9a45-96ea6ca0627f?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/fe56e4d1-fb20-43ed-baa3-bfc40cc5df10?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "93d69bfa-e0fa-47dc-9c75-cff9c1205cff" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "423bf8c8-0d5c-46da-bb10-807b48399f2a" }, "Response" : { - "content-length" : "636", - "x-envoy-upstream-service-time" : "41", + "content-length" : "666", + "x-envoy-upstream-service-time" : "28", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "f85b7c04-e006-4d58-8014-7bce9f0ebe93", + "apim-request-id" : "1f956745-3f5e-4f48-89cf-7993da51fef6", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"408b6c7d-81d0-49a5-9a45-96ea6ca0627f\",\"lastUpdatedDateTime\":\"2022-08-11T18:08:05Z\",\"createdDateTime\":\"2022-08-11T18:08:04Z\",\"expirationDateTime\":\"2022-08-12T18:08:04Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:08:05.1332387Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:08:34 GMT", + "Body" : "{\"jobId\":\"fe56e4d1-fb20-43ed-baa3-bfc40cc5df10\",\"lastUpdatedDateTime\":\"2022-09-01T05:45:16Z\",\"createdDateTime\":\"2022-09-01T05:45:16Z\",\"expirationDateTime\":\"2022-09-02T05:45:16Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T05:45:16.7028975Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", + "Date" : "Thu, 01 Sep 2022 05:45:46 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null }, { "Method" : "GET", - "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/408b6c7d-81d0-49a5-9a45-96ea6ca0627f?api-version=2022-05-01&showStats=false", + "Uri" : "https://REDACTED.cognitiveservices.azure.com/language/analyze-text/jobs/fe56e4d1-fb20-43ed-baa3-bfc40cc5df10?api-version=2022-05-01&showStats=false", "Headers" : { - "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.4 (11.0.10; Windows 10; 10.0)", - "x-ms-client-request-id" : "dc2e57d7-cc45-4ad6-97ef-0d1312573e32" + "User-Agent" : "azsdk-java-azure-ai-textanalytics/5.2.0-beta.5 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "cfd09d9b-7bbc-47ff-9626-696d742c295e" }, "Response" : { - "content-length" : "636", - "x-envoy-upstream-service-time" : "26", + "content-length" : "666", + "x-envoy-upstream-service-time" : "25", "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", "x-content-type-options" : "nosniff", - "apim-request-id" : "b219ca73-4cad-407e-b0ac-ffab3977310f", + "apim-request-id" : "d5a3b9b9-defc-4a19-8570-879f88d2eabb", "retry-after" : "0", "StatusCode" : "200", - "Body" : "{\"jobId\":\"408b6c7d-81d0-49a5-9a45-96ea6ca0627f\",\"lastUpdatedDateTime\":\"2022-08-11T18:08:05Z\",\"createdDateTime\":\"2022-08-11T18:08:04Z\",\"expirationDateTime\":\"2022-08-12T18:08:04Z\",\"status\":\"succeeded\",\"errors\":[],\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-08-11T18:08:05.1332387Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", - "Date" : "Thu, 11 Aug 2022 18:08:34 GMT", + "Body" : "{\"jobId\":\"fe56e4d1-fb20-43ed-baa3-bfc40cc5df10\",\"lastUpdatedDateTime\":\"2022-09-01T05:45:16Z\",\"createdDateTime\":\"2022-09-01T05:45:16Z\",\"expirationDateTime\":\"2022-09-02T05:45:16Z\",\"status\":\"succeeded\",\"errors\":[],\"displayName\":\"operationName\",\"tasks\":{\"completed\":1,\"failed\":0,\"inProgress\":0,\"total\":1,\"items\":[{\"kind\":\"CustomSingleLabelClassificationLROResults\",\"lastUpdateDateTime\":\"2022-09-01T05:45:16.7028975Z\",\"status\":\"succeeded\",\"results\":{\"documents\":[{\"id\":\"0\",\"class\":[{\"category\":\"RateBook\",\"confidenceScore\":0.76}],\"warnings\":[]}],\"errors\":[],\"projectName\":\"659c1851-be0b-4142-b12a-087da9785926\",\"deploymentName\":\"659c1851-be0b-4142-b12a-087da9785926\"}}]}}", + "Date" : "Thu, 01 Sep 2022 05:45:46 GMT", "Content-Type" : "application/json; charset=utf-8" }, "Exception" : null From c09f9cd490854eb5cbf8c00b42473e58bbb7ef08 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Thu, 1 Sep 2022 12:35:12 -0700 Subject: [PATCH 07/18] Prepare metrics for initial release. (#30750) --- sdk/core/azure-core-metrics-opentelemetry/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/azure-core-metrics-opentelemetry/CHANGELOG.md b/sdk/core/azure-core-metrics-opentelemetry/CHANGELOG.md index d2e7ea714fe50..63f9c3371bb42 100644 --- a/sdk/core/azure-core-metrics-opentelemetry/CHANGELOG.md +++ b/sdk/core/azure-core-metrics-opentelemetry/CHANGELOG.md @@ -1,8 +1,8 @@ -## 1.0.0-beta.1 (Unreleased) +## 1.0.0-beta.1 (2022-09-01) ### Features Added -- Initial version +- Initial release. Please see the README for more information. ### Breaking Changes From 01a0cbaca35aa4337d2228cf398159ad4701b8e0 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 1 Sep 2022 16:01:37 -0400 Subject: [PATCH 08/18] [Cosmos] add mapping of database account to number of clients in diagnostics (#30639) * mapping of database account to number of clients * forgot to close object * added client map to client config tests * update to use dynamic number of clients as opposed to static since pipelines might affect * updated to use smoother syntax * update to use fluent api pattern * use entryset instead of keyset * use concurrent hashmap to ensure reads are consistent --- .../DiagnosticsClientContext.java | 25 +++++++- .../implementation/RxDocumentClientImpl.java | 3 + .../azure/cosmos/CosmosDiagnosticsTest.java | 59 +++++++++++++++++++ .../ClientConfigDiagnosticsTest.java | 5 ++ 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java index 3d65070d1534d..21dddf96f1761 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -53,6 +54,15 @@ public void serialize(DiagnosticsClientConfig clientConfig, JsonGenerator genera generator.writeStringField("machineId", ClientTelemetry.getMachineId(clientConfig)); generator.writeStringField("connectionMode", clientConfig.getConnectionMode().toString()); generator.writeNumberField("numberOfClients", clientConfig.getActiveClientsCount()); + generator.writeObjectFieldStart("clientEndpoints"); + for (Map.Entry entry: clientConfig.clientMap.entrySet()) { + try { + generator.writeNumberField(entry.getKey(), entry.getValue()); + } catch (Exception e) { + logger.debug("unexpected failure", e); + } + } + generator.writeEndObject(); generator.writeObjectFieldStart("connCfg"); try { generator.writeStringField("rntbd", clientConfig.rntbdConfig()); @@ -75,6 +85,7 @@ class DiagnosticsClientConfig { private AtomicInteger activeClientsCnt; private int clientId; + private Map clientMap; private ConsistencyLevel consistencyLevel; private boolean connectionSharingAcrossClientsEnabled; @@ -89,16 +100,24 @@ class DiagnosticsClientConfig { private ConnectionMode connectionMode; private String machineId; - public void withMachineId(String machineId) { + public DiagnosticsClientConfig withMachineId(String machineId) { this.machineId = machineId; + return this; } - public void withActiveClientCounter(AtomicInteger activeClientsCnt) { + public DiagnosticsClientConfig withActiveClientCounter(AtomicInteger activeClientsCnt) { this.activeClientsCnt = activeClientsCnt; + return this; } - public void withClientId(int clientId) { + public DiagnosticsClientConfig withClientId(int clientId) { this.clientId = clientId; + return this; + } + + public DiagnosticsClientConfig withClientMap(Map clientMap) { + this.clientMap = clientMap; + return this; } public DiagnosticsClientConfig withEndpointDiscoveryEnabled(boolean endpointDiscoveryEnabled) { diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java index c38a9a6e2ec43..a1eb9f38447ac 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/RxDocumentClientImpl.java @@ -110,6 +110,7 @@ public class RxDocumentClientImpl implements AsyncDocumentClient, IAuthorization DiagnosticsClientContext { private static final String tempMachineId = "uuid:" + UUID.randomUUID(); private static final AtomicInteger activeClientsCnt = new AtomicInteger(0); + private static final Map clientMap = new ConcurrentHashMap<>(); private static final AtomicInteger clientIdGenerator = new AtomicInteger(0); private static final Range RANGE_INCLUDING_ALL_PARTITION_KEY_RANGES = new Range<>( PartitionKeyInternalHelper.MinimumInclusiveEffectivePartitionKey, @@ -332,9 +333,11 @@ private RxDocumentClientImpl(URI serviceEndpoint, activeClientsCnt.incrementAndGet(); this.clientId = clientIdGenerator.incrementAndGet(); + clientMap.put(serviceEndpoint.toString(), clientMap.getOrDefault(serviceEndpoint.toString(), 0) + 1); this.diagnosticsClientConfig = new DiagnosticsClientConfig(); this.diagnosticsClientConfig.withClientId(this.clientId); this.diagnosticsClientConfig.withActiveClientCounter(activeClientsCnt); + this.diagnosticsClientConfig.withClientMap(clientMap); this.diagnosticsClientConfig.withConnectionSharingAcrossClientsEnabled(connectionSharingAcrossClientsEnabled); this.diagnosticsClientConfig.withConsistency(consistencyLevel); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java index 51fe91130acf0..261ab366bec59 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java @@ -383,6 +383,65 @@ public void requestSessionTokenDiagnostics() { } } + @Test(groups = {"simple"}) + public void databaseAccountToClients() { + CosmosClient testClient = null; + try { + testClient = new CosmosClientBuilder() + .endpoint(TestConfigurations.HOST) + .key(TestConfigurations.MASTER_KEY) + .contentResponseOnWriteEnabled(true) + .directMode() + .buildClient(); + CosmosContainer cosmosContainer = + testClient.getDatabase(cosmosAsyncContainer.getDatabase().getId()).getContainer(cosmosAsyncContainer.getId()); + InternalObjectNode internalObjectNode = getInternalObjectNode(); + CosmosItemResponse createResponse = cosmosContainer.createItem(internalObjectNode); + String diagnostics = createResponse.getDiagnostics().toString(); + + // assert diagnostics shows the correct format for tracking client instances + assertThat(diagnostics).contains(String.format("\"clientEndpoints\"" + + ":{\"%s\"", TestConfigurations.HOST)); + // track number of clients currently mapped to account + int clientsIndex = diagnostics.indexOf("\"clientEndpoints\":"); + // we do end at +120 to ensure we grab the bracket even if the account is very long or if + // we have hundreds of clients (triple digit ints) running at once in the pipelines + String[] substrings = diagnostics.substring(clientsIndex, clientsIndex + 120) + .split("}")[0].split(":"); + String intString = substrings[substrings.length-1]; + int intValue = Integer.parseInt(intString); + + + CosmosClient testClient2 = new CosmosClientBuilder() + .endpoint(TestConfigurations.HOST) + .key(TestConfigurations.MASTER_KEY) + .contentResponseOnWriteEnabled(true) + .directMode() + .buildClient(); + + internalObjectNode = getInternalObjectNode(); + createResponse = cosmosContainer.createItem(internalObjectNode); + diagnostics = createResponse.getDiagnostics().toString(); + // assert diagnostics shows the correct format for tracking client instances + assertThat(diagnostics).contains(String.format("\"clientEndpoints\"" + + ":{\"%s\"", TestConfigurations.HOST)); + // grab new value and assert one additional client is mapped to the same account used previously + clientsIndex = diagnostics.indexOf("\"clientEndpoints\":"); + substrings = diagnostics.substring(clientsIndex, clientsIndex + 120) + .split("}")[0].split(":"); + intString = substrings[substrings.length-1]; + assertThat(Integer.parseInt(intString)).isEqualTo(intValue+1); + + //close second client + testClient2.close(); + + } finally { + if (testClient != null) { + testClient.close(); + } + } + } + @Test(groups = {"simple"}, timeOut = TIMEOUT) public void queryPlanDiagnostics() throws JsonProcessingException { CosmosContainer cosmosContainer = directClient.getDatabase(cosmosAsyncContainer.getDatabase().getId()).getContainer(cosmosAsyncContainer.getId()); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java index 6feb9f4cc30ef..1681beed671df 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java @@ -17,6 +17,7 @@ import java.io.StringWriter; import java.time.Duration; +import java.util.HashMap; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -35,6 +36,7 @@ public void bareMinimum() throws Exception { diagnosticsClientConfig.withClientId(1); diagnosticsClientConfig.withConnectionMode(ConnectionMode.DIRECT); diagnosticsClientConfig.withActiveClientCounter(new AtomicInteger(2)); + diagnosticsClientConfig.withClientMap(new HashMap<>()); Mockito.doReturn(diagnosticsClientConfig).when(clientContext).getConfig(); @@ -67,6 +69,7 @@ public void rntbd() throws Exception { diagnosticsClientConfig.withActiveClientCounter(new AtomicInteger(2)); diagnosticsClientConfig.withRntbdOptions( new RntbdTransportClient.Options.Builder(ConnectionPolicy.getDefaultPolicy()).build().toDiagnosticsString()); diagnosticsClientConfig.withGatewayHttpClientConfig(new HttpClientConfig(new Configs()).toDiagnosticsString()); + diagnosticsClientConfig.withClientMap(new HashMap<>()); Mockito.doReturn(diagnosticsClientConfig).when(clientContext).getConfig(); @@ -102,6 +105,7 @@ public void gw() throws Exception { httpConfig.withMaxIdleConnectionTimeout(Duration.ofSeconds(17)); httpConfig.withNetworkRequestTimeout(Duration.ofSeconds(18)); diagnosticsClientConfig.withGatewayHttpClientConfig(httpConfig.toDiagnosticsString()); + diagnosticsClientConfig.withClientMap(new HashMap<>()); Mockito.doReturn(diagnosticsClientConfig).when(clientContext).getConfig(); @@ -139,6 +143,7 @@ public void full() throws Exception { diagnosticsClientConfig.withPreferredRegions(ImmutableList.of("west us 1", "west us 2")); diagnosticsClientConfig.withConnectionSharingAcrossClientsEnabled(true); diagnosticsClientConfig.withEndpointDiscoveryEnabled(true); + diagnosticsClientConfig.withClientMap(new HashMap<>()); Mockito.doReturn(diagnosticsClientConfig).when(clientContext).getConfig(); From 3701e72cf7f0e482ed7fb2390f3e0f42cf0325cb Mon Sep 17 00:00:00 2001 From: Azure SDK Bot <53356347+azure-sdk@users.noreply.github.com> Date: Thu, 1 Sep 2022 16:46:54 -0400 Subject: [PATCH 09/18] Migrate SkipDefaultCheckout to SkipCheckoutNone (#30734) Co-authored-by: Ben Broderick Phillips --- .../pipelines/templates/steps/docs-metadata-release.yml | 2 +- eng/common/pipelines/templates/steps/sparse-checkout.yml | 8 ++++++-- .../pipelines/templates/steps/update-docsms-metadata.yml | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/eng/common/pipelines/templates/steps/docs-metadata-release.yml b/eng/common/pipelines/templates/steps/docs-metadata-release.yml index 2f58b90d4d10e..7b6fb183a54ee 100644 --- a/eng/common/pipelines/templates/steps/docs-metadata-release.yml +++ b/eng/common/pipelines/templates/steps/docs-metadata-release.yml @@ -81,7 +81,7 @@ steps: - ${{ if parameters.SparseCheckoutPaths }}: - template: /eng/common/pipelines/templates/steps/sparse-checkout.yml parameters: - SkipDefaultCheckout: true + SkipCheckoutNone: true Repositories: - Name: ${{ parameters.TargetDocRepoOwner }}/${{ parameters.TargetDocRepoName }} WorkingDirectory: ${{ parameters.WorkingDirectory }}/repo diff --git a/eng/common/pipelines/templates/steps/sparse-checkout.yml b/eng/common/pipelines/templates/steps/sparse-checkout.yml index 0a2b537be1a93..39d5a7bff17dc 100644 --- a/eng/common/pipelines/templates/steps/sparse-checkout.yml +++ b/eng/common/pipelines/templates/steps/sparse-checkout.yml @@ -8,13 +8,17 @@ parameters: - Name: $(Build.Repository.Name) Commitish: $(Build.SourceVersion) WorkingDirectory: $(System.DefaultWorkingDirectory) + # NOTE: SkipDefaultCheckout is being deprecated in favor of SkipCheckoutNone - name: SkipDefaultCheckout type: boolean default: false + - name: SkipCheckoutNone + type: boolean + default: false steps: - - ${{ if not(parameters.SkipDefaultCheckout) }}: - - checkout: none + - ${{ if and(not(parameters.SkipDefaultCheckout), not(parameters.SkipCheckoutNone)) }}: + - checkout: none - task: PowerShell@2 displayName: 'Sparse checkout repositories' diff --git a/eng/common/pipelines/templates/steps/update-docsms-metadata.yml b/eng/common/pipelines/templates/steps/update-docsms-metadata.yml index 571d62fc2aa75..2635ad479432b 100644 --- a/eng/common/pipelines/templates/steps/update-docsms-metadata.yml +++ b/eng/common/pipelines/templates/steps/update-docsms-metadata.yml @@ -46,7 +46,7 @@ steps: - template: /eng/common/pipelines/templates/steps/sparse-checkout.yml parameters: - SkipDefaultCheckout: true + SkipCheckoutNone: true Repositories: - Name: ${{ parameters.TargetDocRepoOwner }}/${{ parameters.TargetDocRepoName }} WorkingDirectory: $(DocRepoLocation) @@ -76,7 +76,7 @@ steps: displayName: Checkout daily docs branch if it exists workingDirectory: $(DocRepoLocation) - # If NOT performing a daily docs build, set the $(TargetBranchName) to the + # If NOT performing a daily docs build, set the $(TargetBranchName) to the # default branch of the documentation repository. - ${{ if ne(parameters.DailyDocsBuild, 'true') }}: - template: /eng/common/pipelines/templates/steps/set-default-branch.yml @@ -103,7 +103,7 @@ steps: -PackageSourceOverride '${{ parameters.PackageSourceOverride }}' ` -TenantId '$(opensource-aad-tenant-id)' ` -ClientId '$(opensource-aad-app-id)' ` - -ClientSecret '$(opensource-aad-secret)' + -ClientSecret '$(opensource-aad-secret)' displayName: Apply Documentation Updates - template: /eng/common/pipelines/templates/steps/git-push-changes.yml From 4b634d82926b14a05b59a2ce8a2a95fdfcce9207 Mon Sep 17 00:00:00 2001 From: Annie Liang <64233642+xinlian12@users.noreply.github.com> Date: Thu, 1 Sep 2022 21:17:45 +0000 Subject: [PATCH 10/18] ReplicaValidation (#29767) * replicaValidationBeforeUse Co-authored-by: annie-mac Co-authored-by: annie-mac --- .../ctl/run_benchmark.sh | 2 +- sdk/cosmos/azure-cosmos/CHANGELOG.md | 2 + .../ReplicaClientSideStatus.png | Bin 0 -> 75607 bytes ...usTransition-ReplicaValidationDisabled.png | Bin 0 -> 135610 bytes ...tusTransition-ReplicaValidationEnabled.png | Bin 0 -> 166175 bytes .../com/azure/cosmos/CosmosAsyncDatabase.java | 2 - .../com/azure/cosmos/CosmosException.java | 23 +- .../azure/cosmos/implementation/Configs.java | 10 + .../implementation/CosmosSchedulers.java | 10 + .../DiagnosticsClientContext.java | 6 +- .../DocumentServiceRequestContext.java | 29 +- .../ImplementationBridgeHelpers.java | 1 + .../caches/AsyncCacheNonBlocking.java | 22 +- .../implementation/caches/AsyncLazy.java | 3 +- .../AddressInformation.java | 8 +- .../directconnectivity/ConsistencyWriter.java | 16 +- .../GatewayAddressCache.java | 244 ++++++++---- ...ectionStateListenerMetricsDiagnostics.java | 47 +++ .../RntbdTransportClient.java | 4 +- .../directconnectivity/StoreReader.java | 116 ++++-- .../directconnectivity/StoreResponse.java | 10 +- .../StoreResponseDiagnostics.java | 8 + .../StoreResultDiagnostics.java | 1 + .../directconnectivity/Uri.java | 161 +++++++- .../addressEnumerator/AddressEnumerator.java | 98 +++++ .../AddressEnumeratorFisherYateShuffle.java | 35 ++ .../AddressEnumeratorUsingPermutations.java | 81 ++++ .../RntbdConnectionStateListenerMetrics.java | 39 +- .../rntbd/RntbdEndpointStatistics.java | 5 +- .../rntbd/RntbdRequestArgs.java | 16 +- .../rntbd/RntbdRequestManager.java | 6 +- .../rntbd/RntbdRequestRecord.java | 4 +- .../rntbd/RntbdServiceEndpoint.java | 5 + .../azure/cosmos/CosmosDiagnosticsTest.java | 14 + .../ClientConfigDiagnosticsTest.java | 20 +- .../DocumentServiceRequestContextTests.java | 48 +++ .../AddressEnumeratorTests.java | 175 +++++++++ .../GatewayAddressCacheTest.java | 367 +++++++++++++++++- .../directconnectivity/ReflectionUtils.java | 5 + .../directconnectivity/StoreReaderTest.java | 51 ++- .../directconnectivity/UriTests.java | 246 ++++++++++++ .../rntbd/RntbdRequestRecordTests.java | 3 +- 42 files changed, 1737 insertions(+), 206 deletions(-) create mode 100644 sdk/cosmos/azure-cosmos/docs/replicaValidation/ReplicaClientSideStatus.png create mode 100644 sdk/cosmos/azure-cosmos/docs/replicaValidation/StatusTransition-ReplicaValidationDisabled.png create mode 100644 sdk/cosmos/azure-cosmos/docs/replicaValidation/StatusTransition-ReplicaValidationEnabled.png create mode 100644 sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdConnectionStateListenerMetricsDiagnostics.java create mode 100644 sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumerator.java create mode 100644 sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorFisherYateShuffle.java create mode 100644 sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorUsingPermutations.java create mode 100644 sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/DocumentServiceRequestContextTests.java create mode 100644 sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressEnumeratorTests.java create mode 100644 sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/UriTests.java diff --git a/sdk/cosmos/azure-cosmos-benchmark/ctl/run_benchmark.sh b/sdk/cosmos/azure-cosmos-benchmark/ctl/run_benchmark.sh index 5db1631848c5a..294fed0b5d1a9 100644 --- a/sdk/cosmos/azure-cosmos-benchmark/ctl/run_benchmark.sh +++ b/sdk/cosmos/azure-cosmos-benchmark/ctl/run_benchmark.sh @@ -70,7 +70,7 @@ else fi if [ -z "$ctl_client_telemetry_endpoint" ]; then - client_telemetry_endpoint=https://tools.cosmos.azure.com/api/clienttelemetry/trace + client_telemetry_endpoint=https://juno-test.documents-dev.windows-int.net/api/clienttelemetry/trace else client_telemetry_enabled=$ctl_client_telemetry_endpoint fi diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 48f111d6c5f66..3030781b3dbbc 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -9,6 +9,8 @@ #### Bugs Fixed #### Other Changes +* Added system property to turn on replica validation - See [PR 29767](https://github.com/Azure/azure-sdk-for-java/pull/29767) +* Added improvement to avoid retry on same replica that previously failed with 410, 408 and >= 500 status codes - See [PR 29767](https://github.com/Azure/azure-sdk-for-java/pull/29767) ### 4.35.1 (2022-08-29) #### Other Changes diff --git a/sdk/cosmos/azure-cosmos/docs/replicaValidation/ReplicaClientSideStatus.png b/sdk/cosmos/azure-cosmos/docs/replicaValidation/ReplicaClientSideStatus.png new file mode 100644 index 0000000000000000000000000000000000000000..9287796901ac996f0f1e3bcc08681cdd57d789a6 GIT binary patch literal 75607 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 Z^yTL-PZn%cdn3KLq^Wy8PyOb@{{!`zZ|wj8 literal 0 HcmV?d00001 diff --git a/sdk/cosmos/azure-cosmos/docs/replicaValidation/StatusTransition-ReplicaValidationDisabled.png b/sdk/cosmos/azure-cosmos/docs/replicaValidation/StatusTransition-ReplicaValidationDisabled.png new file mode 100644 index 0000000000000000000000000000000000000000..947dc51a3c03f0ca531a6320567aa4c7760e893a GIT binary patch literal 135610 zcmeFZXH-*L*9MBm-W3a?0!kG^QL2=HA_Ac*y>k?hUZe!6QNTiP0@8xgA|)UtAT{%ILGt7?;YRw^ZvVx;c&1<*n6$HW_{)}6MfG>>-dq2M>se* zjzhFHj5s(Bf;l+$-#&BzSmAz_9tixm$InPhoui`nG8y<~pYv_~+Z-HKiANa^_5;5k z_R+TVUlEt@E-`crcvXbAg{aId}^Xi`2r+ZIb%s(ER{vhk}>Fd7VhMqlrk!*20Nn7sO zgPQgioxDt%{*8#TrA3s(X3Pt|oR}PL^S#87rT5xeqg8$b-AN~q>&wsAGExH~2 z$A2!|(v76;`_F}^=?BP%|8s#u;yC`ye=S&D{NFd?a%g;f9N*u6`_R=wy=^+Z#ZaH) z>H3e}T;*OIME%fJjEu4}CCv8^j#|Fmjk_GOwY9BoZIujMo)C&UHQhNq%@4fluRBG~ z_V#&HYRW0~&ifr0OfsCKZ*cb@Z?9efAHG@`%0A}Ctzb@d?Kxom?fCAYZr#~k*k}fQWs;d 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

A{NgP-DqnsPQkne!v@IEx= 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

BLd`QFx83N||D@glHMdZ}Y=yYCv zx$P3Q7r<|9Z+lG#gSlghe`f{hjJbQRA0+A**U3PLYkEcl;2n;qzvdyxIEX zsY-Z*PM1c&prz1X|NM_;<(f+&0k6PoIb;)c{<-RZ&;2ZZcq-j3y`kjx@{5}@JOxd{a@S!3NBTyD*I6-P)JpFwpIyM zN{hb~LZ#$CN08l2Kko$+weZz#3R<$Zh#@=COz80KoH-x$N*Ckebt7{$ojqd3?#o#*%rwY_Hi1^ZF!K($3?Sd5j#$_Ir1hbwO13_}UQRqjj zC4v<9i;U5qi@d8Zee38fSZxC`)v3N!#ujDbywaypHS)sClSls8CYpFnB{uWUy)QFT zCLgczt%4%Y3cTEX2D!n(OVqah@;|z`m|*qt83b=Z-UxzT9@X_aNWw2}G)>A#&*L;- zU!=7c5UMf5v{H={0QhdN+X}$N3h+Kn7qlMZj;!=$+@MQ`kT&yU?Y!s~`)8l4HqNmv zsD{^n%JOnQ%JO1U;IYy_WO*;EN@r&AAoprOdxF6cxK{jd+Xsg~-Z&)Pjw4bDMMB#K zYcM#79Sy*O5tWQ_OazUR#nOYzSUB)Y)W_Y@P&~rX={}J*oS19J3AdSOvLkRo(mzY3 zri|``*~$^L@q&v#WmWpsxlz`<2ff5n48`aD#Pg!DpN8`gv;D^MMCk(K!-pDCWyZz| zEJLbiuMl3Ec-vT+QTCjVac0QN2Yyws$>G`Z4Nvf%1 zvvO_-(efG~-_w8sdK%2o5n=hy=~9#HiFu|B8f1G2(Axop zv!P9#`-&c>b7#16xO@1N@{PT@T`uD2{fp?)n(S3K@XU9!gQJYI?V3)2KT^bh$ z#6>y)d}*9_?HhqtwmZ9ATa5BgfcxwOcHC11*>LC$4O8z-dmMIcKY!g$58S5#G-$51 z_rBkbwCjdkD`1ze<}$Ng>1^HgQ0~Y8o{Ttk8Y5>`)RG`h43;31E$>3tL=Z+og-@@i zX4i}%Ph38nYK|dKl|4LvJLR#B2|J`VRTx^pMG5>+t^MO8W;OvDL>3iGhezfP6^Q#a z^kdFX_RFf}+1Hplym2h05o;2fYX{54Vs7z^Y)mLvmwVWUqRNZ=^(2*=mal@Pl>8$4 z&s0K`AUMMoP&~NdaQ~QXx~K6)$>&``y=_Gc{USISGn_>L`m9?=#OFNgfU5=`;%mDj zK8Dd#t9kQV^b!V5I>V2?d7|$bwakZl>2{wEA|`I)kOTQrUn@ zTtIcu^TtG{$gr`ey$sqThFg@EL?~tRj``T+vXQYYF`Gw_+Y5Z;k>E)@!B#&%61Ts$ z0&b@36V?KMTo)1K!h@Wq+lr4$3xHGi*Mtttl=zT0ZP>tTY{Hw)Cea(JfHn6GySVSe z)wPvYsF{*Y_^!g|CfPp>=xpaVoQfoeZfki^o9UMPKqDJdF;j)IyjXpBxCmt+Z+@}9 z1{Lqh!c495R^V~9M2#C~$naHtP>ozRn31AXLlc&RAX4gskWi#`j)FUOFft#r;6SEe zX}!?pxfG;B&E&CP@<&&EZx(A}I=KRf=&#Zn(%D@z6Il=z0f<9xxrna0?g#`iv>JT6V1*hB3LjpU#%w z>XXMd6Hmcx$Rx9I$tsi`e<~_%bO3$#LND$2l5&o88H=hK?R%imPto^Sg$oDa^si4= z2B*{XttvWJ;Dv-JwZ+;Gu{$(uw`1GHS2nJK;#DD4l{f;`8e*fTa;=KQCAna^L%w*% zK6(Obhn?{kyes4g6eusm=*A7$uoQf5Q=#hR@{qZ30rx`Kz~0Vs>BI}!ywuL1ml?7S z$`DnLpkTOMAI^Plh$Sjz+P&}y$xR`@KxUkJwco{^dcx2JX}5VN*7cnCrSf7HfO@{j z7C|0}c1=4qFY4Nd+AjK94Q#$e{lJEa7f|1`^mw31trA&1*#m0kP&0DWSLS9{^XLN; zt8x`3rX?mur|9!wdnWJ8U6z9-=^;pS5+;a@Q;Iz6?eDIIq6Kkj@*dNvyk@y>oc{7u z+lX#%3DzE(kvqi8r@OJ}#5u_p218JJ+ef39)+eYcB4%$PvFg}7@=JPMQzHHnS|2M5 z;+21A0Iff&!w#Cq5lE;Ae-jW|T?1#|M#LQlZ`Ka@hEO0wT2v|*+a5I-4vMuAkL|Fk z+YSVYRMBkFv$>#x(X1Rt^AAnGo?K=Kzut`$ee2|1V& z3+^QlknoS7o<`-d&S)A(HQQL`1kT;|GjYQ`0N~R>SHcKk6f`^lj zaE^qRLxzD++WAt^%1{eDG)5K07s;iQi;4@7T)n#F>^GUPd%#`A=_B=pNRnrnx7?1A zNQbxM#}-armJB*~LP|DSfz%~@A#cYm6G94~b-z08m+n$-(b^wNOkU+}y`7Ty?T;8^ z7A+b|(u5Ylh-?CT%?1KaC4S)xYf%L0ILb7`;uWS4JwZNlrB?bhG?JKc3~K@`ypZ4M zgA+1xyW|JhQ1RK{M7}-yMSdoY?H7oJ1+b}cfy<+>yaywo)4p}0f z%~oMj#bba;>k{!hngBgyO%RPuhS2JP!kUGHeFcc6Kg2$}E;Bn@Ku(U;XNnqZ0Xf>_ zukCr=Ojb(N*kllQph_jeSpabAzGA-Qa}c~)2dF@GnWP5)PE?az++YuY8jVf7z7lNK;hCo+%fGH{mO%&^HhZ;e37-9 zx4BdSh^&x5pc<^3mB(j0hT}mk)^`&_QE`|T<-<0yh)l2kwE;YMBM9{B=q9BbKZa?( zqGpme6g%ObnT7vK6#^vK>QGd|ujkP6Y|`o!Gaw|cvgy*iDDacmB?%INn-xz_GE&=NgS zZnH$~jTLyJ%i0spcteLz-?J{u<)OTsKvS9|O7*w)rN27@^0g!StSg0BX z)mD}hr%_XF?TY2nYao(-Du1$s`;mWH;mOT{<5$4jZP&l&1vI{D2wF@hEL3qv-~{}j zr9nV`e!~KJdv=`b5;Zl_I;gK^6Q`7b9gWDuXML3xZqwMll(`#Q!q!2WV!m~Bbh;XU zef3q9K_UF0N;oVA#J+*eQJpce;p;ZSaFx&*9nhbCSH=g*{dAh3YeNh;;UxAQhljF z^3MzQHpDBE`AzdFrQsX}9jF1%yN`~*v!>XhYdfr_ugR?>7vW(wkiroqJA`Lx0j<;$qZF(7hMgoOkZ_s zwiscjc#A4pBh!S$$1LC%G6-ln^Z3%yVYCgr)VY0XygpkX1QkH4g0s{OFA_fn+;6vI zlU@K3XafaIM)^13sUq8N)T5r-egA%O(T*uZzVvyZkNSBn-N@x6%r&e6#wK_x+6;0Jz# z_!wFqO^~JR)PP1<8wornKJNhRc{b@8Ir@9fmz(T!Ay|j82YCY^lwetfOFEt}&5ruw zhG6yQYHCCJotit>($%1VnKk^Tz5R8B-OBJVUs^a}De42nmv_^6&Zy%j0%coR%kH|T zQC7C%j-T2`uQq%P&G*Fs2msD(T9cx0)itZ$p{;r1b8=~Ie`0dv+OYDdv3|VsTQBd0 z!*7R3Pj#IMcJGp$flv}?_|E-^#U6>(83L;?V)N*J-sGi%_sGc4&he5u03geG8%#A1 zu|=zpB!g{_o`P6uA?>tw8zQ6FPmwYHkHa0tK~cdlbx*=CA0x$mvu8g~M#R^lWFJm{ zo(-zo>XRoPysL3mPd3|$O?f|w{~84%cHTsn%qsVw+xvzEXt^&IY3*nymyqW zrRpB60taW*_q%L86l*gigY6(#g>M1%KdV47l<)fi&QSrITvwj>I1v~?;SjUET`(0C zMkm;lw0CleJUpwM)|A)kWQVQ+nUG6w-Yhr|#e%{D8|Ayu`Nq%gLqG0>2iYTWCpU2% zfWasq2BnDE^e;fYuBRM&EEYFZlQrgC3IvFgM%aM!LzHG7RxkI0?Usjv2Ba_u8rA~()gH#JcN+tfJo zryXIQUaPG}`ek1hPE>)R77n zH1)s*U%YaR>~m9umW1Eq`> zn96(s4!J1^zss2`=2%aMSL0iQNWsnYu4OKtJeF_I=tiXy)Ym8&QD+ZEn#I;jiV>?o zv$e{Ma>-;lK@r58#XMi>vj!Zn8<@JyciZiw+XA z4VA9{#tt+R4tW{)lXyrL-20E=KDZ!$Vk6qo{970Oux>@@L3-5$IMakQI`I3yeA#z% z4*aX=;ML#d@aDFls^B?r{HK%EKQS5uj+MPCn8gdmVogmPC>og2pMa)#{k;+R=Pk-? z6V0V3Ysuqgpe5OGwjh=_D51Rl!PGmcS(UWoa_Wd$MR zRHwXoTDg`Mfx#e8MOu1Wu$=|)qSfg3JIffy5SG~r zX5F#&W%}=|9C1W)YV6z1fwUF|L(WkywA;sxFvdR%NNYVwpvtIxpNtE+ls>s0n&BPA zikR3+6WdH7Lr77%^F{|T_>sxf>A6ImnZe*`Df;fG%fA#<>0(Tm-MW~0xa@l@6tnvL zS59QJAHPV6_OjlB^*QhD{xhwKpUlYm<)H0MM%-+MjZ%KV6xHcOuo^_8opRvn=Dl*f zeya^zm~!{*YgN5drQg{XiL{5$PGCYDYA8UjJOwk~m)Gi=>tfzCgKi(6Vw9TKuw-6M zf&!jB)WBXTvbW!HD0xmdKkxF8yt)e&H|1msdBxvZ@-h%go++fY$d5upZuwbok$g~l z5O$0A_$9w!%9^Vv@yUP55d`Bu*<6hd*nGJ8u7_tjbqq#4kdcVAtQ3RwMuoH{?aY+N z_zRn~=;}u@#Fbp*;NZL{uzma>#jCA-cY?ggCV`wDG&M0_k!nJf6a+%O}}P@SWVG zu@Ctr4#WyKS?3u5Og!0}!caUIjBZ1C8WBfY@Q0;QO=?Ttb+b<`nq%2V6 zz2%)ixgg+7M=!puumYE*j{y~5FFUgY+qVX-{k7kX)jK*P%!)WlJc>Cnc#YIu%K{nK*yb? zyz1nnZ^mS`{KKT~?=$oK*t6|XB~S|c6e6%+(hKc7hFvyRmfyMx)eb6O_%*DGd{Dx} zyVS!YzND8%T->8ksZ)pZk0u2_$8X)_a+M-^&slTk4h8+9a{-+e;q+1?N!G-M(F* zAVoK2Zr*%$W1n|Ghe*rvRXF6lFsV;U7GSbzLjvpZl#R5>nGkYkIGIS;%h-m(`hV(M zKoW|3r=`Rq~~b|Bes1ZARZ)vl&lR^6J_YwqHwR-GyA+sE^BGZev7t4$p0oKKaW#mV(9! z2NJP+jVZ6+mzIBvCf!{7JrvJ=R`v90*DQ*KG5BFf>;nUJ99{y2kIVkUsgMO-6enG* z(&WFxYM|Qh?5BerV=-zrQ!*CbFECHn4`)d~4n<(yT}YJ3?oo$VA*$b5J+X7i)zyq6 zcal66DAqDPn~9jybUwTzGM}#rJ)q)kUH~_`=qxR^1EJ}mb<#pN@{jeC!HAVn>=@o4 zqfmJ6R&_<^+V5FZ?E&DCNKaTKE%NArZ01<jz3=l1ZATckK$wEVWnH;>D0jt=A~VTO`^Pa`+@Ja9Y=uzUGPDbsQD`(R?ZU4B|r@#>3mPlT%Tuqj!p!03~qf<0d}8d$3N zjjn{o=s3IiVGUyQ>sngh+nu@fCf#87(aIA!V9&54hJfdY>{jT$%dAMQXYH+Hjc#PT zm6UhJQAv@XuESgh7YL*kz#A|l{~rUN|HNd*VW4#A8V@Rn$yRikDvnEw6y{KUz<&M> z&xPZ>>yXCvsse3XDm`vN)c8joJF^Y3@E&1tBhx9B-V9Kvtd=ORt(5y%uFaTq_&a8+ zX&(QKy;#m41-2!1XQyc96oDtZ1CFIKtmAxp@b6fYZk!HE0~$qN6Qd9C;^JO2Vqe$d z`IFjE*ur!MF7$4b$P3h5e+nOY8wx3v!AXay3w$bN!?z6422zfp&dY!QrbC{#-oa!? zvvlm6r3UUc7Wf}PvC3Q#W^+I{*C6)8^GYrfRIAnm%}Jorp>F87m6R358QfSnmPw*2 zCWl|#+|1fc8M(4t|4U+=f{(DOn%jNQD*r;#kyJ?bUL8SrZS3L#X^lKdxXV_{#uKW? zMXpgcy}b8wia_1~TFCSI*@9lybx@2P)k7FylhHPGgha@I(gXnaFescl_8w4ji9-u< zFkS3BH|)D~TF=|c1CW2ds#^`m)$IV_w9KB(qVyG^+V1D==oQNzZOSivZfx?!vuJXB zgS)^}?7I1^pbMUv66RRJ9`%q$cP(RafPL?uoEN(&BPOMVsX!59^NJqSA<}Upw}TY% zjdVqyT(t2W_Nm=JZ>#DM_F)z0!z$9%2N#3Jp}8i72}Kb#+AmPt6SYCz+}w*jd~feP zsw(Pgq>`JJIL6<0at8fG)xKjWH9CEGEA9K?G!~0Dnx9 zm;K8?yG-w$qOx`F6khoezm0+3avrfG7!}*4liwvBk>pYH`IAs?=@gp07t&l-cr4}T z5JQx!0lY_m?j;fpQLAe;8mD00Jj+cp5S@pd@Eu!Gu7ZQCz#`N@axFo9%ijZ=q3^cs zo>`WAab8DEW@~Sdv=rsj410I<>Wt=$OIDwjSfDv35_4jV6ECR|+$}L7`$~TihnqA9 z?Hl(8a@*ys_EJGHZ&q&xa%2RKqJSMbCZDP6m0OI)=+6umVySc1`Vwr{y?<=fl_%qm zdRKtL)c2+R3?0@TeS-SR6TN)Bii_$v_;hoBAKEe2vuAyigVmOcXi&N>vYCKyI3<8l zPYIa!c`^O;`<$%%xkD~}2mi5szbG7r_ZRNH&_57K?kpY?zAW9*yUW_OnLaw-PqRaI zXC;uO!Tn~;ZNP2Mua;*?@G&0H_$m%?7WN6`syR$tzv7(YA911QeQB`Qb>jmh_N7x$ zc^N9e+bhY?8(?g&FggX9AW&hqifuHbazUXLp5tp@J|5mDS4j5L zvGA}h3KN?N>{IMN)~FeH8D7MT?nNyR#u+CZZ!9Uae3%~%_Qv^JJ9^P%ReueEt+eK* zY7a9?Aj?Xr({2lgWp54bYc^0p_^j_EA7mg+3~~nq3sZvC(13fL<#Q5Q_>ELkJA|qu zM;}GsjZ8_tfj^eExIp+ARvsVy=F0Lud~5Z19-ic*wHLK$%B!Q{Q!63xE{&?x}$ z6rjA!Q(S*KDe##e(IS5FWc{5x`Uc5i23$%0Hv^g=eG z8G;z*i_w9%v&jy#S(1dQS_zv}+xpON8T8-RTv`S6{@Nq=xOuAAeE|4O zp<}J8@kk&;lWIW!?TJtRCgL4^*s2o-^KX{d!N{kQe8>dyM(G_!{rh}+QqsuAYr=`9 zy|iRtL_B~BsjP+OCaur5i7J_i0T@Q!m!cP8lZz$3R2dQ@ZXt zwaJ{+RP-&mcJka{j@92S`2T_c$l|5g)?4aY*U^lMva#amNq(7M@e^Zg(ey`YHVV6(8d)uFy3^y$2Ktxb^>#ClGof zKApRtKL06{{08;jsK*)rGRn&0h!9Y_#9h=VXxqq&!n*DT* z`J%XbP&E9pN_(yd_5mClF~E6D4q{5*{iKA(N~IXItdKaja@ca=EvHAn{Fo-R_@65% z<0tWazrjLlW%VqMI>Yv)DC;z0jq2Y^i0eV@z-;)(;teUv<*C{_$Oy`DWzPz{!CaJp z2o0!n0gL({3%8~?Mb!~ovcC$ZWNBO%L`zy3?dy`!15vT9#sp>7J~mVN(xn&^a^8C3 zn(0Ez;BSCkd)O?OJQdXYP0F;l$1P}K^95Xar3-dz>|zyl?7VJw#Jp$R)ayW)z#H*> zNLZcOLyEFdFmBrtF+s1F3$Ae%Ca7A_V&p%~!Z-*veUfmI906$@ei^Gwt+$>Zb)gRD zzj0gj%eFHu+!2`H8yv0R%;~6Q?@%V1Kn6OGt%V7vg@KXZnn4$8xm>?rzb&7ujA-L1 zmfOdk7$RpglW8+8ex2Q@PYEKhZEK2?@MFQAe&+(G`>Mt!I*8ckZk#YzxNYE0j45W> zdC>fYPh7YnsNeQJ&t02}ry%Px##SW=L-Sv_WA2TGHbD-e?TNcu+Qt0|Wbz~mNGo-l zn<1yWJwfY_zQ1#?5c+NH|4Y3>LjTo@Z8!dZ)VhQJPb+qN2B?O2&rr+*BgqJRo2gN& z^J6=6ZBr_afp!{+cx2sQrnU6_oP<9NPWt^yRl*ETR(4laj`0NYayT8i@SCa0Lisn7 zZ=T2d`uo32H#RXzsHv&(tOAG0p<^Z1ZfOkvlsealb zeJmPsJxE`_^}^M?r#CEF2wkds}!6_jT|{A z7{QD}XFz{iOK-UM8K=-)&E25txm}T+l{NNxU$lKUCMhGe%z}L#bRmr-T$x$)|6qZn z;I}<(K4dlSIkb9Cy>-2$kYjLAoTMXxSa&->vF?wCUNFA~+jfMS))dD8-a>8qd(^&_ zP1RcQBPubL3Yx6e-Mt3rX1Z4xnI$w+TVQV7GxKt*WwdCp&>GA4o4r@{eAKi$VU(8%BL95w<`=L=jZ(5WTrMW8Y#gEc0{!|ABo5pD|Fn%j?yC7j(G`3@xe(N z)!|WMpootP?JkSo=ikkojEY6I#g_zn{ioM`u*S@><)~Kt&+1WTZ<!CvRHtoIKf!j=P^}E8DU*We~cA!8Kq%=;s;JiM0?=FW&+VQC%!J&^Dec-$& z8IkL`Nd+a~_||VxEs%`@6vHLrtmojz)?`xryUg#-3`vzQiW*ZF#5xVF6j92cAMI-> zw@}LW=Ln6n<&s8wAtn%3?+yZDoQ%jNYwJ0(j z8+z{O1@d3Z`P|lJ?b>5!ui`=>U(|xGP7`OVaZ60~!zJUrQ+e*G+PnQmljxD^1hdO6 zJHGxYEa{anP*{`l+=@qE_q7TSzOT{EfuFYBY}QPw_{$fN-zBwIN4xSYIYKAEV_$ZQ zn%E7R-LVumzMZy@d(S{vSuA@Yjgxr?K7g3my;n8rs(DvizY33DlbD?I%IUJ zY~Vp!T3R->h4f_#A$b!$|Mk1#D+g4mofwmX(F^HY{sYQ1WHw*k_|xgw?StBTz^QH4 zaIF^m{W!CSO(a*A$w(Gn$kS4KM2kl|oikIeSmUqg{B(Zav9}m?#P^Sh`#Y8@P3&U! zwAg=x^Mzn$0@MRQ-b>n7VH^tj0k_*5aP1BK)RMooahTL5(Ii#XD%}51WC!%6FE;uG zB;7F!X?daA)9nVodDlmq-6iew4{y$7$*m8;MJ-=aHX=?Lse-tF0yiL}^`gm^p8;`# zFW5h#ntMWZ>X8hSI>1}3_4Laq8Z-84fP4F10xZ5Qv z^|Daa&)FvKS#5Q7|B8Tvsrr-i&jU^W{Q-gkg(6KZvj-f~5@#HN2i5Vap8x32-*R8w z-ah>HWm>k{Ci(~g&SIpHxar)oaVW2Lw zXoUBonVyCtY}Za( zDEX?tw)O6omNwZYP@Q((-Q9h(er6UEYfa z?yjAH$OKtuQRjuD6ysDpdBom*gmJKZ8-N+wDs%x;RC!BX~kCjMtgcX_X>-)DPVlb$l0)7JKi+0?Y(F!nS5RufcT zA{^ZUS;95g)dX1r>=0`a}nD4fS$GC z1TH2OVeW~k7Y4nSxpU+6?&%|vvW434md}Df2eki*>3`qesX4u8U&O6FR#Ev5bS>uapKsHhFfs2d5wVjr$SQWKYJkaAKh*RP%;G4iRB&W!{& zF>jDO$S`3shO4-te3n8@;ozj0$jIKTVl~e_|LMQp9u{ANrJ;G$rF>D7S72QaK(*?x?7N=OR99hpeJo0TItRLi`I*V^x{5>PcVOj(%W{S z&ho9prx#pu#!)3^2HKEMA8#*^N}H()OMrso=aT&{FBYX6Gy7f6F9)ng%l?lNkeYSS zZ#7ifj{(pcR1bi0ZOxAwlE07qf6I;F6q`$+T`&;UOiB+A0wQ0JG80I(uocA6S1x8W zX!Z-jL9RuZ&|D88@}x>U4`Nq?Mxm2c#WIgU&{LsfHHaybRFw@X8A{j>sD1hl3bb(e zy1{CgQUc>7>h(5%5cZKM=HRsW9-sC;5*<5Jp3RefVH&;H-P^kuntwI#H*J_9BBs@| zAv=n_(eA?)jUN5Fo1j<->nSO(0NpzN`PVmNVS{3WgS3ay6du}| zgTvhv{A4iKVSH@c7X`A7SA=l|txamTLx`xhSFa97^K3XZNU~|n%OibuZo9BV)LkKn zei5c*%!Cd{)^eAkpAll%Ok9s1?~_MsfRG2Y#Fa*ZO1GHaBA>_;{krdf{@_tvT&OSx z!{Ylyb^CKC#n`IxQr?pbm-IkJZ-7eHc^CBN7{0MWHYk5_(C#TRC>BFjDg+v!TjW%1 zvB?>W8b`@2PFXh1{3Fr_QwygypR5fspF8 zd-|&vY|*8ogWxs{lv{DMY1qiNW8g$Bvgk*n)BYJM#+m z%)d`5Xmpf$Y}Kl5?&6l~(dphS=DB?C@-J}XW@gLs{R-iCTACq^UH)@l3~T}V24hg1 zEIFw~gy8-A1W#z=!K>72~4QQPjEOvB~^i#gpa5 zFT>r*F1^fT_(C%wX5L2-_;}3!PQ3}53X`5Tco(_rm073`rmoRq6mGSc>@MI0T0+xh zlImj%ScLpvt&CGHJZZ(4QE~P9A5fbcw_bU;skDnR-rylHi#+Ck^w|+k5j$hO?|2Ha zE}NO?5SbbUbP(QKLfrRHC>HMXf;yxqh8TGrLmnH)r$(@PM{oBD%FM}*bA$I8@9xw3 z(bR)6ddYLAnWId-s$(HU4rr?RR6&7BMTY|Cq-drgYM%}061QW^N2S}#+oLOD7WFHS zOen?Y1ia<>&(=|4s~%neDS0^Pf*<{s=)n?xQ?vMn1cO$^8#}^iWX2cj| z5#E1C#;(KF>(u$j><*sZwhAmKAXlpx1Nl;+(Jvkr^As^UZnuBx$fJ;Xc_c zUu*9Ye&NDcI$saj7QmXiZ=U$1w^#~#mRjFk_&=L=kPY7pug^Ob-v+*ZBvfbP-Y#Jz z<90~F&pVFL>P6WCFLW%qh}~DpFG4>7-D11^oHBG4#T1P2x(iOAhd+Jz5gnFB@MZZg zgT{r9S@Wj=IRv^idojj&9x5eR&AVXOa=r2_5);7(iW7`Nz1F!?^#0#hAq2JhtE*(4 z4H+XAJAVlReW;FY^(oo(`}maf(UOvv{v(U8B&cV!1}JC(H$e|#Rv*MH+?iMBn?Ocq zaXRm6HdaJC@a4dSwEIm(=|$3#xJWdKJzB$N(C_otLlHn3^-o*r3sSCL-Tmf&2v>e) zoYI;fr&t;dsn20qivOd%YmZCvZu@(-R$FbkWzNcsr?zEB%}h-b>0q|XbgQK~MQT=5 z9!T*N2yAm@=IL=|o)XGRH&0~b2}QO}W|(MBNm0xUK@rhFK!Ka*2j(HIXYU`+`@VbL zcmMV2b9?ia_~x(K+(UU9veR1_NxOGe2yp(4lt* zWiv~XlauQ>qJGti&TQZra2okIi2}E5FYXc$FjdJlorJAb;Onu?pW$p6?YFih7KQJj z+#ynnWT~Pmj!XyS0W(0-+yS6Lp#BgOac3=nXPsKaPgRQItJ-@xA{*h8L+nPJ?r>H} zif|ZoM!v>`XMh~a-p^w2Ko=Y7P>QEFNh+~ozov-wyS!V4@BP$|iu2c*JP>Zg`pB~C z#4_j95cr@{dB8_O=d{Zf>CNH z`e?`b2}|IuwllNYTjFBb+c7^6E2|MzVT7Q;BH}O$)$ff0!wkX+cgTr!0YHEfL~GFv zAJWMH9Txbzo*lc=9ij)(2MBY!G{XbYh{{iFMN7QSgE7N&Fe zz+UiuZ1W;)mKgN>d!Og9owXX?>%}5ig>R^>O$W`>({1*+-}&cs%>Ktb!2i`B`ug6_ zrFccZ|3xn_Lm<(se8JU&U}wR&b3Wl_?KJ|%3Li=}V)l)uVcu37ujKNo$G#cs%83R9-NgXtO2V7UU#z3aTl!`&V#|h zz_p*MC9mz0twCp*={vObk<}~A2C`6fPHy+N>s){QR3*2nfp!UeXsph&o)jh2VvS)H zADKG_UgRvntBYDkE4gp@HY;{!<;fL`nJ52#X&vjR>du|S%t8XAwRw;1O$U><)y$c7 zXK;?{(am&yl=#Lsk5^{*zJUM}Mcwmn{Fd>0@HqY1-J3$f+@1uR`;RnUr~&AU7EBub;CHo*z?6lk#Fa$5{f5 zx^MKCF2{lA#|`i6DB2R4mDAF_M+TlZY<SPbHut+Ei-rBU4>WT3HE2GpTZw4W!3rTwCSL2ALH!?b6oW zaleHsf>xX1*3Z>T#B>I*b7n^wtOdh{he+0yPv0e3)|$5M2Yca|P`K7XpDoZq6!xKy5of)L#`|Qoqxchji15*W{blS&TpnWB zpwmzLnDe78L(cVMBKivy&~y35UFoliLV3w{_WX5S95DLAvQlgs9-0t?AMC(=rEEl~ z?qn2R#ij9t!_}1G_Bsxe?}eHtr;wRFPnL5ia+LNtBAQd#zu&LAnji=#;gvKb<(o~* zc}LYayr#yWd2p+`EZLqC1XIVT&-0MSd#{|pSZSZV_JQm zL0uMAc~VIp3~aaq_XSA0VTwEk#FG&t;T=zgMIKC>}X-qp4MpWV<*9gc%b9&&Zc4)ON!NOJMt(~F`i@};CYV}#q_$W=?g}!)l4=41{ zNjQh{_K8S`Ca|xBs7fSa#dv_8FH75c%p2$WjDweWjQF%K{G|tq3r1i^EsZ{Rq<=Lg zQ)t)92V+)W)wmDfqWx4ZqS#4)H2L) zHfF)1Ilc!L^Vc7z3>z@#`)JOALEW&Z=N3#=-GDGk%fING7};%mTSbyrZz@er(2`TM z1zupco)6T;)Mgj6Wc}x15F)U=?MAibngy7zzcfEG)p)U~-rb>8Ezk9GAET}}-AB73 z@w%e6j`w9c(Ucm_SGovDIilSiA_lRc(FT`gHkxYGDg4sFFJ)>xm9$?#wcH9x5OPhY z3N`$=t|%0}Gn*ADwS|h4EDTcu5kv&fr_u#1;%VKDQria3rl6n#Qjwgozw)yC?ET(MTQs!*v+?Y z=A97tqxSDu^?%=?hpg5jk6t)-Al#qHO4P;Qi#0#=U#@O$p5igE zta0vly0i)el$TLeitmZ_yK=*}x4=veGB5Y;ewDCC1U{wcZs}zWP%`wM^(&9beX>VN ziZMi1Mj%er+NFQe^kt)`0Qwu^(D^--$?!28{ z7INn35WpVC^w6IXWs)jOwTulNHal=1rNE20yMoK05qwsG zV!dRf29^sq4_pm03KGvu?x5}qUX_17C{aQpidry`&SCUsBQ{5LYUQ?|YS#QNi2lig z%K6J1Te3?H4ZA!un~s_I3c$epL|(8Q{SCb!!e7EwXVZ3c_3~^yWVhnUV>$SiNUkML z9*aK?22|VE3GMoz{XlP4l6wVU&5<173NX*x5-p-TA=5x%_+iLHFyL_7mFazGR{}I$7eZe$>6fhMuV8D|x z00QJpjqJd;%27Qrc}*ZeyBb9g+aw|^?g{R}iRzXGsHWW9c&zZUBx2JM7Do=!ujSjj zI8N1#QTt`m5cn=+8z6Wa1sc%`vOhApdgz18hD=pn>0;0|a4t4zuq>0J_848u$Izld z_lo^9?&It)%6ZjEbx!eMVqQ=Q;g6~7>}JAFkDRWJ#xbZ9M#GJpe;rh0J+GSeYn{ZF z0hB+YC)l4vyXa_ktApDU2+aNKgMkU|Z?y64kveptld{<}+X43W4MD+ZwdfB#{RwS8 z-Fq7kvnI5`zX)o^F6m!KDu2pDxt%erf9tJ}$vd64eEp|9;Q!4-nOQ8(jdwljWv9n> zznRHKo8h0YI&-n6rlyXGetx5;=Xk^UfSo!kJR0tz)Cw>^@qJSPyvY)T^oJWS)J%jW$~A-_#gbl_1cqiqlST+U6B~M$|XO& z#n}R47-^T}sqpMxTT4qYEDN;TgUZApDr-AP2(1UFS@%T(#yV4FjwCK~fS$iQk(_|^ z082{m=3gckYXL4%*YH14*9HHf>u?({0gqwq#|w!sCkaf{d;h<_LQBFcj8AxaRtG&X ziJE=R{L}Yle6`cikg;Hf(X#j7r=>nPxjAXguDFl3&VI4{=N0r7H)5B~S#xgb`las| zB&}5Wrn$PhT!^>Iyt>e6SHpH=WPYRpBbv~hPpE?mT11&0-;!rlJ-Ec+Ks0%rEv&gk z3OYdnsmlQhu>P!Wv>7Pf>PW9^g8)U#qeuUG;Ll2jh1^)J8Xpe!8(Yz0N(K|rGqvKD zmt!JyvgYi>e&Q1mM8kjE)!EO^Kv>bNC5ai)6*&}b%~K}aWVGA=<;^byprswQ2BBIF z%hJkA>Jtg=Y_Qs>Z%7FSc-G6H#BT$16g`GK_zHs*fqYrx9$DPz=nGH`dg~Zi$_K_v zW&~!TL9$IJ_@E15Gng0Q4^nj@hVZUYc&tu6EczJk3lfpp8N!7i&o{w_3p0erZQ8+9 zfl)N}*vEhrUe9ve{ZUxGuE%G?GTi;;xb!`?4OSrk2mb!AqyC$yYNHSi=b#+h82!c6 z9^SCLbS{qvdNJKf6hX^9*!R46>g;}0HKi?x+nsbLN4-Wm?XsFp;abA8U;5)Q5d^TG zi*Xa*DfEp}tW@+IUiBqIY)#d)r>HvEv)N&!BIx>3>A|0`rd&w&w64wP9H zA8C*dyraM{`WaB*k@V?IQqEh8hih9lwlcZ`IDx0{qa0+JrlrZnXUY6>41I8PxceTp2ciyH!W?i45i%MZB9SPK5*b|-m2=~;lTq{{pkdc zzz*h&8aL9j!r$skPCY*vuE;FKz&#k+CJPvsq`nd{++fglli)(K-r&ru_W7-hq5vZ9x(ts7Vk?XJ)yi#W9J@iyzD2Z#JMM@42 zV`2{SE6k`B^4tQ|T;%7TiRA-osk>u#z9XreL1Z7Xt5WB~xI`!lgXwVJDhNzO%H~HX!ZEer?Cq~1{ zn44~B&xkjB+RBvM4i);Za5gu8;n?dtghG3sD$sClanXBW_(5+cHanGlxvCEGTjk-` z{P%@1D2z>3p=hkEEkNVjUncJUO_f|QuM>zFYsBO5 zT+IpY&Hae;H!TI|KJ>}#EJ1w-gW=OSo$Pyz;EHoQwlTMcy8QZ0_(_v*2nGD0PnaC5 zd7as3$(jdtA+D_6!6e~AJA?@Optq~wg=eB2W+*kdu#gwdZ#K$LLE#1yu;qo*F0^11 z?qZYDMn;G~V(mP1mD4jL|A8gz+0**4IUz~!*>0ERBP=YXEhufofTKnw1|1H|1Udfg zlr4nH{f;6b)`B>f?=f`ImnB+3>euFWgt|;MD*@+(p4822miCBH>e>XZt3%MS)%M9; z)s+)24klO^0zpIJ=%h@%+CJsp!xz#;{qFH6^M0&)pjwf~%nj`@N(s<}uh_up@`xuV zN%xi1u`fF|2bLD#s-npiP~T6n<_CJbFZEB;Wq>e8tnW7^ukaw97Jga3%EXX;fAK!| zcMf}MjB@x7nIp7&>pVMlzfZAkkI*Enp1}e?8=JU~ETQ{RTZ!Z%$<-wuuyEn3$2fIv z18q$(@qVRz(Rq=mP;gaUKBLCZhgb(FV97@Q;0H&?t|sj;=wlw*Q4epQMq_Y$tcOP5 zTOV#hMnI>$JV^2`WJ~II*4?S}=&GS))1@VMOXn%c>IQ{tIp)-(*em&M5)Xt!*^n~N zx$I_zq>`Yr-1TFAQgEN@KwLD7hlV?X>aIH+kt1z2X74zqX17a=#EM%F(eUnazxuU7 z#dn+OBR(njx-6&hd&YX)HyL8NA2vYHOKn1aa$VF^#;QuRv7f4dY#OjU6TFe2g}w-m zVf3`;w3R5~1)|vxrio{kpwfft;vL4%5_`wrBay8J~|o(be!932##gkUFD2YN{@X@%Q%+0TU!jjTF&c)$~uzNGFaO ziiNa91BgrG<9n{BKw6T1{+)^5g#_9*)%w5fUqyu~n7yRni;5He`tH4*+g+Usw;ufA Ep8`_<#sB~S literal 0 HcmV?d00001 diff --git a/sdk/cosmos/azure-cosmos/docs/replicaValidation/StatusTransition-ReplicaValidationEnabled.png b/sdk/cosmos/azure-cosmos/docs/replicaValidation/StatusTransition-ReplicaValidationEnabled.png new file mode 100644 index 0000000000000000000000000000000000000000..9f622b6c47e20139f04ccd2242a8127faf5a650c GIT binary patch literal 166175 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

8SD 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

$T~7yLWCp8PV~#)y5*#-F!%x7a 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 z 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

Cy8etJxP&qcF3)J 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 literal 0 HcmV?d00001 diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosAsyncDatabase.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosAsyncDatabase.java index 1d4c4d3069f29..0e76c1f32bece 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosAsyncDatabase.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosAsyncDatabase.java @@ -17,7 +17,6 @@ import com.azure.cosmos.models.CosmosContainerResponse; import com.azure.cosmos.models.CosmosDatabaseRequestOptions; import com.azure.cosmos.models.CosmosDatabaseResponse; -import com.azure.cosmos.models.CosmosItemRequestOptions; import com.azure.cosmos.models.CosmosQueryRequestOptions; import com.azure.cosmos.models.CosmosUserProperties; import com.azure.cosmos.models.CosmosUserResponse; @@ -34,7 +33,6 @@ import java.util.Collections; import java.util.List; -import java.util.Map; import static com.azure.core.util.FluxUtil.withContext; import static com.azure.cosmos.implementation.Utils.setContinuationTokenAndMaxItemCount; diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java index 6be5b681fdcad..02294e7b5190f 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/CosmosException.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -141,6 +142,11 @@ public class CosmosException extends AzureException { */ private boolean sendingRequestHasStarted; + /*** + * All selectable replica status. + */ + private final List replicaStatusList = new ArrayList<>(); + /** * Creates a new instance of the CosmosException class. * @@ -542,12 +548,27 @@ void setRntbdPendingRequestQueueSize(int rntbdPendingRequestQueueSize) { this.rntbdPendingRequestQueueSize = rntbdPendingRequestQueueSize; } + List getReplicaStatusList() { + return this.replicaStatusList; + } + /////////////////////////////////////////////////////////////////////////////////////////// // the following helper/accessor only helps to access this class outside of this package.// /////////////////////////////////////////////////////////////////////////////////////////// static void initialize() { ImplementationBridgeHelpers.CosmosExceptionHelper.setCosmosExceptionAccessor( - (statusCode, innerException) -> new CosmosException(statusCode, innerException)); + new ImplementationBridgeHelpers.CosmosExceptionHelper.CosmosExceptionAccessor() { + @Override + public CosmosException createCosmosException(int statusCode, Exception innerException) { + return new CosmosException(statusCode, innerException); + } + + @Override + public List getReplicaStatusList(CosmosException cosmosException) { + return cosmosException.getReplicaStatusList(); + } + + }); } static { initialize(); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java index 1d191bd26253b..b7fb57f1123f2 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java @@ -106,6 +106,10 @@ public class Configs { private static final String QUERY_EMPTY_PAGE_DIAGNOSTICS_ENABLED = "COSMOS.QUERY_EMPTY_PAGE_DIAGNOSTICS_ENABLED"; private static final boolean DEFAULT_QUERY_EMPTY_PAGE_DIAGNOSTICS_ENABLED = false; + // whether to enable replica addresses validation + private static final String REPLICA_ADDRESS_VALIDATION_ENABLED = "COSMOS.REPLICA_ADDRESS_VALIDATION_ENABLED"; + private static final boolean DEFAULT_REPLICA_ADDRESS_VALIDATION_ENABLED = false; + public Configs() { this.sslContext = sslContextInit(); } @@ -304,4 +308,10 @@ private static boolean getBooleanValue(String val, boolean defaultValue) { return Boolean.valueOf(val); } } + + public static boolean isReplicaAddressValidationEnabled() { + return getJVMConfigAsBoolean( + REPLICA_ADDRESS_VALIDATION_ENABLED, + DEFAULT_REPLICA_ADDRESS_VALIDATION_ENABLED); + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java index 2e18086f35f9d..aa79b180ef0d0 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java @@ -10,6 +10,7 @@ public class CosmosSchedulers { private final static String COSMOS_PARALLEL_THREAD_NAME = "cosmos-parallel"; private final static String TRANSPORT_RESPONSE_BOUNDED_ELASTIC_THREAD_NAME = "transport-response-bounded-elastic"; private final static String BULK_EXECUTOR_BOUNDED_ELASTIC_THREAD_NAME = "bulk-executor-bounded-elastic"; + private final static String OPEN_CONNECTIONS_BOUNDED_ELASTIC_THREAD_NAME = "open-connections-bounded-elastic"; private final static int TTL_FOR_SCHEDULER_WORKER_IN_SECONDS = 60; // same as BoundedElasticScheduler.DEFAULT_TTL_SECONDS // Using a custom parallel scheduler to be able to schedule retries etc. @@ -37,4 +38,13 @@ public class CosmosSchedulers { TTL_FOR_SCHEDULER_WORKER_IN_SECONDS, true ); + + // Custom bounded elastic scheduler for open connections + public final static Scheduler OPEN_CONNECTIONS_BOUNDED_ELASTIC = Schedulers.newBoundedElastic( + Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, + Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + OPEN_CONNECTIONS_BOUNDED_ELASTIC_THREAD_NAME, + TTL_FOR_SCHEDULER_WORKER_IN_SECONDS, + true + ); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java index 21dddf96f1761..485795c9fb6ac 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DiagnosticsClientContext.java @@ -99,6 +99,7 @@ class DiagnosticsClientConfig { private String rntbdConfigAsString; private ConnectionMode connectionMode; private String machineId; + private boolean replicaValidationEnabled = Configs.isReplicaAddressValidationEnabled(); public DiagnosticsClientConfig withMachineId(String machineId) { this.machineId = machineId; @@ -189,9 +190,10 @@ public String gwConfig() { public String otherConnectionConfig() { if (this.otherCfgAsString == null) { - this.otherCfgAsString = Strings.lenientFormat("(ed: %s, cs: %s)", + this.otherCfgAsString = Strings.lenientFormat("(ed: %s, cs: %s, rv: %s)", this.endpointDiscoveryEnabled, - this.connectionSharingAcrossClientsEnabled); + this.connectionSharingAcrossClientsEnabled, + this.replicaValidationEnabled); } return this.otherCfgAsString; diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DocumentServiceRequestContext.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DocumentServiceRequestContext.java index 4174bfc6c0bb7..e87892e5eaf92 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DocumentServiceRequestContext.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/DocumentServiceRequestContext.java @@ -5,13 +5,17 @@ import com.azure.cosmos.ConsistencyLevel; import com.azure.cosmos.CosmosDiagnostics; +import com.azure.cosmos.CosmosException; import com.azure.cosmos.implementation.directconnectivity.StoreResponse; import com.azure.cosmos.implementation.directconnectivity.StoreResult; import com.azure.cosmos.implementation.directconnectivity.TimeoutHelper; +import com.azure.cosmos.implementation.directconnectivity.Uri; import com.azure.cosmos.implementation.routing.PartitionKeyInternal; import java.net.URI; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class DocumentServiceRequestContext implements Cloneable { public volatile boolean forceAddressRefresh; @@ -37,9 +41,10 @@ public class DocumentServiceRequestContext implements Cloneable { public volatile CosmosDiagnostics cosmosDiagnostics; public volatile String resourcePhysicalAddress; public volatile String throughputControlCycleId; + public volatile boolean replicaAddressValidationEnabled; + private final Set failedEndpoints = ConcurrentHashMap.newKeySet(); - public DocumentServiceRequestContext() { - } + public DocumentServiceRequestContext() {} /** * Sets routing directive for GlobalEndpointManager to resolve the request @@ -75,6 +80,25 @@ public void clearRouteToLocation() { this.usePreferredLocations = null; } + public Set getFailedEndpoints() { + return this.failedEndpoints; + } + + public void addToFailedEndpoints(Exception exception, Uri address) { + + if (exception instanceof CosmosException) { + CosmosException cosmosException = (CosmosException) exception; + + // Tracking the failed endpoints, so during retry, we can prioritize other replicas (replicas have not been tried on) + // If the exception eventually cause a forceRefresh gateway addresses, during that time, we are going to officially mark + // the replica as unhealthy. + // We started by only track 410 exceptions, but can add other exceptions based on the feedback and observations + if (Exceptions.isGone(cosmosException)) { + this.failedEndpoints.add(address); + } + } + } + @Override public DocumentServiceRequestContext clone() { DocumentServiceRequestContext context = new DocumentServiceRequestContext(); @@ -99,6 +123,7 @@ public DocumentServiceRequestContext clone() { context.cosmosDiagnostics = this.cosmosDiagnostics; context.resourcePhysicalAddress = this.resourcePhysicalAddress; context.throughputControlCycleId = this.throughputControlCycleId; + context.replicaAddressValidationEnabled = this.replicaAddressValidationEnabled; return context; } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java index f3663b7347e17..2964e6bc3f281 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/ImplementationBridgeHelpers.java @@ -1018,6 +1018,7 @@ public static void setCosmosExceptionAccessor(final CosmosExceptionAccessor newA public interface CosmosExceptionAccessor { CosmosException createCosmosException(int statusCode, Exception innerException); + List getReplicaStatusList(CosmosException cosmosException); } } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java index 1f5f8bfd16418..18304146a9302 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java @@ -87,9 +87,9 @@ public Mono getAsync( (exception) -> { logger.debug("refresh cache [{}] resulted in error", key, exception); - if (initialLazyValue.shouldRemoveFromCache()) { - // In some scenarios when a background failure occurs like a 404 the initial cache value should be removed. - if (removeNotFoundFromCacheException((CosmosException)exception)) { + // In some scenarios when a background failure occurs like a 404 the initial cache value should be removed. + if (exception instanceof CosmosException && removeNotFoundFromCacheException((CosmosException) exception)) { + if (initialLazyValue.shouldRemoveFromCache()) { this.remove(key); } } @@ -97,9 +97,7 @@ public Mono getAsync( } ); }).onErrorResume((exception) -> { - if (logger.isDebugEnabled()) { - logger.debug("cache[{}] resulted in error", key, exception); - } + logger.debug("cache[{}] resulted in error", key, exception); if (initialLazyValue.shouldRemoveFromCache()) { this.remove(key); } @@ -107,9 +105,7 @@ public Mono getAsync( }); } - if (logger.isDebugEnabled()) { - logger.debug("cache[{}] doesn't exist, computing new value", key); - } + logger.debug("cache[{}] doesn't exist, computing new value", key); AsyncLazyWithRefresh asyncLazyWithRefresh = new AsyncLazyWithRefresh(singleValueInitFunc); AsyncLazyWithRefresh preResult = this.values.putIfAbsent(key, asyncLazyWithRefresh); if (preResult == null) { @@ -119,9 +115,7 @@ public Mono getAsync( return result.getValueAsync().onErrorResume( (exception) -> { - if (logger.isDebugEnabled()) { - logger.debug("cache[{}] resulted in error", key, exception); - } + logger.debug("cache[{}] resulted in error", key, exception); // Remove the failed task from the dictionary so future requests can send other calls. if (result.shouldRemoveFromCache()) { this.remove(key); @@ -132,9 +126,7 @@ public Mono getAsync( } public void set(TKey key, TValue value) { - if (logger.isDebugEnabled()) { - logger.debug("set cache[{}]={}", key, value); - } + logger.debug("set cache[{}]={}", key, value); AsyncLazyWithRefresh updatedValue = new AsyncLazyWithRefresh(value); this.values.put(key, updatedValue); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncLazy.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncLazy.java index cf532ce02bb22..749585fd6e4e2 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncLazy.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncLazy.java @@ -35,7 +35,8 @@ public AsyncLazy(TValue value) { this.failed = false; } - private AsyncLazy(Mono single) { + // TODO: revert back to private modifier after replica validation is changed to use nonblocking cache + public AsyncLazy(Mono single) { logger.debug("constructor"); this.single = single .doOnSuccess(v -> this.value = v) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressInformation.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressInformation.java index e60e03e94f78e..7aff378e1e47c 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressInformation.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressInformation.java @@ -14,10 +14,10 @@ * Used internally to encapsulate a physical address information in the Azure Cosmos DB database service. */ public class AddressInformation { - private Protocol protocol; - private boolean isPublic; - private boolean isPrimary; - private Uri physicalUri; + private final Protocol protocol; + private final boolean isPublic; + private final boolean isPrimary; + private final Uri physicalUri; public AddressInformation(boolean isPublic, boolean isPrimary, String physicalUri, Protocol protocol) { Objects.requireNonNull(protocol); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/ConsistencyWriter.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/ConsistencyWriter.java index 853ec38374708..6abf3ed365b0c 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/ConsistencyWriter.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/ConsistencyWriter.java @@ -34,6 +34,7 @@ import java.net.URI; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -158,6 +159,7 @@ Mono writePrivateAsync( Mono> replicaAddressesObs = this.addressSelector.resolveAddressesAsync(request, forceRefresh); AtomicReference primaryURI = new AtomicReference<>(); + AtomicReference> replicaStatusList = new AtomicReference<>(); return replicaAddressesObs.flatMap(replicaAddresses -> { try { @@ -188,6 +190,8 @@ Mono writePrivateAsync( return Mono.error(e); } + replicaStatusList.set(Arrays.asList(primaryUri.getHealthStatusDiagnosticString())); + return this.transportClient.invokeResourceOperationAsync(primaryUri, request) .doOnError( t -> { @@ -208,7 +212,8 @@ Mono writePrivateAsync( null, ex != null ? ex: rawException, false, false, - primaryUri); + primaryUri, + replicaStatusList.get()); String value = ex != null ? ex .getResponseHeaders() @@ -233,7 +238,14 @@ Mono writePrivateAsync( ); }).flatMap(response -> { - storeReader.createAndRecordStoreResult(request, response, null, false, false, primaryURI.get()); + storeReader.createAndRecordStoreResult( + request, + response, + null, + false, + false, + primaryURI.get(), + replicaStatusList.get()); return barrierForGlobalStrong(request, response); }); } else { diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java index 29cb70ec8e1fc..53d599b3d3a2e 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java @@ -11,6 +11,7 @@ import com.azure.cosmos.implementation.Configs; import com.azure.cosmos.implementation.ConnectionPolicy; import com.azure.cosmos.implementation.Constants; +import com.azure.cosmos.implementation.CosmosSchedulers; import com.azure.cosmos.implementation.DiagnosticsClientContext; import com.azure.cosmos.implementation.DocumentCollection; import com.azure.cosmos.implementation.Exceptions; @@ -38,7 +39,6 @@ import com.azure.cosmos.implementation.Utils; import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; import com.azure.cosmos.implementation.apachecommons.lang.tuple.Pair; -import com.azure.cosmos.implementation.caches.AsyncCache; import com.azure.cosmos.implementation.caches.AsyncCacheNonBlocking; import com.azure.cosmos.implementation.http.HttpClient; import com.azure.cosmos.implementation.http.HttpHeaders; @@ -104,6 +104,7 @@ public class GatewayAddressCache implements IAddressCache { private final GlobalEndpointManager globalEndpointManager; private IOpenConnectionsHandler openConnectionsHandler; private final ConnectionPolicy connectionPolicy; + private final boolean replicaAddressValidationEnabled; public GatewayAddressCache( DiagnosticsClientContext clientContext, @@ -161,6 +162,7 @@ public GatewayAddressCache( this.globalEndpointManager = globalEndpointManager; this.openConnectionsHandler = openConnectionsHandler; this.connectionPolicy = connectionPolicy; + this.replicaAddressValidationEnabled = Configs.isReplicaAddressValidationEnabled(); } public GatewayAddressCache( @@ -274,45 +276,56 @@ public Mono> tryGetAddresses(RxDocumentS this.suboptimalServerPartitionTimestamps.remove(partitionKeyRangeIdentity); } - Mono> addressesObs = this.serverPartitionAddressCache.getAsync( - partitionKeyRangeIdentity, - addressInformation -> this.getAddressesForRangeId( - request, - partitionKeyRangeIdentity, - forceRefreshPartitionAddressesModified), - forceRefresh -> forceRefreshPartitionAddressesModified).map(Utils.ValueHolder::new); - - return addressesObs.map( - addressesValueHolder -> { - if (notAllReplicasAvailable(addressesValueHolder.v)) { - if (logger.isDebugEnabled()) { - logger.debug("not all replicas available {}", JavaStreamUtils.info(addressesValueHolder.v)); + Mono> addressesObs = + this.serverPartitionAddressCache + .getAsync( + partitionKeyRangeIdentity, + cachedAddresses -> this.getAddressesForRangeId( + request, + partitionKeyRangeIdentity, + forceRefreshPartitionAddressesModified, + cachedAddresses), + cachedAddresses -> { + for (Uri failedEndpoints : request.requestContext.getFailedEndpoints()) { + failedEndpoints.setUnhealthy(); + } + return forceRefreshPartitionAddressesModified + || Arrays.stream(cachedAddresses).anyMatch(addressInformation -> addressInformation.getPhysicalUri().shouldRefreshHealthStatus()); + }) + .map(Utils.ValueHolder::new); + + return addressesObs + .map(addressesValueHolder -> { + if (notAllReplicasAvailable(addressesValueHolder.v)) { + if (logger.isDebugEnabled()) { + logger.debug("not all replicas available {}", JavaStreamUtils.info(addressesValueHolder.v)); + } + this.suboptimalServerPartitionTimestamps.putIfAbsent(partitionKeyRangeIdentity, Instant.now()); } - this.suboptimalServerPartitionTimestamps.putIfAbsent(partitionKeyRangeIdentity, Instant.now()); - } - return addressesValueHolder; - }).onErrorResume(ex -> { - Throwable unwrappedException = reactor.core.Exceptions.unwrap(ex); - CosmosException dce = Utils.as(unwrappedException, CosmosException.class); - if (dce == null) { - logger.error("unexpected failure", ex); - if (forceRefreshPartitionAddressesModified) { - this.suboptimalServerPartitionTimestamps.remove(partitionKeyRangeIdentity); - } - return Mono.error(unwrappedException); - } else { - logger.debug("tryGetAddresses dce", dce); - if (Exceptions.isStatusCode(dce, HttpConstants.StatusCodes.NOTFOUND) || - Exceptions.isStatusCode(dce, HttpConstants.StatusCodes.GONE) || - Exceptions.isSubStatusCode(dce, HttpConstants.SubStatusCodes.PARTITION_KEY_RANGE_GONE)) { - //remove from suboptimal cache in case the collection+pKeyRangeId combo is gone. - this.suboptimalServerPartitionTimestamps.remove(partitionKeyRangeIdentity); - logger.debug("tryGetAddresses: inner onErrorResumeNext return null", dce); - return Mono.just(new Utils.ValueHolder<>(null)); - } - return Mono.error(unwrappedException); - } + return addressesValueHolder; + }) + .onErrorResume(ex -> { + Throwable unwrappedException = reactor.core.Exceptions.unwrap(ex); + CosmosException dce = Utils.as(unwrappedException, CosmosException.class); + if (dce == null) { + logger.error("unexpected failure", ex); + if (forceRefreshPartitionAddressesModified) { + this.suboptimalServerPartitionTimestamps.remove(partitionKeyRangeIdentity); + } + return Mono.error(unwrappedException); + } else { + logger.debug("tryGetAddresses dce", dce); + if (Exceptions.isNotFound(dce) || + Exceptions.isGone(dce) || + Exceptions.isSubStatusCode(dce, HttpConstants.SubStatusCodes.PARTITION_KEY_RANGE_GONE)) { + //remove from suboptimal cache in case the collection+pKeyRangeId combo is gone. + this.suboptimalServerPartitionTimestamps.remove(partitionKeyRangeIdentity); + logger.debug("tryGetAddresses: inner onErrorResumeNext return null", dce); + return Mono.just(new Utils.ValueHolder<>(null)); + } + return Mono.error(unwrappedException); + } }); } @@ -600,7 +613,8 @@ private void validatePkRangeIdentity(PartitionKeyRangeIdentity pkRangeIdentity) private Mono getAddressesForRangeId( RxDocumentServiceRequest request, PartitionKeyRangeIdentity pkRangeIdentity, - boolean forceRefresh) { + boolean forceRefresh, + AddressInformation[] cachedAddresses) { Utils.checkNotNullOrThrow(request, "request", ""); validatePkRangeIdentity(pkRangeIdentity); @@ -608,8 +622,12 @@ private Mono getAddressesForRangeId( String collectionRid = pkRangeIdentity.getCollectionRid(); String partitionKeyRangeId = pkRangeIdentity.getPartitionKeyRangeId(); - logger.debug("getAddressesForRangeId collectionRid {}, partitionKeyRangeId {}, forceRefresh {}", - collectionRid, partitionKeyRangeId, forceRefresh); + logger.debug( + "getAddressesForRangeId collectionRid {}, partitionKeyRangeId {}, forceRefresh {}", + collectionRid, + partitionKeyRangeId, + forceRefresh); + Mono> addressResponse = this.getServerAddressesViaGatewayAsync(request, collectionRid, Collections.singletonList(partitionKeyRangeId), forceRefresh); Mono>> addressInfos = @@ -619,42 +637,57 @@ private Mono getAddressesForRangeId( logger.debug("addresses from getServerAddressesViaGatewayAsync in getAddressesForRangeId {}", JavaStreamUtils.info(addresses)); } - return addresses.stream().filter(addressInfo -> - this.protocolScheme.equals(addressInfo.getProtocolScheme())) - .collect(Collectors.groupingBy( - Address::getParitionKeyRangeId)) - .values().stream() - .map(groupedAddresses -> toPartitionAddressAndRange(collectionRid, addresses)) - .collect(Collectors.toList()); + return addresses + .stream() + .filter(addressInfo -> this.protocolScheme.equals(addressInfo.getProtocolScheme())) + .collect(Collectors.groupingBy(Address::getParitionKeyRangeId)) + .values() + .stream() + .map(groupedAddresses -> toPartitionAddressAndRange(collectionRid, addresses)) + .collect(Collectors.toList()); }); - Mono>> result = addressInfos.map(addressInfo -> addressInfo.stream() - .filter(a -> - StringUtils.equals(a.getLeft().getPartitionKeyRangeId(), partitionKeyRangeId)) - .collect(Collectors.toList())); - - return result.flatMap( - list -> { - if (logger.isDebugEnabled()) { - logger.debug("getAddressesForRangeId flatMap got result {}", JavaStreamUtils.info(list)); - } - if (list.isEmpty()) { + Mono>> result = + addressInfos + .map(addressInfo -> addressInfo.stream() + .filter(a -> StringUtils.equals(a.getLeft().getPartitionKeyRangeId(), partitionKeyRangeId)) + .collect(Collectors.toList())); + + return result + .flatMap( + list -> { + if (logger.isDebugEnabled()) { + logger.debug("getAddressesForRangeId flatMap got result {}", JavaStreamUtils.info(list)); + } - String errorMessage = String.format( - RMResources.PartitionKeyRangeNotFound, - partitionKeyRangeId, - collectionRid); + if (list.isEmpty()) { + String errorMessage = String.format( + RMResources.PartitionKeyRangeNotFound, + partitionKeyRangeId, + collectionRid); - PartitionKeyRangeGoneException e = new PartitionKeyRangeGoneException(errorMessage); - BridgeInternal.setResourceAddress(e, collectionRid); + PartitionKeyRangeGoneException e = new PartitionKeyRangeGoneException(errorMessage); + BridgeInternal.setResourceAddress(e, collectionRid); - return Mono.error(e); - } else { - return Mono.just(list.get(0).getRight()); - } - }).doOnError(e -> { - logger.debug("getAddressesForRangeId", e); - }); + return Mono.error(e); + } else { + // merge with the cached addresses + // if the address is being returned from gateway again, then keep using the cached addressInformation object + // for new addresses, use the new addressInformation object + AddressInformation[] mergedAddresses = this.mergeAddresses(list.get(0).getRight(), cachedAddresses); + for (AddressInformation address : mergedAddresses) { + // The main purpose for this step is to move address health status from unhealthy -> unhealthyPending + address.getPhysicalUri().setRefreshed(); + } + + if (this.replicaAddressValidationEnabled) { + this.validateReplicaAddresses(mergedAddresses); + } + + return Mono.just(mergedAddresses); + } + }) + .doOnError(e -> logger.debug("getAddressesForRangeId", e)); } public Mono> getMasterAddressesViaGatewayAsync( @@ -790,6 +823,73 @@ public Mono> getMasterAddressesViaGatewayAsync( }); } + /*** + * merge the new addresses get back from gateway with the cached addresses. + * If the address is being returned from gateway again, then keep using the cached addressInformation object + * If it is a new address being returned, then use the new addressInformation object. + * + * @param newAddresses the latest addresses being returned from gateway. + * @param cachedAddresses the cached addresses. + * + * @return the merged addresses. + */ + private AddressInformation[] mergeAddresses(AddressInformation[] newAddresses, AddressInformation[] cachedAddresses) { + checkNotNull(newAddresses, "Argument 'newAddresses' should not be null"); + + if (cachedAddresses == null) { + return newAddresses; + } + + List mergedAddresses = new ArrayList<>(); + Map> cachedAddressMap = + Arrays + .stream(cachedAddresses) + .collect(Collectors.groupingBy(AddressInformation::getPhysicalUri)); + + for (AddressInformation newAddress : newAddresses) { + boolean useCachedAddress = false; + if (cachedAddressMap.containsKey(newAddress.getPhysicalUri())) { + for (AddressInformation cachedAddress : cachedAddressMap.get(newAddress.getPhysicalUri())) { + if (newAddress.getProtocol() == cachedAddress.getProtocol() + && newAddress.isPublic() == cachedAddress.isPublic() + && newAddress.isPrimary() == cachedAddress.isPrimary()) { + + useCachedAddress = true; + mergedAddresses.add(cachedAddress); + break; + } + } + } + + if (!useCachedAddress) { + mergedAddresses.add(newAddress); + } + } + + return mergedAddresses.toArray(new AddressInformation[mergedAddresses.size()]); + } + + private void validateReplicaAddresses(AddressInformation[] addresses) { + checkNotNull(addresses, "Argument 'addresses' can not be null"); + + // By theory, when we reach here, the status of the address should be in one of the three status: Unknown, Connected, UnhealthyPending + // using open connection to validate addresses in UnhealthyPending status + // Could extend to also open connection for unknown in the future + List addressesNeedToValidation = + Arrays + .stream(addresses) + .map(address -> address.getPhysicalUri()) + .filter(addressUri -> addressUri.getHealthStatus() == Uri.HealthStatus.UnhealthyPending) + .collect(Collectors.toList()); + + if (addressesNeedToValidation.size() > 0) { + this.openConnectionsHandler + .openConnections(addressesNeedToValidation) + .subscribeOn(CosmosSchedulers.OPEN_CONNECTIONS_BOUNDED_ELASTIC) + .subscribe(); + } + } + private Pair toPartitionAddressAndRange(String collectionRid, List

addresses) { if (logger.isDebugEnabled()) { logger.debug("toPartitionAddressAndRange"); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdConnectionStateListenerMetricsDiagnostics.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdConnectionStateListenerMetricsDiagnostics.java new file mode 100644 index 0000000000000..efba55b33a76a --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdConnectionStateListenerMetricsDiagnostics.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity; + +import com.azure.cosmos.implementation.apachecommons.lang.tuple.Pair; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; +import java.io.Serializable; +import java.time.Instant; + +@JsonSerialize(using = RntbdConnectionStateListenerMetricsDiagnostics.RntbdConnectionStateListenerDiagnosticsJsonSerializer.class) +public class RntbdConnectionStateListenerMetricsDiagnostics implements Serializable { + private static final long serialVersionUID = 1L; + private final Instant lastCallTimestamp; + private final Pair lastActionableContext; + + public RntbdConnectionStateListenerMetricsDiagnostics(Instant lastCallTimestamp, Pair lastActionableContext) { + this.lastCallTimestamp = lastCallTimestamp; + this.lastActionableContext = lastActionableContext; + } + + final static class RntbdConnectionStateListenerDiagnosticsJsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { + + public RntbdConnectionStateListenerDiagnosticsJsonSerializer() { + } + + @Override + public void serialize(RntbdConnectionStateListenerMetricsDiagnostics diagnostics, JsonGenerator writer, SerializerProvider serializers) throws IOException { + writer.writeStartObject(); + + if (diagnostics.lastCallTimestamp != null) { + writer.writeStringField( + "lastCallTimestamp", diagnostics.lastCallTimestamp.toString()); + } + + if (diagnostics.lastActionableContext != null) { + writer.writeStringField("lastActionableContext", diagnostics.lastActionableContext.toString()); + } + + writer.writeEndObject(); + } + } +} diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdTransportClient.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdTransportClient.java index 837d00ef7ec55..9cc4af6a894d7 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdTransportClient.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/RntbdTransportClient.java @@ -9,6 +9,7 @@ import com.azure.cosmos.implementation.ConnectionPolicy; import com.azure.cosmos.implementation.GlobalEndpointManager; import com.azure.cosmos.implementation.GoneException; +import com.azure.cosmos.implementation.OpenConnectionResponse; import com.azure.cosmos.implementation.RequestTimeline; import com.azure.cosmos.implementation.RxDocumentServiceRequest; import com.azure.cosmos.implementation.UserAgentContainer; @@ -19,7 +20,6 @@ import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdRequestRecord; import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdServiceEndpoint; import com.azure.cosmos.implementation.guava25.base.Strings; -import com.azure.cosmos.implementation.OpenConnectionResponse; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -228,7 +228,7 @@ public Mono invokeStoreAsync(final Uri addressUri, final RxDocume final URI address = addressUri.getURI(); - final RntbdRequestArgs requestArgs = new RntbdRequestArgs(request, address); + final RntbdRequestArgs requestArgs = new RntbdRequestArgs(request, addressUri); final RntbdEndpoint endpoint = this.endpointProvider.get(address); final RntbdRequestRecord record = endpoint.request(requestArgs); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreReader.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreReader.java index 93ae5c1d9426d..505fbe0c39870 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreReader.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreReader.java @@ -3,20 +3,21 @@ package com.azure.cosmos.implementation.directconnectivity; -import com.azure.cosmos.implementation.BadRequestException; import com.azure.cosmos.BridgeInternal; import com.azure.cosmos.CosmosException; +import com.azure.cosmos.implementation.BadRequestException; import com.azure.cosmos.implementation.GoneException; -import com.azure.cosmos.implementation.InternalServerErrorException; -import com.azure.cosmos.implementation.PartitionIsMigratingException; -import com.azure.cosmos.implementation.PartitionKeyRangeGoneException; -import com.azure.cosmos.implementation.PartitionKeyRangeIsSplittingException; import com.azure.cosmos.implementation.HttpConstants; import com.azure.cosmos.implementation.ISessionContainer; import com.azure.cosmos.implementation.ISessionToken; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; import com.azure.cosmos.implementation.Integers; +import com.azure.cosmos.implementation.InternalServerErrorException; import com.azure.cosmos.implementation.MutableVolatile; import com.azure.cosmos.implementation.OperationType; +import com.azure.cosmos.implementation.PartitionIsMigratingException; +import com.azure.cosmos.implementation.PartitionKeyRangeGoneException; +import com.azure.cosmos.implementation.PartitionKeyRangeIsSplittingException; import com.azure.cosmos.implementation.RMResources; import com.azure.cosmos.implementation.RxDocumentServiceRequest; import com.azure.cosmos.implementation.SessionTokenHelper; @@ -24,6 +25,7 @@ import com.azure.cosmos.implementation.Utils; import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; import com.azure.cosmos.implementation.apachecommons.lang.tuple.Pair; +import com.azure.cosmos.implementation.directconnectivity.addressEnumerator.AddressEnumerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Exceptions; @@ -32,10 +34,11 @@ import reactor.core.scheduler.Schedulers; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static com.azure.cosmos.implementation.Exceptions.isSubStatusCode; @@ -136,10 +139,12 @@ private Flux earlyResultIfNotEnoughReplicas(List replica } } - private Flux toStoreResult(RxDocumentServiceRequest request, - Pair, Uri> storeRespAndURI, - ReadMode readMode, - boolean requiresValidLsn) { + private Flux toStoreResult( + RxDocumentServiceRequest request, + Pair, Uri> storeRespAndURI, + ReadMode readMode, + boolean requiresValidLsn, + List replicaStatusList) { return storeRespAndURI.getLeft() .flatMap(storeResponse -> { @@ -147,9 +152,11 @@ private Flux toStoreResult(RxDocumentServiceRequest request, StoreResult storeResult = this.createAndRecordStoreResult( request, storeResponse, - null, requiresValidLsn, + null, + requiresValidLsn, readMode != ReadMode.Strong, - storeRespAndURI.getRight()); + storeRespAndURI.getRight(), + replicaStatusList); BridgeInternal.getContactedReplicas(request.requestContext.cosmosDiagnostics).add(storeRespAndURI.getRight().getURI()); return Flux.just(storeResult); @@ -165,15 +172,19 @@ private Flux toStoreResult(RxDocumentServiceRequest request, Exception storeException = Utils.as(unwrappedException, Exception.class); if (storeException == null) { return Flux.error(unwrappedException); + } else { + request.requestContext.addToFailedEndpoints(storeException, storeRespAndURI.getRight()); } // Exception storeException = readTask.Exception != null ? readTask.Exception.InnerException : null; StoreResult storeResult = this.createAndRecordStoreResult( request, null, - storeException, requiresValidLsn, + storeException, + requiresValidLsn, readMode != ReadMode.Strong, - storeRespAndURI.getRight()); + storeRespAndURI.getRight(), + replicaStatusList); if (storeException instanceof TransportException) { BridgeInternal.getFailedReplicas(request.requestContext.cosmosDiagnostics).add(storeRespAndURI.getRight().getURI()); } @@ -204,23 +215,32 @@ private Flux> readFromReplicas(List resultCollect return Flux.error(new GoneException()); } List, Uri>> readStoreTasks = new ArrayList<>(); - int uriIndex = StoreReader.generateNextRandom(resolveApiResults.size()); - while (resolveApiResults.size() > 0) { - uriIndex = uriIndex % resolveApiResults.size(); - Uri uri = resolveApiResults.get(uriIndex); + List addressRandomPermutation = AddressEnumerator.getTransportAddresses(entity, resolveApiResults); + + // The health status of the Uri will change as the time goes by + // what we really want to track is the health status snapshot at this moment + List replicaStatusList = + addressRandomPermutation + .stream() + .map(uri -> uri.getHealthStatusDiagnosticString()) + .collect(Collectors.toList()); + + int startIndex = 0; + + while(startIndex < addressRandomPermutation.size()) { + Uri addressUri = addressRandomPermutation.get(startIndex); Pair, Uri> res; try { - res = this.readFromStoreAsync(resolveApiResults.get(uriIndex), - entity); + res = this.readFromStoreAsync(addressUri, entity); } catch (Exception e) { - res = Pair.of(Mono.error(e), uri); + res = Pair.of(Mono.error(e), addressUri); } readStoreTasks.add(Pair.of(res.getLeft().flux(), res.getRight())); - resolveApiResults.remove(uriIndex); - + startIndex++; + resolveApiResults.remove(addressUri); if (!forceReadAll && readStoreTasks.size() == replicasToRead.get()) { break; @@ -232,7 +252,7 @@ private Flux> readFromReplicas(List resultCollect List> storeResult = readStoreTasks .stream() - .map(item -> toStoreResult(entity, item, readMode, requiresValidLsn)) + .map(item -> toStoreResult(entity, item, readMode, requiresValidLsn, replicaStatusList)) .collect(Collectors.toList()); Flux allStoreResults = Flux.merge(storeResult); @@ -505,6 +525,8 @@ private Mono readPrimaryInternalAsync( entity, entity.requestContext.forceRefreshAddressCache); + AtomicReference> replicaStatusList = new AtomicReference<>(); + Mono storeResultObs = primaryUriObs.flatMap( primaryUri -> { try { @@ -512,13 +534,13 @@ private Mono readPrimaryInternalAsync( SessionTokenHelper.setPartitionLocalSessionToken(entity, this.sessionContainer); } else { // Remove whatever session token can be there in headers. - // We don't need it. If it is global - backend will not undersand it. - // But there's no point in producing partition local sesison token. + // We don't need it. If it is global - backend will not understand it. + // But there's no point in producing partition local session token. entity.getHeaders().remove(HttpConstants.HttpHeaders.SESSION_TOKEN); } - Pair, Uri> storeResponseObsAndUri = this.readFromStoreAsync(primaryUri, entity); + replicaStatusList.set(Arrays.asList(primaryUri.getHealthStatusDiagnosticString())); return storeResponseObsAndUri.getLeft().flatMap( storeResponse -> { @@ -530,7 +552,8 @@ private Mono readPrimaryInternalAsync( null, requiresValidLsn, true, - storeResponse != null ? storeResponseObsAndUri.getRight() : null); + storeResponse != null ? storeResponseObsAndUri.getRight() : null, + replicaStatusList.get()); return Mono.just(storeResult); } catch (CosmosException e) { @@ -562,7 +585,8 @@ private Mono readPrimaryInternalAsync( storeTaskException, requiresValidLsn, true, - null); + null, + replicaStatusList.get()); return Mono.just(storeResult); } catch (CosmosException e) { // RxJava1 doesn't allow throwing checked exception from Observable operators @@ -662,9 +686,17 @@ StoreResult createAndRecordStoreResult( Exception responseException, boolean requiresValidLsn, boolean useLocalLSNBasedHeaders, - Uri storePhysicalAddress) { + Uri storePhysicalAddress, + List replicaStatusList) { - StoreResult storeResult = this.createStoreResult(storeResponse, responseException, requiresValidLsn, useLocalLSNBasedHeaders, storePhysicalAddress); + StoreResult storeResult = + this.createStoreResult( + storeResponse, + responseException, + requiresValidLsn, + useLocalLSNBasedHeaders, + storePhysicalAddress, + replicaStatusList); try { BridgeInternal.recordResponse(request.requestContext.cosmosDiagnostics, request, storeResult, transportClient.getGlobalEndpointManager()); @@ -686,7 +718,8 @@ StoreResult createStoreResult(StoreResponse storeResponse, Exception responseException, boolean requiresValidLsn, boolean useLocalLSNBasedHeaders, - Uri storePhysicalAddress) { + Uri storePhysicalAddress, + List replicaStatusList) { if (responseException == null) { String headerValue = null; @@ -697,6 +730,11 @@ StoreResult createStoreResult(StoreResponse storeResponse, int numberOfReadRegions = -1; Double backendLatencyInMs = null; long itemLSN = -1; + + if (replicaStatusList != null) { + storeResponse.getReplicaStatusList().addAll(replicaStatusList); + } + if ((headerValue = storeResponse.getHeaderValue( useLocalLSNBasedHeaders ? WFConstants.BackendHeaders.QUORUM_ACKED_LOCAL_LSN : WFConstants.BackendHeaders.QUORUM_ACKED_LSN)) != null) { quorumAckedLSN = Long.parseLong(headerValue); @@ -792,6 +830,14 @@ StoreResult createStoreResult(StoreResponse storeResponse, int numberOfReadRegions = -1; Double backendLatencyInMs = null; + if (replicaStatusList != null) { + ImplementationBridgeHelpers + .CosmosExceptionHelper + .getCosmosExceptionAccessor() + .getReplicaStatusList(cosmosException) + .addAll(replicaStatusList); + } + String headerValue = cosmosException.getResponseHeaders().get(useLocalLSNBasedHeaders ? WFConstants.BackendHeaders.QUORUM_ACKED_LOCAL_LSN : WFConstants.BackendHeaders.QUORUM_ACKED_LSN); if (!Strings.isNullOrEmpty(headerValue)) { quorumAckedLSN = Long.parseLong(headerValue); @@ -915,12 +961,6 @@ void startBackgroundAddressRefresh(RxDocumentServiceRequest request) { ); } - private static int generateNextRandom(int maxValue) { - // The benefit of using ThreadLocalRandom.current() over Random is - // avoiding the synchronization contention due to multi-threading. - return ThreadLocalRandom.current().nextInt(maxValue); - } - static void verifyCanContinueOnException(CosmosException ex) { if (ex instanceof PartitionKeyRangeGoneException) { throw ex; diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponse.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponse.java index 6448f1a6eca99..3041e16d0dd06 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponse.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponse.java @@ -11,7 +11,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Locale; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** @@ -33,6 +34,7 @@ public class StoreResponse { private RntbdEndpointStatistics rntbdEndpointStatistics; private int rntbdRequestLength; private int rntbdResponseLength; + private final List replicaStatusList; public StoreResponse( int status, @@ -55,6 +57,8 @@ public StoreResponse( if (this.content != null) { this.responsePayloadLength = this.content.length; } + + replicaStatusList = new ArrayList<>(); } public int getStatus() { @@ -200,4 +204,8 @@ int getSubStatusCode() { } return subStatusCode; } + + public List getReplicaStatusList() { + return this.replicaStatusList; + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponseDiagnostics.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponseDiagnostics.java index 0ecbe8aabce6f..bacd16c2d11b8 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponseDiagnostics.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResponseDiagnostics.java @@ -6,12 +6,15 @@ import com.azure.cosmos.BridgeInternal; import com.azure.cosmos.CosmosException; import com.azure.cosmos.implementation.HttpConstants; +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; import com.azure.cosmos.implementation.RequestTimeline; import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdChannelAcquisitionTimeline; import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdEndpointStatistics; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + /** * This represents diagnostics from store response OR from cosmos exception */ @@ -35,6 +38,7 @@ public class StoreResponseDiagnostics { private final int rntbdResponseLength; private final String exceptionMessage; private final String exceptionResponseHeaders; + private final List replicaStatusList; public static StoreResponseDiagnostics createStoreResponseDiagnostics(StoreResponse storeResponse) { return new StoreResponseDiagnostics(storeResponse); @@ -63,6 +67,7 @@ private StoreResponseDiagnostics(StoreResponse storeResponse) { this.rntbdResponseLength = storeResponse.getRntbdResponseLength(); this.exceptionMessage = null; this.exceptionResponseHeaders = null; + this.replicaStatusList = storeResponse.getReplicaStatusList(); } private StoreResponseDiagnostics(CosmosException e) { @@ -84,6 +89,7 @@ private StoreResponseDiagnostics(CosmosException e) { this.rntbdResponseLength = BridgeInternal.getRntbdResponseLength(e); this.exceptionMessage = BridgeInternal.getInnerErrorMessage(e); this.exceptionResponseHeaders = e.getResponseHeaders() != null ? e.getResponseHeaders().toString() : null; + this.replicaStatusList = ImplementationBridgeHelpers.CosmosExceptionHelper.getCosmosExceptionAccessor().getReplicaStatusList(e); } public int getStatusCode() { @@ -157,4 +163,6 @@ public String getCorrelatedActivityId() { public String getExceptionResponseHeaders() { return exceptionResponseHeaders; } + + public List getReplicaStatusList() { return this.replicaStatusList; } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResultDiagnostics.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResultDiagnostics.java index 53a1075ae772b..d66de5a9b7813 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResultDiagnostics.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/StoreResultDiagnostics.java @@ -169,6 +169,7 @@ public void serialize(StoreResultDiagnostics storeResultDiagnostics, jsonGenerator.writeObjectField("backendLatencyInMs", storeResultDiagnostics.backendLatencyInMs); this.writeNonNullStringField(jsonGenerator, "exceptionMessage", storeResponseDiagnostics.getExceptionMessage()); this.writeNonNullStringField(jsonGenerator, "exceptionResponseHeaders", storeResponseDiagnostics.getExceptionResponseHeaders()); + this.writeNonNullObjectField(jsonGenerator, "replicaStatusList", storeResponseDiagnostics.getReplicaStatusList()); jsonGenerator.writeObjectField("transportRequestTimeline", storeResponseDiagnostics.getRequestTimeline()); this.writeNonNullObjectField(jsonGenerator,"transportRequestChannelAcquisitionContext", storeResponseDiagnostics.getChannelAcquisitionTimeline()); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java index a204b5846d207..86813b94abc75 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java @@ -3,12 +3,25 @@ package com.azure.cosmos.implementation.directconnectivity; +import com.azure.cosmos.implementation.directconnectivity.addressEnumerator.AddressEnumerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.net.URI; +import java.time.Instant; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; public class Uri { + private static final Logger logger = LoggerFactory.getLogger(Uri.class); + + private static long DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS = 60 * 1000; private final String uriAsString; private final URI uri; + private final AtomicReference healthStatus; + private volatile Instant lastUnknownTimestamp; + private volatile Instant lastUnhealthyPendingTimestamp; + private volatile Instant lastUnhealthyTimestamp; public static Uri create(String uriAsString) { return new Uri(uriAsString); @@ -24,6 +37,10 @@ public Uri(String uri) { uriValue = null; } this.uri = uriValue; + this.healthStatus = new AtomicReference<>(HealthStatus.Unknown); + this.lastUnknownTimestamp = Instant.now(); + this.lastUnhealthyPendingTimestamp = null; + this.lastUnhealthyTimestamp = null; } public URI getURI() { @@ -34,13 +51,128 @@ public String getURIAsString() { return this.uriAsString; } + /*** + * This method will be called if a connection can be established successfully to the backend. + */ + public void setConnected() { + this.setHealthStatus(HealthStatus.Connected); + } + + /*** + * This method will be called if a request failed with 410 and will cause forceRefresh behavior. + */ + public void setUnhealthy() { + this.setHealthStatus(HealthStatus.Unhealthy); + } + + /*** + * This method will be called if the same address being returned from gateway. + * + * Unknown will remain Unknown. + * Connected will remain Connected. + * UnhealthyPending will remain UnhealthyPending. + * Unhealthy will change into UnhealthyPending. + */ + public void setRefreshed() { + if (this.healthStatus.get() == HealthStatus.Unhealthy) { + this.setHealthStatus(HealthStatus.UnhealthyPending); + } + } + + /*** + * Please reference the /docs/replicaValidation/ReplicaClientSideStatus.png for valid status transition. + * + * @param status the health status. + */ + public void setHealthStatus(HealthStatus status) { + this.healthStatus.updateAndGet(previousStatus -> { + + HealthStatus newStatus = previousStatus; + switch (status) { + case Unhealthy: + this.lastUnhealthyTimestamp = Instant.now(); + newStatus = status; + break; + + case UnhealthyPending: + if (previousStatus == HealthStatus.Unhealthy || previousStatus == HealthStatus.UnhealthyPending) { + this.lastUnhealthyPendingTimestamp = Instant.now(); + newStatus = status; + } + break; + case Connected: + if (previousStatus != HealthStatus.Unhealthy + || (previousStatus == HealthStatus.Unhealthy && + Instant.now().compareTo(this.lastUnhealthyTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0)) { + newStatus = status; + } + break; + case Unknown: + // there is no reason we are going to reach here + throw new IllegalStateException("It is impossible to set to unknown status"); + default: + throw new IllegalStateException("Unsupported health status: " + status); + } + + if (logger.isDebugEnabled()) { + logger.debug( + "Called setHealthStatus with status [{}]. Result: previousStatus [{}], newStatus [{}]", + status, previousStatus, newStatus); + } + + return newStatus; + }); + } + + public HealthStatus getHealthStatus() { + return this.healthStatus.get(); + } + + /*** + * In {@link AddressEnumerator}, it could de-prioritize uri in unhealthyPending/unhealthy health status (depending on whether replica validation is enabled) + * If the replica stuck in those statuses for too long, in order to avoid replica usage skew, + * we are going to rolling them into healthy category, so it is status can be validated by requests again + * + * @return + */ + public HealthStatus getEffectiveHealthStatus() { + HealthStatus snapshot = this.healthStatus.get(); + switch (snapshot) { + case Connected: + case Unhealthy: + return snapshot; + case Unknown: + if (Instant.now() + .compareTo(this.lastUnknownTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0) { + return HealthStatus.Connected; + } + return snapshot; + case UnhealthyPending: + if (Instant.now() + .compareTo(this.lastUnhealthyPendingTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0) { + return HealthStatus.Connected; + } + return snapshot; + default: + throw new IllegalStateException("Unknown status " + snapshot); + } + } + + public boolean shouldRefreshHealthStatus() { + return this.healthStatus.get() == HealthStatus.Unhealthy + && Instant.now().compareTo(this.lastUnhealthyTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0; + } + + public String getHealthStatusDiagnosticString() { + return this.uri.getPort() + ":" + this.healthStatus.get().toString(); + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Uri uri1 = (Uri) o; - return uriAsString.equals(uri1.uriAsString) && - uri.equals(uri1.uri); + return uriAsString.equals(uri1.uriAsString) && uri.equals(uri1.uri); } @Override @@ -52,4 +184,29 @@ public int hashCode() { public String toString() { return this.uriAsString; } + + + /*** + *

+ * NOTE: Please DO NOT change the priority of the enums, + * as it is used in {@link AddressEnumerator} for correct sorting purpose + * + * if you are going to change the priority of this, please reexamine the sorting logic as well. + *

+ */ + public enum HealthStatus { + Connected(0), + Unknown(1), + UnhealthyPending(2), + Unhealthy(3); + + private int priority; + HealthStatus(int priority) { + this.priority = priority; + } + + public int getPriority() { + return this.priority; + } + } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumerator.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumerator.java new file mode 100644 index 0000000000000..521b50072007a --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumerator.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity.addressEnumerator; + +import com.azure.cosmos.implementation.RxDocumentServiceRequest; +import com.azure.cosmos.implementation.directconnectivity.Uri; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Unhealthy; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.UnhealthyPending; +import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull; + +public class AddressEnumerator { + + public static List getTransportAddresses(RxDocumentServiceRequest request, List addresses) { + checkNotNull(addresses, "Argument 'addresses' should not be null"); + checkNotNull(request, "Argument 'request' should not be null"); + + List randomPermutation = getAddressesPermutationInternal(addresses); + + return sortAddresses(randomPermutation, request); + } + + private static List sortAddresses(List addressesPermutation, RxDocumentServiceRequest request) { + if (!request.requestContext.replicaAddressValidationEnabled) { + // When replica address validation is disabled, we will rely on RxDocumentServiceRequest to transition away unknown/unhealthyPending + // so we prefer connected/unknown/unhealthyPending to unhealthy + addressesPermutation.sort(new Comparator() { + @Override + public int compare(Uri o1, Uri o2) { + Uri.HealthStatus o1Status = getEffectiveStatus(o1, request.requestContext.getFailedEndpoints()); + Uri.HealthStatus o2Status = getEffectiveStatus(o2, request.requestContext.getFailedEndpoints()); + + if (o1Status != Unhealthy && o2Status != Unhealthy) { + return 0; + } + if (o1Status == Unhealthy) { + return 1; + } + + return -1; + } + }); + } else { + + // When replica address validation is enabled, we will prefer connected/unknown > unhealthyPending > unhealthy. + // We will prefer to use open connection request to transit away unknown/unhealthyPending status, + // but in case open connection request can not happen due to any reason, + // then after some extended time, we are going to rolling unknown/unhealthyPending into Healthy category (please check details of getEffectiveHealthStatus) + addressesPermutation.sort(new Comparator() { + @Override + public int compare(Uri o1, Uri o2) { + Uri.HealthStatus o1Status = getEffectiveStatus(o1, request.requestContext.getFailedEndpoints()); + Uri.HealthStatus o2Status = getEffectiveStatus(o2, request.requestContext.getFailedEndpoints()); + + if (o1Status == o2Status) { + return 0; + } + + if (o1Status.getPriority() < UnhealthyPending.getPriority() + && o2Status.getPriority() < UnhealthyPending.getPriority()) { + return 0; + } + + return o1Status.getPriority() - o2Status.getPriority(); + } + }); + } + + return addressesPermutation; + } + + private static List getAddressesPermutationInternal(List addresses) { + checkNotNull(addresses, "Argument 'addresses' should not be null"); + + // Permutation is faster and has less over head compared to Fisher-Yates shuffle + // Permutation is optimized for most common scenario where replica count is 5 or less + // Fisher-Yates shuffle is used in-case the passed in URI list is larger than the predefined permutation list. + if (AddressEnumeratorUsingPermutations.isSizeInPermutationLimits(addresses.size())) { + return AddressEnumeratorUsingPermutations.getTransportAddressUris(addresses); + } + return AddressEnumeratorFisherYateShuffle.getTransportAddressUris(addresses); + } + + private static Uri.HealthStatus getEffectiveStatus(Uri addressUri, Set failedEndpoints) { + checkNotNull(addressUri, "Argument 'addressUri' should not be null"); + + if (failedEndpoints != null && failedEndpoints.contains(addressUri)) { + return Unhealthy; + } + + return addressUri.getEffectiveHealthStatus(); + } +} diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorFisherYateShuffle.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorFisherYateShuffle.java new file mode 100644 index 0000000000000..a12972d8db795 --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorFisherYateShuffle.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity.addressEnumerator; + +import com.azure.cosmos.implementation.directconnectivity.Uri; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull; +import static java.util.Collections.swap; + +public class AddressEnumeratorFisherYateShuffle { + public static List getTransportAddressUris(List addresses) { + checkNotNull(addresses, "Argument 'addresses' should not be null"); + + // Fisher Yates Shuffle algorithm: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + List addressesCopy = new ArrayList<>(addresses); + + for (int i = addressesCopy.size(); i > 0; i--) { + int randomIndex = generateNextRandom(i); + swap(addressesCopy, i - 1, randomIndex); + } + + return addressesCopy; + } + + private static int generateNextRandom(int maxValue) { + // The benefit of using ThreadLocalRandom.current() over Random is + // avoiding the synchronization contention due to multi-threading. + return ThreadLocalRandom.current().nextInt(maxValue); + } +} diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorUsingPermutations.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorUsingPermutations.java new file mode 100644 index 0000000000000..55cab79cf5716 --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/addressEnumerator/AddressEnumeratorUsingPermutations.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity.addressEnumerator; + +import com.azure.cosmos.implementation.directconnectivity.Uri; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull; + +public class AddressEnumeratorUsingPermutations { + + private static AtomicReference>>> allPermutations = new AtomicReference<>(null); + + static { + if (allPermutations.compareAndSet(null, new ArrayList<>())) { + for (int i = 0; i <= 6; i++) { + List> permutations = new ArrayList<>(); + permuteIndexPositions(IntStream.range(0, i).toArray(), 0, i, permutations); + allPermutations.get().add(permutations); + } + } + } + + public static List getTransportAddressUris(List addresses) + { + checkNotNull(addresses, "Argument 'addresses' should not be null"); + + List> allPermutationsForSpecificSize = allPermutations.get().get(addresses.size()); + int permutation = generateNextRandom(allPermutationsForSpecificSize.size()); + + List addressList = new ArrayList<>(); + for (int index : allPermutationsForSpecificSize.get(permutation)) { + addressList.add(addresses.get(index)); + } + + return addressList; + } + + public static boolean isSizeInPermutationLimits(int size) { + return size < allPermutations.get().size(); + } + + private static void permuteIndexPositions(int[] array, int start, int length, List> output) + { + if (start == length) + { + output.add(Arrays.stream(array).boxed().collect(Collectors.toList())); + } + else + { + for (int j = start; j < length; j++) + { + swap(array, start, j); // pick the item to be put at the start + permuteIndexPositions(array, start + 1, length, output); + swap(array, start, j); // switch back the change made in previous swap + } + } + } + + private static void swap(int[] array, int leftIndex, int rightIndex) + { + int tmp = array[leftIndex]; + + array[leftIndex] = array[rightIndex]; + array[rightIndex] = tmp; + } + + private static int generateNextRandom(int maxValue) { + // The benefit of using ThreadLocalRandom.current() over Random is + // avoiding the synchronization contention due to multi-threading. + return ThreadLocalRandom.current().nextInt(maxValue); + } +} diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListenerMetrics.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListenerMetrics.java index 3b4a35376edf7..c4f02f60a686b 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListenerMetrics.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListenerMetrics.java @@ -4,22 +4,11 @@ package com.azure.cosmos.implementation.directconnectivity.rntbd; import com.azure.cosmos.implementation.apachecommons.lang.tuple.Pair; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.Serializable; + import java.time.Instant; import java.util.concurrent.atomic.AtomicReference; -@JsonSerialize(using = RntbdConnectionStateListenerMetrics.RntbdConnectionStateListenerMetricsJsonSerializer.class) -public final class RntbdConnectionStateListenerMetrics implements Serializable { - private static final long serialVersionUID = 1L; - private static final Logger logger = LoggerFactory.getLogger(RntbdConnectionStateListenerMetrics.class); - +public final class RntbdConnectionStateListenerMetrics { private final AtomicReference lastCallTimestamp; private final AtomicReference> lastActionableContext; @@ -37,25 +26,11 @@ public void record() { this.lastCallTimestamp.set(Instant.now()); } - final static class RntbdConnectionStateListenerMetricsJsonSerializer extends com.fasterxml.jackson.databind.JsonSerializer { - - public RntbdConnectionStateListenerMetricsJsonSerializer() { - } - - @Override - public void serialize(RntbdConnectionStateListenerMetrics metrics, JsonGenerator writer, SerializerProvider serializers) throws IOException { - writer.writeStartObject(); - - if (metrics.lastCallTimestamp.get() != null) { - writer.writeStringField( - "lastCallTimestamp", metrics.lastCallTimestamp.toString()); - } - - if (metrics.lastActionableContext.get() != null) { - writer.writeStringField("lastActionableContext", metrics.lastActionableContext.get().toString()); - } + public Instant getLastCallTimestamp() { + return this.lastCallTimestamp.get(); + } - writer.writeEndObject(); - } + public Pair getLastActionableContext() { + return this.lastActionableContext.get(); } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdEndpointStatistics.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdEndpointStatistics.java index 2162b6b3a1220..c7f8314e6ba2d 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdEndpointStatistics.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdEndpointStatistics.java @@ -4,6 +4,7 @@ package com.azure.cosmos.implementation.directconnectivity.rntbd; import com.azure.cosmos.implementation.DiagnosticsInstantSerializer; +import com.azure.cosmos.implementation.directconnectivity.RntbdConnectionStateListenerMetricsDiagnostics; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -57,7 +58,7 @@ RntbdEndpointStatistics closed(boolean closed) { } RntbdEndpointStatistics connectionStateListenerMetrics(RntbdConnectionStateListenerMetrics metrics) { - this.connectionStateListenerMetrics = metrics; + this.connectionStateListenerMetrics = new RntbdConnectionStateListenerMetricsDiagnostics(metrics.getLastCallTimestamp(), metrics.getLastActionableContext()); return this; } @@ -69,7 +70,7 @@ RntbdEndpointStatistics connectionStateListenerMetrics(RntbdConnectionStateListe private long lastSuccessfulRequestNanoTime; private long lastRequestNanoTime; private Instant createdTime; - private RntbdConnectionStateListenerMetrics connectionStateListenerMetrics; + private RntbdConnectionStateListenerMetricsDiagnostics connectionStateListenerMetrics; private final static Instant referenceInstant = Instant.now(); private final static long referenceNanoTime = System.nanoTime(); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestArgs.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestArgs.java index 88651a8faf24e..523b4e6e4a786 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestArgs.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestArgs.java @@ -5,6 +5,7 @@ import com.azure.cosmos.implementation.RxDocumentServiceRequest; import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; +import com.azure.cosmos.implementation.directconnectivity.Uri; import com.azure.cosmos.implementation.guava25.base.Stopwatch; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -13,7 +14,6 @@ import io.netty.channel.ChannelHandlerContext; import org.slf4j.Logger; -import java.net.URI; import java.time.Duration; import java.time.Instant; import java.util.UUID; @@ -38,20 +38,20 @@ public final class RntbdRequestArgs { private final long nanoTimeCreated; private final Stopwatch lifetime; private final String origin; - private final URI physicalAddress; + private final Uri physicalAddressUri; private final String replicaPath; private final RxDocumentServiceRequest serviceRequest; private final long transportRequestId; - public RntbdRequestArgs(final RxDocumentServiceRequest serviceRequest, final URI physicalAddress) { + public RntbdRequestArgs(final RxDocumentServiceRequest serviceRequest, final Uri physicalAddressUri) { this.sample = Timer.start(); this.activityId = serviceRequest.getActivityId(); this.timeCreated = Instant.now(); this.nanoTimeCreated = System.nanoTime(); this.lifetime = Stopwatch.createStarted(); - this.origin = physicalAddress.getScheme() + "://" + physicalAddress.getAuthority(); - this.physicalAddress = physicalAddress; - this.replicaPath = StringUtils.stripEnd(physicalAddress.getPath(), "/"); + this.origin = physicalAddressUri.getURI().getScheme() + "://" + physicalAddressUri.getURI().getAuthority(); + this.physicalAddressUri = physicalAddressUri; + this.replicaPath = StringUtils.stripEnd(physicalAddressUri.getURI().getPath(), "/"); this.serviceRequest = serviceRequest; this.transportRequestId = instanceCount.incrementAndGet(); } @@ -79,8 +79,8 @@ public String origin() { } @JsonIgnore - public URI physicalAddress() { - return this.physicalAddress; + public Uri physicalAddressUri() { + return this.physicalAddressUri; } @JsonProperty diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestManager.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestManager.java index 97b774645cac9..e2c6ff2d1585a 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestManager.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestManager.java @@ -768,7 +768,7 @@ private void completeAllPendingRequestsExceptionally( for (RntbdRequestRecord record : this.pendingRequests.values()) { final Map requestHeaders = record.args().serviceRequest().getHeaders(); - final String requestUri = record.args().physicalAddress().toString(); + final String requestUri = record.args().physicalAddressUri().getURI().toString(); final GoneException error = new GoneException(message, cause, null, requestUri); BridgeInternal.setRequestHeaders(error, requestHeaders); @@ -843,8 +843,8 @@ private void messageReceived(final ChannelHandlerContext context, final RntbdRes // ..Create CosmosException based on status and sub-status codes - final String resourceAddress = requestRecord.args().physicalAddress() != null ? - requestRecord.args().physicalAddress().toString() : null; + final String resourceAddress = requestRecord.args().physicalAddressUri() != null ? + requestRecord.args().physicalAddressUri().getURI().toString() : null; switch (status.code()) { diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecord.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecord.java index 6d66271a02df0..5847c8f4dfe4f 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecord.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecord.java @@ -253,10 +253,10 @@ public boolean expire() { // Convert from requestTimeoutException to GoneException for the following two scenarios so they can be safely retried: // 1. RequestOnly request // 2. Write request but not sent yet - error = new GoneException(this.toString(), null, this.args.physicalAddress()); + error = new GoneException(this.toString(), null, this.args.physicalAddressUri().getURI()); } else { // For sent write request, converting to requestTimeout, will not be retried. - error = new RequestTimeoutException(this.toString(), this.args.physicalAddress()); + error = new RequestTimeoutException(this.toString(), this.args.physicalAddressUri().getURI()); } BridgeInternal.setRequestHeaders(error, this.args.serviceRequest().getHeaders()); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java index 476657e4f4b11..432dcfb7e531c 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java @@ -333,6 +333,8 @@ private OpenConnectionRntbdRequestRecord processWhenConnectionOpened( // Releasing the channel back to the pool so other requests can use it this.releaseToPool(channel); + requestRecord.getAddressUri().setConnected(); + openConnectionResponse = new OpenConnectionResponse(requestRecord.getAddressUri(), true); } else { openConnectionResponse = new OpenConnectionResponse(requestRecord.getAddressUri(), false, openChannelFuture.cause()); @@ -444,6 +446,9 @@ private RntbdRequestRecord writeWhenConnected( this.releaseToPool(channel); requestRecord.channelTaskQueueLength(RntbdUtils.tryGetExecutorTaskQueueSize(channel.eventLoop())); channel.write(requestRecord.stage(RntbdRequestRecord.Stage.PIPELINED)); + + // mark address connected + requestRecord.args().physicalAddressUri().setConnected(); return requestRecord; } diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java index 261ab366bec59..221d3e7c2e5e5 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosDiagnosticsTest.java @@ -793,6 +793,17 @@ public void directDiagnosticsOnException() throws Exception { isValidJSON(diagnostics); validateTransportRequestTimelineDirect(diagnostics); validateRegionContacted(createResponse.getDiagnostics(), client.asyncClient()); + + // TODO: add better store result diagnostic validation on exception + ObjectNode diagnosticsNode = (ObjectNode) OBJECT_MAPPER.readTree(diagnostics); + JsonNode responseStatisticsList = diagnosticsNode.get("responseStatisticsList"); + assertThat(responseStatisticsList.isArray()).isTrue(); + assertThat(responseStatisticsList.size()).isGreaterThan(0); + JsonNode storeResult = responseStatisticsList.get(0).get("storeResult"); + assertThat(storeResult).isNotNull(); + JsonNode replicaStatusList = storeResult.get("replicaStatusList"); + assertThat(replicaStatusList.isArray()).isTrue(); + assertThat(replicaStatusList.size()).isGreaterThan(0); } finally { if (client != null) { client.close(); @@ -1042,6 +1053,9 @@ private void validateRntbdStatistics(CosmosDiagnostics cosmosDiagnostics, assertThat(storeResult.get("channelTaskQueueSize").asInt(-1)).isGreaterThanOrEqualTo(0); assertThat(storeResult.get("pendingRequestsCount").asInt(-1)).isGreaterThanOrEqualTo(0); + JsonNode replicaStatusList = storeResult.get("replicaStatusList"); + assertThat(replicaStatusList.isArray()).isTrue(); + assertThat(replicaStatusList.size()).isGreaterThan(0); JsonNode serviceEndpointStatistics = storeResult.get("serviceEndpointStatistics"); assertThat(serviceEndpointStatistics).isNotNull(); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java index 1681beed671df..65148bafcb8c9 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java @@ -31,7 +31,7 @@ public void bareMinimum() throws Exception { DiagnosticsClientContext clientContext = Mockito.mock(DiagnosticsClientContext.class); DiagnosticsClientContext.DiagnosticsClientConfig diagnosticsClientConfig = new DiagnosticsClientContext.DiagnosticsClientConfig(); - String machineId = "vmId:" + UUID.randomUUID().toString(); + String machineId = "vmId:" + UUID.randomUUID(); diagnosticsClientConfig.withMachineId(machineId); diagnosticsClientConfig.withClientId(1); diagnosticsClientConfig.withConnectionMode(ConnectionMode.DIRECT); @@ -53,7 +53,7 @@ public void bareMinimum() throws Exception { assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); assertThat(objectNode.get("connCfg").get("gw").asText()).isEqualTo("null"); - assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: false, cs: false)"); + assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: false, cs: false, rv: false)"); } @@ -62,7 +62,7 @@ public void rntbd() throws Exception { DiagnosticsClientContext clientContext = Mockito.mock(DiagnosticsClientContext.class); DiagnosticsClientContext.DiagnosticsClientConfig diagnosticsClientConfig = new DiagnosticsClientContext.DiagnosticsClientConfig(); - String machineId = "vmId:" + UUID.randomUUID().toString(); + String machineId = "vmId:" + UUID.randomUUID(); diagnosticsClientConfig.withMachineId(machineId); diagnosticsClientConfig.withClientId(1); diagnosticsClientConfig.withConnectionMode(ConnectionMode.DIRECT); @@ -86,8 +86,7 @@ public void rntbd() throws Exception { assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("(cto:PT5S, nrto:PT5S, icto:PT0S, ieto:PT1H, mcpe:130, mrpc:30, cer:true)"); assertThat(objectNode.get("connCfg").get("gw").asText()).isEqualTo("(cps:null, nrto:null, icto:null, p:false)"); - assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: false, cs: false)"); - + assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: false, cs: false, rv: false)"); } @Test(groups = { "unit" }) @@ -95,7 +94,7 @@ public void gw() throws Exception { DiagnosticsClientContext clientContext = Mockito.mock(DiagnosticsClientContext.class); DiagnosticsClientContext.DiagnosticsClientConfig diagnosticsClientConfig = new DiagnosticsClientContext.DiagnosticsClientConfig(); - String machineId = "vmId:" + UUID.randomUUID().toString(); + String machineId = "vmId:" + UUID.randomUUID(); diagnosticsClientConfig.withMachineId(machineId); diagnosticsClientConfig.withClientId(1); diagnosticsClientConfig.withConnectionMode(ConnectionMode.DIRECT); @@ -122,15 +121,16 @@ public void gw() throws Exception { assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); assertThat(objectNode.get("connCfg").get("gw").asText()).isEqualTo("(cps:500, nrto:PT18S, icto:PT17S, p:false)"); - assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: false, cs: false)"); + assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: false, cs: false, rv: false)"); } @Test(groups = { "unit" }) public void full() throws Exception { DiagnosticsClientContext clientContext = Mockito.mock(DiagnosticsClientContext.class); + System.setProperty("COSMOS.REPLICA_ADDRESS_VALIDATION_ENABLED", "true"); DiagnosticsClientContext.DiagnosticsClientConfig diagnosticsClientConfig = new DiagnosticsClientContext.DiagnosticsClientConfig(); - String machineId = "vmId:" + UUID.randomUUID().toString(); + String machineId = "vmId:" + UUID.randomUUID(); diagnosticsClientConfig.withMachineId(machineId); diagnosticsClientConfig.withClientId(1); diagnosticsClientConfig.withConnectionMode(ConnectionMode.DIRECT); @@ -160,6 +160,8 @@ public void full() throws Exception { assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, mm: false, prgns: [westus1,westus2])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); assertThat(objectNode.get("connCfg").get("gw").asText()).isEqualTo("(cps:500, nrto:PT18S, icto:PT17S, p:false)"); - assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: true, cs: true)"); + assertThat(objectNode.get("connCfg").get("other").asText()).isEqualTo("(ed: true, cs: true, rv: true)"); + + System.clearProperty("COSMOS.REPLICA_ADDRESS_VALIDATION_ENABLED"); } } diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/DocumentServiceRequestContextTests.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/DocumentServiceRequestContextTests.java new file mode 100644 index 0000000000000..409414815ed75 --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/DocumentServiceRequestContextTests.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation; + +import com.azure.cosmos.implementation.directconnectivity.Uri; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class DocumentServiceRequestContextTests { + + @DataProvider(name = "exceptionArgProvider") + private Object[][] exceptionArgProvider() { + return new Object[][]{ + { new PartitionKeyRangeGoneException(), true }, + { new PartitionKeyRangeIsSplittingException(), true }, + { new PartitionKeyRangeGoneException(), true }, + { new PartitionIsMigratingException(), true }, + { new GoneException(), true }, + { new NotFoundException(), false }, + { new ServiceUnavailableException(), false }, + { new RequestRateTooLargeException(), false }, + { new BadRequestException(), false }, + { new ConflictException(), false }, + { new ForbiddenException(), false }, + { new LockedException(), false }, + { new NotFoundException(), false }, + { new PreconditionFailedException(), false }, + { new RequestTimeoutException(), false }, + { new UnauthorizedException(), false }, + { new IllegalStateException(), false } + }; + } + + @Test(groups = "unit", dataProvider = "exceptionArgProvider") + public void addFailedEndpointsTests(Exception exception, boolean shouldAdded) { + DocumentServiceRequestContext documentServiceRequestContext = new DocumentServiceRequestContext(); + + Uri testUri = new Uri("http://127.0.0.1:1"); + documentServiceRequestContext.addToFailedEndpoints(exception, testUri); + + if (shouldAdded) { + documentServiceRequestContext.getFailedEndpoints().contains(testUri); + } else { + documentServiceRequestContext.getFailedEndpoints().isEmpty(); + } + } +} diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressEnumeratorTests.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressEnumeratorTests.java new file mode 100644 index 0000000000000..1205ac845f355 --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/AddressEnumeratorTests.java @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity; + +import com.azure.cosmos.implementation.DocumentServiceRequestContext; +import com.azure.cosmos.implementation.GoneException; +import com.azure.cosmos.implementation.RxDocumentServiceRequest; +import com.azure.cosmos.implementation.directconnectivity.addressEnumerator.AddressEnumerator; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Connected; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Unhealthy; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.UnhealthyPending; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Unknown; +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("unchecked") +public class AddressEnumeratorTests { + + @Test(groups = "unit") + public void replicaAddressValidationEnabledComparatorTests() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InvocationTargetException { + Method sortAddressesMethod = AddressEnumerator.class.getDeclaredMethod("sortAddresses", List.class, RxDocumentServiceRequest.class); + sortAddressesMethod.setAccessible(true); + + // set a different health status to each endpoint to test the sorting logic + Uri testUri1 = new Uri("https://127.0.0.1:1"); + assertThat(testUri1.getHealthStatus()).isEqualTo(Unknown); + + Uri testUri2 = new Uri("https://127.0.0.1:2"); + testUri2.setConnected(); + assertThat(testUri2.getHealthStatus()).isEqualTo(Connected); + + Uri testUri3 = new Uri("https://127.0.0.1:3"); + testUri3.setUnhealthy(); + testUri3.setRefreshed(); + assertThat(testUri3.getHealthStatus()).isEqualTo(UnhealthyPending); + + Uri testUri4 = new Uri("https://127.0.0.1:4"); + testUri4.setUnhealthy(); + assertThat(testUri4.getHealthStatus()).isEqualTo(Unhealthy); + + RxDocumentServiceRequest requestMock = Mockito.mock(RxDocumentServiceRequest.class); + requestMock.requestContext = new DocumentServiceRequestContext(); + requestMock.requestContext.replicaAddressValidationEnabled = true; + + // when replicaAddressValidation is enabled, we prefer Connected/Unknown > UnhealthyPending > Unhealthy + List testScenarios = Arrays.asList( + new SortAddressesTestScenario(Arrays.asList(testUri1, testUri2), Arrays.asList(testUri1, testUri2)), // unknown, connected -> unknown, connected + new SortAddressesTestScenario(Arrays.asList(testUri2, testUri1), Arrays.asList(testUri2, testUri1)), // connected, unknown -> connected, unknown + new SortAddressesTestScenario(Arrays.asList(testUri3, testUri2), Arrays.asList(testUri2, testUri3)), // unhealthyPending, connected -> connected, unhealthyPending + new SortAddressesTestScenario(Arrays.asList(testUri4, testUri1), Arrays.asList(testUri1, testUri4)), // unhealthy, unknown -> unknown, unhealthy + new SortAddressesTestScenario(Arrays.asList(testUri4, testUri3), Arrays.asList(testUri3, testUri4))); // unhealthy, unhealthyPending -> unhealthyPending, unhealthy + + for (SortAddressesTestScenario testScenario : testScenarios) { + System.out.println("Test scenario, comparing " + testScenario.getAddresses()); + for (Uri uri : testScenario.getAddresses()) { + this.setTimestamp(uri, Instant.now()); + } + List sortedAddresses = + (List) sortAddressesMethod.invoke(null, testScenario.getAddresses(), requestMock); + assertThat(sortedAddresses).containsExactlyElementsOf(testScenario.expectedAddresses); + } + + System.out.println("Test scenario: comparing when unhealthyPending roll into healthy status after 1 min"); + setTimestamp(testUri3, Instant.now().minusMillis(Duration.ofMinutes(2).toMillis())); + List sortedAddresses = (List) sortAddressesMethod.invoke(null, Arrays.asList(testUri3, testUri2), requestMock); + assertThat(sortedAddresses).containsExactlyElementsOf(Arrays.asList(testUri3, testUri2)); + + System.out.println("Test scenario: comparing when there is failedEndpoints marked in request context"); + requestMock.requestContext.addToFailedEndpoints(new GoneException("Test"), testUri2); + sortedAddresses = (List) sortAddressesMethod.invoke(null, Arrays.asList(testUri4, testUri2), requestMock); + assertThat(sortedAddresses).containsExactlyElementsOf(Arrays.asList(testUri4, testUri2)); + } + + @Test(groups = "unit") + public void replicaAddressValidationDisabledComparatorTests() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InvocationTargetException { + Method sortAddressesMethod = AddressEnumerator.class.getDeclaredMethod("sortAddresses", List.class, RxDocumentServiceRequest.class); + sortAddressesMethod.setAccessible(true); + + // set a different health status to each endpoint to test the sorting logic + Uri testUri1 = new Uri("https://127.0.0.1:1"); + assertThat(testUri1.getHealthStatus()).isEqualTo(Unknown); + + Uri testUri2 = new Uri("https://127.0.0.1:2"); + testUri2.setConnected(); + assertThat(testUri2.getHealthStatus()).isEqualTo(Connected); + + Uri testUri3 = new Uri("https://127.0.0.1:3"); + testUri3.setUnhealthy(); + testUri3.setRefreshed(); + assertThat(testUri3.getHealthStatus()).isEqualTo(UnhealthyPending); + + Uri testUri4 = new Uri("https://127.0.0.1:4"); + testUri4.setUnhealthy(); + assertThat(testUri4.getHealthStatus()).isEqualTo(Unhealthy); + + RxDocumentServiceRequest requestMock = Mockito.mock(RxDocumentServiceRequest.class); + requestMock.requestContext = new DocumentServiceRequestContext(); + + // when replicaAddressValidation is enabled, we prefer Connected/Unknown/UnhealthyPending > Unhealthy + List testScenarios = Arrays.asList( + new SortAddressesTestScenario(Arrays.asList(testUri1, testUri2), Arrays.asList(testUri1, testUri2)), // unknown, connected -> unknown, connected + new SortAddressesTestScenario(Arrays.asList(testUri2, testUri1), Arrays.asList(testUri2, testUri1)), // connected, unknown -> connected, unknown + new SortAddressesTestScenario(Arrays.asList(testUri3, testUri2), Arrays.asList(testUri3, testUri2)), // unhealthyPending, connected -> unhealthyPending, connected + new SortAddressesTestScenario(Arrays.asList(testUri4, testUri1), Arrays.asList(testUri1, testUri4)), // unhealthy, unknown -> unknown, unhealthy + new SortAddressesTestScenario(Arrays.asList(testUri4, testUri3), Arrays.asList(testUri3, testUri4))); // unhealthy, unhealthyPending -> unhealthyPending, unhealthy + + for (SortAddressesTestScenario testScenario : testScenarios) { + System.out.println("Test scenario, comparing " + testScenario.getAddresses()); + for (Uri uri : testScenario.getAddresses()) { + this.setTimestamp(uri, Instant.now()); + } + List sortedAddresses = + (List) sortAddressesMethod.invoke(null, testScenario.getAddresses(), requestMock); + assertThat(sortedAddresses).containsExactlyElementsOf(testScenario.expectedAddresses); + } + + System.out.println("Test scenario: comparing when there is failedEndpoints marked in request context"); + requestMock.requestContext.addToFailedEndpoints(new GoneException("Test"), testUri2); + List sortedAddresses = (List) sortAddressesMethod.invoke(null, Arrays.asList(testUri4, testUri2), requestMock); + assertThat(sortedAddresses).containsExactlyElementsOf(Arrays.asList(testUri4, testUri2)); + } + + private void setTimestamp(Uri testUri, Instant time) throws NoSuchFieldException, IllegalAccessException { + switch (testUri.getHealthStatus()) { + case Unknown: + Field lastUnknownTimestampField = Uri.class.getDeclaredField("lastUnknownTimestamp"); + lastUnknownTimestampField.setAccessible(true); + lastUnknownTimestampField.set(testUri, time); + break; + case Unhealthy: + Field lastUnhealthyTimestampField = Uri.class.getDeclaredField("lastUnhealthyTimestamp"); + lastUnhealthyTimestampField.setAccessible(true); + lastUnhealthyTimestampField.set(testUri, time); + break; + case UnhealthyPending: + Field lastUnhealthyPendingTimestampField = Uri.class.getDeclaredField("lastUnhealthyPendingTimestamp"); + lastUnhealthyPendingTimestampField.setAccessible(true); + lastUnhealthyPendingTimestampField.set(testUri, time); + break; + case Connected: + break; + default: + throw new IllegalStateException("Unknown status " + testUri.getHealthStatus()); + } + } + + private static class SortAddressesTestScenario { + private final List addresses; + private final List expectedAddresses; + + public SortAddressesTestScenario(List addresses, List expectedAddresses) { + this.addresses = addresses; + this.expectedAddresses = expectedAddresses; + } + + public List getAddresses() { + return addresses; + } + + public List getExpectedAddresses() { + return expectedAddresses; + } + } +} \ No newline at end of file diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java index f82d651f50645..1bab56dc0df5d 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java @@ -31,6 +31,7 @@ import com.azure.cosmos.models.PartitionKeyDefinition; import io.reactivex.subscribers.TestSubscriber; import org.assertj.core.api.AssertionsForClassTypes; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; @@ -43,7 +44,12 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -52,10 +58,13 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static com.azure.cosmos.implementation.TestUtils.mockDiagnosticsClientContext; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Connected; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.UnhealthyPending; +import static org.assertj.core.api.Assertions.assertThat; public class GatewayAddressCacheTest extends TestSuiteBase { private Database createdDatabase; @@ -92,6 +101,16 @@ public Object[][] protocolProvider() { }; } + + @DataProvider(name = "replicaValidationArgsProvider") + public Object[][] replicaValidationArgsProvider() { + return new Object[][]{ + // replica validation is enabled + { false }, + { true }, + }; + } + @Test(groups = { "direct" }, dataProvider = "targetPartitionsKeyRangeListAndCollectionLinkParams", timeOut = TIMEOUT) public void getServerAddressesViaGateway(List partitionKeyRangeIds, String collectionLink, @@ -907,6 +926,352 @@ public Mono> answer(InvocationOnMock invocationOnMock) throws Thro assertSameAs(ImmutableList.copyOf(actualAddresses), fetchedAddresses); } + @SuppressWarnings("unchecked") + @Test(groups = { "direct" }, dataProvider = "replicaValidationArgsProvider", timeOut = TIMEOUT) + public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnabled) throws Exception { + Configs configs = ConfigsBuilder.instance().withProtocol(Protocol.TCP).build(); + URI serviceEndpoint = new URI(TestConfigurations.HOST); + IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; + HttpClientUnderTestWrapper httpClientWrapper = getHttpClientUnderTestWrapper(configs); + IOpenConnectionsHandler openConnectionsHandlerMock = Mockito.mock(IOpenConnectionsHandler.class); + Mockito.when(openConnectionsHandlerMock.openConnections(Mockito.any())).thenReturn(Flux.empty()); // what returned here does not really matter + + if (replicaValidationEnabled) { + System.setProperty("COSMOS.REPLICA_ADDRESS_VALIDATION_ENABLED", "true"); + assertThat(Configs.isReplicaAddressValidationEnabled()).isTrue(); + } else { + System.setProperty("COSMOS.REPLICA_ADDRESS_VALIDATION_ENABLED", "false"); + assertThat(Configs.isReplicaAddressValidationEnabled()).isFalse(); + } + + GatewayAddressCache cache = new GatewayAddressCache( + mockDiagnosticsClientContext(), + serviceEndpoint, + Protocol.TCP, + authorizationTokenProvider, + null, + httpClientWrapper.getSpyHttpClient(), + true, + null, + null, + ConnectionPolicy.getDefaultPolicy(), + openConnectionsHandlerMock); + + RxDocumentServiceRequest req = + RxDocumentServiceRequest.create( + mockDiagnosticsClientContext(), + OperationType.Create, + ResourceType.Document, + getCollectionSelfLink(), + new Database(), + new HashMap<>()); + + PartitionKeyRangeIdentity partitionKeyRangeIdentity = new PartitionKeyRangeIdentity(createdCollection.getResourceId(), "0"); + boolean forceRefreshPartitionAddresses = false; + + Mono> addressesInfosFromCacheObs = + cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses); + + ArrayList addressInfosFromCache = + Lists.newArrayList(getSuccessResult(addressesInfosFromCacheObs, TIMEOUT).v); + + assertThat(httpClientWrapper.capturedRequests) + .describedAs("getAddress will read addresses from gateway") + .asList().hasSize(1); + + if (replicaValidationEnabled) { + ArgumentCaptor> openConnectionArguments = ArgumentCaptor.forClass(List.class); + + // Open connection will only be called for unhealthyPending status address + Mockito.verify(openConnectionsHandlerMock, Mockito.times(0)).openConnections(openConnectionArguments.capture()); + } else { + Mockito.verify(openConnectionsHandlerMock, Mockito.never()).openConnections(Mockito.any()); + } + + // Mark one of the uri as unhealthy, others as connected + // and then force refresh the addresses again, make sure the health status of the uri is reserved + httpClientWrapper.capturedRequests.clear(); + Mockito.clearInvocations(openConnectionsHandlerMock); + for (AddressInformation address : addressInfosFromCache) { + address.getPhysicalUri().setConnected(); + } + Uri unhealthyAddressUri = addressInfosFromCache.get(0).getPhysicalUri(); + unhealthyAddressUri.setUnhealthy(); + + ArrayList refreshedAddresses = + Lists.newArrayList(getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, true), TIMEOUT).v); + assertThat(httpClientWrapper.capturedRequests) + .describedAs("getAddress will read addresses from gateway") + .asList().hasSize(1); + assertThat(refreshedAddresses).hasSize(addressInfosFromCache.size()).containsAll(addressInfosFromCache); + + // validate connected status will be reserved + // validate unhealthy status will change into unhealthyPending status + // validate openConnection will only be called for addresses not in connected status + for (AddressInformation addressInformation : refreshedAddresses) { + if (addressInformation.getPhysicalUri().equals(unhealthyAddressUri)) { + assertThat(addressInformation.getPhysicalUri().getHealthStatus()).isEqualTo(UnhealthyPending); + } else { + assertThat(addressInformation.getPhysicalUri().getHealthStatus()).isEqualTo(Connected); + } + } + + if (replicaValidationEnabled) { + ArgumentCaptor> openConnectionArguments = ArgumentCaptor.forClass(List.class); + Mockito.verify(openConnectionsHandlerMock, Mockito.times(1)).openConnections(openConnectionArguments.capture()); + + assertThat(openConnectionArguments.getValue()).hasSize(1).containsExactly(unhealthyAddressUri); + } else { + Mockito.verify(openConnectionsHandlerMock, Mockito.never()).openConnections(Mockito.any()); + } + + System.clearProperty("COSMOS.REPLICA_ADDRESS_VALIDATION_ENABLED"); + } + + @Test(groups = { "direct" }, timeOut = TIMEOUT) + public void tryGetAddress_failedEndpointTests() throws Exception { + Configs configs = ConfigsBuilder.instance().withProtocol(Protocol.TCP).build(); + URI serviceEndpoint = new URI(TestConfigurations.HOST); + IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; + HttpClientUnderTestWrapper httpClientWrapper = getHttpClientUnderTestWrapper(configs); + IOpenConnectionsHandler openConnectionsHandlerMock = Mockito.mock(IOpenConnectionsHandler.class); + Mockito.when(openConnectionsHandlerMock.openConnections(Mockito.any())).thenReturn(Flux.empty()); // what returned here does not really matter + + GatewayAddressCache cache = new GatewayAddressCache( + mockDiagnosticsClientContext(), + serviceEndpoint, + Protocol.TCP, + authorizationTokenProvider, + null, + httpClientWrapper.getSpyHttpClient(), + true, + null, + null, + ConnectionPolicy.getDefaultPolicy(), + openConnectionsHandlerMock); + + RxDocumentServiceRequest req = + RxDocumentServiceRequest.create( + mockDiagnosticsClientContext(), + OperationType.Create, + ResourceType.Document, + getCollectionSelfLink(), + new Database(), + new HashMap<>()); + + PartitionKeyRangeIdentity partitionKeyRangeIdentity = new PartitionKeyRangeIdentity(createdCollection.getResourceId(), "0"); + boolean forceRefreshPartitionAddresses = false; + + Mono> addressesInfosFromCacheObs = + cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses); + + ArrayList addressInfosFromCache = + Lists.newArrayList(getSuccessResult(addressesInfosFromCacheObs, TIMEOUT).v); + + assertThat(httpClientWrapper.capturedRequests) + .describedAs("getAddress will read addresses from gateway") + .asList().hasSize(1); + + // Mark all the uris in connected status + // Setup request failedEndpoints, and then refresh addresses again(with forceRefresh = false), confirm the failed endpoint uri is marked as unhealthy + httpClientWrapper.capturedRequests.clear(); + Mockito.clearInvocations(openConnectionsHandlerMock); + for (AddressInformation address : addressInfosFromCache) { + address.getPhysicalUri().setConnected(); + } + + req.requestContext.getFailedEndpoints().add(addressInfosFromCache.get(0).getPhysicalUri()); + + ArrayList refreshedAddresses = + Lists.newArrayList(getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, false), TIMEOUT).v); + assertThat(httpClientWrapper.capturedRequests) + .describedAs("getAddress will read from cache") + .asList().hasSize(0); + assertThat(refreshedAddresses).hasSize(addressInfosFromCache.size()).containsAll(addressInfosFromCache); + assertThat(refreshedAddresses.get(0).getPhysicalUri().getHealthStatus()).isEqualTo(Uri.HealthStatus.Unhealthy); + } + + @Test(groups = { "direct" }, timeOut = TIMEOUT) + public void tryGetAddress_unhealthyStatus_forceRefresh() throws Exception { + Configs configs = ConfigsBuilder.instance().withProtocol(Protocol.TCP).build(); + URI serviceEndpoint = new URI(TestConfigurations.HOST); + IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; + HttpClientUnderTestWrapper httpClientWrapper = getHttpClientUnderTestWrapper(configs); + IOpenConnectionsHandler openConnectionsHandlerMock = Mockito.mock(IOpenConnectionsHandler.class); + Mockito.when(openConnectionsHandlerMock.openConnections(Mockito.any())).thenReturn(Flux.empty()); // what returned here does not really matter + + GatewayAddressCache cache = new GatewayAddressCache( + mockDiagnosticsClientContext(), + serviceEndpoint, + Protocol.TCP, + authorizationTokenProvider, + null, + httpClientWrapper.getSpyHttpClient(), + true, + null, + null, + ConnectionPolicy.getDefaultPolicy(), + openConnectionsHandlerMock); + + RxDocumentServiceRequest req = + RxDocumentServiceRequest.create( + mockDiagnosticsClientContext(), + OperationType.Create, + ResourceType.Document, + getCollectionSelfLink(), + new Database(), + new HashMap<>()); + + PartitionKeyRangeIdentity partitionKeyRangeIdentity = new PartitionKeyRangeIdentity(createdCollection.getResourceId(), "0"); + boolean forceRefreshPartitionAddresses = false; + + Mono> addressesInfosFromCacheObs = + cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses); + + ArrayList addressInfosFromCache = + Lists.newArrayList(getSuccessResult(addressesInfosFromCacheObs, TIMEOUT).v); + + assertThat(httpClientWrapper.capturedRequests) + .describedAs("getAddress will read addresses from gateway") + .asList().hasSize(1); + + httpClientWrapper.capturedRequests.clear(); + Mockito.clearInvocations(openConnectionsHandlerMock); + + // mark one of the uri as unhealthy, and validate the address cache will be refreshed after 1 min + Uri unhealthyAddressUri = addressInfosFromCache.get(0).getPhysicalUri(); + unhealthyAddressUri.setUnhealthy(); + Field lastUnhealthyTimestampField = Uri.class.getDeclaredField("lastUnhealthyTimestamp"); + lastUnhealthyTimestampField.setAccessible(true); + lastUnhealthyTimestampField.set(unhealthyAddressUri, Instant.now().minusMillis(Duration.ofMinutes(1).toMillis())); + + // using forceRefresh false + // but as there is one address has been stuck in unhealthy status for more than 1 min, + // so after getting the addresses, it will refresh the cache + ArrayList cachedAddresses = + Lists.newArrayList(getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, false), TIMEOUT).v); + + // validate the cache will be refreshed + assertThat(httpClientWrapper.capturedRequests) + .describedAs("getAddress will read addresses from gateway") + .asList().hasSize(1); + assertThat(cachedAddresses).hasSize(addressInfosFromCache.size()).containsAll(addressInfosFromCache); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test(groups = { "direct" }, timeOut = TIMEOUT) + public void validateReplicaAddressesTests() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Configs configs = ConfigsBuilder.instance().withProtocol(Protocol.TCP).build(); + URI serviceEndpoint = new URI(TestConfigurations.HOST); + IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; + HttpClientUnderTestWrapper httpClientWrapper = getHttpClientUnderTestWrapper(configs); + IOpenConnectionsHandler openConnectionsHandlerMock = Mockito.mock(IOpenConnectionsHandler.class); + Mockito.when(openConnectionsHandlerMock.openConnections(Mockito.any())).thenReturn(Flux.empty()); // what returned here does not really matter + + GatewayAddressCache cache = new GatewayAddressCache( + mockDiagnosticsClientContext(), + serviceEndpoint, + Protocol.TCP, + authorizationTokenProvider, + null, + httpClientWrapper.getSpyHttpClient(), + true, + null, + null, + ConnectionPolicy.getDefaultPolicy(), + openConnectionsHandlerMock); + + Method validateReplicaAddressesMethod = GatewayAddressCache.class.getDeclaredMethod("validateReplicaAddresses", new Class[] { AddressInformation[].class }); + validateReplicaAddressesMethod.setAccessible(true); + + // connected status + AddressInformation address1 = new AddressInformation(true, true, "rntbd://127.0.0.1:1", Protocol.TCP); + address1.getPhysicalUri().setConnected(); + + // remain in unknwon status + AddressInformation address2 = new AddressInformation(true, false, "rntbd://127.0.0.1:2", Protocol.TCP); + + // unhealthy status + AddressInformation address3 = new AddressInformation(true, false, "rntbd://127.0.0.1:3", Protocol.TCP); + address3.getPhysicalUri().setUnhealthy(); + + // unhealthy pending status + AddressInformation address4 = new AddressInformation(true, false, "rntbd://127.0.0.1:4", Protocol.TCP); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(address4.getPhysicalUri()); + healthStatus.set(UnhealthyPending); + + validateReplicaAddressesMethod.invoke(cache, new Object[]{ new AddressInformation[]{ address1, address2, address3, address4 }}) ; + + // Validate openConnection will only be called for address in unhealthyPending status + ArgumentCaptor> openConnectionArguments = ArgumentCaptor.forClass(List.class); + Mockito.verify(openConnectionsHandlerMock, Mockito.times(1)).openConnections(openConnectionArguments.capture()); + + assertThat(openConnectionArguments.getValue()).hasSize(1).containsExactlyElementsOf( + Arrays.asList(address4) + .stream() + .map(addressInformation -> addressInformation.getPhysicalUri()) + .collect(Collectors.toList())); + } + + @SuppressWarnings("rawtypes") + @Test(groups = { "direct" }, timeOut = TIMEOUT) + public void mergeAddressesTests() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Configs configs = ConfigsBuilder.instance().withProtocol(Protocol.TCP).build(); + URI serviceEndpoint = new URI(TestConfigurations.HOST); + IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; + HttpClientUnderTestWrapper httpClientWrapper = getHttpClientUnderTestWrapper(configs); + IOpenConnectionsHandler openConnectionsHandlerMock = Mockito.mock(IOpenConnectionsHandler.class); + Mockito.when(openConnectionsHandlerMock.openConnections(Mockito.any())).thenReturn(Flux.empty()); // what returned here does not really matter + + GatewayAddressCache cache = new GatewayAddressCache( + mockDiagnosticsClientContext(), + serviceEndpoint, + Protocol.TCP, + authorizationTokenProvider, + null, + httpClientWrapper.getSpyHttpClient(), + true, + null, + null, + ConnectionPolicy.getDefaultPolicy(), + openConnectionsHandlerMock); + + // connected status + AddressInformation address1 = new AddressInformation(true, true, "rntbd://127.0.0.1:1", Protocol.TCP); + address1.getPhysicalUri().setConnected(); + + // unhealthyStatus + AddressInformation address2 = new AddressInformation(true, false, "rntbd://127.0.0.1:2", Protocol.TCP); + address2.getPhysicalUri().setUnhealthy(); + + AddressInformation address3 = new AddressInformation(true, false, "rntbd://127.0.0.1:3", Protocol.TCP); + AddressInformation address4 = new AddressInformation(true, false, "rntbd://127.0.0.1:4", Protocol.TCP); + AddressInformation address5 = new AddressInformation(true, false, "rntbd://127.0.0.1:5", Protocol.TCP); + AddressInformation address6 = new AddressInformation(true, false, "rntbd://127.0.0.1:6", Protocol.TCP); + + + AddressInformation[] cachedAddresses = new AddressInformation[] { address1, address2, address3, address4 }; + // when decide to whether to use cached addressInformation, it will compare physical uri, protocol, isPrimary + AddressInformation address7 = new AddressInformation(true, true, "rntbd://127.0.0.1:2", Protocol.TCP); + + AddressInformation[] newAddresses = new AddressInformation[] { + new AddressInformation(true, true, "rntbd://127.0.0.1:1", Protocol.TCP), + address7, + address5, + address6 }; + + Method mergeAddressesMethod = + GatewayAddressCache.class.getDeclaredMethod( + "mergeAddresses", + new Class[] { AddressInformation[].class, AddressInformation[].class }); + mergeAddressesMethod.setAccessible(true); + AddressInformation[] mergedAddresses = + (AddressInformation[]) mergeAddressesMethod.invoke(cache, new Object[]{ newAddresses, cachedAddresses }); + + assertThat(mergedAddresses).hasSize(newAddresses.length) + .containsExactly(address1, address7, address5, address6); + } + public static void assertSameAs(List actual, List
expected) { assertThat(actual).asList().hasSize(expected.size()); for(int i = 0; i < expected.size(); i++) { diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java index 7d94c5f50b044..0c66981c0e618 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java @@ -391,4 +391,9 @@ public static AtomicReference getAzureVMMetadata(ClientTelemetr public static void setClientTelemetryMetadataHttpClient(ClientTelemetry clientTelemetry, HttpClient HttpClient) { set(clientTelemetry, HttpClient, "metadataHttpClient"); } + + @SuppressWarnings("unchecked") + public static AtomicReference getHealthStatus(Uri uri) { + return get(AtomicReference.class, uri, "healthStatus"); + } } diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/StoreReaderTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/StoreReaderTest.java index 7db49a4310d8b..ca62351e9c0f0 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/StoreReaderTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/StoreReaderTest.java @@ -36,6 +36,7 @@ import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; @@ -170,7 +171,7 @@ public void exception(Exception ex, Class klass, int expectedStatusCo TimeoutHelper timeoutHelper = Mockito.mock(TimeoutHelper.class); RxDocumentServiceRequest dsr = RxDocumentServiceRequest.createFromName(mockDiagnosticsClientContext(), OperationType.Read, "/dbs/db/colls/col/docs/docId", ResourceType.Document); - dsr.requestContext = Mockito.mock(DocumentServiceRequestContext.class); + dsr.requestContext = new DocumentServiceRequestContext(); dsr.requestContext.timeoutHelper = timeoutHelper; dsr.requestContext.resolvedPartitionKeyRange = partitionKeyRangeWithId("1"); Mono> res = storeReader.readMultipleReplicaAsync(dsr, true, 3, true, true, ReadMode.Strong); @@ -187,6 +188,10 @@ public void exception(Exception ex, Class klass, int expectedStatusCo subscriber.assertNotComplete(); assertThat(subscriber.errorCount()).isEqualTo(1); failureValidator.validate(subscriber.errors().get(0)); + + if (expectedStatusCode == 410) { + assertThat(dsr.requestContext.getFailedEndpoints().size()).isEqualTo(1); + } } /** @@ -631,7 +636,13 @@ public void canParseLongLsn() { .withGlobalCommittedLsn(bigLsn) .build(); - StoreResult result = storeReader.createStoreResult(storeResponse, null, false, false, null); + StoreResult result = storeReader.createStoreResult( + storeResponse, + null, + false, + false, + null, + Arrays.asList(primaryURI.getHealthStatusDiagnosticString())); assertThat(result.globalCommittedLSN).isEqualTo(bigLsn); assertThat(result.lsn).isEqualTo(bigLsn); } @@ -886,6 +897,33 @@ public void storeResponseRecordedOnException(Exception ex, StoreResponse storeRe String cosmosDiagnostics = dsr.requestContext.cosmosDiagnostics.toString(); assertThat(this.getMatchingElementCount(cosmosDiagnostics, "storeResult") >= 1).isTrue(); + + // validate failed endpoints in request context + if (ex != null) { + // validate failed endpoints based on exception type. + if (ex instanceof CosmosException) { + try { + StoreReader.verifyCanContinueOnException((CosmosException) ex); + + // for continuable exception, SDK will retry on all other replicas, so the failed endpoints should match replica counts. + List expectedFailedEndpoints = Arrays.asList(primaryUri, secondaryUri1, secondaryUri2, secondaryUri3); + assertThat(dsr.requestContext.getFailedEndpoints()).hasSize(expectedFailedEndpoints.size()).containsAll(expectedFailedEndpoints); + + } catch (Exception exception) { + if (exception instanceof CosmosException) { + assertThat(dsr.requestContext.getFailedEndpoints()).hasSize(1); + } else { + assertThat(dsr.requestContext.getFailedEndpoints()).isEmpty(); + } + } + } else { + // Not a cosmosException, so the failed endpoints should be empty. + assertThat(dsr.requestContext.getFailedEndpoints()).isEmpty(); + } + } else { + // There is no exception, so the failedEndpoints should be empty. + assertThat(dsr.requestContext.getFailedEndpoints()).isEmpty(); + } } @Test(groups = "unit") @@ -894,7 +932,14 @@ public void createStoreResultOnNonCosmosException() { AddressSelector addressSelector = Mockito.mock(AddressSelector.class); ISessionContainer sessionContainer = Mockito.mock(ISessionContainer.class); StoreReader storeReader = new StoreReader(transportClient, addressSelector, sessionContainer); - StoreResult storeResult = storeReader.createStoreResult(null, new IllegalStateException("Test"), false, false, null); + StoreResult storeResult = + storeReader.createStoreResult( + null, + new IllegalStateException("Test"), + false, + false, + null, + null); assertThat(storeResult.getException().toString()).contains("\"causeInfo\":\"[class: class java.lang.IllegalStateException, message:" + " Test]\""); } diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/UriTests.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/UriTests.java new file mode 100644 index 0000000000000..4757a79842848 --- /dev/null +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/UriTests.java @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity; + +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Connected; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Unhealthy; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.UnhealthyPending; +import static com.azure.cosmos.implementation.directconnectivity.Uri.HealthStatus.Unknown; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class UriTests { + @Test(groups = "unit") + public void setHealthyStatusTests() throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException { + Uri testUri = new Uri("https://127.0.0.1:8080"); + + List statusCanBeOverwritten = + Arrays.asList(Unknown, Connected, UnhealthyPending); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + + for (Uri.HealthStatus initialStatus : statusCanBeOverwritten) { + healthStatus.set(initialStatus); + testUri.setHealthStatus(Connected); + assertThat(testUri.getHealthStatus()).isEqualTo(Connected); + } + + // if the status is unhealthy, it can only be overwritten to connected after some extended time + Field lastUnhealthyTimestampField = Uri.class.getDeclaredField("lastUnhealthyTimestamp"); + lastUnhealthyTimestampField.setAccessible(true); + healthStatus.set(Unhealthy); + + lastUnhealthyTimestampField.set(testUri, Instant.now()); + testUri.setHealthStatus(Connected); + assertThat(testUri.getHealthStatus()).isEqualTo(Unhealthy); + + lastUnhealthyTimestampField.set(testUri, Instant.now().minusMillis(Duration.ofMinutes(2).toMillis())); + testUri.setHealthStatus(Connected); + assertThat(testUri.getHealthStatus()).isEqualTo(Connected); + } + + @Test(groups = "unit") + public void setUnhealthyStatusTests() throws NoSuchFieldException, IllegalAccessException { + + Field lastUnhealthyTimestampField = Uri.class.getDeclaredField("lastUnhealthyTimestamp"); + lastUnhealthyTimestampField.setAccessible(true); + + // Unhealthy status can override any other status + for (Uri.HealthStatus initialStatus : Uri.HealthStatus.values()) { + + Uri testUri = new Uri("https://127.0.0.1:8080"); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + healthStatus.set(initialStatus); + Instant lastUnhealthyTimestampBefore = (Instant) lastUnhealthyTimestampField.get(testUri); + + testUri.setHealthStatus(Unhealthy); + Instant lastUnhealthyTimestampAfter = (Instant) lastUnhealthyTimestampField.get(testUri); + + assertThat(testUri.getHealthStatus()).isEqualTo(Unhealthy); + assertThat(lastUnhealthyTimestampAfter).isNotEqualTo(lastUnhealthyTimestampBefore); + } + } + + @Test(groups = "unit") + public void setUnhealthyPendingStatusTests() throws NoSuchFieldException, IllegalAccessException { + List statusCanBeOverwritten = Arrays.asList(UnhealthyPending, Unhealthy); + List statusSkipped = Arrays.asList(Unknown, Connected); + + Field lastUnhealthyPendingTimestampField = Uri.class.getDeclaredField("lastUnhealthyPendingTimestamp"); + lastUnhealthyPendingTimestampField.setAccessible(true); + + for (Uri.HealthStatus initialHealthStatus : Uri.HealthStatus.values()) { + Uri testUri = new Uri("https://127.0.0.1:8080"); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + + healthStatus.set(initialHealthStatus); + Instant lastUnhealthyPendingTimestampBefore = (Instant) lastUnhealthyPendingTimestampField.get(testUri); + + testUri.setHealthStatus(UnhealthyPending); + Instant lastUnhealthyPendingTimestampAfter = (Instant) lastUnhealthyPendingTimestampField.get(testUri); + + if (statusCanBeOverwritten.contains(initialHealthStatus)) { + assertThat(testUri.getHealthStatus()).isEqualTo(UnhealthyPending); + assertThat(lastUnhealthyPendingTimestampAfter).isNotEqualTo(lastUnhealthyPendingTimestampBefore); + + } else if (statusSkipped.contains(initialHealthStatus)) { + assertThat(testUri.getHealthStatus()).isEqualTo(initialHealthStatus); + if (lastUnhealthyPendingTimestampBefore == null) { + assertThat(lastUnhealthyPendingTimestampAfter).isNull(); + } else { + assertThat(lastUnhealthyPendingTimestampAfter).isEqualTo(lastUnhealthyPendingTimestampBefore); + } + + } else { + throw new IllegalStateException("Unknown health status: " + initialHealthStatus.toString()); + } + } + } + + @Test(groups = "unit") + public void setUnknownStatusTests() throws NoSuchFieldException, IllegalAccessException { + Uri testUri = new Uri("https://127.0.0.1:8080"); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + + Field lastUnknownTimestampField = Uri.class.getDeclaredField("lastUnknownTimestamp"); + lastUnknownTimestampField.setAccessible(true); + + assertThat(lastUnknownTimestampField).isNotNull(); + + for (Uri.HealthStatus initialHealthStatus : Uri.HealthStatus.values()) { + healthStatus.set(initialHealthStatus); + Instant lastUnknownTimestampBefore = (Instant) lastUnknownTimestampField.get(testUri); + + assertThatThrownBy(() -> testUri.setHealthStatus(Unknown)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("It is impossible to set to unknown status"); + + Instant lastUnknownTimestampAfter = (Instant) lastUnknownTimestampField.get(testUri); + assertThat(lastUnknownTimestampBefore).isEqualTo(lastUnknownTimestampAfter); + } + } + + @Test(groups = "unit") + public void setRefreshTests() throws NoSuchFieldException, IllegalAccessException { + + Field lastUnknownTimestampField = Uri.class.getDeclaredField("lastUnknownTimestamp"); + lastUnknownTimestampField.setAccessible(true); + + Field lastUnhealthyPendingTimestampField = Uri.class.getDeclaredField("lastUnhealthyPendingTimestamp"); + lastUnhealthyPendingTimestampField.setAccessible(true); + + Field lastUnhealthyTimestampField = Uri.class.getDeclaredField("lastUnhealthyTimestamp"); + lastUnhealthyTimestampField.setAccessible(true); + + for (Uri.HealthStatus initialHealthStatus : Uri.HealthStatus.values()) { + Uri testUri = new Uri("https://127.0.0.1:8080"); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + healthStatus.set(initialHealthStatus); + Instant time = Instant.now().minusSeconds(2); + + lastUnknownTimestampField.set(testUri, time); + lastUnhealthyPendingTimestampField.set(testUri, time); + lastUnhealthyTimestampField.set(testUri, time); + + testUri.setRefreshed(); + + Instant lastUnknownTimestampAfter = (Instant) lastUnknownTimestampField.get(testUri); + Instant lastUnhealthyPendingTimestampAfter = (Instant) lastUnhealthyPendingTimestampField.get(testUri); + Instant lastUnhealthyTimestampAfter = (Instant) lastUnhealthyTimestampField.get(testUri); + + switch (initialHealthStatus) { + case Unknown: + case Connected: + case UnhealthyPending: + assertThat(testUri.getHealthStatus()).isEqualTo(initialHealthStatus); + assertThat(lastUnknownTimestampAfter).isEqualTo(time); + assertThat(lastUnhealthyPendingTimestampAfter).isEqualTo(time); + assertThat(lastUnhealthyTimestampAfter).isEqualTo(time); + break; + case Unhealthy: + assertThat(testUri.getHealthStatus()).isEqualTo(UnhealthyPending); + assertThat(lastUnknownTimestampAfter).isEqualTo(time); + assertThat(lastUnhealthyPendingTimestampAfter).isAfter(time); + assertThat(lastUnhealthyTimestampAfter).isEqualTo(time); + break; + } + } + } + + @Test(groups = "unit") + public void getEffectiveHealthStatusTest() throws NoSuchFieldException, IllegalAccessException { + Uri testUri = new Uri("https://127.0.0.1:8080"); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + + for (Uri.HealthStatus initialStatus : Uri.HealthStatus.values()) { + healthStatus.set(initialStatus); + + switch (initialStatus) { + case Unhealthy: + case Connected: + assertThat(testUri.getEffectiveHealthStatus()).isEqualTo(initialStatus); + break; + case Unknown: + Field lastUnknownTimestampField = Uri.class.getDeclaredField("lastUnknownTimestamp"); + lastUnknownTimestampField.setAccessible(true); + // if within the DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS time window, then return the status as it is + lastUnknownTimestampField.set(testUri, Instant.now()); + assertThat(testUri.getEffectiveHealthStatus()).isEqualTo(Unknown); + // if already passed the DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS, then return rolling into healthy category + lastUnknownTimestampField.set(testUri, Instant.now().minusMillis(Duration.ofMinutes(2).toMillis())); + assertThat(testUri.getEffectiveHealthStatus()).isEqualTo(Connected); + break; + case UnhealthyPending: + Field lastUnhealthyPendingTimestampField = Uri.class.getDeclaredField("lastUnhealthyPendingTimestamp"); + lastUnhealthyPendingTimestampField.setAccessible(true); + // if within the DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS time window, then return the status as it is + lastUnhealthyPendingTimestampField.set(testUri, Instant.now()); + assertThat(testUri.getEffectiveHealthStatus()).isEqualTo(UnhealthyPending); + // if already passed the DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS, then rolling into healthy category + lastUnhealthyPendingTimestampField.set(testUri, Instant.now().minusMillis(Duration.ofMinutes(2).toMillis())); + assertThat(testUri.getEffectiveHealthStatus()).isEqualTo(Connected); + break; + default: + throw new IllegalStateException("Unknown health status: " + initialStatus); + } + } + } + + @Test(groups = "unit") + public void shouldRefreshHealthStatusTests() throws NoSuchFieldException, IllegalAccessException { + Uri testUri = new Uri("https://127.0.0.1:8080"); + AtomicReference healthStatus = ReflectionUtils.getHealthStatus(testUri); + + for (Uri.HealthStatus initialStatus : Uri.HealthStatus.values()) { + healthStatus.set(initialStatus); + + switch (initialStatus) { + case Unknown: + case Connected: + case UnhealthyPending: + assertThat(testUri.shouldRefreshHealthStatus()).isFalse(); + break; + case Unhealthy: + Field lastUnhealthyTimestampField = Uri.class.getDeclaredField("lastUnhealthyTimestamp"); + lastUnhealthyTimestampField.setAccessible(true); + lastUnhealthyTimestampField.set(testUri, Instant.now()); + assertThat(testUri.shouldRefreshHealthStatus()).isFalse(); + + lastUnhealthyTimestampField.set(testUri, Instant.now().minusMillis(Duration.ofMinutes(2).toMillis())); + assertThat(testUri.shouldRefreshHealthStatus()).isTrue(); + break; + default: + throw new IllegalStateException("Unknown health status: " + initialStatus); + } + } + } +} diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecordTests.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecordTests.java index bd31930fd4923..71ba14dd59e3a 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecordTests.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdRequestRecordTests.java @@ -8,6 +8,7 @@ import com.azure.cosmos.implementation.RequestTimeoutException; import com.azure.cosmos.implementation.ResourceType; import com.azure.cosmos.implementation.RxDocumentServiceRequest; +import com.azure.cosmos.implementation.directconnectivity.Uri; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -36,7 +37,7 @@ public void expireRecord(OperationType operationType, boolean requestSent, Class RntbdRequestArgs requestArgs = new RntbdRequestArgs( RxDocumentServiceRequest.create(mockDiagnosticsClientContext(), operationType, ResourceType.Document), - new URI("http://localhost/replica-path") + new Uri(new URI("http://localhost/replica-path").toString()) ); RntbdRequestTimer requestTimer = new RntbdRequestTimer(5000, 5000); From 7abdfcda2a6fb703f96f6800350d58de760e17f9 Mon Sep 17 00:00:00 2001 From: Alan Zimmer <48699787+alzimmermsft@users.noreply.github.com> Date: Thu, 1 Sep 2022 17:53:52 -0400 Subject: [PATCH 11/18] Use currentContext instead of contextView (#30753) Use currentContext instead of ContextView --- .../amqp/implementation/RequestResponseChannel.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java index 27b343010c406..ca062ec23ce80 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/RequestResponseChannel.java @@ -364,7 +364,7 @@ public Mono sendWithAck(final Message message, DeliveryState deliverySt sendLink.advance(); }); } catch (IOException | RejectedExecutionException e) { - recordDelivery(sink.contextView(), null); + recordDelivery(getSinkContext(sink), null); sink.error(e); } }))); @@ -407,7 +407,7 @@ private void settleMessage(Message message) { return; } - recordDelivery(sink.contextView(), message); + recordDelivery(getSinkContext(sink), message); sink.success(message); } @@ -490,7 +490,7 @@ private void terminateUnconfirmedSends(Throwable error) { while ((next = unconfirmedSends.pollFirstEntry()) != null) { // pollFirstEntry: atomic retrieve and remove of each entry. MonoSink sink = next.getValue(); - recordDelivery(sink.contextView(), null); + recordDelivery(getSinkContext(sink), null); sink.error(error); count++; } @@ -521,6 +521,13 @@ private Mono captureStartTime(Message toSend, Mono publisher) return publisher; } + @SuppressWarnings("deprecation") + private static ContextView getSinkContext(MonoSink sink) { + // Use currentContext instead of contextView as it's supported back to Reactor 3.4.0 and gives the widest + // range of support possible. + return sink.currentContext(); + } + /** * Records send call duration metric. **/ From 6f2d05c8f50b983e23a6a43d84982b8e0d18a184 Mon Sep 17 00:00:00 2001 From: Shawn Fang <45607042+mssfang@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:43:02 -0700 Subject: [PATCH 12/18] [TA] Add client side validation for displayName in healthcare (#30755) --- .../AnalyzeHealthcareEntityAsyncClient.java | 11 +++++++++++ .../ClientSideValidationUnitTests.java | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java index a0b222409555e..9183799bae480 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/AnalyzeHealthcareEntityAsyncClient.java @@ -100,6 +100,7 @@ class AnalyzeHealthcareEntityAsyncClient { Arrays.asList(TextAnalyticsServiceVersion.V3_0), getUnsupportedServiceApiVersionMessage("beginAnalyzeHealthcareEntities", serviceVersion, TextAnalyticsServiceVersion.V3_1)); + throwIfCallingNotAvailableFeatureInOptions(options); inputDocumentsValidation(documents); options = getNotNullAnalyzeHealthcareEntitiesOptions(options); final Context finalContext = getNotNullContext(context) @@ -179,6 +180,7 @@ class AnalyzeHealthcareEntityAsyncClient { Arrays.asList(TextAnalyticsServiceVersion.V3_0), getUnsupportedServiceApiVersionMessage("beginAnalyzeHealthcareEntities", serviceVersion, TextAnalyticsServiceVersion.V3_1)); + throwIfCallingNotAvailableFeatureInOptions(options); inputDocumentsValidation(documents); options = getNotNullAnalyzeHealthcareEntitiesOptions(options); final Context finalContext = getNotNullContext(context) @@ -560,4 +562,13 @@ private AnalyzeHealthcareEntitiesOptions getNotNullAnalyzeHealthcareEntitiesOpti AnalyzeHealthcareEntitiesOptions options) { return options == null ? new AnalyzeHealthcareEntitiesOptions() : options; } + + private void throwIfCallingNotAvailableFeatureInOptions(AnalyzeHealthcareEntitiesOptions options) { + if (options != null && options.getDisplayName() != null) { + throwIfTargetServiceVersionFound(serviceVersion, + Arrays.asList(TextAnalyticsServiceVersion.V3_1), + getUnsupportedServiceApiVersionMessage("AnalyzeHealthcareEntitiesOptions.displayName", + serviceVersion, TextAnalyticsServiceVersion.V2022_05_01)); + } + } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java index 71948af084d74..056e078ae0dad 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/ClientSideValidationUnitTests.java @@ -4,6 +4,7 @@ package com.azure.ai.textanalytics; import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesAction; +import com.azure.ai.textanalytics.models.AnalyzeHealthcareEntitiesOptions; import com.azure.ai.textanalytics.models.AnalyzeSentimentOptions; import com.azure.ai.textanalytics.models.MultiLabelClassifyAction; import com.azure.ai.textanalytics.models.RecognizeCustomEntitiesAction; @@ -62,6 +63,9 @@ public class ClientSideValidationUnitTests { static final String ANALYZE_HEALTHCARE_ENTITIES_ERROR_MESSAGE = getUnsupportedServiceApiVersionMessage("beginAnalyzeHealthcareEntities", TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V3_1); + static final String HEALTHCARE_ENTITIES_DISPLAY_NAME_ERROR_MESSAGE = + getUnsupportedServiceApiVersionMessage("AnalyzeHealthcareEntitiesOptions.displayName", + TextAnalyticsServiceVersion.V3_1, TextAnalyticsServiceVersion.V2022_05_01); static final String RECOGNIZE_CUSTOM_ENTITIES_ERROR_MESSAGE_30 = getUnsupportedServiceApiVersionMessage("beginRecognizeCustomEntities", TextAnalyticsServiceVersion.V3_0, TextAnalyticsServiceVersion.V2022_05_01); @@ -296,10 +300,21 @@ public void analyzeHealthcareEntitiesClientSideValidation() { assertEquals(IllegalStateException.class, exception.getClass()); assertEquals(ANALYZE_HEALTHCARE_ENTITIES_ERROR_MESSAGE, exception.getMessage()); }); + AnalyzeHealthcareEntitiesOptions displayNameOptions = new AnalyzeHealthcareEntitiesOptions() + .setDisplayName("operationName"); + StepVerifier.create(asyncClientV31.beginAnalyzeHealthcareEntities(dummyDocument, null, displayNameOptions)) + .verifyErrorSatisfies(exception -> { + assertEquals(IllegalStateException.class, exception.getClass()); + assertEquals(HEALTHCARE_ENTITIES_DISPLAY_NAME_ERROR_MESSAGE, exception.getMessage()); + }); + // Sync IllegalStateException exception = assertThrows(IllegalStateException.class, () -> clientV30.beginAnalyzeHealthcareEntities(dummyDocument, null, null)); assertEquals(ANALYZE_HEALTHCARE_ENTITIES_ERROR_MESSAGE, exception.getMessage()); + IllegalStateException displayNameException = assertThrows(IllegalStateException.class, + () -> clientV31.beginAnalyzeHealthcareEntities(dummyDocument, null, displayNameOptions)); + assertEquals(HEALTHCARE_ENTITIES_DISPLAY_NAME_ERROR_MESSAGE, displayNameException.getMessage()); } @Test From 98e871ac4a8c2da2cd0bea9042174805437d7206 Mon Sep 17 00:00:00 2001 From: Annie Liang <64233642+xinlian12@users.noreply.github.com> Date: Fri, 2 Sep 2022 02:54:23 +0000 Subject: [PATCH 13/18] ConnectionStateListener - Mark replica unhealthy (#30281) * ConnectionStateListener mark replica as unhealthy instead remove all addresses Co-authored-by: annie-mac Co-authored-by: annie-mac --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 1 + .../directconnectivity/AddressResolver.java | 10 +-- .../GatewayAddressCache.java | 60 --------------- .../GlobalAddressResolver.java | 23 ------ .../GoneAndRetryWithRetryPolicy.java | 3 +- .../directconnectivity/IAddressCache.java | 11 +-- .../directconnectivity/IAddressResolver.java | 6 +- .../rntbd/RntbdConnectionStateListener.java | 22 ++++-- .../rntbd/RntbdServiceEndpoint.java | 11 ++- .../ConnectionStateListenerTest.java | 40 ++++++---- .../GatewayAddressCacheTest.java | 73 ------------------- 11 files changed, 57 insertions(+), 203 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 3030781b3dbbc..8765d0927eb27 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -11,6 +11,7 @@ #### Other Changes * Added system property to turn on replica validation - See [PR 29767](https://github.com/Azure/azure-sdk-for-java/pull/29767) * Added improvement to avoid retry on same replica that previously failed with 410, 408 and >= 500 status codes - See [PR 29767](https://github.com/Azure/azure-sdk-for-java/pull/29767) +* Improvement when `connectionEndpointRediscoveryEnabled` is enabled - See [PR 30281](https://github.com/Azure/azure-sdk-for-java/pull/30281) ### 4.35.1 (2022-08-29) #### Other Changes diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressResolver.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressResolver.java index b7fd625564f7d..69ed8ba22337c 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressResolver.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/AddressResolver.java @@ -9,9 +9,11 @@ import com.azure.cosmos.implementation.DocumentCollection; import com.azure.cosmos.implementation.HttpConstants; import com.azure.cosmos.implementation.ICollectionRoutingMapCache; +import com.azure.cosmos.implementation.IOpenConnectionsHandler; import com.azure.cosmos.implementation.InternalServerErrorException; import com.azure.cosmos.implementation.InvalidPartitionException; import com.azure.cosmos.implementation.NotFoundException; +import com.azure.cosmos.implementation.OpenConnectionResponse; import com.azure.cosmos.implementation.OperationType; import com.azure.cosmos.implementation.PartitionKeyRange; import com.azure.cosmos.implementation.PartitionKeyRangeGoneException; @@ -24,8 +26,6 @@ import com.azure.cosmos.implementation.apachecommons.lang.NotImplementedException; import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; import com.azure.cosmos.implementation.caches.RxCollectionCache; -import com.azure.cosmos.implementation.IOpenConnectionsHandler; -import com.azure.cosmos.implementation.OpenConnectionResponse; import com.azure.cosmos.implementation.routing.CollectionRoutingMap; import com.azure.cosmos.implementation.routing.PartitionKeyInternal; import com.azure.cosmos.implementation.routing.PartitionKeyInternalHelper; @@ -35,7 +35,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.net.URI; import java.util.concurrent.Callable; import java.util.function.Function; @@ -87,11 +86,6 @@ public Mono resolveAsync( }); } - @Override - public int updateAddresses(URI serverKey) { - throw new NotImplementedException("updateAddresses() is not supported in AddressResolver"); - } - @Override public Flux openConnectionsAndInitCaches(String containerLink) { return Flux.empty(); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java index 53d599b3d3a2e..a19be6725f327 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java @@ -63,10 +63,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull; @@ -97,9 +94,6 @@ public class GatewayAddressCache implements IAddressCache { private volatile Pair masterPartitionAddressCache; private volatile Instant suboptimalMasterPartitionTimestamp; - private final ConcurrentHashMap> serverPartitionAddressToPkRangeIdMap; - private final boolean tcpConnectionEndpointRediscoveryEnabled; - private final ConcurrentHashMap lastForcedRefreshMap; private final GlobalEndpointManager globalEndpointManager; private IOpenConnectionsHandler openConnectionsHandler; @@ -114,7 +108,6 @@ public GatewayAddressCache( UserAgentContainer userAgent, HttpClient httpClient, long suboptimalPartitionForceRefreshIntervalInSeconds, - boolean tcpConnectionEndpointRediscoveryEnabled, ApiType apiType, GlobalEndpointManager globalEndpointManager, ConnectionPolicy connectionPolicy, @@ -156,8 +149,6 @@ public GatewayAddressCache( // Set requested API version header for version enforcement. defaultRequestHeaders.put(HttpConstants.HttpHeaders.VERSION, HttpConstants.Versions.CURRENT_VERSION); - this.serverPartitionAddressToPkRangeIdMap = new ConcurrentHashMap<>(); - this.tcpConnectionEndpointRediscoveryEnabled = tcpConnectionEndpointRediscoveryEnabled; this.lastForcedRefreshMap = new ConcurrentHashMap<>(); this.globalEndpointManager = globalEndpointManager; this.openConnectionsHandler = openConnectionsHandler; @@ -172,7 +163,6 @@ public GatewayAddressCache( IAuthorizationTokenProvider tokenProvider, UserAgentContainer userAgent, HttpClient httpClient, - boolean tcpConnectionEndpointRediscoveryEnabled, ApiType apiType, GlobalEndpointManager globalEndpointManager, ConnectionPolicy connectionPolicy, @@ -184,42 +174,12 @@ public GatewayAddressCache( userAgent, httpClient, DefaultSuboptimalPartitionForceRefreshIntervalInSeconds, - tcpConnectionEndpointRediscoveryEnabled, apiType, globalEndpointManager, connectionPolicy, openConnectionsHandler); } - @Override - public int updateAddresses(final URI serverKey) { - - Objects.requireNonNull(serverKey, "expected non-null serverKey"); - - AtomicInteger updatedCacheEntryCount = new AtomicInteger(0); - - if (this.tcpConnectionEndpointRediscoveryEnabled) { - this.serverPartitionAddressToPkRangeIdMap.computeIfPresent(serverKey, (uri, partitionKeyRangeIdentitySet) -> { - - for (PartitionKeyRangeIdentity partitionKeyRangeIdentity : partitionKeyRangeIdentitySet) { - if (partitionKeyRangeIdentity.getPartitionKeyRangeId().equals(PartitionKeyRange.MASTER_PARTITION_KEY_RANGE_ID)) { - this.masterPartitionAddressCache = null; - } else { - this.serverPartitionAddressCache.remove(partitionKeyRangeIdentity); - } - - updatedCacheEntryCount.incrementAndGet(); - } - - return null; - }); - } else { - logger.warn("tcpConnectionEndpointRediscovery is not enabled, should not reach here."); - } - - return updatedCacheEntryCount.get(); - } - @Override public Mono> tryGetAddresses(RxDocumentServiceRequest request, PartitionKeyRangeIdentity partitionKeyRangeIdentity, @@ -905,26 +865,6 @@ private Pair toPartitionAddress .collect(Collectors.toList()) .toArray(new AddressInformation[addresses.size()]); - if (this.tcpConnectionEndpointRediscoveryEnabled) { - for (AddressInformation addressInfo : addressInfos) { - if (logger.isDebugEnabled()) { - logger.debug( - "Added address to serverPartitionAddressToPkRangeIdMap: ({\"partitionKeyRangeIdentity\":{},\"address\":{}})", - partitionKeyRangeIdentity, - addressInfo); - } - - this.serverPartitionAddressToPkRangeIdMap.compute(addressInfo.getServerKey(), (serverKey, partitionKeyRangeIdentitySet) -> { - if (partitionKeyRangeIdentitySet == null) { - partitionKeyRangeIdentitySet = ConcurrentHashMap.newKeySet(); - } - - partitionKeyRangeIdentitySet.add(partitionKeyRangeIdentity); - return partitionKeyRangeIdentitySet; - }); - } - } - return Pair.of(partitionKeyRangeIdentity, addressInfos); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GlobalAddressResolver.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GlobalAddressResolver.java index 77eaf6c48188e..fd7302073f230 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GlobalAddressResolver.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GlobalAddressResolver.java @@ -31,9 +31,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkArgument; @@ -96,26 +94,6 @@ public GlobalAddressResolver( } } - @Override - public int updateAddresses(final URI serverKey) { - - Objects.requireNonNull(serverKey, "expected non-null serverKey"); - - AtomicInteger updatedCount = new AtomicInteger(0); - - if (this.tcpConnectionEndpointRediscoveryEnabled) { - for (EndpointCache endpointCache : this.addressCacheByEndpoint.values()) { - final GatewayAddressCache addressCache = endpointCache.addressCache; - - updatedCount.accumulateAndGet(addressCache.updateAddresses(serverKey), (oldValue, newValue) -> oldValue + newValue); - } - } else { - logger.warn("tcpConnectionEndpointRediscovery is not enabled, should not reach here."); - } - - return updatedCount.get(); - } - @Override public Flux openConnectionsAndInitCaches(String containerLink) { checkArgument(StringUtils.isNotEmpty(containerLink), "Argument 'containerLink' should not be null nor empty"); @@ -211,7 +189,6 @@ private EndpointCache getOrAddEndpoint(URI endpoint) { this.tokenProvider, this.userAgentContainer, this.httpClient, - this.tcpConnectionEndpointRediscoveryEnabled, this.apiType, this.endpointManager, this.connectionPolicy, diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GoneAndRetryWithRetryPolicy.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GoneAndRetryWithRetryPolicy.java index 1aae68b46dc43..c743ef28b0307 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GoneAndRetryWithRetryPolicy.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GoneAndRetryWithRetryPolicy.java @@ -46,8 +46,7 @@ public GoneAndRetryWithRetryPolicy(RxDocumentServiceRequest request, Integer wai waitTimeInSeconds, this.retryContext ); - this.retryWithRetryPolicy = new RetryWithRetryPolicy( - waitTimeInSeconds, this.retryContext); + this.retryWithRetryPolicy = new RetryWithRetryPolicy(waitTimeInSeconds, this.retryContext); this.start = Instant.now(); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressCache.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressCache.java index 8123db36fe31d..4a67cd86f4adf 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressCache.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressCache.java @@ -3,23 +3,14 @@ package com.azure.cosmos.implementation.directconnectivity; +import com.azure.cosmos.implementation.IOpenConnectionsHandler; import com.azure.cosmos.implementation.RxDocumentServiceRequest; import com.azure.cosmos.implementation.Utils; -import com.azure.cosmos.implementation.IOpenConnectionsHandler; import com.azure.cosmos.implementation.routing.PartitionKeyRangeIdentity; import reactor.core.publisher.Mono; -import java.net.URI; - public interface IAddressCache { - /** - * Update the physical address of the {@link PartitionKeyRangeIdentity partition key range identity} associated to the serverKey. - * - * - */ - int updateAddresses(URI serverKey); - /** * Resolves physical addresses by either PartitionKeyRangeIdentity. * diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressResolver.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressResolver.java index 0d70c26b60b3c..5fbcd28a139fe 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressResolver.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/IAddressResolver.java @@ -3,22 +3,18 @@ package com.azure.cosmos.implementation.directconnectivity; -import com.azure.cosmos.implementation.RxDocumentServiceRequest; import com.azure.cosmos.implementation.IOpenConnectionsHandler; import com.azure.cosmos.implementation.OpenConnectionResponse; +import com.azure.cosmos.implementation.RxDocumentServiceRequest; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.net.URI; - public interface IAddressResolver { Mono resolveAsync( RxDocumentServiceRequest request, boolean forceRefreshPartitionAddresses); - int updateAddresses(URI serverKey); - /*** * Warm up caches and open connections to all replicas of the container for the current read region. * diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListener.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListener.java index 105040b0d286a..448471667b46a 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListener.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdConnectionStateListener.java @@ -3,13 +3,15 @@ package com.azure.cosmos.implementation.directconnectivity.rntbd; -import com.azure.cosmos.implementation.directconnectivity.IAddressResolver; +import com.azure.cosmos.implementation.directconnectivity.Uri; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.time.Instant; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull; @@ -18,24 +20,29 @@ public class RntbdConnectionStateListener { private static final Logger logger = LoggerFactory.getLogger(RntbdConnectionStateListener.class); - private final IAddressResolver addressResolver; private final RntbdEndpoint endpoint; private final RntbdConnectionStateListenerMetrics metrics; + private final Set addressUris; // endregion // region Constructors - public RntbdConnectionStateListener(final IAddressResolver addressResolver, final RntbdEndpoint endpoint) { - this.addressResolver = checkNotNull(addressResolver, "expected non-null addressResolver"); + public RntbdConnectionStateListener(final RntbdEndpoint endpoint) { this.endpoint = checkNotNull(endpoint, "expected non-null endpoint"); this.metrics = new RntbdConnectionStateListenerMetrics(); + this.addressUris = ConcurrentHashMap.newKeySet(); } // endregion // region Methods + public void onBeforeSendRequest(Uri addressUri) { + checkNotNull(addressUri, "Argument 'addressUri' should not be null"); + this.addressUris.add(addressUri); + } + public void onException(Throwable exception) { checkNotNull(exception, "expect non-null exception"); @@ -81,7 +88,12 @@ private int onConnectionEvent(final RntbdConnectionEvent event, final Throwable RntbdObjectMapper.toJson(exception)); } - return this.addressResolver.updateAddresses(this.endpoint.serverKey()); + for (Uri addressUri : this.addressUris) { + addressUri.setUnhealthy(); + } + + return addressUris.size(); + } else { if (logger.isDebugEnabled()) { logger.debug("Endpoint closed while onConnectionEvent: {}", this.endpoint); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java index 432dcfb7e531c..9b5def28f2e25 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java @@ -123,8 +123,7 @@ private RntbdServiceEndpoint( this.maxConcurrentRequests = config.maxConcurrentRequestsPerEndpoint(); this.connectionStateListener = this.provider.addressResolver != null && config.isConnectionEndpointRediscoveryEnabled() - ? new RntbdConnectionStateListener(this.provider.addressResolver, this) - : null; + ? new RntbdConnectionStateListener(this) : null; this.channelPool = new RntbdClientChannelPool(this, bootstrap, config, clientTelemetry, this.connectionStateListener); this.clientTelemetry = clientTelemetry; @@ -270,6 +269,10 @@ public RntbdRequestRecord request(final RntbdRequestArgs args) { int concurrentRequestSnapshot = this.concurrentRequests.incrementAndGet(); + if (this.connectionStateListener != null) { + this.connectionStateListener.onBeforeSendRequest(args.physicalAddressUri()); + } + RntbdEndpointStatistics stat = endpointMetricsSnapshot(concurrentRequestSnapshot); if (concurrentRequestSnapshot > this.maxConcurrentRequests) { @@ -307,6 +310,10 @@ public OpenConnectionRntbdRequestRecord openConnection(Uri addressUri) { this.throwIfClosed(); + if (this.connectionStateListener != null) { + this.connectionStateListener.onBeforeSendRequest(addressUri); + } + OpenConnectionRntbdRequestRecord requestRecord = new OpenConnectionRntbdRequestRecord(addressUri); final Future openChannelFuture = this.channelPool.acquire(requestRecord); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ConnectionStateListenerTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ConnectionStateListenerTest.java index b9bddc9e8b819..b8ee6a722b4cb 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ConnectionStateListenerTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ConnectionStateListenerTest.java @@ -11,10 +11,10 @@ import com.azure.cosmos.implementation.ResourceType; import com.azure.cosmos.implementation.RxDocumentServiceRequest; import com.azure.cosmos.implementation.UserAgentContainer; -import com.azure.cosmos.implementation.directconnectivity.TcpServerMock.TcpServerFactory; -import com.azure.cosmos.implementation.directconnectivity.TcpServerMock.TcpServer; import com.azure.cosmos.implementation.directconnectivity.TcpServerMock.RequestResponseType; import com.azure.cosmos.implementation.directconnectivity.TcpServerMock.SslContextUtils; +import com.azure.cosmos.implementation.directconnectivity.TcpServerMock.TcpServer; +import com.azure.cosmos.implementation.directconnectivity.TcpServerMock.TcpServerFactory; import com.azure.cosmos.implementation.routing.PartitionKeyRangeIdentity; import io.netty.handler.ssl.SslContext; import org.mockito.Mockito; @@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger; import static com.azure.cosmos.implementation.TestUtils.mockDiagnosticsClientContext; +import static org.assertj.core.api.Assertions.assertThat; public class ConnectionStateListenerTest { private static final Logger logger = LoggerFactory.getLogger(ConnectionStateListenerTest.class); @@ -42,13 +43,13 @@ public class ConnectionStateListenerTest { @DataProvider(name = "connectionStateListenerConfigProvider") public Object[][] connectionStateListenerConfigProvider() { return new Object[][]{ - // isTcpConnectionEndpointRediscoveryEnabled, serverResponseType, updateAddresses() called times on request, updateAddresses() called times when server shutdown - {true, RequestResponseType.CHANNEL_FIN, 1, 0}, - {false, RequestResponseType.CHANNEL_FIN, 0, 0}, - {true, RequestResponseType.CHANNEL_RST, 0, 0}, - {false, RequestResponseType.CHANNEL_RST, 0, 0}, - {true, RequestResponseType.NONE, 0, 1}, // the request will be timed out, but the connection will be active. When tcp server shutdown, the connection will be closed gracefully - {false, RequestResponseType.NONE, 0, 0}, + // isTcpConnectionEndpointRediscoveryEnabled, serverResponseType, replicaStatusUpdated, replicaStatusUpdated when server shutdown + {true, RequestResponseType.CHANNEL_FIN, true, false}, + {false, RequestResponseType.CHANNEL_FIN, false, false}, + {true, RequestResponseType.CHANNEL_RST, false, false}, + {false, RequestResponseType.CHANNEL_RST, false, false}, + {true, RequestResponseType.NONE, false, true}, // the request will be timed out, but the connection will be active. When tcp server shutdown, the connection will be closed gracefully + {false, RequestResponseType.NONE, false, false}, }; } @@ -56,8 +57,8 @@ public Object[][] connectionStateListenerConfigProvider() { public void connectionStateListener_OnConnectionEvent( boolean isTcpConnectionEndpointRediscoveryEnabled, RequestResponseType responseType, - int timesOnRequest, - int timesOnServerShutdown) throws ExecutionException, InterruptedException { + boolean markUnhealthy, + boolean markUnhealthyWhenServerShutdown) throws ExecutionException, InterruptedException { // using a random generated server port int serverPort = port + randomPort.getAndIncrement(); @@ -96,12 +97,21 @@ public void connectionStateListener_OnConnectionEvent( logger.info("expected failed request with reason {}", e); } - Mockito.verify(addressResolver, Mockito.times(timesOnRequest)).updateAddresses(Mockito.any()); + if (markUnhealthy) { + assertThat(targetUri.getHealthStatus()).isEqualTo(Uri.HealthStatus.Unhealthy); + TcpServerFactory.shutdownRntbdServer(server); + assertThat(targetUri.getHealthStatus()).isEqualTo(Uri.HealthStatus.Unhealthy); - Mockito.clearInvocations(addressResolver); + } else { + assertThat(targetUri.getHealthStatus()).isEqualTo(Uri.HealthStatus.Connected); - TcpServerFactory.shutdownRntbdServer(server); - Mockito.verify(addressResolver, Mockito.times(timesOnServerShutdown)).updateAddresses(Mockito.any()); + TcpServerFactory.shutdownRntbdServer(server); + if (markUnhealthyWhenServerShutdown) { + assertThat(targetUri.getHealthStatus()).isEqualTo(Uri.HealthStatus.Unhealthy); + } else { + assertThat(targetUri.getHealthStatus()).isEqualTo(Uri.HealthStatus.Connected); + } + } } private Document getDocumentDefinition() { diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java index 1bab56dc0df5d..d5b6f89436f1b 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java @@ -126,7 +126,6 @@ public void getServerAddressesViaGateway(List partitionKeyRangeIds, authorizationTokenProvider, null, getHttpClient(configs), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -164,7 +163,6 @@ public void getMasterAddressesViaGatewayAsync(Protocol protocol) throws Exceptio authorizationTokenProvider, null, getHttpClient(configs), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -213,7 +211,6 @@ public void tryGetAddresses_ForDataPartitions(String partitionKeyRangeId, String authorizationTokenProvider, null, getHttpClient(configs), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -240,63 +237,6 @@ public void tryGetAddresses_ForDataPartitions(String partitionKeyRangeId, String assertSameAs(addressInfosFromCache, expectedAddresses); } - @Test(groups = { "direct" }, dataProvider = "targetPartitionsKeyRangeAndCollectionLinkParams", timeOut = TIMEOUT) - public void tryGetAddress_OnConnectionEvent_Refresh(String partitionKeyRangeId, String collectionLink, Protocol protocol) throws Exception { - - Configs configs = ConfigsBuilder.instance().withProtocol(protocol).build(); - URI serviceEndpoint = new URI(TestConfigurations.HOST); - IAuthorizationTokenProvider authorizationTokenProvider = (RxDocumentClientImpl) client; - HttpClientUnderTestWrapper httpClientWrapper = getHttpClientUnderTestWrapper(configs); - - GatewayAddressCache cache = new GatewayAddressCache( - mockDiagnosticsClientContext(), - serviceEndpoint, - protocol, - authorizationTokenProvider, - null, - httpClientWrapper.getSpyHttpClient(), - true, - null, - null, - ConnectionPolicy.getDefaultPolicy(), - null); - - RxDocumentServiceRequest req = - RxDocumentServiceRequest.create(mockDiagnosticsClientContext(), OperationType.Create, ResourceType.Document, - collectionLink, - new Database(), new HashMap<>()); - - PartitionKeyRangeIdentity partitionKeyRangeIdentity = new PartitionKeyRangeIdentity(createdCollection.getResourceId(), partitionKeyRangeId); - boolean forceRefreshPartitionAddresses = false; - - Mono> addressesInfosFromCacheObs = - cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses); - - ArrayList addressInfosFromCache = - Lists.newArrayList(getSuccessResult(addressesInfosFromCacheObs, TIMEOUT).v); - - assertThat(httpClientWrapper.capturedRequests) - .describedAs("getAddress will read addresses from gateway") - .asList().hasSize(1); - - httpClientWrapper.capturedRequests.clear(); - - // for the second request with the same partitionkeyRangeIdentity, the address result should be fetched from the cache - getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses), TIMEOUT); - assertThat(httpClientWrapper.capturedRequests) - .describedAs("getAddress should read from cache") - .asList().hasSize(0); - - httpClientWrapper.capturedRequests.clear(); - - // Now emulate onConnectionEvent happened, and the address should be removed from the cache - cache.updateAddresses(addressInfosFromCache.get(0).getServerKey()); - getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, forceRefreshPartitionAddresses), TIMEOUT); - assertThat(httpClientWrapper.capturedRequests) - .describedAs("getAddress will read addresses from gateway after onConnectionEvent") - .asList().hasSize(1); - } - @DataProvider(name = "openAsyncTargetAndTargetPartitionsKeyRangeAndCollectionLinkParams") public Object[][] openAsyncTargetAndPartitionsKeyRangeTargetAndCollectionLinkParams() { return new Object[][] { @@ -329,7 +269,6 @@ public void tryGetAddresses_ForDataPartitions_AddressCachedByOpenAsync_NoHttpReq authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -394,7 +333,6 @@ public void tryGetAddresses_ForDataPartitions_ForceRefresh( authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -462,7 +400,6 @@ public void tryGetAddresses_ForDataPartitions_Suboptimal_Refresh( null, httpClientWrapper.getSpyHttpClient(), suboptimalRefreshTime, - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -576,7 +513,6 @@ public void tryGetAddresses_ForMasterPartition(Protocol protocol) throws Excepti authorizationTokenProvider, null, getHttpClient(configs), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -628,7 +564,6 @@ public void tryGetAddresses_ForMasterPartition_MasterPartitionAddressAlreadyCach null, clientWrapper.getSpyHttpClient(), suboptimalPartitionForceRefreshIntervalInSeconds, - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -679,7 +614,6 @@ public void tryGetAddresses_ForMasterPartition_ForceRefresh() throws Exception { authorizationTokenProvider, null, clientWrapper.getSpyHttpClient(), - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -737,7 +671,6 @@ public void tryGetAddresses_SuboptimalMasterPartition_NotStaleEnough_NoRefresh() null, clientWrapper.getSpyHttpClient(), refreshPeriodInSeconds, - false, ApiType.SQL, null, ConnectionPolicy.getDefaultPolicy(), @@ -835,7 +768,6 @@ public void tryGetAddresses_SuboptimalMasterPartition_Stale_DoRefresh() throws E null, clientWrapper.getSpyHttpClient(), refreshPeriodInSeconds, - false, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -951,7 +883,6 @@ public void tryGetAddress_replicaValidationTests(boolean replicaValidationEnable authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - true, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -1044,7 +975,6 @@ public void tryGetAddress_failedEndpointTests() throws Exception { authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - true, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -1107,7 +1037,6 @@ public void tryGetAddress_unhealthyStatus_forceRefresh() throws Exception { authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - true, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -1175,7 +1104,6 @@ public void validateReplicaAddressesTests() throws URISyntaxException, NoSuchMet authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - true, null, null, ConnectionPolicy.getDefaultPolicy(), @@ -1230,7 +1158,6 @@ public void mergeAddressesTests() throws URISyntaxException, NoSuchMethodExcepti authorizationTokenProvider, null, httpClientWrapper.getSpyHttpClient(), - true, null, null, ConnectionPolicy.getDefaultPolicy(), From 5d45713037b3d971081e6a8e2153e73caa8bd5a4 Mon Sep 17 00:00:00 2001 From: annie-mac Date: Fri, 2 Sep 2022 07:54:41 -0700 Subject: [PATCH 14/18] update --- .../implementation/CosmosSchedulers.java | 10 +++ .../caches/AsyncCacheNonBlocking.java | 77 +++++++++++++++---- .../GatewayAddressCache.java | 55 ++++++++----- 3 files changed, 108 insertions(+), 34 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java index aa79b180ef0d0..e8016e1da4421 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/CosmosSchedulers.java @@ -11,6 +11,7 @@ public class CosmosSchedulers { private final static String TRANSPORT_RESPONSE_BOUNDED_ELASTIC_THREAD_NAME = "transport-response-bounded-elastic"; private final static String BULK_EXECUTOR_BOUNDED_ELASTIC_THREAD_NAME = "bulk-executor-bounded-elastic"; private final static String OPEN_CONNECTIONS_BOUNDED_ELASTIC_THREAD_NAME = "open-connections-bounded-elastic"; + private final static String ASYNC_CACHE_BACKGROUND_REFRESH_THREAD_NAME = "async-cache-background-refresh-bounded-elastic"; private final static int TTL_FOR_SCHEDULER_WORKER_IN_SECONDS = 60; // same as BoundedElasticScheduler.DEFAULT_TTL_SECONDS // Using a custom parallel scheduler to be able to schedule retries etc. @@ -47,4 +48,13 @@ public class CosmosSchedulers { TTL_FOR_SCHEDULER_WORKER_IN_SECONDS, true ); + + // Custom bounded elastic scheduler for async cache background refresh task + public final static Scheduler ASYNC_CACHE_BACKGROUND_REFRESH_BOUNDED_ELASTIC = Schedulers.newBoundedElastic( + Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, + Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + ASYNC_CACHE_BACKGROUND_REFRESH_THREAD_NAME, + TTL_FOR_SCHEDULER_WORKER_IN_SECONDS, + true + ); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java index 18304146a9302..6862f7ed199d1 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java @@ -3,6 +3,7 @@ package com.azure.cosmos.implementation.caches; import com.azure.cosmos.CosmosException; +import com.azure.cosmos.implementation.CosmosSchedulers; import com.azure.cosmos.implementation.Exceptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,7 +82,7 @@ public Mono getAsync( return Mono.just(value); } - Mono refreshMono = initialLazyValue.createAndWaitForBackgroundRefreshTaskAsync(key, singleValueInitFunc); + Mono refreshMono = initialLazyValue.getOrCreateBackgroundRefreshTaskAsync(singleValueInitFunc); return refreshMono.onErrorResume( (exception) -> { @@ -125,6 +126,22 @@ public Mono getAsync( ); } + public void refresh( + TKey key, + Function> singleValueInitFunc) { + + logger.debug("refreshing cache[{}]", key); + AsyncLazyWithRefresh initialLazyValue = values.get(key); + if (initialLazyValue != null) { + Mono backgroundRefreshTask = initialLazyValue.refresh(singleValueInitFunc); + if (backgroundRefreshTask != null) { + backgroundRefreshTask + .subscribeOn(CosmosSchedulers.ASYNC_CACHE_BACKGROUND_REFRESH_BOUNDED_ELASTIC) + .subscribe(); + } + } + } + public void set(TKey key, TValue value) { logger.debug("set cache[{}]={}", key, value); AsyncLazyWithRefresh updatedValue = new AsyncLazyWithRefresh(value); @@ -141,11 +158,9 @@ public void remove(TKey key) { * to use the stale value while the refresh is occurring. */ private class AsyncLazyWithRefresh { -// private final Function> createValueFunc; private final AtomicBoolean removeFromCache = new AtomicBoolean(false); private final AtomicReference> value; - private Mono refreshInProgress; - private final AtomicBoolean refreshInProgressCompleted = new AtomicBoolean(false); + private final AtomicReference> refreshInProgress; public AsyncLazyWithRefresh(TValue value) { this.value = new AtomicReference<>(); @@ -169,25 +184,57 @@ public Mono value() { } @SuppressWarnings("unchecked") - public Mono createAndWaitForBackgroundRefreshTaskAsync(TKey key, Function> createRefreshFunction) { - Mono valueMono = this.value.get(); + public Mono getOrCreateBackgroundRefreshTaskAsync(Function> createRefreshFunction) { + if (this.refreshInProgress.compareAndSet(null, this.createBackgroundRefreshTask(createRefreshFunction))) { + logger.debug("Started a new background task"); + } else { + logger.debug("Background refresh task is already in progress"); + } - return valueMono.flatMap(value -> { - if(this.refreshInProgressCompleted.compareAndSet(false, true)) { - this.refreshInProgress = createRefreshFunction.apply(value).cache(); - return this.refreshInProgress + Mono refreshInProgressSnapshot = this.refreshInProgress.get(); + return refreshInProgressSnapshot == null ? this.value.get() : refreshInProgressSnapshot; + } + + private Mono createBackgroundRefreshTask(Function> createRefreshFunction) { + return this.value + .get() + .flatMap(cachedValue -> createRefreshFunction.apply(cachedValue)) .flatMap(response -> { + this.refreshInProgress.set(null); this.value.set(Mono.just(response)); - this.refreshInProgressCompleted.set(false); return this.value.get(); - }).doOnError(e -> this.refreshInProgressCompleted.set(false)); + }) + .doOnError(throwable -> { + logger.warn("Background refresh task failed", throwable); + this.refreshInProgress.set(null); + }) + .cache(); + } - } - return this.refreshInProgress == null ? valueMono : refreshInProgress; - }); + /*** + * If there is no refresh in progress background task, then create a new one, else skip + * + * @param createRefreshFunction the createRefreshFunction + * @return if there is already a refreshInProgress task ongoing, then return Mono.empty, else return the newly created background refresh task + */ + public Mono refresh(Function> createRefreshFunction) { + if (this.refreshInProgress.compareAndSet(null, this.createBackgroundRefreshTask(createRefreshFunction))) { + logger.debug("Started a new background task"); + return this.refreshInProgress.get(); + } + + logger.debug("Background refresh task is already in progress, skip creating a new one"); + return Mono.empty(); } public boolean shouldRemoveFromCache() { + // Multiple threads could subscribe to the Mono, only one of them will be allowed to remove the Mono from the cache + // For example for the following scenario: + // Request1 -> getAsync -> Mono1 + // Request2 -> getAsync -> Mono1 + // Mono1 failed, and we decided to remove this entry from the cache. Request1 has removed the entry from the cache + // Request3 -> getAsync -> Mono2 + // without this check, request2 will end up removing the cache entry created by request3 return this.removeFromCache.compareAndSet(false, true); } } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java index c17629620081a..0f0d24d6a9ee3 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCache.java @@ -60,10 +60,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -100,7 +100,7 @@ public class GatewayAddressCache implements IAddressCache { private IOpenConnectionsHandler openConnectionsHandler; private final ConnectionPolicy connectionPolicy; private final boolean replicaAddressValidationEnabled; - private final List replicaValidationScopes; + private final Set replicaValidationScopes; public GatewayAddressCache( DiagnosticsClientContext clientContext, @@ -156,7 +156,7 @@ public GatewayAddressCache( this.openConnectionsHandler = openConnectionsHandler; this.connectionPolicy = connectionPolicy; this.replicaAddressValidationEnabled = Configs.isReplicaAddressValidationEnabled(); - this.replicaValidationScopes = new ArrayList<>(); + this.replicaValidationScopes = ConcurrentHashMap.newKeySet(); if (this.replicaAddressValidationEnabled) { this.replicaValidationScopes.add(Uri.HealthStatus.UnhealthyPending); } @@ -255,8 +255,7 @@ public Mono> tryGetAddresses(RxDocumentS for (Uri failedEndpoints : request.requestContext.getFailedEndpoints()) { failedEndpoints.setUnhealthy(); } - return forceRefreshPartitionAddressesModified - || Arrays.stream(cachedAddresses).anyMatch(addressInformation -> addressInformation.getPhysicalUri().shouldRefreshHealthStatus()); + return forceRefreshPartitionAddressesModified; }) .map(Utils.ValueHolder::new); @@ -269,6 +268,20 @@ public Mono> tryGetAddresses(RxDocumentS this.suboptimalServerPartitionTimestamps.putIfAbsent(partitionKeyRangeIdentity, Instant.now()); } + // Refresh the cache if there was an address has been marked as unhealthy long enough and need to revalidate its status + // If you are curious about why we do not depend on 410 to force refresh the addresses, the reason being: + // When an address is marked as unhealthy, then the address enumerator will move it to the end of the list + // So it could happen that no request will use the unhealthy address for an extended period of time + // So the 410 -> forceRefresh workflow may not happen + if (Arrays + .stream(addressesValueHolder.v) + .anyMatch(addressInformation -> addressInformation.getPhysicalUri().shouldRefreshHealthStatus())) { + logger.info("refresh cache due to address uri in unhealthy status"); + this.serverPartitionAddressCache.refresh( + partitionKeyRangeIdentity, + cachedAddresses -> this.getAddressesForRangeId(request, partitionKeyRangeIdentity, true, cachedAddresses)); + } + return addressesValueHolder; }) .onErrorResume(ex -> { @@ -841,20 +854,24 @@ private void validateReplicaAddresses(AddressInformation[] addresses) { // By theory, when we reach here, the status of the address should be in one of the three status: Unknown, Connected, UnhealthyPending // using open connection to validate addresses in UnhealthyPending status // Could extend to also open connection for unknown in the future - List addressesNeedToValidation = - Arrays - .stream(addresses) - .map(address -> address.getPhysicalUri()) - .filter(addressUri -> this.replicaValidationScopes.contains(addressUri.getHealthStatus())) - .sorted(new Comparator() { - @Override - public int compare(Uri o1, Uri o2) { - // Generally, an unhealthyPending replica has more chances to fail the request compared to unknown replica - // and we will want to validate replicas with the highest chance to fail the request first - return o2.getHealthStatus().getPriority() - o1.getHealthStatus().getPriority(); - } - }) - .collect(Collectors.toList()); + + List addressesNeedToValidation = new ArrayList<>(); + for (AddressInformation address : addresses) { + if (this.replicaValidationScopes.contains(address.getPhysicalUri().getHealthStatus())) { + switch (address.getPhysicalUri().getHealthStatus()) { + case UnhealthyPending: + // Generally, an unhealthyPending replica has more chances to fail the request compared to unknown replica + // so we want to put it at the head of the validation list + addressesNeedToValidation.add(0, address.getPhysicalUri()); + break; + case Unknown: + addressesNeedToValidation.add(address.getPhysicalUri()); + break; + default: + throw new IllegalStateException("Validate replica status is not support for status " + address.getPhysicalUri().getHealthStatus()); + } + } + } if (addressesNeedToValidation.size() > 0) { this.openConnectionsHandler From 45d5a27c3e9ba4a34d59c6ec5014aba49d3d67cb Mon Sep 17 00:00:00 2001 From: annie-mac Date: Fri, 2 Sep 2022 08:10:20 -0700 Subject: [PATCH 15/18] fix tests --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 1 + .../cosmos/implementation/caches/AsyncCacheNonBlocking.java | 4 ++-- .../directconnectivity/GatewayAddressCacheTest.java | 6 +++++- .../implementation/directconnectivity/ReflectionUtils.java | 5 +++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 8765d0927eb27..fbc3f8933cde4 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -12,6 +12,7 @@ * Added system property to turn on replica validation - See [PR 29767](https://github.com/Azure/azure-sdk-for-java/pull/29767) * Added improvement to avoid retry on same replica that previously failed with 410, 408 and >= 500 status codes - See [PR 29767](https://github.com/Azure/azure-sdk-for-java/pull/29767) * Improvement when `connectionEndpointRediscoveryEnabled` is enabled - See [PR 30281](https://github.com/Azure/azure-sdk-for-java/pull/30281) +* Added replica validation for Unknown status if `openConnectionsAndInitCaches` is used and replica validation is enabled - See [PR 30277](https://github.com/Azure/azure-sdk-for-java/pull/30277) ### 4.35.1 (2022-08-29) #### Other Changes diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java index 6862f7ed199d1..cc792a9575f19 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java @@ -165,13 +165,13 @@ private class AsyncLazyWithRefresh { public AsyncLazyWithRefresh(TValue value) { this.value = new AtomicReference<>(); this.value.set(Mono.just(value)); - this.refreshInProgress = null; + this.refreshInProgress = new AtomicReference<>(null); } public AsyncLazyWithRefresh(Function> taskFactory) { this.value = new AtomicReference<>(); this.value.set(taskFactory.apply(null).cache()); - this.refreshInProgress = null; + this.refreshInProgress = new AtomicReference<>(null); } public Mono getValueAsync() { diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java index 52804a5c0f8ba..a6f6f6ddae92a 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/GatewayAddressCacheTest.java @@ -55,6 +55,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -1114,6 +1115,9 @@ public void tryGetAddress_unhealthyStatus_forceRefresh() throws Exception { ArrayList cachedAddresses = Lists.newArrayList(getSuccessResult(cache.tryGetAddresses(req, partitionKeyRangeIdentity, false), TIMEOUT).v); + // since the refresh will happen asynchronously in the background, wait here some time for it to happen + Thread.sleep(500); + // validate the cache will be refreshed assertThat(httpClientWrapper.capturedRequests) .describedAs("getAddress will read addresses from gateway") @@ -1163,7 +1167,7 @@ public void validateReplicaAddressesTests() throws URISyntaxException, NoSuchMet healthStatus.set(UnhealthyPending); // Set the replica validation scope - List replicaValidationScopes = ReflectionUtils.getReplicaValidationScopes(cache); + Set replicaValidationScopes = ReflectionUtils.getReplicaValidationScopes(cache); replicaValidationScopes.add(Unknown); replicaValidationScopes.add(UnhealthyPending); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java index 8941c9ab82413..56dcd9b11160a 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/directconnectivity/ReflectionUtils.java @@ -49,6 +49,7 @@ import java.lang.reflect.Method; import java.time.Duration; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; @@ -398,7 +399,7 @@ public static AtomicReference getHealthStatus(Uri uri) { } @SuppressWarnings("unchecked") - public static List getReplicaValidationScopes(GatewayAddressCache gatewayAddressCache) { - return get(List.class, gatewayAddressCache, "replicaValidationScopes"); + public static Set getReplicaValidationScopes(GatewayAddressCache gatewayAddressCache) { + return get(Set.class, gatewayAddressCache, "replicaValidationScopes"); } } From d05b33661cc37af496d63a91c374b6b6d960c311 Mon Sep 17 00:00:00 2001 From: annie-mac Date: Fri, 2 Sep 2022 08:15:19 -0700 Subject: [PATCH 16/18] clean code --- .../cosmos/implementation/caches/AsyncCacheNonBlocking.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java index cc792a9575f19..061a8c9a56ea5 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java @@ -201,8 +201,7 @@ private Mono createBackgroundRefreshTask(Function> .flatMap(cachedValue -> createRefreshFunction.apply(cachedValue)) .flatMap(response -> { this.refreshInProgress.set(null); - this.value.set(Mono.just(response)); - return this.value.get(); + return this.value.updateAndGet(existingValue -> Mono.just(response)); }) .doOnError(throwable -> { logger.warn("Background refresh task failed", throwable); From 729b5b63ad0c574dc293a3a9a0450df0d0140a3a Mon Sep 17 00:00:00 2001 From: annie-mac Date: Fri, 2 Sep 2022 08:30:29 -0700 Subject: [PATCH 17/18] add refresh function test --- .../caches/AsyncCacheNonBlockingTest.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlockingTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlockingTest.java index c905b63dd3f51..ef246c9cd7b7a 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlockingTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlockingTest.java @@ -7,6 +7,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -15,7 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class AsyncCacheNonBlockingTest { - private static final int TIMEOUT = 2000; + private static final int TIMEOUT = 20000; @Test(groups = {"unit"}, timeOut = TIMEOUT) public void getAsync() { @@ -80,6 +81,40 @@ public void getAsync() { assertThat(numberOfCacheRefreshes.get()).isEqualTo(20); // verify that we still have the old value in the cache assertThat(cache.getAsync(2, value -> refreshFunc2.apply(2), forceRefresh -> false).block()).isEqualTo(5); + } + + @Test(groups = {"unit"}, timeOut = TIMEOUT) + public void refreshAsync() throws InterruptedException { + AtomicInteger numberOfCacheRefreshes = new AtomicInteger(0); + final Function> refreshFunc = key -> { + return Mono.just(key * 2) + .doOnNext(t -> { + numberOfCacheRefreshes.incrementAndGet(); + }); + }; + + AsyncCacheNonBlocking cache = new AsyncCacheNonBlocking<>(); + // populate the cache + int cacheKey = 1; + cache.getAsync(cacheKey, value -> refreshFunc.apply(cacheKey), forceRefresh -> false).block(); + assertThat(numberOfCacheRefreshes.get()).isEqualTo(1); + + // refresh the cache, since there is no refresh in progress, it will start a new one + Function> refreshFuncWithDelay = key -> { + return Mono.just(key * 2) + .doOnNext(t -> numberOfCacheRefreshes.incrementAndGet()) + .delayElement(Duration.ofMinutes(5)); + }; + + cache.refresh(cacheKey, refreshFuncWithDelay); + //since the refresh happens asynchronously, so wait for sometime + Thread.sleep(100); + assertThat(numberOfCacheRefreshes.get()).isEqualTo(2); + // start another refresh, since there is a refresh in progress, so it will not start a new one + cache.refresh(cacheKey, refreshFunc); + //since the refresh happens asynchronously, so wait for sometime + Thread.sleep(100); + assertThat(numberOfCacheRefreshes.get()).isEqualTo(2); } } From 40eda1498b48966d5813f9c010eddc5d2197351e Mon Sep 17 00:00:00 2001 From: annie-mac Date: Fri, 2 Sep 2022 10:33:59 -0700 Subject: [PATCH 18/18] resolve comments --- .../caches/AsyncCacheNonBlocking.java | 32 ++++++++++--------- .../directconnectivity/Uri.java | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java index 061a8c9a56ea5..5b86a8194fb5f 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/caches/AsyncCacheNonBlocking.java @@ -86,22 +86,22 @@ public Mono getAsync( return refreshMono.onErrorResume( (exception) -> { - logger.debug("refresh cache [{}] resulted in error", key, exception); - // In some scenarios when a background failure occurs like a 404 the initial cache value should be removed. if (exception instanceof CosmosException && removeNotFoundFromCacheException((CosmosException) exception)) { if (initialLazyValue.shouldRemoveFromCache()) { this.remove(key); } } + + logger.debug("refresh cache [{}] resulted in error", key, exception); return Mono.error(exception); } ); }).onErrorResume((exception) -> { - logger.debug("cache[{}] resulted in error", key, exception); if (initialLazyValue.shouldRemoveFromCache()) { this.remove(key); } + logger.debug("cache[{}] resulted in error", key, exception); return Mono.error(exception); }); } @@ -116,11 +116,11 @@ public Mono getAsync( return result.getValueAsync().onErrorResume( (exception) -> { - logger.debug("cache[{}] resulted in error", key, exception); // Remove the failed task from the dictionary so future requests can send other calls. if (result.shouldRemoveFromCache()) { this.remove(key); } + logger.debug("cache[{}] resulted in error", key, exception); return Mono.error(exception); } ); @@ -157,7 +157,7 @@ public void remove(TKey key) { * be used to update the value. This allows concurrent requests * to use the stale value while the refresh is occurring. */ - private class AsyncLazyWithRefresh { + private static class AsyncLazyWithRefresh { private final AtomicBoolean removeFromCache = new AtomicBoolean(false); private final AtomicReference> value; private final AtomicReference> refreshInProgress; @@ -183,29 +183,31 @@ public Mono value() { return value.get(); } - @SuppressWarnings("unchecked") public Mono getOrCreateBackgroundRefreshTaskAsync(Function> createRefreshFunction) { - if (this.refreshInProgress.compareAndSet(null, this.createBackgroundRefreshTask(createRefreshFunction))) { - logger.debug("Started a new background task"); - } else { - logger.debug("Background refresh task is already in progress"); - } + Mono refreshInProgressSnapshot = this.refreshInProgress.updateAndGet(existingMono -> { + if (existingMono == null) { + logger.debug("Started a new background task"); + return this.createBackgroundRefreshTask(createRefreshFunction); + } else { + logger.debug("Background refresh task is already in progress"); + } - Mono refreshInProgressSnapshot = this.refreshInProgress.get(); + return existingMono; + }); return refreshInProgressSnapshot == null ? this.value.get() : refreshInProgressSnapshot; } private Mono createBackgroundRefreshTask(Function> createRefreshFunction) { return this.value .get() - .flatMap(cachedValue -> createRefreshFunction.apply(cachedValue)) + .flatMap(createRefreshFunction) .flatMap(response -> { this.refreshInProgress.set(null); return this.value.updateAndGet(existingValue -> Mono.just(response)); }) .doOnError(throwable -> { - logger.warn("Background refresh task failed", throwable); this.refreshInProgress.set(null); + logger.warn("Background refresh task failed", throwable); }) .cache(); } @@ -223,7 +225,7 @@ public Mono refresh(Function> createRefreshFunction } logger.debug("Background refresh task is already in progress, skip creating a new one"); - return Mono.empty(); + return null; } public boolean shouldRemoveFromCache() { diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java index d201e3dbaeef8..02179a15454a6 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/Uri.java @@ -160,7 +160,7 @@ public HealthStatus getEffectiveHealthStatus() { public boolean shouldRefreshHealthStatus() { return this.healthStatus.get() == HealthStatus.Unhealthy - && Instant.now().compareTo(this.lastUnhealthyTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0; + && Instant.now().compareTo(this.lastUnhealthyTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) >= 0; } public String getHealthStatusDiagnosticString() {