Тайм-аут запроса веб-Api?

Как и MVC WebApi работает на асинхронном конвейере ASP.NET, что означает время ожидания выполнения не поддерживается.

В MVC я использую фильтр [AsyncTimeout], у WebApi этого нет. Итак, как мне пропустить запрос в WebApi?

Ответы

Ответ 1

Итак, для ASP.NET Core он выглядит как эквивалент AsyncTimeOutAttribute не будет реализован.

Все не потеряно, хотя в ASP.NET Core WebApi и MVC были объединены в один проект MVC, и в отличие от предыдущих версий (по крайней мере, при размещении с использованием IIS) Тайм-аут запроса поддерживается через web.config.

Значение по умолчанию - 2 минуты

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore 
        requestTimeout="00:02:00"
        processPath="%LAUNCHER_PATH%" 
        arguments="%LAUNCHER_ARGS%" 
        stdoutLogEnabled="false" 
        stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>

Если у вас нет синхронной блокировки кода, тайм-ауты запросов не являются проблемой в ASP.NET Core. Возвращение асинхронной задачи в ASP.NET Core возвращает поток обратно в пул потоков, чтобы можно было обрабатывать другие запросы. Дополнительная информация о том, как Kestrel работает с IIS в качестве резервного прокси.

Ответ 2

Основываясь на предложении Мендхака, можно делать то, что вы хотите, хотя и не совсем так, как вы хотели бы сделать это, не пробираясь через несколько обручей. Выполнение без фильтра может выглядеть примерно так:

public class ValuesController : ApiController
{
    public async Task<HttpResponseMessage> Get( )
    {
        var work    = this.ActualWork( 5000 );
        var timeout = this.Timeout( 2000 );

        var finishedTask = await Task.WhenAny( timeout, work );
        if( finishedTask == timeout )
        {
            return this.Request.CreateResponse( HttpStatusCode.RequestTimeout );
        }
        else
        {
            return this.Request.CreateResponse( HttpStatusCode.OK, work.Result );
        }
    }

    private async Task<string> ActualWork( int sleepTime )
    {
        await Task.Delay( sleepTime );
        return "work results";
    }

    private async Task Timeout( int timeoutValue )
    {
        await Task.Delay( timeoutValue );
    }
}

Здесь вы получите тайм-аут, потому что фактическая "работа", которую мы делаем, займет больше времени, чем таймаут.

Чтобы сделать то, что вам нужно с, возможно, этот атрибут не идеален. Это та же основная идея, что и раньше, но фильтр действительно может быть использован для выполнения действия посредством отражения. Я не думаю, что рекомендую этот маршрут, но на этом надуманном примере вы можете увидеть, как это можно сделать:

public class TimeoutFilter : ActionFilterAttribute
{
    public int Timeout { get; set; }

    public TimeoutFilter( )
    {
        this.Timeout = int.MaxValue;
    }
    public TimeoutFilter( int timeout )
    {
        this.Timeout = timeout;
    }


    public override async Task OnActionExecutingAsync( HttpActionContext actionContext, CancellationToken cancellationToken )
    {

        var     controller     = actionContext.ControllerContext.Controller;
        var     controllerType = controller.GetType( );
        var     action         = controllerType.GetMethod( actionContext.ActionDescriptor.ActionName );
        var     tokenSource    = new CancellationTokenSource( );
        var     timeout        = this.TimeoutTask( this.Timeout );
        object result          = null;

        var work = Task.Run( ( ) =>
                             {
                                 result = action.Invoke( controller, actionContext.ActionArguments.Values.ToArray( ) );
                             }, tokenSource.Token );

        var finishedTask = await Task.WhenAny( timeout, work );

        if( finishedTask == timeout )
        {
            tokenSource.Cancel( );
            actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.RequestTimeout );
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.OK, result );
        }
    }

    private async Task TimeoutTask( int timeoutValue )
    {
        await Task.Delay( timeoutValue );
    }
}

Затем это можно использовать следующим образом:

[TimeoutFilter( 10000 )]
public string Get( )
{
    Thread.Sleep( 5000 );
    return "Results";
}

Это работает для простых типов (например, string), давая нам: <z:anyType i:type="d1p1:string">Results</z:anyType> в Firefox, хотя, как вы можете видеть, сериализация не идеальна. Использование пользовательских типов с этим точным кодом будет немного проблематичным, поскольку сериализация идет, но с некоторой работой это может быть полезно в некоторых конкретных сценариях. То, что параметры действия поступают в виде словаря, а не массива, также может представлять некоторые проблемы с точки зрения упорядочения параметров. Очевидно, что иметь реальную поддержку для этого было бы лучше.

Что касается материалов vNext, они вполне могут планировать добавить возможность выполнять тайм-ауты на стороне сервера для веб-API, поскольку контроллеры MVC и API объединяются. Если они это сделают, это, скорее всего, не будет проходить через класс System.Web.Mvc.AsyncTimeoutAttribute, поскольку они явно удаляют зависимости от System.Web.

На сегодняшний день не кажется, что добавление записи System.Web.Mvc в файл project.json работает, но это может измениться. Если это произойдет, хотя вы не сможете использовать новую оптимизированную для облаков инфраструктуру с таким кодом, вы можете использовать атрибут AsyncTimeout для кода, который предназначен только для работы с полной платформой .NET.

Для чего это стоит, я попытался добавить к project.json. Возможно, конкретная версия сделала бы ее более счастливой?

"frameworks": {
    "net451": {
        "dependencies": { 
            "System.Web.Mvc": ""
        }
    }
}

Ссылка на него отображается в списке рекомендаций Solution Explorer, но она делает это с желтым восклицательным знаком, указывающим на проблему. Само приложение возвращает 500 ошибок, пока эта ссылка остается.

Ответ 3

В WebAPI вы обычно обрабатываете тайм-ауты на стороне клиента, а не на стороне сервера. Это потому, что и цитирую:

Способ отмены HTTP-запросов заключается в немедленном их удалении по HttpClient. Причина заключается в том, что несколько запросов могут повторно использовать TCP-соединения в одном HttpClient, и поэтому вы не можете безопасно отменить один запрос, не влияя на другие запросы.

Вы можете управлять таймаутом для запросов - я думаю, что это на HttpClientHandler, если я правильно помню.

Если вам действительно нужно реализовать тайм-аут самой стороны API, я бы рекомендовал создать поток для выполнения вашей работы, а затем отменить его через определенный период. Например, вы можете положить его в Task, создать свою задачу "timeout" с помощью Task.Wait и использовать Task.WaitAny для первой, чтобы вернуться. Это может имитировать тайм-аут.

Аналогично, если вы выполняете определенную операцию, проверьте, поддерживает ли она время ожидания. Довольно часто я выполняю HttpWebRequest из своего WebAPI и указываю его свойство Timeout.

Ответ 4

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

    protected async Task<T> RunTask<T>(Task<T> action, int timeout) {
        var timeoutTask = Task.Delay(timeout);
        var firstTaskFinished = await Task.WhenAny(timeoutTask, action);

        if (firstTaskFinished == timeoutTask) {
            throw new Exception("Timeout");
        }

        return action.Result;
    }

Теперь каждый контроллер, который наследует ваш базовый контроллер, может получить доступ к методу RunTask. Теперь в вашем API вызовите метод RunTask так:

    [HttpPost]
    public async Task<ResponseModel> MyAPI(RequestModel request) {
        try {
            return await RunTask(Action(), Timeout);
        } catch (Exception e) {
            return null;
        }
    }

    private async Task<ResponseModel> Action() {
        return new ResponseModel();
    }