紀念抓了個把小時的茶包。
同事回報一個詭異狀況,某個使用者登入時遇到異常,由於是線上系統,只能靠 Log 推敲還原事發經過,但卻有個矛盾之處:Log 顯示使用者只登入過一次,但登入後的初始化作業 Log 卻有兩次記錄。為了找出第二次沒有登入卻觸發初始化的來源,翻箱倒櫃清查過所有相關程式碼,一一推敲被執行的可能性,不料所有嫌犯都亮出完美的不在場證明,除了見鬼,我們想破頭也找不到合理解釋。
當案情陷入膠著,同事倒發現一處可疑,登入初始化作業的 Log 記載的客戶帳號,結尾多了一個空白。這段 Log 我們反覆看了 N 遍,結尾空白八成被人類認知模組給自動過濾了,白繞了一大圈才發現。
有了這條線索,很快就拼湊出我們的推論 - 使用者第二次登入時在帳號結尾多敲了空白,而前後端程式未加攔阻,八成是心想:反正帳號沒輸對 SQL 比對帳號密碼就不會正確,因此未對帳號字數或格式進行檢核。不料,SQL WHERE 比對時會自動略過結尾空白,多了空白結尾的帳號就這麼通過了密碼檢核,但要寫登入 Log 時帳號被當成資料夾名稱,結尾空白導致名稱不合法失效,NLog 忽略寫 Log 錯誤,程式繼續往下執行初始化,直到其他地方因帳號結尾多出空白出錯。登入 Log 只有一筆,初始化卻有兩筆,終於有了合理的解釋。
用以下這段程式驗證 SQL WHERE 比對行為:
如上圖所示,用 WHERE N = "Jeffrey " 可以查得 "Jeffrey",但取回至 C# 比對二者並不相等,形成奇妙的「在 SQL 端相等,但 C# 不相等」的奇特結果,也證明 SQL WHERE 比對會自動忽略結尾空白。而這個行為遇到固定寬度欄位 CHAR(N) 時也很精彩,一樣會造成要 SQL WHERE 找回相等的字串,在 C# 卻不相對的矛盾狀況。
至於結尾空白導致寫 Log 失敗的問題,依據 MSDN 文件空白或句點不宜做為檔案或資料夾結尾:(要硬幹也成,但實在沒必要庸人自擾)
Do not end a file or directory name with a space or a period. Although the underlying file system may support such names, the Windows shell and user interface does not. However, it is acceptable to specify a period as the first character of a name. For example, ".temp".
而 NLog 預設會忽略 Log 寫入例外以避免因寫 Log 出錯干擾主要作業,在 NLog.config 加上 throwExceptions="true" 可觀察到因路徑名帶有空白結尾出錯的訊息,證實我們的猜測。
又學個經驗:SQL WHERE 忽略結尾空白、沒看到 Log 不一定代表事情沒發生。