The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET MVC 開發心得分享 (22):關於 executionTimeout

當我們想要限制或加長 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 本來就是無效的!

好多小細節,知道的越多越有趣,我經常就是悠游在這些雞毛蒜皮,但會把人搞瘋的事情上打轉,呵呵~

相關連結