This repository has been archived by the owner on Dec 4, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 115
[SDK][Bot-Azure] Add AzureQueueStorage component #1033
Merged
tracyboehrer
merged 12 commits into
microsoft:main
from
southworks:external/southworks/AzureQueue/base
Mar 23, 2021
Merged
Changes from 3 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
562d73a
Implement AzureQueueStorage
VictorGrycuk b1d1fbd
Add and fix unit tests
VictorGrycuk e36752e
Merge branch 'main' into external/southworks/AzureQueue/base
Batta32 6506c60
Improve createQueueIfNotExists handling
VictorGrycuk 260d1e2
Merge remote-tracking branch 'Microsoft/main' into external/southwork…
VictorGrycuk af20f70
Merge branch 'main' into external/southworks/AzureQueue/base
Batta32 b5981fc
Merge branch 'main' into external/southworks/AzureQueue/base
Batta32 8229a64
Replace assertEmulator with runIfEmulator
VictorGrycuk e054073
Merge branch 'main' into external/southworks/AzureQueue/base
Batta32 f7b9584
Merge branch 'main' into external/southworks/AzureQueue/base
VictorGrycuk 7e22ebd
Remove double brace to standard initialization
Batta32 a7d46a5
Merge branch 'main' into external/southworks/AzureQueue/base
Batta32 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/AzureQueueStorage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.microsoft.bot.azure.queues; | ||
|
||
import com.azure.storage.queue.QueueClient; | ||
import com.azure.storage.queue.QueueClientBuilder; | ||
import com.azure.storage.queue.models.QueueStorageException; | ||
import com.azure.storage.queue.models.SendMessageResult; | ||
import com.microsoft.bot.builder.QueueStorage; | ||
import com.microsoft.bot.restclient.serializer.JacksonAdapter; | ||
import com.microsoft.bot.schema.Activity; | ||
import org.apache.commons.lang3.StringUtils; | ||
|
||
import javax.annotation.Nullable; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.time.Duration; | ||
import java.util.Base64; | ||
import java.util.concurrent.CompletableFuture; | ||
|
||
/** | ||
* Service used to add messages to an Azure.Storage.Queues. | ||
*/ | ||
public class AzureQueueStorage extends QueueStorage { | ||
private Boolean createQueueIfNotExists = true; | ||
private final QueueClient queueClient; | ||
|
||
/** | ||
* Initializes a new instance of the {@link AzureQueueStorage} class. | ||
* @param queuesStorageConnectionString Azure Storage connection string. | ||
* @param queueName Name of the storage queue where entities will be queued. | ||
*/ | ||
public AzureQueueStorage(String queuesStorageConnectionString, String queueName) { | ||
if (StringUtils.isBlank(queuesStorageConnectionString)) { | ||
throw new IllegalArgumentException("queuesStorageConnectionString is required."); | ||
} | ||
|
||
if (StringUtils.isBlank(queueName)) { | ||
throw new IllegalArgumentException("queueName is required."); | ||
} | ||
|
||
queueClient = new QueueClientBuilder() | ||
.connectionString(queuesStorageConnectionString) | ||
.queueName(queueName) | ||
.buildClient(); | ||
} | ||
|
||
/** | ||
* Queue an Activity to an Azure.Storage.Queues.QueueClient. | ||
* The visibility timeout specifies how long the message should be invisible | ||
* to Dequeue and Peek operations. The message content must be a UTF-8 encoded string that is up to 64KB in size. | ||
* @param activity This is expected to be an {@link Activity} retrieved from a call to | ||
* activity.GetConversationReference().GetContinuationActivity(). | ||
* This enables restarting the conversation using BotAdapter.ContinueConversationAsync. | ||
* @param visibilityTimeout Default value of 0. Cannot be larger than 7 days. | ||
* @param timeToLive Specifies the time-to-live interval for the message. | ||
* @return {@link SendMessageResult} as a Json string, from the QueueClient SendMessageAsync operation. | ||
*/ | ||
@Override | ||
public CompletableFuture<String> queueActivity(Activity activity, | ||
@Nullable Duration visibilityTimeout, | ||
@Nullable Duration timeToLive) { | ||
return CompletableFuture.supplyAsync(() -> { | ||
if (createQueueIfNotExists) { | ||
// This is an optimization flag to check if the container creation call has been made. | ||
// It is okay if this is called more than once. | ||
createQueueIfNotExists = false; | ||
try { | ||
queueClient.create(); | ||
} catch (QueueStorageException e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
try { | ||
JacksonAdapter jacksonAdapter = new JacksonAdapter(); | ||
String serializedActivity = jacksonAdapter.serialize(activity); | ||
byte[] encodedBytes = serializedActivity.getBytes(StandardCharsets.UTF_8); | ||
String encodedString = Base64.getEncoder().encodeToString(encodedBytes); | ||
|
||
SendMessageResult receipt = queueClient.sendMessage(encodedString); | ||
return jacksonAdapter.serialize(receipt); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
} | ||
return null; | ||
}); | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
libraries/bot-azure/src/main/java/com/microsoft/bot/azure/queues/package-info.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for | ||
// license information. | ||
|
||
/** | ||
* This package contains the classes for bot-integration-core. | ||
*/ | ||
package com.microsoft.bot.azure.queues; |
244 changes: 244 additions & 0 deletions
244
libraries/bot-azure/src/test/java/com/microsoft/bot/azure/AzureQueueTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.microsoft.bot.azure; | ||
|
||
import com.azure.storage.queue.QueueClient; | ||
import com.azure.storage.queue.QueueClientBuilder; | ||
import com.azure.storage.queue.models.QueueMessageItem; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.microsoft.bot.azure.queues.AzureQueueStorage; | ||
import com.microsoft.bot.builder.ConversationState; | ||
import com.microsoft.bot.builder.MemoryStorage; | ||
import com.microsoft.bot.builder.QueueStorage; | ||
import com.microsoft.bot.builder.UserState; | ||
import com.microsoft.bot.builder.adapters.TestAdapter; | ||
import com.microsoft.bot.builder.adapters.TestFlow; | ||
import com.microsoft.bot.dialogs.Dialog; | ||
import com.microsoft.bot.dialogs.DialogContext; | ||
import com.microsoft.bot.dialogs.DialogManager; | ||
import com.microsoft.bot.dialogs.DialogTurnResult; | ||
import com.microsoft.bot.schema.Activity; | ||
import com.microsoft.bot.schema.ActivityEventNames; | ||
import com.microsoft.bot.schema.ActivityTypes; | ||
import com.microsoft.bot.schema.ConversationReference; | ||
import org.apache.commons.codec.binary.Base64; | ||
import org.junit.Assert; | ||
import org.junit.BeforeClass; | ||
import org.junit.Test; | ||
|
||
import java.io.IOException; | ||
import java.text.SimpleDateFormat; | ||
import java.time.Duration; | ||
import java.time.LocalDateTime; | ||
import java.time.ZoneOffset; | ||
import java.time.ZonedDateTime; | ||
import java.time.format.DateTimeParseException; | ||
import java.util.Calendar; | ||
import java.util.concurrent.CompletableFuture; | ||
|
||
import com.microsoft.bot.restclient.serializer.JacksonAdapter; | ||
|
||
public class AzureQueueTests { | ||
private static final Integer DEFAULT_DELAY = 2000; | ||
private static Boolean EMULATOR_IS_RUNNING = false; | ||
private final String connectionString = "UseDevelopmentStorage=true"; | ||
private static final String NO_EMULATOR_MESSAGE = "This test requires Azure STORAGE Emulator! Go to https://docs.microsoft.com/azure/storage/common/storage-use-emulator to download and install."; | ||
|
||
@BeforeClass | ||
public static void allTestsInit() throws IOException, InterruptedException { | ||
Process p = Runtime.getRuntime().exec | ||
("cmd /C \"" + System.getenv("ProgramFiles") + " (x86)\\Microsoft SDKs\\Azure\\Storage Emulator\\AzureStorageEmulator.exe\" start"); | ||
int result = p.waitFor(); | ||
// status = 0: the service was started. | ||
// status = -5: the service is already started. Only one instance of the application | ||
// can be run at the same time. | ||
EMULATOR_IS_RUNNING = result == 0 || result == -5; | ||
} | ||
|
||
// These tests require Azure Storage Emulator v5.7 | ||
public QueueClient containerInit(String name) { | ||
QueueClient queue = new QueueClientBuilder() | ||
.connectionString(connectionString) | ||
.queueName(name) | ||
.buildClient(); | ||
queue.create(); | ||
queue.clearMessages(); | ||
return queue; | ||
} | ||
|
||
@Test | ||
public void continueConversationLaterTests() { | ||
assertEmulator(); | ||
String queueName = "continueconversationlatertests"; | ||
QueueClient queue = containerInit(queueName); | ||
ConversationReference cr = TestAdapter.createConversationReference("ContinueConversationLaterTests", "User1", "Bot"); | ||
TestAdapter adapter = new TestAdapter(cr) | ||
.useStorage(new MemoryStorage()) | ||
.useBotState(new ConversationState(new MemoryStorage()), new UserState(new MemoryStorage())); | ||
|
||
AzureQueueStorage queueStorage = new AzureQueueStorage(connectionString, queueName); | ||
|
||
Calendar cal = Calendar.getInstance(); | ||
cal.add(Calendar.SECOND, 2); | ||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); | ||
|
||
ContinueConversationLater ccl = new ContinueConversationLater() { | ||
{ | ||
setDate(sdf.format(cal.getTime())); | ||
setValue("foo"); | ||
} | ||
}; | ||
DialogManager dm = new DialogManager(ccl, "DialogStateProperty"); | ||
dm.getInitialTurnState().replace("QueueStorage", queueStorage); | ||
|
||
new TestFlow(adapter, turnContext -> CompletableFuture.runAsync(() -> dm.onTurn(turnContext))) | ||
.send("hi") | ||
.startTest().join(); | ||
|
||
try { | ||
Thread.sleep(DEFAULT_DELAY); | ||
} catch (InterruptedException e) { | ||
e.printStackTrace(); | ||
Assert.fail(); | ||
} | ||
|
||
QueueMessageItem messages = queue.receiveMessage(); | ||
JacksonAdapter jacksonAdapter = new JacksonAdapter(); | ||
String messageJson = new String(Base64.decodeBase64(messages.getMessageText())); | ||
Activity activity = null; | ||
|
||
try { | ||
activity = jacksonAdapter.deserialize(messageJson, Activity.class); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
Assert.fail(); | ||
} | ||
|
||
Assert.assertTrue(activity.isType(ActivityTypes.EVENT)); | ||
Assert.assertEquals(ActivityEventNames.CONTINUE_CONVERSATION, activity.getName()); | ||
Assert.assertEquals("foo", activity.getValue()); | ||
Assert.assertNotNull(activity.getRelatesTo()); | ||
ConversationReference cr2 = activity.getConversationReference(); | ||
cr.setActivityId(null); | ||
cr2.setActivityId(null); | ||
|
||
try { | ||
Assert.assertEquals(jacksonAdapter.serialize(cr), jacksonAdapter.serialize(cr2)); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
Assert.fail(); | ||
} | ||
} | ||
|
||
private void assertEmulator() { | ||
if (!EMULATOR_IS_RUNNING) { | ||
Assert.fail(NO_EMULATOR_MESSAGE); | ||
} | ||
} | ||
|
||
private class ContinueConversationLater extends Dialog { | ||
@JsonProperty("disabled") | ||
private Boolean disabled = false; | ||
|
||
@JsonProperty("date") | ||
private String date; | ||
|
||
@JsonProperty("value") | ||
private String value; | ||
|
||
/** | ||
* Initializes a new instance of the Dialog class. | ||
*/ | ||
public ContinueConversationLater() { | ||
super(ContinueConversationLater.class.getName()); | ||
} | ||
|
||
@Override | ||
public CompletableFuture<DialogTurnResult> beginDialog(DialogContext dc, Object options) { | ||
if (this.disabled) { | ||
return dc.endDialog(); | ||
} | ||
|
||
String dateString = this.date; | ||
LocalDateTime date = null; | ||
try { | ||
date = LocalDateTime.parse(dateString); | ||
} catch (DateTimeParseException ex) { | ||
throw new IllegalArgumentException("Date is invalid"); | ||
} | ||
|
||
ZonedDateTime zonedDate = date.atZone(ZoneOffset.UTC); | ||
ZonedDateTime now = LocalDateTime.now().atZone(ZoneOffset.UTC); | ||
if (zonedDate.isBefore(now)) { | ||
throw new IllegalArgumentException("Date must be in the future"); | ||
} | ||
|
||
// create ContinuationActivity from the conversation reference. | ||
Activity activity = dc.getContext().getActivity().getConversationReference().getContinuationActivity(); | ||
activity.setValue(this.value); | ||
|
||
Duration visibility = Duration.between(zonedDate, now); | ||
Duration ttl = visibility.plusMinutes(2); | ||
|
||
QueueStorage queueStorage = dc.getContext().getTurnState().get("QueueStorage"); | ||
if (queueStorage == null) { | ||
throw new NullPointerException("Unable to locate QueueStorage in HostContext"); | ||
} | ||
return queueStorage.queueActivity(activity, visibility, ttl).thenCompose(receipt -> { | ||
// return the receipt as the result | ||
return dc.endDialog(receipt); | ||
}); | ||
} | ||
|
||
/** | ||
* Gets an optional expression which if is true will disable this action. | ||
* "user.age > 18". | ||
* @return A boolean expression. | ||
*/ | ||
public Boolean getDisabled() { | ||
return disabled; | ||
} | ||
|
||
/** | ||
* Sets an optional expression which if is true will disable this action. | ||
* "user.age > 18". | ||
* @param withDisabled A boolean expression. | ||
*/ | ||
public void setDisabled(Boolean withDisabled) { | ||
this.disabled = withDisabled; | ||
} | ||
|
||
/** | ||
* Gets the expression which resolves to the date/time to continue the conversation. | ||
* @return Date/time string in ISO 8601 format to continue conversation. | ||
*/ | ||
public String getDate() { | ||
return date; | ||
} | ||
|
||
/** | ||
* Sets the expression which resolves to the date/time to continue the conversation. | ||
* @param withDate Date/time string in ISO 8601 format to continue conversation. | ||
*/ | ||
public void setDate(String withDate) { | ||
this.date = withDate; | ||
} | ||
|
||
/** | ||
* Gets an optional value to use for EventActivity.Value. | ||
* @return The value to use for the EventActivity.Value payload. | ||
*/ | ||
public String getValue() { | ||
return value; | ||
} | ||
|
||
/** | ||
* Sets an optional value to use for EventActivity.Value. | ||
* @param withValue The value to use for the EventActivity.Value payload. | ||
*/ | ||
public void setValue(String withValue) { | ||
this.value = withValue; | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
libraries/bot-builder/src/main/java/com/microsoft/bot/builder/QueueStorage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.microsoft.bot.builder; | ||
|
||
import com.microsoft.bot.schema.Activity; | ||
|
||
import javax.annotation.Nullable; | ||
import java.time.Duration; | ||
import java.util.concurrent.CompletableFuture; | ||
|
||
/** | ||
* A base class for enqueueing an Activity for later processing. | ||
*/ | ||
public abstract class QueueStorage { | ||
|
||
/** | ||
* Enqueues an Activity for later processing. The visibility timeout specifies how long the message | ||
* should be invisible to Dequeue and Peek operations. | ||
* @param activity The {@link Activity} to be queued for later processing. | ||
* @param visibilityTimeout Visibility timeout. Optional with a default value of 0. Cannot be larger than 7 days. | ||
* @param timeToLive Specifies the time-to-live interval for the message. | ||
* @return A result string. | ||
*/ | ||
public abstract CompletableFuture<String> queueActivity(Activity activity, | ||
@Nullable Duration visibilityTimeout, | ||
@Nullable Duration timeToLive); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it acceptable for the code to continue at this point or should we return here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @LeeParrishMSFT, we will review this and apply the corresponding fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @LeeParrishMSFT, we improved this behaviour to throw a
RuntimeException
like the CosmosDbPartitionedStorage does.