IBotDataStore.FlushAsync Exception: The data is changed

前言

最新有個需求就是要判斷目前 User 是不是在進行某項作業, 如果不是就推送包含 Button 的訊息給 User, 不然就推文字通知訊息等目前的作業完成後,再顯示待辦的 Button 訊息給 User .

研究

最簡單的就是將這個 flag 寫在 BotState 之中,所以我們可以在使用者加入時,將它的 Conversation 記錄下來,如下,

if (message.Type == ActivityTypes.ConversationUpdate)
{
Func<ChannelAccount, bool> isChatbot =
channelAcct => channelAcct.Id == message.Recipient.Id;
if (message.MembersAdded.Any(isChatbot))
{
var memberAdded = message.MembersAdded.FirstOrDefault();
//這裡請自行處理將該 user 的 Conversation 儲存起來,為了方便,這裡將它存在一個變數之中
WebApiApplication._ConversationReference = message.ToConversationReference();
}
}

然後在申請作業記錄這個 Flag ,當然,結束時請記得將這個 Flag 清除哦!

using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, context.Activity.AsMessageActivity()))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = Address.FromActivity(context.Activity);
var botUserData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, default(CancellationToken));
botUserData.SetProperty<string>(“dialog”, “RootDialog:MessageReceivedAsync”);
await botDataStore.SaveAsync(key, BotStoreType.BotUserData, botUserData, default(CancellationToken));
await botDataStore.FlushAsync(key, default(CancellationToken));
}

可是執行時,卻發生了「Exception: The data is changed」的錯誤,如下圖,
[Exception: The data is changed]

因為 botframework 會自動去幫我們將 State 存檔,所以當它存檔時,發現狀態被改掉了,所以就出現「Exception: The data is changed」的錯誤 .

嗯, 那怎麼辦呢? 那就調整程式, 不要去呼叫 FlushAsync, 讓 botframework 去存就可以了, 所以程式調整如下,

using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, context.Activity.AsMessageActivity()))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = Address.FromActivity(context.Activity);
var botUserData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, default(CancellationToken));
botUserData.SetProperty<string>(“dialog”, “RootDialog:MessageReceivedAsync”);
await botDataStore.SaveAsync(key, BotStoreType.BotUserData, botUserData, default(CancellationToken));
//await botDataStore.FlushAsync(key, default(CancellationToken));
}

所以我們的通知程式就可以透過 ConversationReference 轉回 Activity, 取回 BotState, 例如,我新增一個 api method 來顯示 BotState , 如下,

[Route(“api/Messages/GetDialogData”)]
[HttpGet]
public async Task<HttpResponseMessage> GetDialogData()
{
var resp = new HttpResponseMessage(HttpStatusCode.OK);
if (WebApiApplication._ConversationReference != null)
{
var message = WebApiApplication._ConversationReference.GetPostToBotMessage();
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message.AsMessageActivity()))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = Address.FromActivity(message);
var botUserData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, default(CancellationToken));
var dialogData = botUserData.GetProperty<string>(“dialog”);
resp.Content = new StringContent($”<html><body>Dialogs:{dialogData}</body></html>”, System.Text.Encoding.UTF8, @”text/html”);
}
}
return resp;
}

所以在 Dialog 中儲存 State 時 , 可以注意一下這種狀況哦 ^_^

Demo 專案可以參考 rainmakerho/Exception_DataChanged

參考資料

Microsoft Bot Framework: Exception: The data is changed

Asp.Net MVC Model Validation:Custom Validation – 以 Required If 為例

