WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

HttpClient のキャンセルは要求の中断に相当? (CORE)

by WebSurfer 13. July 2021 12:31

下の画像のシステムで、クライアントがブラウザを操作して要求を中断した場合、Web API でのサーバーで実行中の処理をキャンセルできるでしょうか? 自分が検証した限りではできるようです。その詳細を以下に書きます。

システム構成

クライアントがブラウザを使って MVC にアクセスすると、MVC のサーバーは HttpClient クラスを使って Web API にアクセスして必要な情報を取得し、ブラウザに応答として返すというシステムです。

クライアントが要求を送信した後待ちきれなくなって、サーバーによる処理が終わって応答が返ってくる前に要求を中断した場合、それ以上サーバーのリソースを消費しないで済むよう、サーバー側の処理を MVC でも Web API でも中断できるかがポイントです。

なお、クライアントによる要求の中断とは、下の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

要求の中断

先の記事「要求の中断による処理のキャンセル (CORE)」に書きましたように、処理のキャンセルには HttpContext.RequestAborted プロパティで取得できる CancellationToken を利用します。クライアントが要求を中断すると、取得した CancellationToken がキャンセル通知を配信しますので、それをリッスンして処理の中断を行います。

ブラウザ ⇔ MVC の間は、先の記事に書いたように、MVC のアクションメソッドの引数に渡された CancellationToken によるキャンセル通知を利用して MVC のサーバー内での処理を中断できます。

その先の MVC ⇔ Web API の間は MVC のサーバーから HttpClient クラスを利用して Web API にアクセスするというシステムですが、そこがどうできるかをこの記事の下の方に載せた検証用のコードを使って調べてみました。

MVC のアクションメソッドでは次のようにします。HttpClient クラスの SendAsync メソッドや PostAsync メソッドには引数に CancellationToken を取るオーバーロードがあるので、それに HttpContext.RequestAborted プロパティで取得できる CancellationToken を渡します。そうすることで、クライアントによる要求の中断で SendAsync メソッドや PostAsync メソッドの実行をキャンセルできます。

Web API のアクションメソッドの引数にも Web API のサーバー内で HttpContext.RequestAborted プロパティで取得できる CancellationToken を渡します。そのキャンセル通知をリッスンして処理を中断します。

そうした場合、MVC のサーバー内で SendAsync メソッドや PostAsync メソッドの実行がキャンセルされると、Web API のアクションメソッドの引数に渡した CancellationToken はキャンセル通知を配信してくれるかが問題です。

下に載せたコードで検証した結果 Web API の CancellationToken もキャンセル通知を配信してくれることが分かりました。

なので、ブラウザ ⇔ MVC ⇔ Web API という構成でも、適切に CancellationToken を渡してキャンセル通知で処理の中断を行う実装をしておけば、ブラウザで要求を中断しても、MVC でも Web API でもサーバー内の処理を中断できるようです。

参考に検証に使った MVC および Web API のコードを以下に載せておきます。.NET 5.0 の ASP.NET Core アプリで MVC と Web API のプロジェクトは異なります (検証の際のホストが異なりますので、HttpContext.RequestAborted プロパティで取得できる CancellationToken は MVC と Web API で違うものになります)。

MVC(MvcCore5App4)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MvcCore5App4.Models;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;

namespace MvcCore5App4.Controllers
{
    public class HomeController : Controller
    {        
        private readonly ILogger<HomeController> _logger;
        private readonly IHttpClientFactory _clientFactory;

        public HomeController(ILogger<HomeController> logger,
                              IHttpClientFactory clientFactory)
        {
            _logger = logger;
            _clientFactory = clientFactory;
        }

        // ・・・中略・・・

        public async Task<IActionResult> Cancel(CancellationToken token)
        {
            HttpClient client = _clientFactory.CreateClient();
            var url = "https://localhost:44398/api/values";

            // GET 要求する場合はこちら
            //var request = new HttpRequestMessage(HttpMethod.Get, url);
            //HttpResponseMessage response = 
            //                await client.SendAsync(request, token);

            // POST 要求する場合はこちら
            HttpResponseMessage response =
                            await client.PostAsync(url, null, token);

            if (response.IsSuccessStatusCode)
            {
                string result = 
                    await response.Content.ReadAsStringAsync(token);
                ViewBag.Result = result;
            }

            return View();
        }
    }
}

Web API (MvcCore5App2)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using System;

namespace MvcCore5App2.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ILogger<ValuesController> _logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public async Task<IActionResult> Get(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return Ok("GET 処理完了");
        }

        [HttpPost]
        public async Task<IActionResult> Post(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return Ok("POST 処理完了");
        }
    }
}

なお、キャンセルができるのは、IIS を使ったインプロセス ホスティング モデルに限った話ですので注意してください。

