From f66d9edc3ac970ad2aeb008fb4f3788c72ea2790 Mon Sep 17 00:00:00 2001 From: Shawn Fang <45607042+mssfang@users.noreply.github.com> Date: Fri, 24 Feb 2023 11:32:59 -0800 Subject: [PATCH 1/8] [AppConfig] Add back the appconfig artifacts in tests.yml (#33665) --- .../azure-data-appconfiguration/pom.xml | 2 ++ .../ConfigurationAsyncClientTest.java | 16 +++++++--------- .../ConfigurationClientBuilderTest.java | 3 +-- sdk/appconfiguration/tests.yml | 4 ++++ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/pom.xml b/sdk/appconfiguration/azure-data-appconfiguration/pom.xml index 32f1aba3f17a5..861ef99480a5c 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/pom.xml +++ b/sdk/appconfiguration/azure-data-appconfiguration/pom.xml @@ -35,6 +35,8 @@ + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED + --add-exports com.azure.core/com.azure.core.implementation.util=ALL-UNNAMED --add-exports com.azure.core/com.azure.core.implementation.http=ALL-UNNAMED --add-opens com.azure.data.appconfiguration/com.azure.data.appconfiguration=ALL-UNNAMED diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java index 69335260080c9..56d0d3e694cfc 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java @@ -1133,14 +1133,13 @@ public void listRevisionsWithPagination(HttpClient httpClient, ConfigurationServ settings.add(new ConfigurationSetting().setKey(keyPrefix).setValue("myValue" + value).setLabel(labelPrefix)); } - List>> results = new ArrayList<>(); for (ConfigurationSetting setting : settings) { - results.add(client.setConfigurationSettingWithResponse(setting, false)); + StepVerifier.create(client.setConfigurationSetting(setting)) + .expectNextCount(1) + .verifyComplete(); } SettingSelector filter = new SettingSelector().setKeyFilter(keyPrefix).setLabelFilter(labelPrefix); - - Flux.merge(results).blockLast(); StepVerifier.create(client.listRevisions(filter)) .expectNextCount(numberExpected) .verifyComplete(); @@ -1156,17 +1155,16 @@ public void listRevisionsWithPaginationAndRepeatStream(HttpClient httpClient, Co client = getConfigurationAsyncClient(httpClient, serviceVersion); final int numberExpected = 50; List settings = new ArrayList<>(numberExpected); - List>> results = new ArrayList<>(); for (int value = 0; value < numberExpected; value++) { ConfigurationSetting setting = new ConfigurationSetting().setKey(keyPrefix).setValue("myValue" + value).setLabel(labelPrefix); settings.add(setting); - results.add(client.setConfigurationSettingWithResponse(setting, false)); + StepVerifier.create(client.setConfigurationSetting(setting)) + .expectNextCount(1) + .verifyComplete(); } SettingSelector filter = new SettingSelector().setKeyFilter(keyPrefix).setLabelFilter(labelPrefix); - Flux.merge(results).blockLast(); - List configurationSettingList1 = new ArrayList<>(); List configurationSettingList2 = new ArrayList<>(); @@ -1192,7 +1190,7 @@ public void listRevisionsWithPaginationAndRepeatIterator(HttpClient httpClient, for (int value = 0; value < numberExpected; value++) { ConfigurationSetting setting = new ConfigurationSetting().setKey(keyPrefix).setValue("myValue" + value).setLabel(labelPrefix); settings.add(setting); - results.add(client.setConfigurationSettingWithResponse(setting, false)); + StepVerifier.create(client.setConfigurationSetting(setting)).expectNextCount(1).verifyComplete(); } SettingSelector filter = new SettingSelector().setKeyFilter(keyPrefix).setLabelFilter(labelPrefix); diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientBuilderTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientBuilderTest.java index 5ff44fb694f2d..5c4eb51327874 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientBuilderTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientBuilderTest.java @@ -6,7 +6,6 @@ import com.azure.core.exception.HttpResponseException; import com.azure.core.http.HttpClient; import com.azure.core.http.HttpPipelineBuilder; -import com.azure.core.http.netty.NettyAsyncHttpClientBuilder; import com.azure.core.http.policy.ExponentialBackoffOptions; import com.azure.core.http.policy.FixedDelay; import com.azure.core.http.policy.HttpLogDetailLevel; @@ -190,7 +189,7 @@ public void defaultPipeline() { () -> clientBuilder.buildClient().setConfigurationSetting(key, null, value)); } HttpClient defaultHttpClient = interceptorManager.isPlaybackMode() ? interceptorManager.getPlaybackClient() - : new NettyAsyncHttpClientBuilder().wiretap(true).build(); + : HttpClient.createDefault(); clientBuilder.pipeline(null).httpClient(defaultHttpClient); diff --git a/sdk/appconfiguration/tests.yml b/sdk/appconfiguration/tests.yml index 00a60bf76ab21..69d1357679f3a 100644 --- a/sdk/appconfiguration/tests.yml +++ b/sdk/appconfiguration/tests.yml @@ -4,6 +4,10 @@ stages: - template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: ServiceDirectory: appconfiguration + Artifacts: + - name: azure-data-appconfiguration + groupId: com.azure + safeName: azuredataappconfiguration TimeoutInMinutes: 90 SupportedClouds: "Public,UsGov,China" EnvVars: From 71759bc99ff6b2ee90b27a7a3dc624b4e37e524a Mon Sep 17 00:00:00 2001 From: wangrui-msft <86443899+wangrui-msft@users.noreply.github.com> Date: Fri, 24 Feb 2023 11:44:33 -0800 Subject: [PATCH 2/8] Callautomation/speech (#33735) * Add new reason code * Add speechlanguage to recognize option * fix string empty check --- .../callautomation/CallConnectionAsync.java | 1 - .../callautomation/CallMediaAsync.java | 6 +++++ .../models/RecognizeOptionsInternal.java | 26 +++++++++++++++++++ .../TransferToParticipantRequestInternal.java | 26 ------------------- .../CallMediaRecognizeChoiceOptions.java | 26 +++++++++++++++++++ .../CallMediaAsyncUnitTests.java | 2 ++ .../swagger/README.md | 2 +- 7 files changed, 61 insertions(+), 28 deletions(-) diff --git a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallConnectionAsync.java b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallConnectionAsync.java index 62a621e3d8b58..eeaacd7d7f4c5 100644 --- a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallConnectionAsync.java +++ b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallConnectionAsync.java @@ -266,7 +266,6 @@ Mono> transferToParticipantCallWithResponseInternal TransferToParticipantRequestInternal request = new TransferToParticipantRequestInternal() .setTargetParticipant(CommunicationIdentifierConverter.convert(transferToParticipantCallOptions.getTargetCallInvite().getTarget())) - .setTransfereeCallerId(PhoneNumberIdentifierConverter.convert(transferToParticipantCallOptions.getTargetCallInvite().getSourceCallIdNumber())) .setOperationContext(transferToParticipantCallOptions.getOperationContext()); CallInvite callInvite = transferToParticipantCallOptions.getTargetCallInvite(); diff --git a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallMediaAsync.java b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallMediaAsync.java index 2c51aceb3f8be..e29fabf8808fb 100644 --- a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallMediaAsync.java +++ b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/CallMediaAsync.java @@ -197,6 +197,12 @@ Mono> recognizeWithResponseInternal(CallMediaRecognizeOptions rec playSourceInternal = translatePlaySourceToPlaySourceInternal(playSource); } + if (choiceRecognizeOptions.getSpeechLanguage() != null) { + if (!choiceRecognizeOptions.getSpeechLanguage().isEmpty()) { + recognizeOptionsInternal.setSpeechLanguage(choiceRecognizeOptions.getSpeechLanguage()); + } + } + RecognizeRequest recognizeRequest = new RecognizeRequest() .setRecognizeInputType(RecognizeInputTypeInternal.fromString(choiceRecognizeOptions.getRecognizeInputType().toString())) .setInterruptCallMediaOperation(choiceRecognizeOptions.isInterruptCallMediaOperation()) diff --git a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/RecognizeOptionsInternal.java b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/RecognizeOptionsInternal.java index 1e43bb1efea4f..d5060c9c4752c 100644 --- a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/RecognizeOptionsInternal.java +++ b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/RecognizeOptionsInternal.java @@ -29,6 +29,12 @@ public final class RecognizeOptionsInternal { @JsonProperty(value = "targetParticipant", required = true) private CommunicationIdentifierModel targetParticipant; + /* + * Speech language to be recognized, If not set default is en-US + */ + @JsonProperty(value = "speechLanguage") + private String speechLanguage; + /* * Defines configurations for DTMF. */ @@ -101,6 +107,26 @@ public RecognizeOptionsInternal setTargetParticipant(CommunicationIdentifierMode return this; } + /** + * Get the speechLanguage property: Speech language to be recognized, If not set default is en-US. + * + * @return the speechLanguage value. + */ + public String getSpeechLanguage() { + return this.speechLanguage; + } + + /** + * Set the speechLanguage property: Speech language to be recognized, If not set default is en-US. + * + * @param speechLanguage the speechLanguage value to set. + * @return the RecognizeOptionsInternal object itself. + */ + public RecognizeOptionsInternal setSpeechLanguage(String speechLanguage) { + this.speechLanguage = speechLanguage; + return this; + } + /** * Get the dtmfOptions property: Defines configurations for DTMF. * diff --git a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/TransferToParticipantRequestInternal.java b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/TransferToParticipantRequestInternal.java index 83789bf0b2f64..bd44f141a4345 100644 --- a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/TransferToParticipantRequestInternal.java +++ b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/implementation/models/TransferToParticipantRequestInternal.java @@ -16,12 +16,6 @@ public final class TransferToParticipantRequestInternal { @JsonProperty(value = "targetParticipant", required = true) private CommunicationIdentifierModel targetParticipant; - /* - * The caller ID of the transferee when transferring to PSTN. - */ - @JsonProperty(value = "transfereeCallerId") - private PhoneNumberIdentifierModel transfereeCallerId; - /* * Used by customer to send custom context to targets */ @@ -55,26 +49,6 @@ public TransferToParticipantRequestInternal setTargetParticipant(CommunicationId return this; } - /** - * Get the transfereeCallerId property: The caller ID of the transferee when transferring to PSTN. - * - * @return the transfereeCallerId value. - */ - public PhoneNumberIdentifierModel getTransfereeCallerId() { - return this.transfereeCallerId; - } - - /** - * Set the transfereeCallerId property: The caller ID of the transferee when transferring to PSTN. - * - * @param transfereeCallerId the transfereeCallerId value to set. - * @return the TransferToParticipantRequestInternal object itself. - */ - public TransferToParticipantRequestInternal setTransfereeCallerId(PhoneNumberIdentifierModel transfereeCallerId) { - this.transfereeCallerId = transfereeCallerId; - return this; - } - /** * Get the customContext property: Used by customer to send custom context to targets. * diff --git a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/models/CallMediaRecognizeChoiceOptions.java b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/models/CallMediaRecognizeChoiceOptions.java index c8f17f62b8d4b..04d78bba4edde 100644 --- a/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/models/CallMediaRecognizeChoiceOptions.java +++ b/sdk/communication/azure-communication-callautomation/src/main/java/com/azure/communication/callautomation/models/CallMediaRecognizeChoiceOptions.java @@ -14,6 +14,11 @@ public class CallMediaRecognizeChoiceOptions extends CallMediaRecognizeOptions { */ private final List recognizeChoices; + /* + * Speech language to be recognized, If not set default is en-US + */ + private String speechLanguage; + /** * Get the list of recognize choice. * @@ -23,6 +28,27 @@ public List getRecognizeChoices() { return this.recognizeChoices; } + /** + * Set the speech language property. + * @param speechLanguage the interToneTimeout value to set. + * @return the CallMediaRecognizeChoiceOptions object itself. + */ + public CallMediaRecognizeChoiceOptions setSpeechLanguage(String speechLanguage) { + this.speechLanguage = speechLanguage; + return this; + } + + /** + * Get the list of recognize choice. + * + * @return the speech language. + */ + public String getSpeechLanguage() { + return this.speechLanguage; + } + + + /** * Initializes a CallMediaRecognizeDtmfOptions object. * diff --git a/sdk/communication/azure-communication-callautomation/src/test/java/com/azure/communication/callautomation/CallMediaAsyncUnitTests.java b/sdk/communication/azure-communication-callautomation/src/test/java/com/azure/communication/callautomation/CallMediaAsyncUnitTests.java index 1c1808c5c524e..aafa8e8de3d45 100644 --- a/sdk/communication/azure-communication-callautomation/src/test/java/com/azure/communication/callautomation/CallMediaAsyncUnitTests.java +++ b/sdk/communication/azure-communication-callautomation/src/test/java/com/azure/communication/callautomation/CallMediaAsyncUnitTests.java @@ -179,6 +179,7 @@ public void recognizeWithResponseWithFileSourceChoiceOptions() { recognizeOptions.setOperationContext("operationContext"); recognizeOptions.setInterruptPrompt(true); recognizeOptions.setInitialSilenceTimeout(Duration.ofSeconds(4)); + recognizeOptions.setSpeechLanguage("en-US"); StepVerifier.create( callMedia.startRecognizingWithResponse(recognizeOptions)) @@ -205,6 +206,7 @@ public void recognizeWithResponseTextChoiceOptions() { recognizeOptions.setOperationContext("operationContext"); recognizeOptions.setInterruptPrompt(true); recognizeOptions.setInitialSilenceTimeout(Duration.ofSeconds(4)); + recognizeOptions.setSpeechLanguage("en-US"); StepVerifier.create( callMedia.startRecognizingWithResponse(recognizeOptions)) diff --git a/sdk/communication/azure-communication-callautomation/swagger/README.md b/sdk/communication/azure-communication-callautomation/swagger/README.md index db1ea61ce406f..e76912d8fb29e 100644 --- a/sdk/communication/azure-communication-callautomation/swagger/README.md +++ b/sdk/communication/azure-communication-callautomation/swagger/README.md @@ -33,7 +33,7 @@ To update generated files for call automation, run the following command ``` yaml tag: package-2023-01-15-preview require: - - https://raw.githubusercontent.com/williamzhao87/azure-rest-api-specs/0d0cd5af40aa17af76ce0307ac5512351c38e3bc/specification/communication/data-plane/CallAutomation/readme.md + - https://raw.githubusercontent.com/williamzhao87/azure-rest-api-specs/becaba47bb961445fa2f8ab55b0ed199b391d179/specification/communication/data-plane/CallAutomation/readme.md java: true output-folder: ../ license-header: MICROSOFT_MIT_SMALL From 653e52afe9464fc7fe6e37668d6932c77419844c Mon Sep 17 00:00:00 2001 From: Shawn Fang <45607042+mssfang@users.noreply.github.com> Date: Fri, 24 Feb 2023 13:29:19 -0800 Subject: [PATCH 3/8] refactor to pass live test (#33738) --- .../appconfiguration/ConfigurationAsyncClientTest.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java index 56d0d3e694cfc..aa0df57d0c83f 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java @@ -1186,7 +1186,6 @@ public void listRevisionsWithPaginationAndRepeatIterator(HttpClient httpClient, client = getConfigurationAsyncClient(httpClient, serviceVersion); final int numberExpected = 50; List settings = new ArrayList<>(numberExpected); - List>> results = new ArrayList<>(); for (int value = 0; value < numberExpected; value++) { ConfigurationSetting setting = new ConfigurationSetting().setKey(keyPrefix).setValue("myValue" + value).setLabel(labelPrefix); settings.add(setting); @@ -1195,8 +1194,6 @@ public void listRevisionsWithPaginationAndRepeatIterator(HttpClient httpClient, SettingSelector filter = new SettingSelector().setKeyFilter(keyPrefix).setLabelFilter(labelPrefix); - Flux.merge(results).blockLast(); - List configurationSettingList1 = new ArrayList<>(); List configurationSettingList2 = new ArrayList<>(); @@ -1222,14 +1219,12 @@ public void listConfigurationSettingsWithPagination(HttpClient httpClient, Confi settings.add(new ConfigurationSetting().setKey(keyPrefix + "-" + value).setValue("myValue").setLabel(labelPrefix)); } - List>> results = new ArrayList<>(); for (ConfigurationSetting setting : settings) { - results.add(client.setConfigurationSettingWithResponse(setting, false)); + StepVerifier.create(client.setConfigurationSetting(setting)).expectNextCount(1).verifyComplete(); } SettingSelector filter = new SettingSelector().setKeyFilter(keyPrefix + "-*").setLabelFilter(labelPrefix); - Flux.merge(results).blockLast(); StepVerifier.create(client.listConfigurationSettings(filter)) .expectNextCount(numberExpected) .verifyComplete(); From f99b2b6b201962dceeb12580b2d63e13c6db974b Mon Sep 17 00:00:00 2001 From: Azure SDK Bot <53356347+azure-sdk@users.noreply.github.com> Date: Fri, 24 Feb 2023 16:36:31 -0500 Subject: [PATCH 4/8] Sync eng/common directory with azure-sdk-tools for PR 5568 (#33732) * we encourage folks to place their assets.jsons at the package level * update generate-assets-json.ps1 to only include src/**/session-records so as to avoid picking up the duplicated 'target' sessionrecords --------- Co-authored-by: scbedd <45376673+scbedd@users.noreply.github.com> --- eng/common/testproxy/transition-scripts/README.md | 8 ++++---- .../testproxy/transition-scripts/generate-assets-json.ps1 | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eng/common/testproxy/transition-scripts/README.md b/eng/common/testproxy/transition-scripts/README.md index 862d22ed6de8a..06290bd0c7840 100644 --- a/eng/common/testproxy/transition-scripts/README.md +++ b/eng/common/testproxy/transition-scripts/README.md @@ -60,12 +60,12 @@ You will not be able to clean them up however. There exists [planned work](https - `language` repo - An individual language repository eg. azure-sdk-for-python or azure-sdk-for-net etc. - `assets` repo - The repository where assets are being moved to. -The `test-proxy` tool is integrated with the ability to automatically restore these assets. This process is kick-started by the presence of an `assets.json` alongside a dev's actual code. This means that while assets will be cloned down externally, the _map_ to those assets will be stored alongside the tests. Normally, it is recommended to create an `assets.json` under the path `sdk/`. However, more granular storage is also possible. +The `test-proxy` tool is integrated with the ability to automatically restore these assets. This process is kick-started by the presence of an `assets.json` alongside a dev's actual code. This means that while assets will be cloned down externally, the _map_ to those assets will be stored alongside the tests. Normally, it is recommended to create an `assets.json` under the path `sdk//`. More granular storage than on an individual package level is possible, but each language's test framework would need to support that on a case-by-case basis. -Service/Package-Level examples: +Examples of current assets.json locations: -- `sdk/storage/assets.json` -- `sdk/storage/azure-storage-file-datalake/assets.json` +- [`sdk/data/aztables/assets.json`](https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/data/aztables/assets.json) +- [`sdk/keyvault/azure-keyvault-keys/assets.json`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/keyvault/azure-keyvault-keys/assets.json) The location of the actual test code is referred to as the `language repo`. diff --git a/eng/common/testproxy/transition-scripts/generate-assets-json.ps1 b/eng/common/testproxy/transition-scripts/generate-assets-json.ps1 index 2b24bdcb1ab85..3f43c7d3adff8 100644 --- a/eng/common/testproxy/transition-scripts/generate-assets-json.ps1 +++ b/eng/common/testproxy/transition-scripts/generate-assets-json.ps1 @@ -72,7 +72,7 @@ if ($UseTestRepo) { # 3. ios $LangRecordingDirs = @{"cpp" = "recordings"; "go" = "recordings"; - "java" = "session-records"; + "java" = "src.*?session-records"; "js" = "recordings"; "net" = "SessionRecords"; "python" = "recordings"; @@ -321,8 +321,8 @@ Function Move-AssetsFromLangRepo { ) $filter = $LangRecordingDirs[$language] Write-Host "Language recording directory name=$filter" - Write-Host "Get-ChildItem -Recurse -Filter ""*.json"" | Where-Object { `$_.DirectoryName.Split([IO.Path]::DirectorySeparatorChar) -contains ""$filter"" }" - $filesToMove = Get-ChildItem -Recurse -Filter "*.json" | Where-Object { $_.DirectoryName.Split([IO.Path]::DirectorySeparatorChar) -contains "$filter" } + Write-Host "Get-ChildItem -Recurse -Filter ""*.json"" | Where-Object { if ($filter.Contains(""*"")) { $_.DirectoryName -match $filter } else { $_.DirectoryName.Split([IO.Path]::DirectorySeparatorChar) -contains ""$filter"" }" + $filesToMove = Get-ChildItem -Recurse -Filter "*.json" | Where-Object { if ($filter.Contains("*")) { $_.DirectoryName -match $filter } else { $_.DirectoryName.Split([IO.Path]::DirectorySeparatorChar) -contains "$filter" } } [string] $currentDir = Get-Location foreach ($fromFile in $filesToMove) { From bff0243077172420ae250b25c9e3001734140069 Mon Sep 17 00:00:00 2001 From: Shawn Fang <45607042+mssfang@users.noreply.github.com> Date: Fri, 24 Feb 2023 14:48:45 -0800 Subject: [PATCH 5/8] [TA] Replace json object Map by BinaryData (#33381) --- ...lthcareEntitiesResultPropertiesHelper.java | 7 +- .../implementation/AnalyzeTextsImpl.java | 3 +- .../HealthcareEntitiesDocumentResult.java | 8 +- ...entResultWithDocumentDetectedLanguage.java | 4 +- .../models/HealthcareResultDocumentsItem.java | 4 +- .../AnalyzeHealthcareEntitiesResult.java | 14 +- .../lro/AnalyzeHealthcareEntities.java | 4 +- ...AnalyzeHealthcareEntitiesActionSample.java | 4 +- ...zeHealthcareEntitiesActionSampleAsync.java | 24 +- .../lro/AnalyzeHealthcareEntitiesAsync.java | 4 +- .../com/azure/ai/textanalytics/TestUtils.java | 13 +- .../TextAnalyticsClientTestBase.java | 2 +- .../azure-ai-textanalytics/swagger/README.md | 276 +++++++++++++++++- 13 files changed, 317 insertions(+), 50 deletions(-) diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesResultPropertiesHelper.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesResultPropertiesHelper.java index b5895df4bfce2..dcb1d8d96a81f 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesResultPropertiesHelper.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeHealthcareEntitiesResultPropertiesHelper.java @@ -8,10 +8,9 @@ import com.azure.ai.textanalytics.models.HealthcareEntity; import com.azure.ai.textanalytics.models.HealthcareEntityRelation; import com.azure.ai.textanalytics.models.TextAnalyticsWarning; +import com.azure.core.util.BinaryData; import com.azure.core.util.IterableStream; -import java.util.Map; - /** * The helper class to set the non-public properties of an {@link AnalyzeHealthcareEntitiesResult} instance. */ @@ -31,7 +30,7 @@ void setWarnings(AnalyzeHealthcareEntitiesResult entitiesResult, IterableStream warnings); void setEntityRelations(AnalyzeHealthcareEntitiesResult entitiesResult, IterableStream entityRelations); - void setFhirBundle(AnalyzeHealthcareEntitiesResult entitiesResult, Map fhirBundle); + void setFhirBundle(AnalyzeHealthcareEntitiesResult entitiesResult, BinaryData fhirBundle); } /** @@ -64,7 +63,7 @@ public static void setEntityRelations(AnalyzeHealthcareEntitiesResult entitiesRe accessor.setEntityRelations(entitiesResult, entityRelations); } - public static void setFhirBundle(AnalyzeHealthcareEntitiesResult entitiesResult, Map fhirBundle) { + public static void setFhirBundle(AnalyzeHealthcareEntitiesResult entitiesResult, BinaryData fhirBundle) { accessor.setFhirBundle(entitiesResult, fhirBundle); } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeTextsImpl.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeTextsImpl.java index 1421ae0862bb3..aa1a993b3c434 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeTextsImpl.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/AnalyzeTextsImpl.java @@ -27,9 +27,8 @@ import com.azure.core.http.rest.RestProxy; import com.azure.core.util.Context; import com.azure.core.util.FluxUtil; -import reactor.core.publisher.Mono; - import java.util.UUID; +import reactor.core.publisher.Mono; /** An instance of this class provides access to all the operations defined in AnalyzeTexts. */ public final class AnalyzeTextsImpl { diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResult.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResult.java index 5210b5388841f..352ad65b0303a 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResult.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResult.java @@ -5,9 +5,9 @@ package com.azure.ai.textanalytics.implementation.models; import com.azure.core.annotation.Fluent; +import com.azure.core.util.BinaryData; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -import java.util.Map; /** The HealthcareEntitiesDocumentResult model. */ @Fluent @@ -29,7 +29,7 @@ public class HealthcareEntitiesDocumentResult extends DocumentResult { * information see https://www.hl7.org/fhir/overview.html. */ @JsonProperty(value = "fhirBundle") - private Map fhirBundle; + private BinaryData fhirBundle; /** Creates an instance of HealthcareEntitiesDocumentResult class. */ public HealthcareEntitiesDocumentResult() {} @@ -80,7 +80,7 @@ public HealthcareEntitiesDocumentResult setRelations(List re * * @return the fhirBundle value. */ - public Map getFhirBundle() { + public BinaryData getFhirBundle() { return this.fhirBundle; } @@ -91,7 +91,7 @@ public Map getFhirBundle() { * @param fhirBundle the fhirBundle value to set. * @return the HealthcareEntitiesDocumentResult object itself. */ - public HealthcareEntitiesDocumentResult setFhirBundle(Map fhirBundle) { + public HealthcareEntitiesDocumentResult setFhirBundle(BinaryData fhirBundle) { this.fhirBundle = fhirBundle; return this; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage.java index 4cf01edbccda5..2fee4c2696849 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage.java @@ -5,9 +5,9 @@ package com.azure.ai.textanalytics.implementation.models; import com.azure.core.annotation.Fluent; +import com.azure.core.util.BinaryData; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -import java.util.Map; /** The HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage model. */ @Fluent @@ -62,7 +62,7 @@ public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setRelations /** {@inheritDoc} */ @Override - public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setFhirBundle(Map fhirBundle) { + public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setFhirBundle(BinaryData fhirBundle) { super.setFhirBundle(fhirBundle); return this; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareResultDocumentsItem.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareResultDocumentsItem.java index ed3da44a5e174..2714318ec4716 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareResultDocumentsItem.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareResultDocumentsItem.java @@ -5,9 +5,9 @@ package com.azure.ai.textanalytics.implementation.models; import com.azure.core.annotation.Fluent; +import com.azure.core.util.BinaryData; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -import java.util.Map; /** The HealthcareResultDocumentsItem model. */ @Fluent @@ -57,7 +57,7 @@ public HealthcareResultDocumentsItem setRelations(List relat /** {@inheritDoc} */ @Override - public HealthcareResultDocumentsItem setFhirBundle(Map fhirBundle) { + public HealthcareResultDocumentsItem setFhirBundle(BinaryData fhirBundle) { super.setFhirBundle(fhirBundle); return this; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesResult.java b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesResult.java index 638319e198c67..0174475a51afb 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesResult.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/main/java/com/azure/ai/textanalytics/models/AnalyzeHealthcareEntitiesResult.java @@ -5,10 +5,9 @@ import com.azure.ai.textanalytics.implementation.AnalyzeHealthcareEntitiesResultPropertiesHelper; import com.azure.core.annotation.Immutable; +import com.azure.core.util.BinaryData; import com.azure.core.util.IterableStream; -import java.util.Map; - /** * The {@link AnalyzeHealthcareEntitiesResult} model. */ @@ -18,7 +17,7 @@ public final class AnalyzeHealthcareEntitiesResult extends TextAnalyticsResult { private IterableStream warnings; private IterableStream entities; private IterableStream entityRelations; - private Map fhirBundle; + private BinaryData fhirBundle; static { AnalyzeHealthcareEntitiesResultPropertiesHelper.setAccessor( @@ -48,8 +47,7 @@ public void setEntityRelations(AnalyzeHealthcareEntitiesResult entitiesResult, } @Override - public void setFhirBundle(AnalyzeHealthcareEntitiesResult entitiesResult, - Map fhirBundle) { + public void setFhirBundle(AnalyzeHealthcareEntitiesResult entitiesResult, BinaryData fhirBundle) { entitiesResult.setFhirBundle(fhirBundle); } }); @@ -101,9 +99,9 @@ public IterableStream getEntityRelations() { /** * Gets the value of FHIR Bundle. See more information in https://www.hl7.org/fhir/overview.html. * - * @return The value of FHIR Bundle. + * @return The value of FHIR Bundle in BinaryData. */ - public Map getFhirBundle() { + public BinaryData getFhirBundle() { return this.fhirBundle; } @@ -133,7 +131,7 @@ private void setEntityRelations(IterableStream entityR this.entityRelations = entityRelations; } - private void setFhirBundle(Map fhirBundle) { + private void setFhirBundle(BinaryData fhirBundle) { this.fhirBundle = fhirBundle; } } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntities.java b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntities.java index f509255d0a9c5..eae25bc3d617e 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntities.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntities.java @@ -19,12 +19,12 @@ import com.azure.ai.textanalytics.util.AnalyzeHealthcareEntitiesPagedIterable; import com.azure.ai.textanalytics.util.AnalyzeHealthcareEntitiesResultCollection; import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.core.util.polling.SyncPoller; import java.util.Arrays; import java.util.List; -import java.util.Map; /** * Sample demonstrates how to analyze a healthcare task. @@ -110,7 +110,7 @@ public static void main(String[] args) { } System.out.printf("Relation confidence score: %f.%n", entityRelation.getConfidenceScore()); // FHIR bundle in JSON format - final Map fhirBundle = healthcareEntitiesResult.getFhirBundle(); + final BinaryData fhirBundle = healthcareEntitiesResult.getFhirBundle(); if (fhirBundle != null) { System.out.printf("FHIR bundle: %s%n", fhirBundle); } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSample.java b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSample.java index becc196bb465e..7c4f72ff3e68a 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSample.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSample.java @@ -20,11 +20,11 @@ import com.azure.ai.textanalytics.models.TextAnalyticsActions; import com.azure.ai.textanalytics.util.AnalyzeActionsResultPagedIterable; import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.util.BinaryData; import com.azure.core.util.polling.SyncPoller; import java.util.Arrays; import java.util.List; -import java.util.Map; /** * Sample demonstrates how to synchronously execute an "Healthcare Entities Analysis" action in a batch of documents. @@ -101,7 +101,7 @@ private static void processAnalyzeActionsResult(AnalyzeActionsResult actionsResu } // FHIR bundle in JSON format - final Map fhirBundle = healthcareEntitiesResult.getFhirBundle(); + final BinaryData fhirBundle = healthcareEntitiesResult.getFhirBundle(); if (fhirBundle != null) { System.out.printf("FHIR bundle: %s%n", fhirBundle); } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSampleAsync.java b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSampleAsync.java index c0d0da09d94c4..7a7e3eab90a2a 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSampleAsync.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesActionSampleAsync.java @@ -19,10 +19,10 @@ import com.azure.ai.textanalytics.models.HealthcareEntityRelationRole; import com.azure.ai.textanalytics.models.TextAnalyticsActions; import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.util.BinaryData; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -36,17 +36,17 @@ public class AnalyzeHealthcareEntitiesActionSampleAsync { */ public static void main(String[] args) { TextAnalyticsAsyncClient client = new TextAnalyticsClientBuilder() - .credential(new AzureKeyCredential("{key}")) - .endpoint("{endpoint}") - .buildAsyncClient(); + .credential(new AzureKeyCredential("{key}")) + .endpoint("{endpoint}") + .buildAsyncClient(); List documents = Arrays.asList( - "Woman in NAD with a h/o CAD, DM2, asthma and HTN on ramipril for 8 years awoke from sleep around" - + " 2:30 am this morning of a sore throat and swelling of tongue. She came immediately to the ED" - + " b/c she was having difficulty swallowing.", - "Patient's brother died at the age of 64 from lung cancer. She was admitted for likely gastroparesis" - + " but remains unsure if she wants to start adjuvant hormonal therapy. Please hold lactulose " - + "if diarrhea worsen."); + "Woman in NAD with a h/o CAD, DM2, asthma and HTN on ramipril for 8 years awoke from sleep around" + + " 2:30 am this morning of a sore throat and swelling of tongue. She came immediately to the ED" + + " b/c she was having difficulty swallowing.", + "Patient's brother died at the age of 64 from lung cancer. She was admitted for likely gastroparesis" + + " but remains unsure if she wants to start adjuvant hormonal therapy. Please hold lactulose " + + "if diarrhea worsen."); client.beginAnalyzeActions(documents, new TextAnalyticsActions() @@ -58,7 +58,7 @@ public static void main(String[] args) { .flatMap(result -> { AnalyzeActionsOperationDetail operationDetail = result.getValue(); System.out.printf("Action display name: %s, Successfully completed actions: %d, in-process actions: %d," - + " failed actions: %d, total actions: %d%n", + + " failed actions: %d, total actions: %d%n", operationDetail.getDisplayName(), operationDetail.getSucceededCount(), operationDetail.getInProgressCount(), operationDetail.getFailedCount(), operationDetail.getTotalCount()); @@ -116,7 +116,7 @@ private static void processAnalyzeActionsResult(AnalyzeActionsResult actionsResu } System.out.printf("Relation confidence score: %f.%n", entityRelation.getConfidenceScore()); // FHIR bundle in JSON format - final Map fhirBundle = healthcareEntitiesResult.getFhirBundle(); + final BinaryData fhirBundle = healthcareEntitiesResult.getFhirBundle(); if (fhirBundle != null) { System.out.printf("FHIR bundle: %s%n", fhirBundle); } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesAsync.java b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesAsync.java index 4d8d273550657..8e86be173d9e9 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesAsync.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/samples/java/com/azure/ai/textanalytics/lro/AnalyzeHealthcareEntitiesAsync.java @@ -19,10 +19,10 @@ import com.azure.ai.textanalytics.util.AnalyzeHealthcareEntitiesResultCollection; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.http.rest.PagedResponse; +import com.azure.core.util.BinaryData; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -123,7 +123,7 @@ private static void processAnalyzeHealthcareEntitiesResultCollection( } System.out.printf("Relation confidence score: %f.%n", entityRelation.getConfidenceScore()); // FHIR bundle in JSON format - final Map fhirBundle = healthcareEntitiesResult.getFhirBundle(); + final BinaryData fhirBundle = healthcareEntitiesResult.getFhirBundle(); if (fhirBundle != null) { System.out.printf("FHIR bundle: %s%n", fhirBundle); } 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 31aa3ccaa7993..ffe5a9a3c1b1b 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 @@ -98,6 +98,7 @@ import com.azure.ai.textanalytics.util.RecognizePiiEntitiesResultCollection; import com.azure.core.exception.HttpResponseException; import com.azure.core.http.HttpClient; +import com.azure.core.util.BinaryData; import com.azure.core.util.Configuration; import com.azure.core.util.CoreUtils; import com.azure.core.util.IterableStream; @@ -107,9 +108,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -820,9 +819,8 @@ static AnalyzeHealthcareEntitiesResultCollection getExpectedAnalyzeHealthcareEnt static AnalyzeHealthcareEntitiesResult getRecognizeHealthcareEntitiesResultWithFhir1(String documentId) { AnalyzeHealthcareEntitiesResult recognizeHealthcareEntitiesResult1 = getRecognizeHealthcareEntitiesResult1(documentId); - Map fhir1 = new HashMap<>(); - fhir1.put("dummyString", "dummyObject"); - AnalyzeHealthcareEntitiesResultPropertiesHelper.setFhirBundle(recognizeHealthcareEntitiesResult1, fhir1); + AnalyzeHealthcareEntitiesResultPropertiesHelper.setFhirBundle(recognizeHealthcareEntitiesResult1, + BinaryData.fromString("{\n \"string\": \"Hello World\"\n}")); return recognizeHealthcareEntitiesResult1; } @@ -920,9 +918,8 @@ static AnalyzeHealthcareEntitiesResult getRecognizeHealthcareEntitiesResult1(Str static AnalyzeHealthcareEntitiesResult getRecognizeHealthcareEntitiesResultWithFhir2() { AnalyzeHealthcareEntitiesResult recognizeHealthcareEntitiesResult2 = getRecognizeHealthcareEntitiesResult2(); - Map fhir2 = new HashMap<>(); - fhir2.put("dummyString2", "dummyObject2"); - AnalyzeHealthcareEntitiesResultPropertiesHelper.setFhirBundle(recognizeHealthcareEntitiesResult2, fhir2); + AnalyzeHealthcareEntitiesResultPropertiesHelper.setFhirBundle(recognizeHealthcareEntitiesResult2, + BinaryData.fromString("{\n \"string\": \"Hello World\"\n}")); return recognizeHealthcareEntitiesResult2; } diff --git a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTestBase.java b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTestBase.java index 4fdc3d85812f1..3511e416d6017 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTestBase.java +++ b/sdk/textanalytics/azure-ai-textanalytics/src/test/java/com/azure/ai/textanalytics/TextAnalyticsClientTestBase.java @@ -1986,7 +1986,7 @@ static void validateEntityDataSourceList(IterableStream expect static void validateHealthcareEntityDocumentResult(AnalyzeHealthcareEntitiesResult expected, AnalyzeHealthcareEntitiesResult actual) { if (expected.getFhirBundle() != null) { - assertTrue(actual.getFhirBundle() != null && actual.getFhirBundle().size() != 0); + assertTrue(actual.getFhirBundle() != null); } else { assertNull(actual.getFhirBundle()); } diff --git a/sdk/textanalytics/azure-ai-textanalytics/swagger/README.md b/sdk/textanalytics/azure-ai-textanalytics/swagger/README.md index 18cb83b5dcd28..c66266fd0f034 100644 --- a/sdk/textanalytics/azure-ai-textanalytics/swagger/README.md +++ b/sdk/textanalytics/azure-ai-textanalytics/swagger/README.md @@ -31,7 +31,7 @@ autorest --java --use:@autorest/java@4.1.9 README.md ### Code generation settings ``` yaml -use: '@autorest/java@4.1.2' +use: '@autorest/java@4.1.9' input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/527f6d35fb0d85c48210ca0f6f6f42814d63bd33/specification/cognitiveservices/data-plane/Language/preview/2022-10-01-preview/analyzetext.json java: true output-folder: ..\ @@ -42,8 +42,282 @@ enable-sync-stack: true license-header: MICROSOFT_MIT_SMALL add-context-parameter: true models-subpackage: implementation.models +customization-class: TextAnalyticsCustomization custom-types-subpackage: models context-client-method-parameter: true service-interface-as-public: true generic-response-type: true ``` + +### Customization + +```java +import org.slf4j.Logger; + +/** + * This class contains the customization code to customize the AutoRest generated code for TextAnalytics. + */ +public class TextAnalyticsCustomization extends Customization { + private static final String nextLine = System.lineSeparator(); + private static final String CLASS_HEADER = + "// Copyright (c) Microsoft Corporation. All rights reserved." + nextLine + + "// Licensed under the MIT License." + nextLine + + "// Code generated by Microsoft (R) AutoRest Code Generator." + nextLine + nextLine + + "package com.azure.ai.textanalytics.implementation.models;" + nextLine + nextLine; + + @Override + public void customize(LibraryCustomization customization, Logger logger) { + PackageCustomization implementationPackage = customization.getPackage("com.azure.ai.textanalytics.implementation"); + // Explore AnalyzeTextsImpl to public TextAnalyticsAsyncClient, they are in different package. + int publicModifier = 1; + changeConstructorModifier(implementationPackage.getClass("AnalyzeTextsImpl"), "AnalyzeTextsImpl(MicrosoftCognitiveLanguageServiceTextAnalysisImpl client)", publicModifier); + // Customize HealthcareEntitiesDocumentResult + customization.getRawEditor() + .replaceFile( + "src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResult.java", + createHealthcareEntitiesDocumentResult()); + // Customize HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage + customization.getRawEditor() + .replaceFile( + "src/main/java/com/azure/ai/textanalytics/implementation/models/HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage.java", + createHealthcareEntitiesDocumentResultWithDocumentDetectedLanguage()); + + } + + private String createHealthcareEntitiesDocumentResultWithDocumentDetectedLanguage() { + StringBuilder sb = new StringBuilder(); + sb.append(CLASS_HEADER); + sb.append("import com.azure.core.annotation.Fluent;").append(nextLine); + sb.append("import com.azure.core.util.BinaryData;").append(nextLine); + sb.append("import com.fasterxml.jackson.annotation.JsonProperty;").append(nextLine); + sb.append("import java.util.List;").append(nextLine); + sb.append("import java.util.Map;").append(nextLine); + sb.append(nextLine); + sb.append("/** The HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage model. */").append(nextLine); + sb.append("@Fluent").append(nextLine); + sb.append("public final class HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage").append(nextLine); + sb.append(" extends HealthcareEntitiesDocumentResult {").append(nextLine); + sb.append(" /*").append(nextLine); + sb.append(" * If 'language' is set to 'auto' for the document in the request this field will contain a 2 letter ISO 639-1").append(nextLine); + sb.append(" * representation of the language detected for this document.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" @JsonProperty(value = \"detectedLanguage\")").append(nextLine); + sb.append(" private String detectedLanguage;").append(nextLine); + sb.append(nextLine); + sb.append(" /** Creates an instance of HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage class. */").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage() {}").append(nextLine); + sb.append(nextLine); + // Getters and Setters + sb.append(" /**").append(nextLine); + sb.append(" * Get the detectedLanguage property: If 'language' is set to 'auto' for the document in the request this field will").append(nextLine); + sb.append(" * contain a 2 letter ISO 639-1 representation of the language detected for this document.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @return the detectedLanguage value.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public String getDetectedLanguage() {").append(nextLine); + sb.append(" return this.detectedLanguage;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /**").append(nextLine); + sb.append(" * Set the detectedLanguage property: If 'language' is set to 'auto' for the document in the request this field will").append(nextLine); + sb.append(" * contain a 2 letter ISO 639-1 representation of the language detected for this document.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @param detectedLanguage the detectedLanguage value to set.").append(nextLine); + sb.append(" * @return the HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage object itself.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setDetectedLanguage(String detectedLanguage) {").append(nextLine); + sb.append(" this.detectedLanguage = detectedLanguage;").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setEntities(List entities) {").append(nextLine); + sb.append(" super.setEntities(entities);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setRelations(").append(nextLine); + sb.append(" List relations) {").append(nextLine); + sb.append(" super.setRelations(relations);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setFhirBundle(BinaryData fhirBundle) {").append(nextLine); + sb.append(" super.setFhirBundle(fhirBundle);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setId(String id) {").append(nextLine); + sb.append(" super.setId(id);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setWarnings(List warnings) {").append(nextLine); + sb.append(" super.setWarnings(warnings);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResultWithDocumentDetectedLanguage setStatistics(DocumentStatistics statistics) {").append(nextLine); + sb.append(" super.setStatistics(statistics);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append("}"); + sb.append(nextLine); + return sb.toString(); + } + + private String createHealthcareEntitiesDocumentResult() { + StringBuilder sb = new StringBuilder(); + sb.append(CLASS_HEADER); + sb.append("import com.azure.core.annotation.Fluent;").append(nextLine); + sb.append("import com.azure.core.util.BinaryData;").append(nextLine); + sb.append("import com.fasterxml.jackson.annotation.JsonProperty;").append(nextLine); + sb.append("import java.util.List;").append(nextLine); + sb.append("import java.util.Map;").append(nextLine); + sb.append(nextLine); + sb.append("/** The HealthcareEntitiesDocumentResult model. */").append(nextLine); + sb.append("@Fluent").append(nextLine); + sb.append("public class HealthcareEntitiesDocumentResult extends DocumentResult {").append(nextLine); + sb.append(" /*").append(nextLine); + sb.append(" * Healthcare entities.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" @JsonProperty(value = \"entities\", required = true)").append(nextLine); + sb.append(" private List entities;").append(nextLine); + sb.append(nextLine); + sb.append(" /*").append(nextLine); + sb.append(" * Healthcare entity relations.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" @JsonProperty(value = \"relations\", required = true)").append(nextLine); + sb.append(" private List relations;").append(nextLine); + sb.append(nextLine); + sb.append(" /*").append(nextLine); + sb.append(" * JSON bundle containing a FHIR compatible object for consumption in other Healthcare tools. For additional").append(nextLine); + sb.append(" * information see https://www.hl7.org/fhir/overview.html.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" @JsonProperty(value = \"fhirBundle\")").append(nextLine); + sb.append(" private BinaryData fhirBundle;").append(nextLine); // Change to BinaryData from Map + sb.append(nextLine); + sb.append(" /** Creates an instance of HealthcareEntitiesDocumentResult class. */").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult() {}").append(nextLine); + sb.append(nextLine); + // Getters and Setters + sb.append(" /**").append(nextLine); + sb.append(" * Get the entities property: Healthcare entities.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @return the entities value.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public List getEntities() {").append(nextLine); + sb.append(" return this.entities;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /**").append(nextLine); + sb.append(" * Set the entities property: Healthcare entities.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @param entities the entities value to set.").append(nextLine); + sb.append(" * @return the HealthcareEntitiesDocumentResult object itself.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult setEntities(List entities) {").append(nextLine); + sb.append(" this.entities = entities;").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /**").append(nextLine); + sb.append(" * Get the relations property: Healthcare entity relations.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @return the relations value.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public List getRelations() {").append(nextLine); + sb.append(" return this.relations;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /**").append(nextLine); + sb.append(" * Set the relations property: Healthcare entity relations.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @param relations the relations value to set.").append(nextLine); + sb.append(" * @return the HealthcareEntitiesDocumentResult object itself.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult setRelations(List relations) {").append(nextLine); + sb.append(" this.relations = relations;").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /**").append(nextLine); + sb.append(" * Get the fhirBundle property: JSON bundle containing a FHIR compatible object for consumption in other Healthcare").append(nextLine); + sb.append(" * tools. For additional information see https://www.hl7.org/fhir/overview.html.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @return the fhirBundle value.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public BinaryData getFhirBundle() {").append(nextLine); + sb.append(" return this.fhirBundle;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /**").append(nextLine); + sb.append(" * Set the fhirBundle property: JSON bundle containing a FHIR compatible object for consumption in other Healthcare").append(nextLine); + sb.append(" * tools. For additional information see https://www.hl7.org/fhir/overview.html.").append(nextLine); + sb.append(" *").append(nextLine); + sb.append(" * @param fhirBundle the fhirBundle value to set.").append(nextLine); + sb.append(" * @return the HealthcareEntitiesDocumentResult object itself.").append(nextLine); + sb.append(" */").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult setFhirBundle(BinaryData fhirBundle) {").append(nextLine); + sb.append(" this.fhirBundle = fhirBundle;").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult setId(String id) {").append(nextLine); + sb.append(" super.setId(id);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult setWarnings(List warnings) {").append(nextLine); + sb.append(" super.setWarnings(warnings);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append(nextLine); + + sb.append(" /** {@inheritDoc} */").append(nextLine); + sb.append(" @Override").append(nextLine); + sb.append(" public HealthcareEntitiesDocumentResult setStatistics(DocumentStatistics statistics) {").append(nextLine); + sb.append(" super.setStatistics(statistics);").append(nextLine); + sb.append(" return this;").append(nextLine); + sb.append(" }").append(nextLine); + sb.append("}"); + sb.append(nextLine); + return sb.toString(); + } + + private void changeConstructorModifier(ClassCustomization classCustomization, String constructorInText, int modifierLevel) { + ConstructorCustomization constructor = classCustomization.getConstructor(constructorInText); + constructor.setModifier(1); + } +} + +``` From 019a65553c3bf2a5879d7a1d524299e6af8c74e8 Mon Sep 17 00:00:00 2001 From: Helen <56097766+heyams@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:11:25 -0800 Subject: [PATCH 6/8] Fix GlobalOpenTelemetry usage (#33678) * Fix sample * Add copyright * Fix banned dependencies * Fix banned dependencies * Upgrade to 1.23 across all libraries * Fix nullpointerexception * Wait longer * Remove debug code for enabling fiddler * Remove since it will get overwritten by the script * Address comments --- eng/versioning/external_dependencies.txt | 14 +-- .../azure-security-attestation/pom.xml | 12 +- .../azure-core-metrics-opentelemetry/pom.xml | 20 +-- .../pom.xml | 4 +- .../azure-core-tracing-opentelemetry/pom.xml | 12 +- .../azure-messaging-eventhubs/pom.xml | 4 +- .../pom.xml | 14 +-- .../exporter/AzureMonitorExporterBuilder.java | 1 + .../opentelemetry/exporter/NoopTracer.java | 46 +++++++ .../AzureMonitorMetricExporterSample.java | 119 ++++++++++++++++++ .../heartbeat/HeartbeatTests.java | 2 +- .../azure-messaging-servicebus/pom.xml | 4 +- 12 files changed, 209 insertions(+), 43 deletions(-) create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/NoopTracer.java create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/src/samples/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorMetricExporterSample.java diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index fa80235031a98..bedc54ed0b3f3 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -194,13 +194,13 @@ com.microsoft.azure:azure-storage;8.0.0 com.microsoft.azure:msal4j;1.13.4 com.microsoft.azure:msal4j-persistence-extension;1.1.0 com.sun.activation:jakarta.activation;1.2.2 -io.opentelemetry:opentelemetry-api;1.20.0 -io.opentelemetry:opentelemetry-sdk;1.20.0 -io.opentelemetry:opentelemetry-exporter-logging;1.20.0 -io.opentelemetry:opentelemetry-exporter-jaeger;1.20.0 -io.opentelemetry:opentelemetry-exporter-otlp;1.20.0 -io.opentelemetry:opentelemetry-sdk-testing;1.20.0 -io.opentelemetry:opentelemetry-sdk-logs;1.20.0-alpha +io.opentelemetry:opentelemetry-api;1.23.0 +io.opentelemetry:opentelemetry-sdk;1.23.0 +io.opentelemetry:opentelemetry-exporter-logging;1.23.0 +io.opentelemetry:opentelemetry-exporter-jaeger;1.23.0 +io.opentelemetry:opentelemetry-exporter-otlp;1.23.0 +io.opentelemetry:opentelemetry-sdk-testing;1.23.0 +io.opentelemetry:opentelemetry-sdk-logs;1.23.0-alpha io.projectreactor:reactor-test;3.4.26 junit:junit;4.13.2 commons-cli:commons-cli;1.3 diff --git a/sdk/attestation/azure-security-attestation/pom.xml b/sdk/attestation/azure-security-attestation/pom.xml index 3d9df2bab7707..4b5c9b2c16ff9 100644 --- a/sdk/attestation/azure-security-attestation/pom.xml +++ b/sdk/attestation/azure-security-attestation/pom.xml @@ -77,19 +77,19 @@ io.opentelemetry opentelemetry-api - 1.20.0 + 1.23.0 test io.opentelemetry opentelemetry-exporter-logging - 1.20.0 + 1.23.0 test io.opentelemetry opentelemetry-sdk - 1.20.0 + 1.23.0 test @@ -149,9 +149,9 @@ com.nimbusds:nimbus-jose-jwt:[9.22] - io.opentelemetry:opentelemetry-api:[1.20.0] - io.opentelemetry:opentelemetry-sdk:[1.20.0] - io.opentelemetry:opentelemetry-exporter-logging:[1.20.0] + io.opentelemetry:opentelemetry-api:[1.23.0] + io.opentelemetry:opentelemetry-sdk:[1.23.0] + io.opentelemetry:opentelemetry-exporter-logging:[1.23.0] diff --git a/sdk/core/azure-core-metrics-opentelemetry/pom.xml b/sdk/core/azure-core-metrics-opentelemetry/pom.xml index 8df7cfd7ae0fd..d2d26b2b48d73 100644 --- a/sdk/core/azure-core-metrics-opentelemetry/pom.xml +++ b/sdk/core/azure-core-metrics-opentelemetry/pom.xml @@ -41,7 +41,7 @@ io.opentelemetry opentelemetry-api - 1.20.0 + 1.23.0 com.azure @@ -59,14 +59,14 @@ io.opentelemetry opentelemetry-sdk - 1.20.0 + 1.23.0 test io.opentelemetry opentelemetry-sdk-testing - 1.20.0 + 1.23.0 test @@ -121,7 +121,7 @@ io.opentelemetry opentelemetry-exporter-otlp - 1.20.0 + 1.23.0 test @@ -136,12 +136,12 @@ - io.opentelemetry:opentelemetry-api:[1.20.0] - io.opentelemetry:opentelemetry-sdk:[1.20.0] - io.opentelemetry:opentelemetry-sdk-testing:[1.20.0] - io.opentelemetry:opentelemetry-exporter-logging:[1.20.0] - io.opentelemetry:opentelemetry-exporter-otlp:[1.20.0] - io.opentelemetry:opentelemetry-exporter-jaeger:[1.20.0] + io.opentelemetry:opentelemetry-api:[1.23.0] + io.opentelemetry:opentelemetry-sdk:[1.23.0] + io.opentelemetry:opentelemetry-sdk-testing:[1.23.0] + io.opentelemetry:opentelemetry-exporter-logging:[1.23.0] + io.opentelemetry:opentelemetry-exporter-otlp:[1.23.0] + io.opentelemetry:opentelemetry-exporter-jaeger:[1.23.0] diff --git a/sdk/core/azure-core-tracing-opentelemetry-samples/pom.xml b/sdk/core/azure-core-tracing-opentelemetry-samples/pom.xml index 32239b6c116a6..bd3da5f71b4bd 100644 --- a/sdk/core/azure-core-tracing-opentelemetry-samples/pom.xml +++ b/sdk/core/azure-core-tracing-opentelemetry-samples/pom.xml @@ -54,13 +54,13 @@ io.opentelemetry opentelemetry-exporter-logging - 1.20.0 + 1.23.0 test io.opentelemetry opentelemetry-exporter-jaeger - 1.20.0 + 1.23.0 test diff --git a/sdk/core/azure-core-tracing-opentelemetry/pom.xml b/sdk/core/azure-core-tracing-opentelemetry/pom.xml index c301d27c0815e..5138728b19310 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/pom.xml +++ b/sdk/core/azure-core-tracing-opentelemetry/pom.xml @@ -44,7 +44,7 @@ io.opentelemetry opentelemetry-api - 1.20.0 + 1.23.0 com.azure @@ -62,7 +62,7 @@ io.opentelemetry opentelemetry-sdk - 1.20.0 + 1.23.0 test @@ -99,7 +99,7 @@ io.opentelemetry opentelemetry-sdk-testing test - 1.20.0 + 1.23.0 org.openjdk.jmh @@ -125,9 +125,9 @@ - io.opentelemetry:opentelemetry-api:[1.20.0] - io.opentelemetry:opentelemetry-sdk:[1.20.0] - io.opentelemetry:opentelemetry-sdk-testing:[1.20.0] + io.opentelemetry:opentelemetry-api:[1.23.0] + io.opentelemetry:opentelemetry-sdk:[1.23.0] + io.opentelemetry:opentelemetry-sdk-testing:[1.23.0] diff --git a/sdk/eventhubs/azure-messaging-eventhubs/pom.xml b/sdk/eventhubs/azure-messaging-eventhubs/pom.xml index c55416e126439..8850dba79eb6f 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/pom.xml +++ b/sdk/eventhubs/azure-messaging-eventhubs/pom.xml @@ -101,14 +101,14 @@ io.opentelemetry opentelemetry-api - 1.20.0 + 1.23.0 test io.opentelemetry opentelemetry-sdk - 1.20.0 + 1.23.0 test diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml b/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml index f2dbb42d9be6e..007ae5dfd40ef 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml @@ -54,17 +54,17 @@ io.opentelemetry opentelemetry-api - 1.20.0 + 1.23.0 io.opentelemetry opentelemetry-sdk - 1.20.0 + 1.23.0 io.opentelemetry opentelemetry-sdk-logs - 1.20.0-alpha + 1.23.0-alpha com.github.spotbugs @@ -112,7 +112,7 @@ io.opentelemetry opentelemetry-sdk-testing - 1.20.0 + 1.23.0 test @@ -164,9 +164,9 @@ - io.opentelemetry:opentelemetry-api:[1.20.0] - io.opentelemetry:opentelemetry-sdk:[1.20.0] - io.opentelemetry:opentelemetry-sdk-logs:[1.20.0-alpha] + io.opentelemetry:opentelemetry-api:[1.23.0] + io.opentelemetry:opentelemetry-sdk:[1.23.0] + io.opentelemetry:opentelemetry-sdk-logs:[1.23.0-alpha] com.github.spotbugs:spotbugs-annotations:[4.2.2] diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java index 93e081eb4e76b..761b8dbdc88fe 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorExporterBuilder.java @@ -329,6 +329,7 @@ private HttpPipeline createHttpPipeline() { return new HttpPipelineBuilder() .policies(policies.toArray(new HttpPipelinePolicy[0])) .httpClient(httpClient) + .tracer(new NoopTracer()) .build(); } diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/NoopTracer.java b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/NoopTracer.java new file mode 100644 index 0000000000000..cc00bd944d080 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/main/java/com/azure/monitor/opentelemetry/exporter/NoopTracer.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.exporter; + +import com.azure.core.util.Context; +import com.azure.core.util.tracing.Tracer; + +import java.util.Objects; + +class NoopTracer implements Tracer { + + static final AutoCloseable NOOP_CLOSEABLE = () -> { + }; + + static final Tracer INSTANCE = new NoopTracer(); + + NoopTracer() { + } + + @Override + public Context start(String spanName, Context context) { + Objects.requireNonNull(spanName, "'spanName' cannot be null"); + return context; + } + + @Override + public void end(String statusMessage, Throwable error, Context context) { + } + + @Override + public void setAttribute(String key, String value, Context context) { + Objects.requireNonNull(key, "'key' cannot be null"); + Objects.requireNonNull(value, "'value' cannot be null"); + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public AutoCloseable makeSpanCurrent(Context context) { + return NOOP_CLOSEABLE; + } +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/samples/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorMetricExporterSample.java b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/samples/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorMetricExporterSample.java new file mode 100644 index 0000000000000..ac0f3c3ca2bd2 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/samples/java/com/azure/monitor/opentelemetry/exporter/AzureMonitorMetricExporterSample.java @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.monitor.opentelemetry.exporter; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; + +import static java.util.concurrent.TimeUnit.MINUTES; + +public class AzureMonitorMetricExporterSample { + + private static final String APPINSIGHTS_CONNECTION_STRING = ""; + + public static void main(String[] args) { + sendDoubleHistogram(); + } + + private static void sendDoubleHistogram() { + try { + MetricExporter exporter = new AzureMonitorExporterBuilder() + .connectionString(APPINSIGHTS_CONNECTION_STRING) + .buildMetricExporter(); + PeriodicMetricReader periodicMetricReader = PeriodicMetricReader + .builder(exporter) + .build(); + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(periodicMetricReader) + .build(); + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .buildAndRegisterGlobal(); + Meter meter = openTelemetry.meterBuilder("OTEL.AzureMonitor.Demo") + .build(); + DoubleHistogram histogram = meter.histogramBuilder("histogram").build(); + histogram.record(1.0); + histogram.record(100.0); + histogram.record(30.0); + + // metrics are exported every 60 seconds by default + MINUTES.sleep(5); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private static void sendLongCounter() { + try { + MetricExporter exporter = new AzureMonitorExporterBuilder() + .connectionString(APPINSIGHTS_CONNECTION_STRING) + .buildMetricExporter(); + PeriodicMetricReader periodicMetricReader = PeriodicMetricReader + .builder(exporter) + .build(); + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(periodicMetricReader) + .build(); + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .buildAndRegisterGlobal(); + Meter meter = openTelemetry.meterBuilder("OTEL.AzureMonitor.Demo") + .build(); + + LongCounter myFruitCounter = meter + .counterBuilder("MyFruitCounter") + .build(); + + myFruitCounter.add(1, Attributes.of(AttributeKey.stringKey("name"), "apple", AttributeKey.stringKey("color"), "red")); + myFruitCounter.add(2, Attributes.of(AttributeKey.stringKey("name"), "lemon", AttributeKey.stringKey("color"), "yellow")); + myFruitCounter.add(1, Attributes.of(AttributeKey.stringKey("name"), "lemon", AttributeKey.stringKey("color"), "yellow")); + myFruitCounter.add(2, Attributes.of(AttributeKey.stringKey("name"), "apple", AttributeKey.stringKey("color"), "green")); + myFruitCounter.add(5, Attributes.of(AttributeKey.stringKey("name"), "apple", AttributeKey.stringKey("color"), "red")); + myFruitCounter.add(4, Attributes.of(AttributeKey.stringKey("name"), "lemon", AttributeKey.stringKey("color"), "yellow")); + + // metrics are exported every 60 seconds by default + MINUTES.sleep(5); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private static void sendGaugeMetric() { + try { + MetricExporter exporter = new AzureMonitorExporterBuilder() + .connectionString(APPINSIGHTS_CONNECTION_STRING) + .buildMetricExporter(); + PeriodicMetricReader periodicMetricReader = PeriodicMetricReader + .builder(exporter) + .build(); + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(periodicMetricReader) + .build(); + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .buildAndRegisterGlobal(); + Meter meter = openTelemetry.getMeter("OTEL.AzureMonitor.Demo"); + + meter.gaugeBuilder("gauge") + .buildWithCallback( + observableMeasurement -> { + double randomNumber = Math.floor(Math.random() * 100); + observableMeasurement.record(randomNumber, Attributes.of(AttributeKey.stringKey("testKey"), "testValue")); + }); + + // metrics are exported every 60 seconds by default + MINUTES.sleep(5); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java index eff9e4eaa564a..c2a6effd07a12 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java @@ -31,7 +31,7 @@ void heartBeatPayloadContainsDataByDefault() throws InterruptedException { }, telemetryItemsConsumer); // some of the initialization above happens in a separate thread - Thread.sleep(500); + Thread.sleep(2000); // then MetricsData data = (MetricsData) provider.gatherData().getData().getBaseData(); diff --git a/sdk/servicebus/azure-messaging-servicebus/pom.xml b/sdk/servicebus/azure-messaging-servicebus/pom.xml index 268b47cdd8623..297fac6bbf151 100644 --- a/sdk/servicebus/azure-messaging-servicebus/pom.xml +++ b/sdk/servicebus/azure-messaging-servicebus/pom.xml @@ -131,14 +131,14 @@ io.opentelemetry opentelemetry-api - 1.20.0 + 1.23.0 test io.opentelemetry opentelemetry-sdk - 1.20.0 + 1.23.0 test From 6c9c6d9ba29482f39ca6daab1399d3567faddece Mon Sep 17 00:00:00 2001 From: Helen <56097766+heyams@users.noreply.github.com> Date: Fri, 24 Feb 2023 18:51:20 -0800 Subject: [PATCH 7/8] Prepre to release beta 8 (#33740) * Prepre to release beta 8 * Use awaitility * Push pom change * Verify verison in pom * Fix banned dependency * Fix pom * Fix pom --- eng/versioning/external_dependencies.txt | 1 + .../azure-monitor-opentelemetry-exporter/CHANGELOG.md | 10 ++++------ .../azure-monitor-opentelemetry-exporter/pom.xml | 7 ++++++- .../implementation/heartbeat/HeartbeatTests.java | 4 ++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index bedc54ed0b3f3..7ba257c7339ff 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -232,6 +232,7 @@ org.openjdk.jmh:jmh-core;1.22 org.openjdk.jmh:jmh-generator-annprocess;1.22 org.spockframework:spock-core;2.3-groovy-4.0 org.testng:testng;7.5 +org.awaitility:awaitility;4.2.0 uk.org.lidalia:slf4j-test;1.2.0 uk.org.webcompere:system-stubs-jupiter;2.0.1 com.google.truth:truth;1.1.3 diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index 3fb5d3dd30f88..3bb791233c3cd 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -1,14 +1,12 @@ # Release History -## 1.0.0-beta.8 (Unreleased) +## 1.0.0-beta.8 (2023-02-24) -### Features Added - -### Breaking Changes +### Dependency Update +- Update OpenTelemetry Java Instrumentation to 1.23.0 ### Bugs Fixed - -### Other Changes +- [Fix GlobalOpenTelemetry usage](https://github.com/Azure/azure-sdk-for-java/pull/33678) ## 1.0.0-beta.7 (2023-02-09) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml b/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml index 007ae5dfd40ef..a2b4c483ccc15 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/pom.xml @@ -151,7 +151,11 @@ 1.8.0 test - + + org.awaitility + awaitility + 4.2.0 + @@ -168,6 +172,7 @@ io.opentelemetry:opentelemetry-sdk:[1.23.0] io.opentelemetry:opentelemetry-sdk-logs:[1.23.0-alpha] com.github.spotbugs:spotbugs-annotations:[4.2.2] + org.awaitility:awaitility:[4.2.0] diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java index c2a6effd07a12..2a2e3956920a7 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/src/test/java/com/azure/monitor/opentelemetry/exporter/implementation/heartbeat/HeartbeatTests.java @@ -18,6 +18,7 @@ import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; class HeartbeatTests { @@ -30,8 +31,7 @@ void heartBeatPayloadContainsDataByDefault() throws InterruptedException { HeartbeatExporter provider = new HeartbeatExporter(60, (b, r) -> { }, telemetryItemsConsumer); - // some of the initialization above happens in a separate thread - Thread.sleep(2000); + await().until(() -> ((MetricsData) provider.gatherData().getData().getBaseData()).getProperties().size() > 0); // then MetricsData data = (MetricsData) provider.gatherData().getData().getBaseData(); From 09b62d1c534deec5da68d50f27a4291feb820bc9 Mon Sep 17 00:00:00 2001 From: Anu Thomas Chandy Date: Fri, 24 Feb 2023 18:57:58 -0800 Subject: [PATCH 8/8] [Phase-1] Disposition handling in Amqp-Core (#33593) * Adding ReceiverUnsettledDeliveries type dedicated for disposition handlings and tests * Integrating ReceiverUnsettledDeliveries with ServiceBusReactorReceiver * better name for api to partial terminate UnsettledDeliveries, address code review feedback * renamed 'completeUncompletedDispositionWorksOnClose' to 'completeDispositionWorksOnClose' --- .../resources/spotbugs/spotbugs-exclude.xml | 16 + eng/versioning/version_client.txt | 2 + .../amqp/implementation/ReactorReceiver.java | 13 + .../implementation/handler/LinkHandler.java | 6 +- .../handler/ReceiverUnsettledDeliveries.java | 739 ++++++++++++++++++ ...ceiverUnsettledDeliveriesIsolatedTest.java | 121 +++ .../ReceiverUnsettledDeliveriesTest.java | 435 +++++++++++ .../azure-messaging-eventhubs/pom.xml | 2 +- .../azure-messaging-servicebus/pom.xml | 2 +- .../ServiceBusReactorReceiver.java | 344 +------- .../ServiceBusReactorReceiverTest.java | 5 +- 11 files changed, 1357 insertions(+), 328 deletions(-) create mode 100644 sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveries.java create mode 100644 sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesIsolatedTest.java create mode 100644 sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesTest.java 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 b8c3cced66514..199403cd8611d 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 @@ -2345,6 +2345,22 @@ + + + + + + + + + + + + + + diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index aeb660d36a5c5..777ed20e4bc0b 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -407,6 +407,8 @@ com.azure.tools:azure-sdk-build-tool;1.0.0-beta.1;1.0.0-beta.2 # note: The unreleased dependencies will not be manipulated with the automatic PR creation code. # In the pom, the version update tag after the version should name the unreleased package and the dependency version: # + +unreleased_com.azure:azure-core-amqp;2.9.0-beta.1 unreleased_com.azure:azure-core;1.37.0-beta.1 # Released Beta dependencies: Copy the entry from above, prepend "beta_", remove the current diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorReceiver.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorReceiver.java index 229662d4ab0fd..cae487b58bc48 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorReceiver.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/ReactorReceiver.java @@ -352,6 +352,18 @@ protected Mono getIsClosedMono() { return isClosedMono.asMono().publishOn(Schedulers.boundedElastic()); } + protected void onHandlerClose() { + // Note: Given the disposition is a generic AMQP feature of brokers that support receive-link with UNSETTLED + // settlement mode, in near future we will enable delivery disposition API in amqp-core 'ReceiverLinkHandler'. + // Such a future API in 'ReceiverLinkHandler' means the handler will own the 'ReceiverUnsettledDeliveries' + // object, and the closing of the handler (i.e., handler.close()) will close 'ReceiverUnsettledDeliveries'. + // TODO: anuchan: Remove onHandlerClose + // This 'onHandlerClose' method is a temporary internal method for the 'ServiceBusReactorReceiver' to close + // the 'ReceiverUnsettledDeliveries' for the interim while we rollout the full disposition API support in + // amqp-core. The 'onHandlerClose' method will be removed once ownership of the 'ReceiverUnsettledDeliveries' + // is abstracted within 'ReceiverLinkHandler', so 'ServiceBusReactorReceiver' no longer have to own it. + } + /** * Beings the client side close by initiating local-close on underlying receiver. * @@ -459,6 +471,7 @@ private void completeClose() { } handler.close(); + onHandlerClose(); receiver.free(); try { trackPrefetchSeqNoSubscription.close(); diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/LinkHandler.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/LinkHandler.java index 3acb046c9dc0d..df89a68447d9e 100644 --- a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/LinkHandler.java +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/LinkHandler.java @@ -85,6 +85,10 @@ public void onLinkFinal(Event event) { } public AmqpErrorContext getErrorContext(Link link) { + return getErrorContext(getHostname(), entityPath, link); + } + + static AmqpErrorContext getErrorContext(String hostName, String entityPath, Link link) { final String referenceId; if (link.getRemoteProperties() != null && link.getRemoteProperties().containsKey(TRACKING_ID_PROPERTY)) { referenceId = link.getRemoteProperties().get(TRACKING_ID_PROPERTY).toString(); @@ -92,7 +96,7 @@ public AmqpErrorContext getErrorContext(Link link) { referenceId = link.getName(); } - return new LinkErrorContext(getHostname(), entityPath, referenceId, link.getCredit()); + return new LinkErrorContext(hostName, entityPath, referenceId, link.getCredit()); } private void handleRemoteLinkClosed(final String eventName, final Event event) { diff --git a/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveries.java b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveries.java new file mode 100644 index 0000000000000..1620afe45de68 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/main/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveries.java @@ -0,0 +1,739 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation.handler; + +import com.azure.core.amqp.AmqpRetryOptions; +import com.azure.core.amqp.AmqpRetryPolicy; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpErrorContext; +import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.implementation.ExceptionUtil; +import com.azure.core.amqp.implementation.ReactorDispatcher; +import com.azure.core.amqp.implementation.RetryUtil; +import com.azure.core.util.logging.ClientLogger; +import org.apache.qpid.proton.amqp.messaging.Outcome; +import org.apache.qpid.proton.amqp.messaging.Rejected; +import org.apache.qpid.proton.amqp.messaging.Released; +import org.apache.qpid.proton.amqp.transaction.TransactionalState; +import org.apache.qpid.proton.amqp.transport.DeliveryState; +import org.apache.qpid.proton.amqp.transport.DeliveryState.DeliveryStateType; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.engine.Delivery; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.azure.core.amqp.implementation.ClientConstants.DELIVERY_STATE_KEY; +import static com.azure.core.util.FluxUtil.monoError; + +/** + * Manages the received deliveries which are not settled on the broker. The client can later request settlement + * of each delivery by sending a disposition frame with a state representing the desired-outcome, + * which the application wishes to occur at the broker. The broker acknowledges this with a disposition frame + * with a state (a.k.a. remote-state) representing the actual outcome (a.k.a. remote-outcome) of any work + * the broker performed upon processing the settlement request and a flag (a.k.a. remotely-settled) indicating + * whether the broker settled the delivery. + */ +public final class ReceiverUnsettledDeliveries implements AutoCloseable { + // Ideally value of this const should be 'deliveryTag' but given the only use case today is as Service Bus + // LockToken, while logging, use the value 'lockToken' to ease log parsing. + // (TODO: anuchan; consider parametrizing the value of deliveryTag?). + private static final String DELIVERY_TAG_KEY = "lockToken"; + private final AtomicBoolean isTerminated = new AtomicBoolean(); + private final String hostName; + private final String entityPath; + private final String receiveLinkName; + private final ReactorDispatcher dispatcher; + private final AmqpRetryPolicy retryPolicy; + private final Duration timeout; + private final UUID deliveryEmptyTag; + private final ClientLogger logger; + private final Disposable timoutTimer; + + // The deliveries received, for those the application haven't sent disposition frame to the broker requesting + // settlement or disposition frame is sent, but yet to receive acknowledgment disposition frame from + // the broker indicating the outcome (a.k.a. remote-outcome). + private final ConcurrentHashMap deliveries = new ConcurrentHashMap<>(); + // A collection of work, where each work representing the disposition frame that the application sent, + // waiting to receive an acknowledgment disposition frame from the broker indicating the outcome + // (a.k.a. remote-outcome). + private final ConcurrentHashMap pendingDispositions = new ConcurrentHashMap<>(); + + /** + * Creates ReceiverUnsettledDeliveries. + * + * @param hostName the name of the host hosting the messaging entity identified by {@code entityPath}. + * @param entityPath the relative path identifying the messaging entity from which the deliveries are + * received from, the application can later disposition these deliveries by sending + * disposition frames to the broker. + * @param receiveLinkName the name of the amqp receive-link 'Attach'-ed to the messaging entity from + * which the deliveries are received from. + * @param dispatcher the dispatcher to invoke the ProtonJ library API to send disposition frame. + * @param retryOptions the retry configuration to use when resending a disposition frame that the broker 'Rejected'. + * @param deliveryEmptyTag reference to static UUID indicating absence of delivery tag in deliveries. + * @param logger the logger. + */ + public ReceiverUnsettledDeliveries(String hostName, String entityPath, String receiveLinkName, ReactorDispatcher dispatcher, + AmqpRetryOptions retryOptions, UUID deliveryEmptyTag, ClientLogger logger) { + this.hostName = hostName; + this.entityPath = entityPath; + this.receiveLinkName = receiveLinkName; + this.dispatcher = dispatcher; + this.retryPolicy = RetryUtil.getRetryPolicy(retryOptions); + this.timeout = retryOptions.getTryTimeout(); + this.deliveryEmptyTag = deliveryEmptyTag; + this.logger = logger; + this.timoutTimer = Flux.interval(timeout).subscribe(__ -> completeDispositionWorksOnTimeout("timer")); + } + + /** + * Function to notify a received delivery that is unsettled on the broker side; the application can later use + * {@link ReceiverUnsettledDeliveries#sendDisposition(String, DeliveryState)} to send a disposition frame requesting + * settlement of this delivery at the broker. + * + * @param deliveryTag the unique delivery tag associated with the {@code delivery}. + * @param delivery the delivery. + * @return {@code false} if the instance was closed upon notifying the delivery, {@code true} otherwise. + */ + public boolean onDelivery(UUID deliveryTag, Delivery delivery) { + if (isTerminated.get()) { + return false; + } else { + // Continue using putIfAbsent as legacy T1 library. + deliveries.putIfAbsent(deliveryTag.toString(), delivery); + return true; + } + } + + /** + * Check if a delivery with the given delivery tag was received. + * + * @param deliveryTag the delivery tag. + * @return {@code true} if delivery with the given delivery tag exists {@code false} otherwise. + */ + public boolean containsDelivery(UUID deliveryTag) { + return deliveryTag != deliveryEmptyTag && deliveries.containsKey(deliveryTag.toString()); + } + + /** + * Request settlement of delivery (with the unique {@code deliveryTag}) by sending a disposition frame + * with a state representing the desired-outcome, which the application wishes to occur at the broker. + * Disposition frame is sent via the same amqp receive-link that delivered the delivery, which was + * notified through {@link ReceiverUnsettledDeliveries#onDelivery(UUID, Delivery)}. + * + * @param deliveryTag the unique delivery tag identifying the delivery. + * @param desiredState The state to include in the disposition frame indicating the desired-outcome + * that the application wish to occur at the broker. + * @return the {@link Mono} upon subscription starts the work by requesting ProtonJ library to send + * disposition frame to settle the delivery on the broker, and this Mono terminates once the broker + * acknowledges with disposition frame indicating outcome (a.ka. remote-outcome). + * The Mono can terminate if the configured timeout elapses or cannot initiate the request to ProtonJ + * library. + */ + public Mono sendDisposition(String deliveryTag, DeliveryState desiredState) { + if (isTerminated.get()) { + return monoError(logger, new IllegalStateException("Cannot perform sendDisposition on a disposed receiver.")); + } else { + return sendDispositionImpl(deliveryTag, desiredState); + } + } + + /** + * The function to notify the broker's acknowledgment in response to a disposition frame sent to the broker + * via {@link ReceiverUnsettledDeliveries#sendDisposition(String, DeliveryState)}. + * The broker acknowledgment is also a disposition frame; the ProtonJ library will map this disposition + * frame to the same Delivery in-memory object for which the application requested disposition. + * As part of mapping, the remote-state (representing remote-outcome) and is-remotely-settled (boolean) + * property of the Delivery object is updated from the disposition frame ack. + * + * @param deliveryTag the unique delivery tag of the delivery that application requested disposition. + * @param delivery the delivery object updated from the broker's transfer frame ack. + */ + public void onDispositionAck(UUID deliveryTag, Delivery delivery) { + final DeliveryState remoteState = delivery.getRemoteState(); + + logger.atVerbose() + .addKeyValue(DELIVERY_TAG_KEY, deliveryTag) + .addKeyValue(DELIVERY_STATE_KEY, remoteState) + .log("Received update disposition delivery."); + + final Outcome remoteOutcome; + if (remoteState instanceof Outcome) { + remoteOutcome = (Outcome) remoteState; + } else if (remoteState instanceof TransactionalState) { + remoteOutcome = ((TransactionalState) remoteState).getOutcome(); + } else { + remoteOutcome = null; + } + + if (remoteOutcome == null) { + logger.atWarning() + .addKeyValue(DELIVERY_TAG_KEY, deliveryTag) + .addKeyValue("delivery", delivery) + .log("No outcome associated with delivery."); + + return; + } + + final DispositionWork work = pendingDispositions.get(deliveryTag.toString()); + if (work == null) { + logger.atWarning() + .addKeyValue(DELIVERY_TAG_KEY, deliveryTag) + .addKeyValue("delivery", delivery) + .log("No pending update for delivery."); + return; + } + + // the outcome that application desired. + final DeliveryStateType desiredOutcomeType = work.getDesiredState().getType(); + // the outcome that broker actually attained. + final DeliveryStateType remoteOutcomeType = remoteState.getType(); + + if (desiredOutcomeType == remoteOutcomeType) { + completeDispositionWorkWithSettle(work, delivery, null); + } else { + logger.atInfo() + .addKeyValue(DELIVERY_TAG_KEY, deliveryTag) + .addKeyValue("receivedDeliveryState", remoteState) + .addKeyValue(DELIVERY_STATE_KEY, work.getDesiredState()) + .log("Received delivery state doesn't match expected state."); + + if (remoteOutcomeType == DeliveryStateType.Rejected) { + handleRetriableRejectedRemoteOutcome(work, delivery, (Rejected) remoteOutcome); + } else { + handleReleasedOrUnknownRemoteOutcome(work, delivery, remoteOutcome); + } + } + } + + /** + * Terminate this {@link ReceiverUnsettledDeliveries} including expired disposition works, and await to complete + * disposition work in progress, with AmqpRetryOptions_tryTimeout as the upper bound for the wait time. + *

+ * Given this is a terminal API in which the disposition timeout timer will be used last time, termination disposes + * the timer as well. Future attempts to notify unsettled deliveries or send delivery dispositions will be rejected. + *

+ * From the point of view of this function's call site, it is still possible that the receive-link and dispatcher + * may healthy, but not guaranteed. If healthy, send-receive of disposition frames are possible, enabling + * 'graceful' completion of works. + *

+ * e.g., if the user proactively initiates the closing of client, it is likely that the receive-link may be + * healthy. On the other hand, if the broker initiates the closing of the link, further frame transfer may not be + * possible. + * + * @return a {@link Mono} that await to complete disposition work in progress, the wait has an upper bound + * of AmqpRetryOptions_tryTimeout. + */ + public Mono terminateAndAwaitForDispositionsInProgressToComplete() { + // 1. Mark this ReceiverUnsettledDeliveries as terminated, so it no longer accept unsettled deliveries + // or disposition requests + isTerminated.getAndSet(true); + + // 2. then complete timed out works if any + completeDispositionWorksOnTimeout("terminateAndAwaitForDispositionsInProgressToComplete"); + + // 3. then obtain a Mono that wait, with AmqpRetryOptions_tryTimeout as the upper bound for the maximum + // wait, for the completion of any disposition work in progress, which includes committing open transaction + // work. The upper bound for the wait time is imposed through timeoutTimer. + final List> workMonoList = new ArrayList<>(); + final StringJoiner deliveryTags = new StringJoiner(", "); + for (DispositionWork work : pendingDispositions.values()) { + if (work == null || work.hasTimedout()) { + continue; + } + if (work.getDesiredState() instanceof TransactionalState) { + final Mono workMono = sendDispositionImpl(work.getDeliveryTag(), Released.getInstance()); + workMonoList.add(workMono); + } else { + workMonoList.add(work.getMono()); + } + deliveryTags.add(work.getDeliveryTag()); + } + + final Mono workMonoListMerged; + if (!workMonoList.isEmpty()) { + logger.info("Waiting for pending updates to complete. Locks: {}", deliveryTags.toString()); + workMonoListMerged = Mono.whenDelayError(workMonoList) + .onErrorResume(error -> { + logger.info("There was exception(s) while disposing of all disposition work.", error); + return Mono.empty(); + }); + } else { + workMonoListMerged = Mono.empty(); + } + final Mono dispositionsWithTimeout = workMonoListMerged; + + // 4. finally, disposes the timeoutTimer after its final use (to timeout disposition works in-progress). + return dispositionsWithTimeout.doFinally(__ -> timoutTimer.dispose()); + } + + /** + * Closes this {@link ReceiverUnsettledDeliveries} and force complete any uncompleted work. Future attempts + * to notify unsettled deliveries or send delivery dispositions will be rejected. + */ + @Override + public void close() { + isTerminated.getAndSet(true); + + // Disposes of timeoutTimer's internal subscription to the global interval timer. + timoutTimer.dispose(); + + // Note: Once disposition API support is enabled in ReceiveLinkHandler - this close() method should have + // logic to free the tracked QPID deliveries. The ReceiveLinkHandler will no longer track "ALL" QPID + // deliveries (using 'queuedDeliveries' set), because, the plan is, upon arrival of any delivery in + // reactor-thread, we will be draining the delivery buffer and settle those deliveries already settled + // by the broker, so we need settle only the deliveries in ReceiverUnsettledDeliveries.deliveries in this close. + + // Force complete all uncompleted works. + completeDispositionWorksOnClose(); + } + + /** + * See the doc for {@link ReceiverUnsettledDeliveries#sendDisposition(String, DeliveryState)}. + * + * @param deliveryTag the unique delivery tag identifying the delivery. + * @param desiredState The state to include in the disposition frame indicating the desired-outcome + * that the application wish to occur at the broker. + * @return the {@link Mono} representing disposition work. + */ + private Mono sendDispositionImpl(String deliveryTag, DeliveryState desiredState) { + final Delivery delivery = deliveries.get(deliveryTag); + if (delivery == null) { + logger.atWarning() + .addKeyValue(DELIVERY_TAG_KEY, deliveryTag) + .log("Delivery not found to update disposition."); + + return monoError(logger, Exceptions.propagate(new IllegalArgumentException( + "Delivery not on receive link."))); + } + + final DispositionWork work = new DispositionWork(deliveryTag, desiredState, timeout); + + final Mono mono = Mono.create(sink -> { + work.onStart(sink); + try { + dispatcher.invoke(() -> { + delivery.disposition(desiredState); + if (pendingDispositions.putIfAbsent(deliveryTag, work) != null) { + work.onComplete(new AmqpException(false, + "A disposition requested earlier is waiting for the broker's ack; " + + "a new disposition request is not allowed.", + null)); + } + }); + } catch (IOException | RejectedExecutionException dispatchError) { + work.onComplete(new AmqpException(false, "updateDisposition failed while dispatching to Reactor.", + dispatchError, getErrorContext(delivery))); + } + }); + + work.setMono(mono); + + return work.getMono(); + } + + /** + * Handles the 'Rejected' outcome (in a disposition ack) from the broker in-response to a disposition frame + * application sent. + * + * @param work the work that sent the disposition frame with a desired-outcome which broker 'Rejected'. + * @param delivery the Delivery in-memory object for which the application had sent the disposition frame; + * the ProtonJ library updates the remote-state (representing remote-outcome) and + * is-remotely-settled (boolean) property of the Delivery object from the disposition frame ack. + * @param remoteOutcome the 'Rejected' remote-outcome describing the rejection reason, this is derived from + * the remote-state. + */ + private void handleRetriableRejectedRemoteOutcome(DispositionWork work, Delivery delivery, Rejected remoteOutcome) { + final AmqpErrorContext amqpErrorContext = getErrorContext(delivery); + final ErrorCondition errorCondition = remoteOutcome.getError(); + final Throwable error = ExceptionUtil.toException(errorCondition.getCondition().toString(), + errorCondition.getDescription(), amqpErrorContext); + + final Duration retry = retryPolicy.calculateRetryDelay(error, work.getTryCount()); + if (retry != null) { + work.onRetriableRejectedOutcome(error); + try { + dispatcher.invoke(() -> delivery.disposition(work.getDesiredState())); + } catch (IOException | RejectedExecutionException dispatchError) { + final Throwable amqpException = logger.atError() + .addKeyValue(DELIVERY_TAG_KEY, work.getDeliveryTag()) + .log(new AmqpException(false, + String.format("linkName[%s], deliveryTag[%s]. Retrying updateDisposition failed to dispatch to Reactor.", + receiveLinkName, work.getDeliveryTag()), + dispatchError, getErrorContext(delivery))); + + completeDispositionWorkWithSettle(work, delivery, amqpException); + } + } else { + logger.atInfo() + .addKeyValue(DELIVERY_TAG_KEY, work.getDeliveryTag()) + .addKeyValue(DELIVERY_STATE_KEY, delivery.getRemoteState()) + .log("Retry attempts exhausted.", error); + + completeDispositionWorkWithSettle(work, delivery, error); + } + } + + /** + * Handles the 'Released' or unknown outcome (in a disposition ack) from the broker in-response to a disposition + * frame application sent. + * + * @param work the work that sent the disposition frame with a desired-outcome. + * @param delivery the Delivery in-memory object for which the application had sent the disposition frame; + * the ProtonJ library updates the remote-state (representing remote-outcome) and + * is-remotely-settled (boolean) property of the Delivery object from the disposition frame ack. + * @param remoteOutcome the remote-outcome from the broker describing the reason for broker choosing an outcome + * different from requested desired-outcome, this is derived from the remote-state. + */ + private void handleReleasedOrUnknownRemoteOutcome(DispositionWork work, Delivery delivery, Outcome remoteOutcome) { + final AmqpErrorContext amqpErrorContext = getErrorContext(delivery); + final AmqpException completionError; + + final DeliveryStateType remoteOutcomeType = delivery.getRemoteState().getType(); + if (remoteOutcomeType == DeliveryStateType.Released) { + completionError = new AmqpException(false, AmqpErrorCondition.OPERATION_CANCELLED, + "AMQP layer unexpectedly aborted or disconnected.", amqpErrorContext); + } else { + completionError = new AmqpException(false, remoteOutcome.toString(), amqpErrorContext); + } + + logger.atInfo() + .addKeyValue(DELIVERY_TAG_KEY, work.getDeliveryTag()) + .addKeyValue(DELIVERY_STATE_KEY, delivery.getRemoteState()) + .log("Completing pending updateState operation with exception.", completionError); + + completeDispositionWorkWithSettle(work, delivery, completionError); + } + + /** + * Iterate through all the current {@link DispositionWork} and complete the work those are timed out. + */ + private void completeDispositionWorksOnTimeout(String callSite) { + if (pendingDispositions.isEmpty()) { + return; + } + + final int[] completionCount = new int[1]; + final StringJoiner deliveryTags = new StringJoiner(", "); + + pendingDispositions.forEach((deliveryTag, work) -> { + if (work == null || !work.hasTimedout()) { + return; + } + + if (completionCount[0] == 0) { + logger.info("Starting completion of timed out disposition works (call site:{}).", callSite); + } + + final Throwable completionError; + if (work.getRejectedOutcomeError() != null) { + completionError = work.getRejectedOutcomeError(); + } else { + completionError = new AmqpException(true, AmqpErrorCondition.TIMEOUT_ERROR, + "Update disposition request timed out.", getErrorContext(deliveries.get(work.getDeliveryTag()))); + } + deliveryTags.add(work.getDeliveryTag()); + completeDispositionWork(work, completionError); + completionCount[0]++; + }); + + if (completionCount[0] > 0) { + // The log help debug if the user code chained to the work-mono (DispositionWork::getMono()) never returns. + logger.info("Completed {} timed-out disposition works (call site:{}). Locks {}", + callSite, completionCount[0], deliveryTags.toString()); + } + } + + /** + * Iterate through all the {@link DispositionWork}, and 'force' to complete the uncompleted works because + * this {@link ReceiverUnsettledDeliveries} is closed. + */ + private void completeDispositionWorksOnClose() { + // Note: Possible to have one function for cleaning both timeout and incomplete works, but readability + // seems to be affected, hence separate functions. + + if (pendingDispositions.isEmpty()) { + return; + } + + final int[] completionCount = new int[1]; + final StringJoiner deliveryTags = new StringJoiner(", "); + + final AmqpException completionError = new AmqpException(false, + "The receiver didn't receive the disposition acknowledgment due to receive link closure.", null); + + pendingDispositions.forEach((deliveryTag, work) -> { + if (work == null || work.isCompleted()) { + return; + } + + if (completionCount[0] == 0) { + logger.info("Starting completion of disposition works as part of receive link closure."); + } + + deliveryTags.add(work.getDeliveryTag()); + completeDispositionWork(work, completionError); + completionCount[0]++; + }); + + if (completionCount[0] > 0) { + // The log help debug if the user code chained to the work-mono (DispositionWork::getMono()) never returns. + logger.info("Completed {} disposition works as part of receive link closure. Locks {}", + completionCount[0], deliveryTags.toString()); + } + } + + /** + * Completes the given {@link DispositionWork}, which results in termination of the {@link Mono} returned + * from the {@link DispositionWork#getMono()} API. If the broker settled the {@link Delivery} associated + * with the work, it would also be locally settled. + *

+ * Invocations of this function are guaranteed to be serial, as all call sites originate from + * {@link ReceiverUnsettledDeliveries#onDispositionAck(UUID, Delivery)} running on the ProtonJ Reactor event-loop thread. + * + * @param work the work to complete. + * @param delivery the delivery that the work attempted the disposition, to be locally settled if the broker + * settled it on the remote end. + * @param completionError a null value indicates that the work has to complete successfully, otherwise complete + * the work with the error value. + */ + private void completeDispositionWorkWithSettle(DispositionWork work, Delivery delivery, Throwable completionError) { + // The operation ordering same as the T1 Lib: "delivery-settling -> work-completion -> work-delivery-removal". + + final boolean isRemotelySettled = delivery.remotelySettled(); + if (isRemotelySettled) { + delivery.settle(); + } + + if (completionError != null) { + final Throwable loggedError = completionError instanceof RuntimeException + ? logger.logExceptionAsError((RuntimeException) completionError) + : completionError; + work.onComplete(loggedError); + } else { + work.onComplete(); + } + + if (isRemotelySettled) { + final String deliveryTag = work.getDeliveryTag(); + pendingDispositions.remove(deliveryTag); + deliveries.remove(deliveryTag); + } + } + + /** + * Completes the given {@link DispositionWork} with error, which results in termination of the {@link Mono} + * returned from the {@link DispositionWork#getMono()} API. + * + * @param work the work to complete with error. + * @param completionError the non-null error value. + */ + private void completeDispositionWork(DispositionWork work, Throwable completionError) { + // The operation ordering same as the T1 Lib: "work-removal -> work-completion". + + pendingDispositions.remove(work.getDeliveryTag()); + + final Throwable loggedError = completionError instanceof RuntimeException + ? logger.logExceptionAsError((RuntimeException) completionError) + : completionError; + work.onComplete(loggedError); + } + + /** + * Gets the error context from the receive-link associated with the delivery. + * + * @param delivery the delivery. + * @return the error context from delivery's receive-link, {@code null} if the delivery or + * receive-link is {@code null}. + */ + private AmqpErrorContext getErrorContext(Delivery delivery) { + if (delivery == null || delivery.getLink() == null) { + return null; + } + return LinkHandler.getErrorContext(hostName, entityPath, delivery.getLink()); + } + + /** + * Represents a work that, upon starting, requests ProtonJ library to send a disposition frame to settle + * a delivery on the broker and the work completes when the broker acknowledges with a disposition frame + * indicating the outcome. The work can complete with an error if it cannot initiate the request + * to the ProtonJ library or the configured timeout elapses. + *

+ * The work is started once the application is subscribed to the {@link Mono} returned by + * {@link DispositionWork#getMono()}; the Mono is terminated upon the work completion. + */ + private static final class DispositionWork extends AtomicBoolean { + private final AtomicInteger tryCount = new AtomicInteger(1); + private final String deliveryTag; + private final DeliveryState desiredState; + private final Duration timeout; + private Mono mono; + private MonoSink monoSink; + private Instant expirationTime; + private Throwable rejectedOutcomeError; + + /** + * Create a DispositionWork. + * + * @param deliveryTag The delivery tag of the Delivery for which to send the disposition frame requesting + * delivery settlement on the broker. + * @param desiredState The state to include in the disposition frame indicating the desired-outcome + * the application wish to occur at the broker. + * @param timeout after requesting the ProtonJ library to send the disposition frame, how long to wait for + * an acknowledgment disposition frame to arrive from the broker. + */ + DispositionWork(String deliveryTag, DeliveryState desiredState, Duration timeout) { + this.deliveryTag = deliveryTag; + this.desiredState = desiredState; + this.timeout = timeout; + this.monoSink = null; + } + + /** + * Gets the delivery tag. + * + * @return the delivery tag. + */ + String getDeliveryTag() { + return deliveryTag; + } + + /** + * Gets the state indicating the desired-outcome which the application wishes to occur at the broker. + * The disposition frame send to the broker includes this desired state. + * + * @return the desired state. + */ + DeliveryState getDesiredState() { + return desiredState; + } + + /** + * Gets the number of times the work was tried. + * + * @return the try count. + */ + int getTryCount() { + return tryCount.get(); + } + + /** + * Gets the error received from the broker when the outcome of the last disposition attempt + * (by sending a disposition frame) happened to be 'Rejected'. + * + * @return the error in the disposition ack frame from the broker with 'Rejected' outcome, + * null if no such disposition ack frame received. + */ + Throwable getRejectedOutcomeError() { + return rejectedOutcomeError; + } + + /** + * Check if the work has timed out. + * + * @return {@code true} if the work has timed out, {@code false} otherwise. + */ + boolean hasTimedout() { + return expirationTime != null && expirationTime.isBefore(Instant.now()); + } + + /** + * Gets the {@link Mono} upon subscription starts the work by requesting ProtonJ library to send + * disposition frame to settle a delivery on the broker, and this Mono terminates once the broker + * acknowledges with disposition frame indicating settlement outcome (a.k.a. remote-outcome) + * The Mono can terminate if the configured timeout elapses or cannot initiate the request to + * ProtonJ library. + * + * @return the mono + */ + Mono getMono() { + return mono; + } + + /** + * Sets the {@link Mono}, where the application can obtain cached version of it + * from {@link DispositionWork#getMono()} and subscribe to start the work, the mono terminates + * upon the successful or unsuccessful completion of the work. + * + * @param mono the mono + */ + void setMono(Mono mono) { + // cache() the mono to replay the result when subscribed more than once, avoid multiple + // disposition placement (and enables a possible second subscription to be safe when closing + // the UnsettledDeliveries type). + this.mono = mono.cache(); + } + + /** + * Check if this work is already completed. + * + * @return {@code true} if the work is completed, {@code true} otherwise. + */ + boolean isCompleted() { + return this.get(); + } + + /** + * The function invoked once the application start the work by subscribing to the {@link Mono} + * obtained from {@link DispositionWork#getMono()}. + * + * @param monoSink the {@link MonoSink} to notify the completion of the work, which triggers + * termination of the same {@link Mono} that started the work. + */ + void onStart(MonoSink monoSink) { + this.monoSink = monoSink; + expirationTime = Instant.now().plus(timeout); + } + + /** + * The function invoked when the work is about to be restarted/retried. The broker may return an + * outcome named 'Rejected' if it is unable to attain the desired-outcome that the application + * specified in the disposition frame; in this case, the work is retried based on the configured + * retry settings. + * + * @param error the error that the broker returned upon Reject-ing the last work execution attempting + * the disposition. + */ + void onRetriableRejectedOutcome(Throwable error) { + this.rejectedOutcomeError = error; + expirationTime = Instant.now().plus(timeout); + tryCount.incrementAndGet(); + } + + /** + * the function invoked upon the successful completion of the work. + */ + void onComplete() { + this.set(true); + Objects.requireNonNull(monoSink); + monoSink.success(); + } + + /** + * the function invoked when the work is completed with an error. + * + * @param error the error reason. + */ + void onComplete(Throwable error) { + this.set(true); + Objects.requireNonNull(monoSink); + monoSink.error(error); + } + } +} diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesIsolatedTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesIsolatedTest.java new file mode 100644 index 0000000000000..ebf87fa531d83 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesIsolatedTest.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation.handler; + +import com.azure.core.amqp.AmqpRetryOptions; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.implementation.ReactorDispatcher; +import com.azure.core.util.logging.ClientLogger; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.engine.Delivery; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.Isolated; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +import java.io.IOException; +import java.time.Duration; +import java.util.UUID; +import java.util.function.Supplier; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +@Execution(ExecutionMode.SAME_THREAD) +@Isolated +public class ReceiverUnsettledDeliveriesIsolatedTest { + private static final UUID DELIVERY_EMPTY_TAG = new UUID(0L, 0L); + private static final String HOSTNAME = "hostname"; + private static final String ENTITY_PATH = "/orders"; + private static final String RECEIVER_LINK_NAME = "orders-link"; + private static final Duration VERIFY_TIMEOUT = Duration.ofSeconds(20); + private static final Duration OPERATION_TIMEOUT = Duration.ofSeconds(3); + private static final Duration VIRTUAL_TIME_SHIFT = OPERATION_TIMEOUT.plusSeconds(30); + private final ClientLogger logger = new ClientLogger(ReceiverUnsettledDeliveriesTest.class); + private final AmqpRetryOptions retryOptions = new AmqpRetryOptions(); + private AutoCloseable mocksCloseable; + @Mock + private ReactorDispatcher reactorDispatcher; + @Mock + private Delivery delivery; + + @BeforeEach + public void setup() throws IOException { + mocksCloseable = MockitoAnnotations.openMocks(this); + retryOptions.setTryTimeout(OPERATION_TIMEOUT); + } + + @AfterEach + public void teardown() throws Exception { + Mockito.framework().clearInlineMock(this); + + if (mocksCloseable != null) { + mocksCloseable.close(); + } + } + + @Test + @Execution(ExecutionMode.SAME_THREAD) + public void sendDispositionTimeoutOnExpiration() throws Exception { + final UUID deliveryTag = UUID.randomUUID(); + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), Accepted.getInstance()); + try (VirtualTimeStepVerifier verifier = new VirtualTimeStepVerifier()) { + verifier.create(() -> dispositionMono, VIRTUAL_TIME_SHIFT) + .expectErrorSatisfies(error -> { + Assertions.assertTrue(error instanceof AmqpException); + final AmqpException amqpError = (AmqpException) error; + Assertions.assertEquals(AmqpErrorCondition.TIMEOUT_ERROR, amqpError.getErrorCondition()); + }) + .verify(VERIFY_TIMEOUT); + } + } + } + + private ReceiverUnsettledDeliveries createUnsettledDeliveries() { + return new ReceiverUnsettledDeliveries(HOSTNAME, ENTITY_PATH, RECEIVER_LINK_NAME, + reactorDispatcher, retryOptions, DELIVERY_EMPTY_TAG, logger); + } + + private static Answer byRunningRunnable() { + return invocation -> { + final Runnable runnable = invocation.getArgument(0); + runnable.run(); + return null; + }; + } + + private static final class VirtualTimeStepVerifier implements AutoCloseable { + private final VirtualTimeScheduler scheduler; + + VirtualTimeStepVerifier() { + scheduler = VirtualTimeScheduler.create(); + } + + StepVerifier.Step create(Supplier> scenarioSupplier, Duration timeShift) { + return StepVerifier.withVirtualTime(scenarioSupplier, () -> scheduler, 1) + .thenAwait(timeShift); + } + + @Override + public void close() { + scheduler.dispose(); + } + } +} diff --git a/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesTest.java b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesTest.java new file mode 100644 index 0000000000000..f182ffba40822 --- /dev/null +++ b/sdk/core/azure-core-amqp/src/test/java/com/azure/core/amqp/implementation/handler/ReceiverUnsettledDeliveriesTest.java @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.amqp.implementation.handler; + +import com.azure.core.amqp.AmqpRetryOptions; +import com.azure.core.amqp.exception.AmqpErrorCondition; +import com.azure.core.amqp.exception.AmqpException; +import com.azure.core.amqp.implementation.AmqpErrorCode; +import com.azure.core.amqp.implementation.ReactorDispatcher; +import com.azure.core.util.logging.ClientLogger; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.Rejected; +import org.apache.qpid.proton.amqp.messaging.Released; +import org.apache.qpid.proton.amqp.transaction.Declared; +import org.apache.qpid.proton.amqp.transport.DeliveryState; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.engine.Delivery; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.RejectedExecutionException; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ReceiverUnsettledDeliveriesTest { + private static final UUID DELIVERY_EMPTY_TAG = new UUID(0L, 0L); + private static final String HOSTNAME = "hostname"; + private static final String ENTITY_PATH = "/orders"; + private static final String RECEIVER_LINK_NAME = "orders-link"; + private static final String DISPOSITION_ERROR_ON_CLOSE = "The receiver didn't receive the disposition " + + "acknowledgment due to receive link closure."; + private final ClientLogger logger = new ClientLogger(ReceiverUnsettledDeliveriesTest.class); + private final AmqpRetryOptions retryOptions = new AmqpRetryOptions(); + private AutoCloseable mocksCloseable; + @Mock + private ReactorDispatcher reactorDispatcher; + @Mock + private Delivery delivery; + + @BeforeEach + public void setup() throws IOException { + mocksCloseable = MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void teardown() throws Exception { + Mockito.framework().clearInlineMock(this); + + if (mocksCloseable != null) { + mocksCloseable.close(); + } + } + + @Test + public void tracksOnDelivery() throws IOException { + doNothing().when(reactorDispatcher).invoke(any(Runnable.class)); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + final UUID deliveryTag = UUID.randomUUID(); + deliveries.onDelivery(deliveryTag, delivery); + assertTrue(deliveries.containsDelivery(deliveryTag)); + } + } + + @Test + public void sendDispositionErrorsForUntrackedDelivery() { + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + final UUID deliveryTag = UUID.randomUUID(); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), Accepted.getInstance()); + StepVerifier.create(dispositionMono) + .verifyError(IllegalArgumentException.class); + } + } + + @Test + public void sendDispositionErrorsOnDispatcherIOException() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + + doThrow(new IOException()).when(reactorDispatcher).invoke(any(Runnable.class)); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), Accepted.getInstance()); + StepVerifier.create(dispositionMono) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + Assertions.assertNotNull(error.getCause()); + Assertions.assertInstanceOf(IOException.class, error.getCause()); + }); + } + } + + @Test + public void sendDispositionErrorsOnDispatcherRejectedException() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + + doThrow(new RejectedExecutionException()).when(reactorDispatcher).invoke(any(Runnable.class)); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), Accepted.getInstance()); + StepVerifier.create(dispositionMono) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + Assertions.assertNotNull(error.getCause()); + Assertions.assertInstanceOf(RejectedExecutionException.class, error.getCause()); + }); + } + } + + @Test + public void sendDispositionErrorsIfSameDeliveryDispositionInProgress() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono1 = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + dispositionMono1.subscribe(); + final Mono dispositionMono2 = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono2) + .verifyError(AmqpException.class); + } + } + + @Test + public void sendDispositionCompletesOnSuccessfulOutcome() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = desiredState; + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyComplete(); + verify(delivery).disposition(desiredState); + Assertions.assertFalse(deliveries.containsDelivery(deliveryTag)); + } + } + + @Test + public void sendDispositionErrorsOnReleaseOutcome() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = Released.getInstance(); + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + final AmqpException amqpError = (AmqpException) error; + Assertions.assertNotNull(amqpError.getErrorCondition()); + Assertions.assertEquals(AmqpErrorCondition.OPERATION_CANCELLED, amqpError.getErrorCondition()); + }); + verify(delivery).disposition(desiredState); + Assertions.assertFalse(deliveries.containsDelivery(deliveryTag)); + } + } + + @Test + public void sendDispositionErrorsOnUnknownOutcome() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = new Declared(); + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + Assertions.assertEquals(remoteState.toString(), error.getMessage()); + }); + verify(delivery).disposition(desiredState); + Assertions.assertFalse(deliveries.containsDelivery(deliveryTag)); + } + } + + @Test + public void sendDispositionRetriesOnRejectedOutcome() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final Rejected remoteState = new Rejected(); + final ErrorCondition remoteError = new ErrorCondition(AmqpErrorCode.SERVER_BUSY_ERROR, null); + remoteState.setError(remoteError); + final int[] dispositionCallCount = new int[1]; + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + doAnswer(__ -> { + if (dispositionCallCount[0] != 0) { + deliveries.onDispositionAck(deliveryTag, delivery); + } + dispositionCallCount[0]++; + return null; + }).when(delivery).disposition(any()); + + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + final AmqpException amqpError = (AmqpException) error; + Assertions.assertEquals(AmqpErrorCondition.SERVER_BUSY_ERROR, amqpError.getErrorCondition()); + }); + Assertions.assertEquals(retryOptions.getMaxRetries() + 1, dispositionCallCount[0]); + } + } + + @Test + public void sendDispositionMonoCacheCompletion() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = desiredState; + final int[] dispositionCallCount = new int[1]; + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + doAnswer(invocation -> { + dispositionCallCount[0]++; + return null; + }).when(delivery).disposition(any()); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyComplete(); + for (int i = 0; i < 3; i++) { + StepVerifier.create(dispositionMono).verifyComplete(); + } + Assertions.assertEquals(1, dispositionCallCount[0]); + } + } + + @Test + public void sendDispositionMonoCacheError() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = new Declared(); + final int[] dispositionCallCount = new int[1]; + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + doAnswer(invocation -> { + dispositionCallCount[0]++; + return null; + }).when(delivery).disposition(any()); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + final Throwable[] lastError = new Throwable[1]; + StepVerifier.create(dispositionMono) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyErrorSatisfies(error -> lastError[0] = error); + for (int i = 0; i < 3; i++) { + StepVerifier.create(dispositionMono) + .verifyErrorSatisfies(error -> { + Assertions.assertEquals(lastError[0], error, + "Expected replay of the last error object, but received a new error object."); + }); + } + Assertions.assertEquals(1, dispositionCallCount[0]); + } + } + + @Test + public void pendingSendDispositionErrorsOnClose() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = new Declared(); + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + final ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries(); + try { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + StepVerifier.create(dispositionMono) + .then(() -> deliveries.close()) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + Assertions.assertEquals(DISPOSITION_ERROR_ON_CLOSE, error.getMessage()); + }); + } finally { + deliveries.close(); + } + } + + @Test + public void shouldTerminateAndAwaitForDispositionInProgressToComplete() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = desiredState; + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + try (ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries()) { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + dispositionMono.subscribe(); + + StepVerifier.create(deliveries.terminateAndAwaitForDispositionsInProgressToComplete()) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyComplete(); + + StepVerifier.create(dispositionMono).verifyComplete(); + } + } + + @Test + public void closeDoNotWaitForSendDispositionCompletion() throws IOException { + final UUID deliveryTag = UUID.randomUUID(); + final DeliveryState desiredState = Accepted.getInstance(); + final DeliveryState remoteState = desiredState; + + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + when(delivery.getRemoteState()).thenReturn(remoteState); + when(delivery.remotelySettled()).thenReturn(true); + + final ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries(); + try { + deliveries.onDelivery(deliveryTag, delivery); + final Mono dispositionMono = deliveries.sendDisposition(deliveryTag.toString(), desiredState); + dispositionMono.subscribe(); + + StepVerifier.create(Mono.fromRunnable(() -> deliveries.close())) + .then(() -> deliveries.onDispositionAck(deliveryTag, delivery)) + .verifyComplete(); + + StepVerifier.create(dispositionMono) + .verifyErrorSatisfies(error -> { + Assertions.assertInstanceOf(AmqpException.class, error); + Assertions.assertEquals(DISPOSITION_ERROR_ON_CLOSE, error.getMessage()); + }); + } finally { + deliveries.close(); + } + } + + @Test + public void nopOnDeliveryOnceClosed() { + final UUID deliveryTag = UUID.randomUUID(); + + final ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries(); + try { + deliveries.close(); + Assertions.assertFalse(deliveries.onDelivery(deliveryTag, delivery)); + Assertions.assertFalse(deliveries.containsDelivery(deliveryTag)); + } finally { + deliveries.close(); + } + } + + @Test + @Disabled("Enable in once disposition API exposed in ReceiveLinkHandler") + public void settlesUnsettledDeliveriesOnClose() throws IOException { + // See the notes in ReceiverUnsettledDeliveries.close() + // + doAnswer(byRunningRunnable()).when(reactorDispatcher).invoke(any(Runnable.class)); + + final ReceiverUnsettledDeliveries deliveries = createUnsettledDeliveries(); + try { + deliveries.onDelivery(UUID.randomUUID(), delivery); + deliveries.close(); + verify(delivery).disposition(any()); + verify(delivery).settle(); + } finally { + deliveries.close(); + } + } + + private ReceiverUnsettledDeliveries createUnsettledDeliveries() { + return new ReceiverUnsettledDeliveries(HOSTNAME, ENTITY_PATH, RECEIVER_LINK_NAME, + reactorDispatcher, retryOptions, DELIVERY_EMPTY_TAG, logger); + } + + private static Answer byRunningRunnable() { + return invocation -> { + final Runnable runnable = invocation.getArgument(0); + runnable.run(); + return null; + }; + } +} diff --git a/sdk/eventhubs/azure-messaging-eventhubs/pom.xml b/sdk/eventhubs/azure-messaging-eventhubs/pom.xml index 8850dba79eb6f..da3a7d594f9b2 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/pom.xml +++ b/sdk/eventhubs/azure-messaging-eventhubs/pom.xml @@ -42,7 +42,7 @@ com.azure azure-core-amqp - 2.8.2 + 2.9.0-beta.1 diff --git a/sdk/servicebus/azure-messaging-servicebus/pom.xml b/sdk/servicebus/azure-messaging-servicebus/pom.xml index 297fac6bbf151..e74c5e7f3a741 100644 --- a/sdk/servicebus/azure-messaging-servicebus/pom.xml +++ b/sdk/servicebus/azure-messaging-servicebus/pom.xml @@ -57,7 +57,7 @@ com.azure azure-core-amqp - 2.8.2 + 2.9.0-beta.1 com.azure diff --git a/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiver.java b/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiver.java index 72fd318efa930..ef7a7dba81d29 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiver.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiver.java @@ -6,59 +6,40 @@ import com.azure.core.amqp.AmqpConnection; import com.azure.core.amqp.AmqpEndpointState; import com.azure.core.amqp.AmqpRetryPolicy; -import com.azure.core.amqp.exception.AmqpErrorCondition; -import com.azure.core.amqp.exception.AmqpException; -import com.azure.core.amqp.implementation.ExceptionUtil; import com.azure.core.amqp.implementation.ReactorProvider; import com.azure.core.amqp.implementation.ReactorReceiver; import com.azure.core.amqp.implementation.TokenManager; import com.azure.core.amqp.implementation.handler.ReceiveLinkHandler; +import com.azure.core.amqp.implementation.handler.ReceiverUnsettledDeliveries; import com.azure.core.util.logging.ClientLogger; import com.azure.messaging.servicebus.models.ServiceBusReceiveMode; import org.apache.qpid.proton.Proton; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.messaging.Accepted; -import org.apache.qpid.proton.amqp.messaging.Outcome; -import org.apache.qpid.proton.amqp.messaging.Rejected; -import org.apache.qpid.proton.amqp.messaging.Released; import org.apache.qpid.proton.amqp.messaging.Source; -import org.apache.qpid.proton.amqp.transaction.TransactionalState; import org.apache.qpid.proton.amqp.transport.DeliveryState; import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.amqp.transport.SenderSettleMode; import org.apache.qpid.proton.engine.Delivery; import org.apache.qpid.proton.engine.Receiver; import org.apache.qpid.proton.message.Message; -import reactor.core.Disposable; -import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; import reactor.core.scheduler.Schedulers; -import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.StringJoiner; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import static com.azure.core.amqp.implementation.ClientConstants.ENTITY_PATH_KEY; import static com.azure.core.amqp.implementation.ClientConstants.LINK_NAME_KEY; import static com.azure.core.util.FluxUtil.monoError; import static com.azure.messaging.servicebus.implementation.MessageUtils.LOCK_TOKEN_SIZE; -import static com.azure.messaging.servicebus.implementation.ServiceBusConstants.DELIVERY_STATE_KEY; -import static com.azure.messaging.servicebus.implementation.ServiceBusConstants.LOCK_TOKEN_KEY; import static com.azure.messaging.servicebus.implementation.ServiceBusReactorSession.LOCKED_UNTIL_UTC; import static com.azure.messaging.servicebus.implementation.ServiceBusReactorSession.SESSION_FILTER; @@ -69,10 +50,8 @@ public class ServiceBusReactorReceiver extends ReactorReceiver implements Servic private static final Message EMPTY_MESSAGE = Proton.message(); private final ClientLogger logger; - private final ConcurrentHashMap unsettledDeliveries = new ConcurrentHashMap<>(); - private final ConcurrentHashMap pendingUpdates = new ConcurrentHashMap<>(); + private final ReceiverUnsettledDeliveries receiverUnsettledDeliveries; private final AtomicBoolean isDisposed = new AtomicBoolean(); - private final Disposable subscription; private final Receiver receiver; /** @@ -80,10 +59,7 @@ public class ServiceBusReactorReceiver extends ReactorReceiver implements Servic * ServiceBusReceiveMode#RECEIVE_AND_DELETE} is used. */ private final boolean isSettled; - private final Duration timeout; - private final AmqpRetryPolicy retryPolicy; private final ReceiveLinkHandler handler; - private final ReactorProvider provider; private final Mono sessionIdMono; private final Mono sessionLockedUntil; @@ -94,17 +70,16 @@ public ServiceBusReactorReceiver(AmqpConnection connection, String entityPath, R retryPolicy.getRetryOptions()); this.receiver = receiver; this.handler = handler; - this.provider = provider; this.isSettled = receiver.getSenderSettleMode() == SenderSettleMode.SETTLED; - this.timeout = timeout; - this.retryPolicy = retryPolicy; - this.subscription = Flux.interval(timeout).subscribe(i -> cleanupWorkItems()); Map loggingContext = new HashMap<>(2); loggingContext.put(LINK_NAME_KEY, this.handler.getLinkName()); loggingContext.put(ENTITY_PATH_KEY, entityPath); this.logger = new ClientLogger(ServiceBusReactorReceiver.class, loggingContext); + this.receiverUnsettledDeliveries = new ReceiverUnsettledDeliveries(handler.getHostname(), entityPath, handler.getLinkName(), + provider.getReactorDispatcher(), retryPolicy.getRetryOptions(), MessageUtils.ZERO_LOCK_TOKEN, logger); + this.sessionIdMono = getEndpointStates().filter(x -> x == AmqpEndpointState.ACTIVE) .next() .flatMap(state -> { @@ -142,7 +117,7 @@ public Mono updateDisposition(String lockToken, DeliveryState deliveryStat if (isDisposed.get()) { return monoError(logger, new IllegalStateException("Cannot perform operations on a disposed receiver.")); } - return updateDispositionInternal(lockToken, deliveryState); + return this.receiverUnsettledDeliveries.sendDisposition(lockToken, deliveryState); } @Override @@ -173,37 +148,8 @@ protected Mono closeAsync(String message, ErrorCondition errorCondition) { if (isDisposed.getAndSet(true)) { return super.getIsClosedMono(); } - - cleanupWorkItems(); - - final Mono disposeMono; - if (!pendingUpdates.isEmpty()) { - final List> pending = new ArrayList<>(); - final StringJoiner builder = new StringJoiner(", "); - for (UpdateDispositionWorkItem workItem : pendingUpdates.values()) { - - if (workItem.hasTimedout()) { - continue; - } - - if (workItem.getDeliveryState() instanceof TransactionalState) { - pending.add(updateDispositionInternal(workItem.getLockToken(), Released.getInstance())); - } else { - pending.add(workItem.getMono()); - } - builder.add(workItem.getLockToken()); - } - - logger.info("Waiting for pending updates to complete. Locks: {}", builder.toString()); - disposeMono = Mono.when(pending); - } else { - disposeMono = Mono.empty(); - } - - return disposeMono.onErrorResume(error -> { - logger.info("There was an exception while disposing of all links.", error); - return Mono.empty(); - }).doFinally(signal -> subscription.dispose()).then(super.closeAsync(message, errorCondition)); + return receiverUnsettledDeliveries.terminateAndAwaitForDispositionsInProgressToComplete() + .then(super.closeAsync(message, errorCondition)); } @Override @@ -216,282 +162,34 @@ protected Message decodeDelivery(Delivery delivery) { lockToken = MessageUtils.ZERO_LOCK_TOKEN; } - final String lockTokenString = lockToken.toString(); - - // There is no lock token associated with this delivery, or the lock token is not in the unsettledDeliveries. - if (lockToken == MessageUtils.ZERO_LOCK_TOKEN || !unsettledDeliveries.containsKey(lockTokenString)) { + if (receiverUnsettledDeliveries.containsDelivery(lockToken)) { + receiverUnsettledDeliveries.onDispositionAck(lockToken, delivery); + // Return empty update disposition messages. The deliveries themselves are ACKs. There is no actual message + // to propagate. + return EMPTY_MESSAGE; + } else { + // There is no lock token associated with this delivery, or the lock token is not in the receiverUnsettledDeliveries. final int messageSize = delivery.pending(); final byte[] buffer = new byte[messageSize]; final int read = receiver.recv(buffer, 0, messageSize); final Message message = Proton.message(); message.decode(buffer, 0, read); - // The delivery was already settled from the message broker. - // This occurs in the case of receive and delete. if (isSettled) { + // The delivery was already settled from the message broker. This occurs in the case of receive and delete. delivery.disposition(Accepted.getInstance()); delivery.settle(); } else { - unsettledDeliveries.putIfAbsent(lockToken.toString(), delivery); + receiverUnsettledDeliveries.onDelivery(lockToken, delivery); receiver.advance(); } return new MessageWithLockToken(message, lockToken); - } else { - updateOutcome(lockTokenString, delivery); - - // Return empty update disposition messages. The deliveries themselves are ACKs. There is no actual message - // to propagate. - return EMPTY_MESSAGE; - } - } - - private Mono updateDispositionInternal(String lockToken, DeliveryState deliveryState) { - final Delivery unsettled = unsettledDeliveries.get(lockToken); - if (unsettled == null) { - - logger.atWarning() - // TODO: it used to be deliveryTag, is it ok to change? - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .log("Delivery not found to update disposition."); - - return monoError(logger, Exceptions.propagate(new IllegalArgumentException( - "Delivery not on receive link."))); - } - - final UpdateDispositionWorkItem workItem = new UpdateDispositionWorkItem(lockToken, deliveryState, timeout); - final Mono result = Mono.create(sink -> { - workItem.start(sink); - try { - provider.getReactorDispatcher().invoke(() -> { - unsettled.disposition(deliveryState); - pendingUpdates.put(lockToken, workItem); - }); - } catch (IOException | RejectedExecutionException error) { - sink.error(new AmqpException(false, "updateDisposition failed while dispatching to Reactor.", - error, handler.getErrorContext(receiver))); - } - }).cache(); // cache because closeAsync use `when` to subscribe this Mono again. - - workItem.setMono(result); - - return result; - } - - /** - * Updates the outcome of a delivery. This occurs when a message is being settled from the receiver side. - * @param delivery Delivery to update. - */ - private void updateOutcome(String lockToken, Delivery delivery) { - final DeliveryState remoteState = delivery.getRemoteState(); - - logger.atVerbose() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue(DELIVERY_STATE_KEY, remoteState) - .log("Received update disposition delivery."); - - final Outcome remoteOutcome; - if (remoteState instanceof Outcome) { - remoteOutcome = (Outcome) remoteState; - } else if (remoteState instanceof TransactionalState) { - remoteOutcome = ((TransactionalState) remoteState).getOutcome(); - } else { - remoteOutcome = null; - } - - if (remoteOutcome == null) { - logger.atWarning() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue("delivery", delivery) - .log("No outcome associated with delivery."); - - return; - } - - final UpdateDispositionWorkItem workItem = pendingUpdates.get(lockToken); - if (workItem == null) { - logger.atWarning() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue("delivery", delivery) - .log("No pending update for delivery."); - - return; - } - - // If the statuses match, then we settle the delivery and move on. - if (remoteState.getType() == workItem.getDeliveryState().getType()) { - completeWorkItem(lockToken, delivery, workItem.getSink(), null); - return; - } - - logger.atInfo() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue("receivedDeliveryState", remoteState) - .addKeyValue(DELIVERY_STATE_KEY, workItem.getDeliveryState()) - .log("Received delivery state doesn't match expected state."); - - switch (remoteState.getType()) { - case Rejected: - final Rejected rejected = (Rejected) remoteOutcome; - final ErrorCondition errorCondition = rejected.getError(); - final Throwable exception = ExceptionUtil.toException(errorCondition.getCondition().toString(), - errorCondition.getDescription(), handler.getErrorContext(receiver)); - - final Duration retry = retryPolicy.calculateRetryDelay(exception, workItem.incrementRetry()); - if (retry == null) { - logger.atInfo() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue(DELIVERY_STATE_KEY, remoteState) - .log("Retry attempts exhausted.", exception); - - completeWorkItem(lockToken, delivery, workItem.getSink(), exception); - } else { - workItem.setLastException(exception); - workItem.resetStartTime(); - try { - provider.getReactorDispatcher().invoke(() -> delivery.disposition(workItem.getDeliveryState())); - } catch (IOException | RejectedExecutionException error) { - final Throwable amqpException = logger.atError() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .log(new AmqpException(false, - String.format("linkName[%s], deliveryTag[%s]. Retrying updateDisposition failed to dispatch to Reactor.", getLinkName(), lockToken), - error, handler.getErrorContext(receiver))); - - completeWorkItem(lockToken, delivery, workItem.getSink(), amqpException); - } - } - - break; - case Released: - final Throwable cancelled = new AmqpException(false, AmqpErrorCondition.OPERATION_CANCELLED, - "AMQP layer unexpectedly aborted or disconnected.", handler.getErrorContext(receiver)); - - logger.atInfo() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue(DELIVERY_STATE_KEY, remoteState) - .log("Completing pending updateState operation with exception.", cancelled); - - completeWorkItem(lockToken, delivery, workItem.getSink(), cancelled); - break; - default: - final AmqpException error = new AmqpException(false, remoteOutcome.toString(), - handler.getErrorContext(receiver)); - - logger.atInfo() - .addKeyValue(LOCK_TOKEN_KEY, lockToken) - .addKeyValue(DELIVERY_STATE_KEY, remoteState) - .log("Completing pending updateState operation with exception.", error); - - completeWorkItem(lockToken, delivery, workItem.getSink(), error); - break; - } - } - - private void cleanupWorkItems() { - if (pendingUpdates.isEmpty()) { - return; } - - logger.verbose("Cleaning timed out update work tasks."); - pendingUpdates.forEach((key, value) -> { - if (value == null || !value.hasTimedout()) { - return; - } - - pendingUpdates.remove(key); - final Throwable error = value.getLastException() != null - ? value.getLastException() - : new AmqpException(true, AmqpErrorCondition.TIMEOUT_ERROR, "Update disposition request timed out.", - handler.getErrorContext(receiver)); - - completeWorkItem(key, null, value.getSink(), error); - }); } - private void completeWorkItem(String lockToken, Delivery delivery, MonoSink sink, Throwable error) { - final boolean isSettled = delivery != null && delivery.remotelySettled(); - if (isSettled) { - delivery.settle(); - } - - if (error != null) { - final Throwable loggedError = error instanceof RuntimeException - ? logger.logExceptionAsError((RuntimeException) error) - : error; - sink.error(loggedError); - } else { - sink.success(); - } - - if (isSettled) { - pendingUpdates.remove(lockToken); - unsettledDeliveries.remove(lockToken); - } - } - - private static final class UpdateDispositionWorkItem { - private final String lockToken; - private final DeliveryState state; - private final Duration timeout; - private final AtomicInteger retryAttempts = new AtomicInteger(); - private final AtomicBoolean isDisposed = new AtomicBoolean(); - - private Mono mono; - private Instant expirationTime; - private MonoSink sink; - private Throwable throwable; - - private UpdateDispositionWorkItem(String lockToken, DeliveryState state, Duration timeout) { - this.lockToken = lockToken; - this.state = state; - this.timeout = timeout; - } - - private boolean hasTimedout() { - return expirationTime.isBefore(Instant.now()); - } - - private void resetStartTime() { - this.expirationTime = Instant.now().plus(timeout); - } - - private int incrementRetry() { - return retryAttempts.incrementAndGet(); - } - - private Throwable getLastException() { - return throwable; - } - - private void setLastException(Throwable throwable) { - this.throwable = throwable; - } - - private void setMono(Mono mono) { - this.mono = mono; - } - - private Mono getMono() { - return mono; - } - - private MonoSink getSink() { - return sink; - } - - private void start(MonoSink sink) { - Objects.requireNonNull(sink, "'sink' cannot be null."); - this.sink = sink; - this.sink.onDispose(() -> isDisposed.set(true)); - this.sink.onCancel(() -> isDisposed.set(true)); - resetStartTime(); - } - - private DeliveryState getDeliveryState() { - return state; - } - - public String getLockToken() { - return lockToken; - } + @Override + protected void onHandlerClose() { + // See the code comment in ReactorReceiver.onHandlerClose(), [temporary method, tobe removed.] + receiverUnsettledDeliveries.close(); } } diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiverTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiverTest.java index bca1c4c635bb2..6e4b8693ae9a5 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiverTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/implementation/ServiceBusReactorReceiverTest.java @@ -4,7 +4,9 @@ package com.azure.messaging.servicebus.implementation; import com.azure.core.amqp.AmqpConnection; +import com.azure.core.amqp.AmqpRetryOptions; import com.azure.core.amqp.AmqpRetryPolicy; +import com.azure.core.amqp.FixedAmqpRetryPolicy; import com.azure.core.amqp.exception.AmqpResponseCode; import com.azure.core.amqp.implementation.ReactorDispatcher; import com.azure.core.amqp.implementation.ReactorProvider; @@ -68,8 +70,7 @@ class ServiceBusReactorReceiverTest { private ReactorProvider reactorProvider; @Mock private ReactorDispatcher reactorDispatcher; - @Mock - private AmqpRetryPolicy retryPolicy; + private final AmqpRetryPolicy retryPolicy = new FixedAmqpRetryPolicy(new AmqpRetryOptions()); @Mock private ReceiveLinkHandler receiveLinkHandler; @Mock