在前篇「Asp.Net MVC Model Validation:Remote validation」的介紹之下,想必大家對於 .Net MVC 的 Model Validation 運作機制都有一定程度的了解,接下來將進一步介紹如何在 .Net MVC Model Validation 的運作機制加上自訂驗證規則(前、後端,使我們專案的表單驗證機制更加完善

繼續閱讀 “Asp.Net MVC Model Validation:Custom Validation – 以 Required If 為例”

透過 WinDbg 來找出 ASP.NET CPU 100% ASP.NET 程式的問題

問題

我們有一個 ASP.NET 的系統部署到 IIS 上(Windows 2012, .NET 4.x),有時候會導致 w3wp.exe 吃掉所有的 CPU 資源,一直要等到應用程式回收後,程式再重新啟動後就正常了。這種狀況不定期會發生。

解法

我們可以使用 WinDbg 來找出導致 IIS CPU 100% 的原因,方法如下,

  • 透過「工作管理員」來「建立傾印檔案」
    當發生 IIS 導致 CPU 100% 時,開啟「工作管理員」,按右鍵選取「建立傾印檔案」。要依 Web 應用程式的平台(x86/x64)來開啟「工作管理員」(x86的工作管理員在 C:\Windows\SysWOW64\taskmgr.exe )。
  • 安裝 WinDbg
    請參考 Debugging Tools for Windows (WinDbg, KD, CDB, NTSD) 來安裝對應的版本。
  • 設定 Symbol Path
    先建立一個 C:\RTX64_SYMBOLS 目錄(依您自行定義),然後開啟 Command 設定 _NT_SYMBOL_PATH 的環境變數,如下,

    set _NT_SYMBOL_PATH=srv*C:\RTX64_SYMBOLS*https://msdl.microsoft.com/download/symbols

    set _NT_SYMBOL_PATH所以在 command 中,輸入 set 後,就可以看到 _NT_SYMBOL_PATH 的設定值哦,

    _NT_SYMBOL_PATH當然,如果常常會用到的話,就直接設定到 環境變數之前,下次 debug 時,就不用再設定一次了哦! 詳細請參考Set up your system to use Microsoft’s public Symbol Server

  • 在 WinDbg 中找問題
    • 載入 sos.dll
      WinDbg 有 x86/x64 的版本,我是使用 x64 的版本,所以 sos.dll 的路徑是 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll,您輸入如果發生錯誤的話,請使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll 。
    • 設定 Symbol 檔目錄
      !sym noisy
      .cordll -ve -u -l
    • 透過 !runaway 顯示Threads所佔的時間
      輸入 !runaway 後,會顯示各 Thread 所花費的時間,花最多的會在最上面,如下圖,

      !runaway

      !runaway

    • 透過 ~[thread]s 切換到該 thread 的位置
      由上圖所示,Thread 40 佔最多時間,所以我們切到它的位罝去,

      ~40s

      ~40s

    • 透過 !clrstack 來查看呼叫堆疉
      !clrstack

      !clrstack從上圖可以發現,應該是有關 Dictionary 操作的問題,而它是由我們系統中 TemplateCfg.initial() 這個 Method 所引起的。

  • 檢視並調整程式碼
    開啟 TemplateCfg 程式碼,在 initial 這個 Method 之中會建立 Dictionary 物件,Clear 它,並 Insert 資料,但這些 Dictionary 的變數卻又設定成 static 。TemplateCfg.initial()即然這些 Dictionary 是 static 的,而且它們值又都是相同的,就沒有必要每次 request 時,就重新建立並 insert 這些資料。
    所以將 initial 裡面的 Code 搬到 static constructor 。
    調整完程式碼後,從去年觀察到目前,已經沒有再發生 CPU 100% 的狀況。

參考資料

Debugging in Production Part 1 – Analyzing 100% CPU Usage Using Windbg
WinDBG 應用實例:找出 ASP.NET CPU 100% 原因
Debugging Tools for Windows (WinDbg, KD, CDB, NTSD)
Set up your system to use Microsoft’s public Symbol Server
High CPU Hangs – 05
中小型研发团队架构实践:生产环境诊断利器WinDbg帮你快速分析异常情况Dump文件
Intro to WinDBG for .NET Developers
.NET Debugging Demos Lab 4: High CPU Hang – Review

無法載入檔案或組件 ‘log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=692fbea5521e1304’ 或其相依性的其中之一

最近同事詢問他們使用 Crytal Report Viewer 時,會發生 log4net 版本衝突的問題。如下圖所示,
CrystalReportViewer Runtime Error

如果是不同版本衝突的話,可以參考 如何讓不同 PublicKeyToken 的 DLL assemblyBinding 到可以用版本? 透過 assemblyBinding 的方式就可以解決。 但剛好我們的 AP 自已用的 log4net 版本也是 1.2.10.0,所以是 相同版本,不同的 PublickToken 的衝突。 詳細訊息如下,

沒有辦法解決 “log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821” 和 “log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=692fbea5521e1304” 之間的衝突。
任意選擇 “log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821”。

有查到 Error: Could not load log4net assembly 似乎是因為不同的平台(x86/x64) 所用的 log4net 會不同。
x86(32位元)用的 PublicToken 是 692fbea5521e1304,x64(64位元)或是 AnyCPU 用的 PublicToken 則是 1b44e1d426115821。
而我們 ap 平台是 x64 的,但 Crystal Report Viewer 卻使用了 x86(32位元) 的 log4net 。

嗯… 我想應該是那裡設定出了錯吧 …

跟同事討論後,同事又說只有在某台機器上才會出現那個錯誤,如果是透過 Browser 直接使用系統就正常,透過 VS.NET 啟動程式去 Debug 時,就會出現那個錯誤。
我想是因為如果透過 Browser 去使用系統的話,是連到 IIS ,而 IIS 設定的是 x64(64位元);如果透過 VS.NET 則是連到 IIS Express,預設它是使用 x86(32位元)。
於是筆者將 IIS 上的應用程式集區設定成 啟用 32 位元,再執行程式,則一樣會出現 log4net 的衝突狀況(  重現問題了 …^_^)。
所以就請同事在 VS.NET 中設定讓 IIS Express 使用 64 位元就沒問題了哦!
工具 -> 選項 -> (search textbox 中輸入 iis) , 並勾選 將 64 位元版本的 IIS Express 用於網站和專案(U) 選項勾起來就可以了哦! 如下,
將 64 位元版本的 IIS Express 用於網站和專案(U)

如果您是系統出了錯,請 Check 應用程式集區是否設定成了 「啟用32位元應用程式」哦!

分享 Call Language Understanding (LUIS) predictions api 的 3 種方式

在前一篇 「線上客服 + Chatbot = 智慧客服」我們透過 LUIS + Vital ESP(FAQ系統)  來打造智慧客服 Chatbot 。 在上線之前,需要先透過一堆問題去 Call LUIS  的 api ,來驗證我們在 LUIS  中設定的意圖及 Entities 是否正確。

即然是 Call LUIS  api 雲端服務,就會有費用問題,所以我們先來看一下 LUIS api 的價格說明,

luis.ai api 價格說明

  1. LUIS API – 免費: 每月最多可以 Call 10,000 次
  2. LUIS API – 基本: 每月最多可以 Call 10,000 次 (免費),超過後,每 Call 1,000 次約台幣 45.09 元
    詳細請參考 認知服務定價 – Language Understanding (LUIS)

目前我們可以在 LUIS 上手動測試意圖,但如果想要更有彈性的話,就要透過程式去呼叫它的 api ,所以以下我們就介紹 Call api 的 3 種方式,

Call luis.ai predictions api 的 3 種方式

在 LUIS predictions GET api 中有各語言的使用範例,以C#來看,使用非常方便,只要設定一些參數就可以取回 LUIS 解析的資料,本文將跟大家分享 Call 正式 api 的 GET/POST 方式,及 Call 測試 api 的方式,共 3 透方式。

  • call 正式 api 使用 GET,如下,
    static async Task<string> GetLuisResult(string reqString)
    {
    //最多是 500 個 ascii chars,中文我先取 230 個
    if (reqString.Length > 230)
    {
    reqString = reqString.Substring(0, 230);
    }
    var client = new HttpClient();
    var queryString = HttpUtility.ParseQueryString(string.Empty);
    const string appId = “你的appid”;
    const string subscriptionKey = “你的訂閱key”;
    // Request headers
    client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, subscriptionKey);
    // Request parameters
    //如果回傳的內容中需要各 Intent 的話,請將 verbose 設定為 true
    queryString[“verbose”] = “true”;
    //queryString[“spellCheck”] = “{boolean}”;
    //queryString[“staging”] = “{boolean}”;
    //queryString[“bing-spell-check-subscription-key”] = “bing key”;
    //queryString[“log”] = “{boolean}”;
    var uri = $”https://eastasia.api.cognitive.microsoft.com/luis/v2.0/apps/{appId}?q=” + HttpUtility.UrlEncode(reqString)+ “&” + queryString;;
    var response = await client.GetAsync(uri);
    var result = await response.Content.ReadAsStringAsync();
    return result;
    }
  • call 正式 api 使用 POST (筆者試了老半天都試不出來,後來詢問 MS Herman 大大才知道,POST 的 body 前後要加 雙引號),如下,
    static async Task<string> GetLuisResultPost(string reqString)
    {
    //最多是 500 個 ascii chars,中文我先取 230 個
    if (reqString.Length > 230)
    {
    reqString = reqString.Substring(0, 230);
    }
    var client = new HttpClient();
    var queryString = HttpUtility.ParseQueryString(string.Empty);
    const string appId = “你的appid”;
    const string subscriptionKey = “你的訂閱key”;
    // Request headers
    client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, subscriptionKey);
    // Request parameters
    //如果回傳的內容中需要各 Intent 的話,請將 verbose 設定為 true
    queryString[“verbose”] = “true”;
    //queryString[“spellCheck”] = “{boolean}”;
    //queryString[“staging”] = “{boolean}”;
    //queryString[“bing-spell-check-subscription-key”] = “bing key”;
    //queryString[“log”] = “{boolean}”;
    var uri = $”https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/{appId}?” + queryString;
    HttpResponseMessage response;
    // Request body,前後要加上 雙引號 哦!
    byte[] byteData = Encoding.UTF8.GetBytes(“\”” + reqString + “\””);
    using (var content = new ByteArrayContent(byteData))
    {
    response = await client.PostAsync(uri, content);
    var result = await response.Content.ReadAsStringAsync();
    return result;
    }
    }
  • 註: 在 LUIS 設定好,按下 Train 後,請記得要到 PUBLISH , 按下 「Publish to production slot」,這樣才會部到各區域去哦!