インプロセス ホスティング モデル

先の記事「要求の中断による処理のキャンセル (CORE)」に書きましたように、アウトプロセス ホスティング モデルや Linux 系の OS で Nginx とか Apache をリバースプロキシに使う場合は CancellationToken のキャンセル通知を配信できませんので、サーバーでの処理の中断はできません。

それから、データベースサーバーを相手にする場合、Entity Framework で使う ToListAsync とか SaveChangesAsync などではどうなるかですが、そこはまだ調べ切れていません。走り出したらキャンセルは効かないということもあるかもしれません。今後の検討課題にしたいと思います。

Tags: , , ,

CORE

要求の中断による処理のキャンセル (MVC5)

by WebSurfer 12. July 2021 10:30

.NET Framework 版の ASP.NET Web アプリで、クライアントによる要求の中断を検出してサーバー側の処理をキャンセルする話を書きます。(ASP.NET Core の場合は先の記事「要求の中断による処理のキャンセル (CORE)」を見てください)

要求の中断

クライアントによる要求の中断とは、上の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

ASP.NET 4.5 以降には HttpResponse.ClientDisconnectedToken プロパティが追加されています。このプロパティで取得できる CancellationToken によって、クライアントが要求の中断操作を行った場合に操作を取り消す通知を配信できます。(ASP.NET Core の HttpContext.RequestAborted プロパティと同様)

ただし、ASP.NET 4.5 以降という条件以外にも、上に紹介した Microsoft のドキュメントに書いてあるように IIS 7.5 以降の統合モードでのみサポートされているということですので注意してください

Visual Studio 2019 を使って、以下のコードを [デバッグ(D)] ⇒ [デバッグの開始(S)] で IIS 10 Express で実行して検証してみました。検証に使用したブラウザは Edge v91.0.864.67, Chrome v91.0.4472.124, Firefox v89.0.2, IE11, Opera v77.0.4054.203 で、いずれもクライアントによる要求の中断によってサーバー側での処理の中断が確認できました。

using System;
using System.Web.Mvc;
using System.Threading.Tasks;
using System.Diagnostics;

namespace Mvc5App2.Controllers
{
    public class HomeController : Controller
    {

        // ・・・中略・・・

        public async Task<ActionResult> Cancel()
        {
            var token = Response.ClientDisconnectedToken;

            Debug.WriteLine($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            Debug.WriteLine($"end: {DateTime.Now:ss.fff}");
            return View();
        }
    }
}

ASP.NET Core MVC と違ってアクションメソッドの引数に CancellationToken を追加しても ClientDisconnectedToken プロパティで取得される CancellationToken はバインドされないので注意してください。なので、上のコードのようにする必要があります。(引数に追加すると CancellationToken がバインドされますが、それは ClientDisconnectedToken プロパティで取得できるものとは別物のようで要求の中断による通知は出ません)

上のコードの Task.Delay(5000, token) では渡した token を Deley メソッドの中で継続的に観察しているようで、このメソッドが実行が開始された後でもそれから 5 秒以内ならブラウザで要求を中断すると TaskCanceledException がスローされて処理が終わります。

ただし、Entity Framework を使ってデータベースにアクセスして処置を行うときに使う ToListAsync とか SaveChangesAsync などや、HttpClient の SendAsync とか PostAsync なども同様かどうかは調べてなくて分かりません。今後の検討課題ということで・・・

Tags: , , ,

ASP.NET

要求の中断による処理のキャンセル (CORE)

by WebSurfer 11. July 2021 13:07

IIS を使ってのインプロセス ホスティング モデル(この記事の下の方の図参照)でホストされる ASP.NET Core Web アプリは、クライアントによる要求の中断を検出してサーバー側の処理をキャンセルすることができます。(.NET Framework 版の ASP.NET の場合は別の記事「要求の中断による処理のキャンセル (MVC5)」を見てください)

要求の中断

クライアントによる要求の中断とは、上の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

処理のキャンセルには HttpContext.RequestAborted プロパティで取得できる CancellationToken を利用します。クライアントが上に書いた要求の中断操作を行うと、取得した CancellationToken は操作を取り消す通知を配信します。

キャンセル処理は基本的に先の記事「非同期タスクのキャンセル」に書いたことと同様で、以下のようになると思います。

