當我們想要限制或加長 ASP.NET 可執行的時間長度時,通常都會到 web.config 的 <system.web> 區段新增一個 httpRuntime 元素並且指派 executionTimeout 屬性一個秒數,像是我們在設定檔案上傳的程式時,由於上傳檔案的執行時間可能會超過系統的預設值( 110 秒 ),所以這時我們就必須把這個數值調大。當然你也可以將這個數值縮小,以免過多、過長的執行要求把伺服器拖垮。不過,在 ASP.NET MVC 裡有一個鮮為人知的秘密,那就是 ASP.NET MVC 根本不吃這套,預設執行時間是沒有上限的,所以你的 ASP.NET MVC 程式要是出問題,那可是會執行到天荒地老海枯石爛的,最慘的狀況就是 IIS 的 Request Queue 被塞爆。
首先,我先標示出 <httpRuntime executionTimeout="300" /> 的使用範例,如下圖示:
請注意:設定 executionTimeout 時,其 compilation 的 debug 屬性也必須設為 false 才有用。
由於從 ASP.NET MVC 2.0 開始,ASP.NET MVC 的 MvcHandler 就改用 IHttpAsyncHandler 來實做,所以所有透過 ASP.NET MVC 接收到的 Requests 都會以非同步的方式來執行,又基於「某些」不明原因所以不加上 Timeout 的限制(原因釐清中…)。
如果要解決這個問題,可能就必須要耍一點點小小的手段,才能讓你的 Controller 在執行時 Action 方法時能夠有個基本的 Timeout 時間,以下為設計為 Base Controller 的程式碼範例:
備註1:以下程式碼只會針對 Action 的執行時間設定 Timeout,並不會計算 View 所執行的時間!
備註2:以下程式碼會終止目前執行中的 Thread 運作,由於 ASP.NET MVC 使用非同步 Handler 的關係,同一個 Thread.CurrentThread 可能會同時服務好幾個不同的 Requests,所以強制 Abort 這個 Thread 很有可能會導致其他使用相同 Thread 的 Requests 也被中斷作業,並非為最佳解法! [ 參考討論 ]
using System;
using System.Web.Mvc;
using System.Threading;
namespace MvcApplication3.Controllers
{
public class TimeoutController : Controller
{
// Controller Timeout ( seconds )
private int _controllerTimeout = 60;
private bool _isExecuting = false;
private Thread _executingThread;
private readonly object _syncRoot = new object();
protected override void ExecuteCore()
{
_executingThread = Thread.CurrentThread;
ThreadPool.QueueUserWorkItem(o =>
{
Thread.Sleep(_controllerTimeout * 1000);
if (_isExecuting)
{
_executingThread.Abort();
}
});
base.ExecuteCore();
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
_isExecuting = true;
base.OnActionExecuting(filterContext);
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
_isExecuting = false;
base.OnActionExecuted(filterContext);
}
public int ControllerTimeout
{
get
{
int retVal;
lock (_syncRoot)
{
retVal = _controllerTimeout;
}
return retVal;
}
set
{
lock (_syncRoot)
{
_controllerTimeout = value;
}
}
}
}
}
以上 Base Controller 的使用方法也很簡單,直接繼承 TimeoutController 類別即可:
事實上,還有另外一個更加暴力的解決辦法,那就是直接透過 .NET 內建的 Reflection 機制修改一個在 HttpContext 裡的 _timeoutState 私有欄位,將其狀態值改成 1 也可以啟用 ASP.NET MVC 的 Timeout 設定,而這樣就會真的套用原本 ASP.NET Framework 裡 executionTimeout 的設定!
以下是設定 _timeoutState 私有欄位的方法,你只要在任意程式碼開始執行之前執行他即可:
System.Web.HttpContext.Current.GetType()
.GetField("_timeoutState",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic)
.SetValue(System.Web.HttpContext.Current, 1);
不過這樣的寫法會有個小問題,那就是這段程式碼只能執行在無法在 High Trust 的執行環境裡,像有些提供 ASP.NET 的虛擬主機商會特別把 ASP.NET 的執行環境調整為 Middle Trust 的環境,那你就無法使用這段程式碼了。不過我想大多數的網站都還是跑在 High Trust 的執行環境下,所以應該不太會遇到問題。
套用標準 ASP.NET 的 executionTimeout 設定必須在 web.config 將 debug 模式設定為 false 才會生效,如果你的 debug 模式設定為 true 的話,executionTimeout 本來就是無效的!
好多小細節,知道的越多越有趣,我經常就是悠游在這些雞毛蒜皮,但會把人搞瘋的事情上打轉,呵呵~
相關連結