知道如何 Call Production 的 api 之後,如果我們想要像 線上客服 + BOT 之路 一文中,先用個 Excel 檔去試看看的話(目前 luis.ai 上 support 上傳 json , 整批測試),多試個幾次,免費的 quota 應該一下子就沒了吧! 如下所示,
超過免費可呼叫的Quota

那怎麼辦呢?

  • 1.到 Azure 那買個服務,然後再將訂閱的 Key 加到 luis.ai 之中,如下,從Azure買付費的方案
  • 2.使用 luis.ai 測試的 api ,您可以在測試時,錄一下 network ,如下,luis.ai 測試的 api

所以 Call luis.ai 測試 api 的方式如下,

static async Task<string> GetLuisResultTest(string reqString)
{
//最多是 500 個 ascii chars,中文我先取 230 個
if (reqString.Length > 230)
{
reqString = reqString.Substring(0, 230);
}
var client = new HttpClient();
var queryString = HttpUtility.ParseQueryString(string.Empty);
const string appId = “你的appid”;
const string subscriptionKey = “你的免費訂閱key”;
// Request headers
client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, subscriptionKey);
var uri = $”https://westus.api.cognitive.microsoft.com/luis/webapi/v2.0/apps/{appId}/versions/0.1/predict?example=” + HttpUtility.UrlEncode(reqString);
var response = await client.GetAsync(uri);
var result = await response.Content.ReadAsStringAsync();
return result;
}