  1. HttpContext.RequestAborted プロパティで CancellationToken を取得しキャンセルをリッスンするタスクに渡す。(CancellationToken の取得先の CancellationTokenSource の初期化等は ASP.NET Core フレームワークがやってくれるようです)  
  2. タスクにはキャンセルをリッスンして適切に処置を行うコードを実装しておく。
  3. クライアントによる要求の中断が検出されると、フレームワークは CancellationTokenSource.Cancel メソッドを呼び出し、CancellationToken を通じてリッスンしているタスクにキャンセルを通知する。
  4. キャンセル通知を受けたタスクは、あらかじめ実装されているコードに従ってキャンセル処置を行う。  

CancellationToken を渡す方法ですが、ネットで見つけた記事 Handling aborted requests in ASP.NET Core に書いてある通り、渡し先のタスクが MVC や Web API のアクションメソッドであれば引数に CancellationToken を追加しておけば、それに HttpContext.RequestAborted から取得した CancellationToken をモデルバインドしてくれます。

検証は Visual Studio 2019 を使って、以下のコードを [デバッグ(D)] ⇒ [デバッグの開始(S)] で IIS 10 Express のインプロセスホスティングで実行して行いました。検証に使用したブラウザは Edge v91.0.864.67, Chrome v91.0.4472.124, Firefox v89.0.2, IE11, Opera v77.0.4054.203 で、いずれもこの記事を書いた時点での最新版です。

View

@{
    ViewData["Title"] = "Cancel";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Cancel</h1>

<input type="button" id="ajaxRequest" value="Ajax Request" />
<br />
<input type="button" id="abortButton" value="Abort" />
<br />
<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        var xhr;

        $(function () {
            $('#ajaxRequest').on('click', function (e) {
                xhr = $.ajax({
                    url: '/home/cancel',
                    method: 'get',
                }).done(function (response) {
                    $("#result").empty;
                    $("#result").text(response);
                }).fail(function (jqXHR, textStatus, errorThrown) {
                    $("#result").empty;
                    $("#result").text('textStatus: ' + textStatus +
                        ', errorThrown: ' + errorThrown);
                });
            });

            $('#abortButton').on('click', function (e) {
                if (xhr) {
                    xhr.abort();
                }
            });
        });
        //]]>
    </script>
}

Controller / Action Method

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MvcCore5App4.Models;
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using System.Threading;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Identity;
using MvcCore5App4.Data;

namespace MvcCore5App4.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        // ・・・中略・・・

        public async Task<IActionResult> Cancel(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return View();
        }
    }
}

上のコードの Task.Delay(5000, token) では渡した token を Deley メソッドの中で継続的に観察しているようで、このメソッドが実行が開始された後でもそれから 5 秒以内ならブラウザで要求を中断すると TaskCanceledException がスローされて処理が終わります。

下の Visual Studio 2022 の「出力 (Output)」画像を見てください。Visual Studio 2022 でデバッグ実行すると上のコードの _logger.LogInformation メソッド出力が表示されます。

Visual Studio 2022 の「出力 (Output)」

赤文字の Start - Complete はブラウザからアクションメソッドを呼び出してから何もしないで 5 秒待って処理を完了させた結果、Start again - Cancelled はアクションメソッドを呼び出して 5 秒以内にブラウザで要求を中断した結果です。

Entity Framework を使ってデータベースにアクセスして処置を行うときに使う ToListAsync とか SaveChangesAsync などや、HttpClient の SendAsync とか PostAsync なども同様かというのが問題と思いますが、詳しくは調べてなくて分かりません。

ToListAsync(token) メソッドで少し調べてみた限りでは、ToListAsync(token) の次の行で CancellationTokenSource.Cancel() としても完了してしまいました。Task.Delay(5000, token) と違って即終わってしまうので間に合わないのか、走り出したらキャンセルは効かないということなのかは分かりません。

ホスティングモデルによる違いですが、Visual Studio 2019 で IIS 10 Express を使ってのアウトプロセス ホスティング モデルでは要求の中断による処理のキャンセルはできませんでした。

Microsoft のドキュメント「インプロセスおよびアウトプロセス ホスティングの相違点」にインプロセス ホスティングでは "クライアントの切断が検出されます。 クライアントが切断されると、HttpContext.RequestAborted キャンセル トークンが取り消されます" と書いてあります。裏を返すとアウトプロセスホスティングではダメと言っているようです。

構成の違いは以下の図(Microsoft のドキュメントから借用)の通りですが、IIS では切断は検出されるものの Kestrel との間は HTTP 通信なので Kestrel に切断を伝えるすべがないということではないかと思います。ちなみに IIS をリバースプロキシとして使わず Kestrel をエッジサーバーとした場合はインプロセスホスティングと同様に切断は検出されキャンセルも効きます。

インプロセス ホスティング

インプロセス ホスティング モデル

アウトプロセス ホスティング

アウトプロセス ホスティング モデル

Linux 系の OS の場合は Nginx とか Apache をリバースプロキシに使って Kestrel で ASP.NET Core アプリをホストする構成(IIS を使ってのアウトプロセスホスティングと同じ)になるので、この記事に書いてある手段ではキャンセルはできないということになります。

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  October 2024  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar