Почему мой метод действия не заканчивается?

У меня есть следующие контроллеры:

[TimeoutFilter]
public abstract class BaseController: Controller
{
}

public class IntegrationTestController : BaseController
{
    [HttpGet]
    public ActionResult TimeoutSeconds()
    {
        return Content(HttpContext.Server.ScriptTimeout.ToString(CultureInfo.InvariantCulture));
    }

    [HttpGet]
    public ActionResult ForceTimeout()
    {
        var timeoutWindow = TimeoutFilter.TimeoutSeconds;
        Thread.Sleep((timeoutWindow + 5) * 1000);
        return Content("This should never get returned, mwahahaaa!");
    }
}

Для моего тестового сценария я использую настройку конфигурации 5 секунд в TimeoutFilter, и я знаю, что это работает, потому что, когда мои тестовые вызовы TimeoutSeconds, я получаю правильное значение 5, но когда тестовые вызовы ForceTimeout, я получаю HTTP-ответ 200 и мой "никогда не возвращенный" текст.

И фильтр:

public class TimeoutFilter : ActionFilterAttribute
{
    internal const string TimeoutSecondsSettingsKey = "MvcActionTimeoutSeconds";
    internal static int TimeoutSeconds;
    public TimeoutFilter()
    {
        TimeoutSeconds = int.Parse(ConfigurationManager.AppSettings[TimeoutSecondsSettingsKey]);
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.Controller.ControllerContext.HttpContext.Server.ScriptTimeout = TimeoutSeconds;
        base.OnActionExecuting(filterContext);
    }
}

Ответы

Ответ 1

Я не мог заставить это работать, установив свойство ScriptTimeout, даже если установить debug="false" в web.config, как было предложено пользователем Erik Funkenbusch:

<configuration>
   <system.web>
      <compilation debug="false" targetFramework="4.5"/>
      ...

Он продолжал возвращать текст "Это никогда не возвращался", а не тайминг во время Thread.Sleep.

Также стоит отметить, что я также расширил Thread.Sleep до уровня Server.ScriptTimeout по умолчанию 110 секунд, но он все же в конечном итоге вернул тот же текст, а не тайминг.

Вместо этого я попытался установить executionTimeout в web.config:

<configuration>
   <system.web>
      <httpRuntime targetFramework="4.5" executionTimeout="5"/>
      ...

Если вы должны добавить точку останова в TimeoutFilter, вы заметите, что значение ScriptTimeout установлено в значение executionTimeout из web.config. Увы, это все еще не сработало для меня (независимо от того, debug="false").

Затем я наткнулся на эту ссылку о ScriptTimeout и executionTimeout, не работающем с аналогичной настройкой с тем, что вы описываете. Первый ответ в сообщении описывает использование debug="false", а также упоминание о том, что таймаут будет иметь задержку от 5 до 15 секунд. Мне все еще не повезло, даже когда вы используете большое значение Thread.Sleep. Второй ответ в этой статье предполагает, что параметр конфигурации executionTimeout является заменой свойства ScriptTimeout (который, по-видимому, является интерфейсом COM, используемым в классическом ASP). Этот ответ предполагает, что невозможно установить определенный тайм-аут без использования собственной логики тайм-аута.

Далее я наткнулся на следующую (более позднюю) ссылку, где первый ответ предполагает, что тайм-аут отключен в MVC. Еще один ответ предполагает, что это связано с тем, что MVCHandler (который выбирает контроллер, который будет обрабатывать HTTPRequest), это IHttpAsyncHandler и поэтому он может выполнять другой запрос (который является точкой использования асинхронного запроса), поэтому он внутренне отключает состояние тайм-аута (это то, что я получаю от чтения этой ссылки). Он должен работать с прямым asp.net, хотя использование ScriptTimeout кажется приемлемым способом в этом ответе.

Ссылка предполагает, что добавление следующей строки позволит ей работать (но не в средстве доверия):

System.Web.HttpContext.Current.GetType().GetField("_timeoutState", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(System.Web.HttpContext.Current, 1);

Поэтому, изменив TimeoutFilter OnActionExecuting() на:

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    System.Web.HttpContext.Current.GetType().GetField("_timeoutState", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(System.Web.HttpContext.Current, 1);
    base.OnActionExecuting(filterContext);
}

и установите web.config:

<configuration>
   <system.web>
      <compilation debug="false" targetFramework="4.5"/>
      <httpRuntime targetFramework="4.5" executionTimeout="5"/>
      ...

позволяет тайм-ауту работать, но имеет небольшую задержку в 5 секунд, указанную в первом сообщении.

Примечание. Использование этого метода не позволяет вам установить свойство ScriptTimeout в фильтре. Попытка установить ScriptTimeout и переопределить значение, установленное в web.config, не работает.

Ответ 2

Используете ли вы сборку отладки?

Из документации ScriptTimeout:

http://msdn.microsoft.com/en-us/library/system.web.httpserverutility.scripttimeout(v=vs.110).aspx

Если вы установите для атрибута debug элемента компиляции значение true в файле Web.config, значение ScriptTimeout будет проигнорировано.

Кроме того, поскольку это значение такое же, как установлено в элементе httpRuntime, я действительно не понимаю этого, так как вы можете просто настроить этот параметр в своем web.config.

Edit:

Dangerous проделал хорошую работу по выяснению деталей, и, действительно, ScriptTimeout не поддерживается в асинхронных конвейерах (какой MVC был, по крайней мере, MVC4, я думаю, а также WebApi.. даже если не использовать методы async)

"Обходной путь", предложенный в этом отчете о подключении:

https://connect.microsoft.com/VisualStudio/feedback/details/781171/asp-net-mvc-executiontimeout-does-not-work

Используйте атрибут [AsyncTimeout] и возьмите маркер отмены в качестве параметра, затем вызовите CanclationToken.ThrowIfCancelationRequested периодически или используйте маркер отмены в методе async.

Вот пример:

[AsyncTimeout(5000)]
public async Task<ContentResult> Index(CancellationToken ct)
{
    await Task.Delay(10 * 1000, ct);
    return Content("This should never get returned, mwahahaaa!");
}

Это выдает исключение OperationCanceled с YSOD через 5 секунд. Бонус для этого заключается в том, что он работает даже в режиме отладки;)