這個測試的 api 跟正式機的差別除了內容不太相同,它取出的 Entity 中間會有空白,所以再使用上需要將空白清掉哦! 因為它是測試用的,所以在整批 Call 的時候,有時會 block 一段時間,我的做法是如果出錯就停個 15 秒,讓它再重試一次,後來就可以正常再 Call 了哦!

正式 api 的結果如下 ( verbose 參數為 true, 內容才會有 intents ),

{
“query”: “下載下來可以用了,可是什麼是網址識別”,
“topScoringIntent”: {
“intent”: “FAQ”,
“score”: 1.0
},
“intents”: [
{
“intent”: “FAQ”,
“score”: 1.0
},
{
“intent”: “None”,
“score”: 0.05330094
}
],
“entities”: [
{
“entity”: “網址”,
“type”: “Keyword”,
“startIndex”: 14,
“endIndex”: 15,
“score”: 0.9081365
},
{
“entity”: “識別”,
“type”: “Keyword”,
“startIndex”: 16,
“endIndex”: 17,
“score”: 0.9102927
},
{
“entity”: “下載”,
“type”: “Action”,
“startIndex”: 0,
“endIndex”: 1,
“score”: 0.9999258
}
]
}

測試 api 的結果如下( entityPredictions 中的 phrase 字之間會有空白哦!!! ),

{
    "id": null,
    "text": "下載下來可以用了,可是什麼是網址識別",
    "alteredText": "下載下來可以用了,可是什麼是網址識別",
    "tokenizedText": [
        "下"
    ],
    "intentPredictions": [
        {
            "id": "9e70ed0a-561c-45f4-b8a4-2439de42acb1",
            "name": "FAQ",
            "score": 1.0
        },
        {
            "id": "8d898fa2-667f-471d-b87f-309011627b5d",
            "name": "None",
            "score": 0.05
        }
    ],
    "entityPredictions": [
        {
            "id": "cb403a63-347c-448c-9b08-3e48d6ac0c61",
            "entityName": "Keyword",
            "startTokenIndex": 14,
            "endTokenIndex": 15,
            "phrase": "網 址",
            "entityType": 1
        },
        {
            "id": "cb403a63-347c-448c-9b08-3e48d6ac0c61",
            "entityName": "Keyword",
            "startTokenIndex": 16,
            "endTokenIndex": 17,
            "phrase": "識 別",
            "entityType": 1
        },
        {
            "id": "1771cbab-fb7b-4a83-b46d-4c085ba88341",
            "entityName": "Action",
            "startTokenIndex": 0,
            "endTokenIndex": 1,
            "phrase": "下 載",
            "entityType": 1
        }
    ],
    "tokenMetadata": null
}

預設 call 正式 api 時,會 log 查詢的資料,最近幾筆資料可以從 「 Review endpoint utterances 」 看到,如下
Review endpoint utterances

那如果我需要的是全部的 Log 呢? 可以到「 My apps 」,在那個 app 右邊的 … ,選取「 Export Endpoint Log 」,就可以了哦,如下,
Export Endpoint Logs