Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting Scaleout sample from C# to JavaScript #3968

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Samples are designed to illustrate functionality you'll need to implement to bui
|19|Custom dialogs | Demonstrates complex conversation flow using the Dialogs library. |[.NET Core][cs#19]|[JavaScript][js#19]|[Python][py#19]|[Java][java#19]
|21|Application Insights | Demonstrates how to add telemetry logging to your bot, storing telemetry within Application Insights.|[.NET Core][cs#21] |[JavaScript][js#21] ||[Java][java#21]
|23|Facebook events | Integrate and consume Facebook specific payloads, such as post-backs, quick replies and opt-in events.|[.NET Core][cs#23] |[JavaScript][js#23] |[Python][py#23]|[Java][java#23]
|42|Scale out | Demonstrates how you can build your own state solution from the ground up that supports scaled out deployment with ETag based optimistic locking. |[.NET Core][cs#42] | |[Python][py#42]|[Java][java#42]
|42|Scale out | Demonstrates how you can build your own state solution from the ground up that supports scaled out deployment with ETag based optimistic locking. |[.NET Core][cs#42] |[JavaScript][js#42] |[Python][py#42]|[Java][java#42]
|44|Basic custom prompts | Demonstrates how to implement your own _basic_ prompts to ask the user for information. |[.NET Core][cs#44]|[JavaScript][js#44]|[Python][py#44]|[Java][java#44]
|47|Inspection middleware | Demonstrates how to use middleware to allow the Bot Framework Emulator to debug traffic into and out of the bot in addition to looking at the current state of the bot. | [.NET Core][cs#47] | [JavaScript][js#47] |[Python][py#47]|[Java][java#47]
|70|Styling webchat | This sample shows how to create a web page with custom Web Chat component.| | [ECMAScript 6][es#70] |
Expand Down Expand Up @@ -179,6 +179,7 @@ A [collection of **experimental** samples](./experimental) exist, intended to pr
[js#23]:samples/javascript_nodejs/23.facebook-events
[js#24]:samples/javascript_nodejs/24.bot-authentication-msgraph
[js#40]:samples/javascript_nodejs/40.timex-resolution
[js#42]:samples/javascript_nodejs/42.scale-out
[js#43]:samples/javascript_nodejs/43.complex-dialog
[js#44]:samples/javascript_nodejs/44.prompt-for-user-input
[js#45]:samples/javascript_nodejs/45.state-management
Expand Down
15 changes: 15 additions & 0 deletions samples/javascript_nodejs/42.scale-out/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable */
module.exports = {
"extends": "standard",
"rules": {
"semi": [2, "always"],
"indent": [2, 4],
"no-return-await": 0,
"space-before-function-paren": [2, {
"named": "never",
"anonymous": "never",
"asyncArrow": "always"
}],
"template-curly-spacing": [2, "always"]
}
};
73 changes: 73 additions & 0 deletions samples/javascript_nodejs/42.scale-out/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Scale Out

Bot Framework v4 bot Scale Out sample

This bot has been created using [Bot Framework](https://dev.botframework.com), is shows how to use a custom storage solution that supports a deployment scaled out across multiple machines.

The custom storage solution is implemented against memory for testing purposes and against Azure Blob Storage. The sample shows how storage solutions with different policies can be implemented and integrated with the framework. The solution makes use of the standard HTTP ETag/If-Match mechanisms commonly found on cloud storage technologies.

## Prerequisites

- [Node.js](https://nodejs.org) version 16.16.0 or higher

```bash
# determine node version
node --version
```
- Update `.env` with required configuration settings
- MicrosoftAppId
- MicrosoftAppPassword
- ConnectionName

## To try this sample

- Clone the repository

```bash
git clone https://github.com/microsoft/botbuilder-samples.git
```

- In a terminal, navigate to `samples/javascript_nodejs/42.scale-out`

```bash
cd samples/javascript_nodejs/42.scale-out
```

- Install modules

```bash
npm install
```

- Start the bot

```bash
npm start
```

## Testing the bot using Bot Framework Emulator

[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.

- Install the latest Bot Framework Emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)

### Connect to the bot using Bot Framework Emulator

- Launch Bot Framework Emulator
- File -> Open Bot
- Enter a Bot URL of `http://localhost:3978/api/messages`

## Further reading

- [Bot Framework Documentation](https://docs.botframework.com)
- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
- [Implementing custom storage for you bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-custom-storage?view=azure-bot-service-4.0)
- [Bot Storage](https://docs.microsoft.com/en-us/azure/bot-service/dotnet/bot-builder-dotnet-state?view=azure-bot-service-3.0&viewFallbackFrom=azure-bot-service-4.0)
- [HTTP ETag](https://en.wikipedia.org/wiki/HTTP_ETag)
- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
- [.NET Core CLI tools](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x)
- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
- [Azure Portal](https://portal.azure.com)
- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
87 changes: 87 additions & 0 deletions samples/javascript_nodejs/42.scale-out/blobStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const { BlobServiceClient, StorageSharedKeyCredential } = require('@azure/storage-blob');

class BlobStore {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subclass Store

constructor(accountName, accountKey, containerName) {
if (!accountName) {
throw new Error('accountName is required');
}

if (!accountKey) {
throw new Error('accountKey is required');
}

if (!containerName) {
throw new Error('containerName is required');
}

const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);
const blobServiceClient = new BlobServiceClient(`https://${ accountName }.blob.core.windows.net`, sharedKeyCredential);
this.containerClient = blobServiceClient.getContainerClient(containerName);
}

async loadAsync(key) {
if (!key) {
throw new Error('key is required');
}

const blobClient = this.containerClient.getBlockBlobClient(key);
try {
const downloadBlockBlobResponse = await blobClient.download();
const content = await streamToString(downloadBlockBlobResponse.readableStreamBody);
const obj = JSON.parse(content);
const etag = downloadBlockBlobResponse.properties.etag;
return { content: obj, etag: etag };
} catch (error) {
if (error.statusCode === 404) {
return { content: {}, etag: null };
}
throw error;
}
}

async saveAsync(key, obj, etag) {
if (!key) {
throw new Error('key is required');
}

if (!obj) {
throw new Error('obj is required');
}

const blobClient = this.containerClient.getBlockBlobClient(key);
blobClient.properties.contentType = 'application/json';
const content = JSON.stringify(obj);
if (etag) {
try {
await blobClient.upload(content, content.length, { conditions: { ifMatch: etag } });
} catch (error) {
if (error.statusCode === 412) {
return false;
}
throw error;
}
} else {
await blobClient.upload(content, content.length);
}

return true;
}
}

async function streamToString(readableStream) {
return new Promise((resolve, reject) => {
const chunks = [];
readableStream.on('data', (data) => {
chunks.push(data.toString());
});
readableStream.on('end', () => {
resolve(chunks.join(''));
});
readableStream.on('error', reject);
});
}

module.exports.BlobStore = BlobStore;
54 changes: 54 additions & 0 deletions samples/javascript_nodejs/42.scale-out/bots/scaleoutBot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const { ActivityHandler } = require('botbuilder');
const { MemoryStore } = require('../memoryStore');
const { DialogHost } = require('../dialogHost');

class ScaleoutBot extends ActivityHandler {
/**
*
* @param {Dialog} dialog
*/
constructor(dialog) {
super();
if (!dialog) throw new Error('[ScaleoutBot]: Missing parameter. dialog is required');

this.dialog = dialog;

this.onMessage(async (context, next) => {
// Create the storage key for this conversation.
const key = `${ context.activity.channelId }/conversations/${ context.activity.conversation?.id }`;

var store = new MemoryStore();
var dialogHost = new DialogHost();

// The execution sits in a loop because there might be a retry if the save operation fails.
while (true) {
// Load any existing state associated with this key
const { oldState, etag } = await store.loadAsync(key);

// Run the dialog system with the old state and inbound activity, the result is a new state and outbound activities.
const { activities, newState } = await dialogHost.runAsync(this.dialog, context.activity, oldState);

// Save the updated state associated with this key.
const success = await store.saveAsync(key, newState, etag);

// Following a successful save, send any outbound Activities, otherwise retry everything.
if (success) {
if (activities.length > 0) {
// This is an actual send on the TurnContext we were given and so will actual do a send this time.
await context.sendActivities(activities);
}

break;
}
}

// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
}

module.exports.ScaleoutBot = ScaleoutBot;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"azureBotId": {
"value": ""
},
"azureBotSku": {
"value": "S1"
},
"azureBotRegion": {
"value": "global"
},
"botEndpoint": {
"value": ""
},
"appType": {
"value": "MultiTenant"
},
"appId": {
"value": ""
},
"UMSIName": {
"value": ""
},
"UMSIResourceGroupName": {
"value": ""
},
"tenantId": {
"value": ""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appServiceName": {
"value": ""
},
"existingAppServicePlanName": {
"value": ""
},
"existingAppServicePlanLocation": {
"value": ""
},
"newAppServicePlanName": {
"value": ""
},
"newAppServicePlanLocation": {
"value": ""
},
"newAppServicePlanSku": {
"value": {
"name": "S1",
"tier": "Standard",
"size": "S1",
"family": "S",
"capacity": 1
}
},
"appType": {
"value": "MultiTenant"
},
"appId": {
"value": ""
},
"appSecret": {
"value": ""
},
"UMSIName": {
"value": ""
},
"UMSIResourceGroupName": {
"value": ""
},
"tenantId": {
"value": ""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Usage
The BotApp must be deployed prior to AzureBot.

Command line:
- az login
- az deployment group create --resource-group <group-name> --template-file <template-file> --parameters @<parameters-file>

# parameters-for-template-BotApp-with-rg:

- **appServiceName**:(required) The Name of the Bot App Service.

- (choose an existingAppServicePlan or create a new AppServicePlan)
- **existingAppServicePlanName**: The name of the App Service Plan.
- **existingAppServicePlanLocation**: The location of the App Service Plan.
- **newAppServicePlanName**: The name of the App Service Plan.
- **newAppServicePlanLocation**: The location of the App Service Plan.
- **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values.

- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.**

- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings.

- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings.

- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication.

- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication.

- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to <Subscription Tenant ID>.

MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource



# parameters-for-template-AzureBot-with-rg:

- **azureBotId**:(required) The globally unique and immutable bot ID.
- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**.
- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**.
- **botEndpoint**: Use to handle client messages, Such as https://<botappServiceName>.azurewebsites.net/api/messages.

- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.**
- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings.
- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication.
- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication.
- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to <Subscription Tenant ID>.

MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource
Loading
Loading