WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

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

by WebSurfer 2021年7月13日 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

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

by WebSurfer 2021年7月11日 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 で、いずれもこの記事を書いた時点での最新版です。

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 がスローされて処理が終わります。

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 をエッジサーバーとした場合はインプロセスホスティングと同様に切断は検出されキャンセルも効きます。

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

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

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

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

IIS を使える環境で Kestrel をエッジに使うことはなさそうですし、Linux 系の OS の場合は Nginx とか Apache をリバースプロキシに使って、Kestrel で ASP.NET Core アプリをホストする、即ち IIS を使ってのアウトプロセスホスティングと同じ構成になるので、結局は IIS を使っての インプロセスホスティングモデルでないとキャンセルはできないということではないかと思います。

Tags: , , , ,

CORE

ASP.NET Core アプリの Web サーバー

by WebSurfer 2020年6月29日 15:43

ASP.NET Core 3.1 の Web アプリをホストするのに利用できる Web サーバーは何で、どのような構成になるかということを調べたので、備忘録として書いておきます。

(1) 開発環境

ASP.NET Core 3.1 の Web アプリを Visual Studio Community 2019 のテンプレートを利用して作成し、ツールバーの[デバッグ(D)]⇒[デバッグの開始(S)](または[デバッグなしで開始(H)]) で Visual Studio からアプリを実行すると IIS Express が起動されますので、デフォルトでは IIS Express が使われているのは間違いなさそうです。(注: 2021/11/9 にリリースされた Visual Studio 2022 .NET 6.0 プロジェクトではデフォルトでは Kestrel に変わっているようです)

IIS Express

Microsoft のドキュメント「ASP.NET Core での Web サーバーの実装」によると、IIS または IIS Express を使用するとインプロセス ホスティング モデルまたはアウトプロセス ホスティング モデルのどちらかで実行されるそうです。(下図参照・・・Microsoft のドキュメントから借用しました)

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

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

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

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

Visual Studio 2019 から実行した場合どちらで動いているかですがデフォルトでは「インプロセス ホスティング モデル」と思われます。

そのものズバリを書いた Microsoft の文書は見つからなかったので、関係する資料や Visual Studio のテンプレートで作ったアプリの内容を調べての想像が入ってますが、自信度は 90% ぐらいあります。根拠は以下の通りです。

根拠 0 (2021/5/25 追記)

後で気が付いたのですが、下記が根拠としては一番確かなようです。下の「根拠 1」~「根拠 3」はせっかく書いたので残しておきますが、見てもらわわなくても良さそうです。(笑)

Visual Studio 2019 のツールバーにあるドロップダウンはデフォルトでは以下のように IIS Express が選択されています。(注: 2021/11/9 にリリースされた Visual Studio 2022 では、デフォルトではプロジェクト名 MvcCore5App2 が選択されており、そのまま実行するとアプリは Kestrel で実行されます。選択によってどう変わるかの詳細は別の記事「開発環境で Kestrel 利用 (CORE)」を見てください)

ドロップダウンの選択

上の画像のドロップダウンで IIS Express が選択された状態で、Visual Studio 2019 のソリューションエクスプローラーからプロジェクトのプロパティを開き、[デバッグ]タブの Web サーバーの設定で[ホスティングモデル]を見るとデフォルトの選択が「(規定値) インプロセス」になっています。

Web サーバーの設定

というわけで、開発環境で Visual Studio 2019 を使ってデフォルト設定のまま ASP.NET Core アプリを実行すれば「インプロセス ホスティング モデル」で動くということに間違いなさそうです。

また、上の画像のドロップダウンで「アウトプロセス」を選択して実行すると「アウトプロセス ホスティング モデル」で動くのも間違いなさそうです。

根拠 1

Microsoft のドキュメント「IIS を使用した Windows での ASP.NET Core のホスト」には、以下のように、デフォルトでは「インプロセス ホスティング モデル」を使うと思える説���があります。

"既存のアプリではインプロセス ホスティングがオプトインされています。ASP.NET Core Web テンプレートでは、インプロセス ホスティング モデルが使用されます。"

"CreateDefaultBuilder では、UseIIS メソッドを呼び出し、CoreCLR を起動して IIS ワーカー プロセス(w3wp.exe または iisexpress.exe) 内のアプリをホストすることで、IServer インスタンスを追加します。"

"CreateHostBuilder (Program.cs) でホストを構築する場合は、CreateDefaultBuilder を呼び出して IIS 統合を有効にします。"

実際にテンプレートを使って作成したアプリの Program.cs は以下のようになっています。

namespace RazorApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

根拠 2

さらに、開発環境で IIS Express が使用する applicationHost.config に以下の設定があります。(場所に注意。IIS 用とは違います。詳しくは先の記事「ApplicationHost.config の場所」を見てください)

<system.webServer>
  <!-- 中略 -->
  <handlers>
    <add name="aspNetCore" path="*" verb="*"
      modules="AspNetCoreModuleV2"
      resourceType="Unspecified" />
  </handlers>
  <aspNetCore processPath="%LAUNCHER_PATH%"
    arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false"
    hostingModel="InProcess" startupTimeLimit="3600"
    requestTimeout="23:00:00" />
  <!-- 中略 -->
</system.webServer>

aspNetCore という名前の HTTP ハンドラが追加され、全ての要求に AspNetCoreModuleV2 ハンドラを使うよう設定されています。AspNetCoreModuleV2 というのは「ASP.NET Core モジュール」らしいです。

また、aspNetCore 要素に hostingModel="InProcess" とあり、「インプロセス ホスティング モデル」が指定されています。

ちなみに上の設定は IIS 用の applicationHost.config には含まれません。

根拠 3

Microsoft のドキュメント「ASP.NET Core モジュール」によると、"アウトプロセス ホスティング用にアプリを構成するには、プロジェクトファイル ( .csproj) で、<AspNetCoreHostingModel> プロパティの値を OutOfProcess に設定します" とのことです。

テンプレートで作ったアプリのプロジェクトファイルにはそのような設定はなく、デフォルトは InPorcess とのことなので、デフォルトでは「インプロセス ホスティング モデル」になるはずです。


ただし、別の Microsoft のドキュメント「ASP.NET Core への Kestrel Web サーバーの実装」には以下の記述があるのが気になります。

"ASP.NET Core プロジェクト テンプレートは既定では Kestrel を使用します。 Program.cs では、ConfigureWebHostDefaults メソッドにより UseKestrel が呼び出されます。"

これが自信度が 90% にとどまっている理由です。(笑)

上に述べた「ASP.NET Core への Kestrel Web サーバーの実装」の記述の他にも分からない点がいろいろあります。それらは以下の通りですが今後の調査課題ということで・・・

  • ASP.NET Core モジュールというのは、チュートリアル「IIS に ASP.NET Core アプリを発行する」のリンク先「現在の .NET Core ホスティング バンドルのインストーラー (直接ダウンロード)」からダウンロードされる dotnet-hosting-3.1.x-win.exe に含まれる Microsoft .NET Core 3.1.x - Windows Server Hosting のことだと思われる。でも、それをインストールする前から Visual Studio からアプリを実行できた。Visual Studio 2019 をインストールした時点で含まれていた?
  • IIS Express と IISHttpServer の間の設定は何もしてないがそれで動くのは何故? Microsoft のドキュメント「IIS を使用した Windows での ASP.NET Core のホスト」によると、ASP.NET Core モジュールがアプリの初期化を実行(Loads the CoreCLR と Calls Program.Main)すると書いてある。それだけで十分なのか?
  • そもそも、デフォルトで IIS Express を使うように設定される理由は何?
  • 開発環境で IIS Express リバースプロキシとして使わないで Kestrel に直接アクセスするように設定することはできるか?(2020/9/28 追記: できるようです。詳しくは別の記事「開発環境で Kestrel 利用」を見てください)

(2) 運用環境

運用環境での Web serverについては、Microsoft のドキュメント「ASP.NET Core での Web サーバーの実装」がまとまっていて概要を理解するのに分かりやすいと思いました。

Linux 系の OS の場合は、上で紹介したドキュメントによると、要するに Nginx とか Apache をリバースプロキシに使って、Kestrel で ASP.NET Core アプリをホストするということのようです。Linux 系の OS は自分は触ったこともないので、残念ながらそれ以上詳しい話はできないです。

Windows OS の場合は、ASP.NET Core に付属している以下のサーバーを利用できるそうです。

  • Kestrel
  • IISHttpServer
  • HTTP.sys (旧称 WebListener)

運用環境で IIS によってホストする場合の説明は、Microsoft のドキュメント「IIS を使用した Windows での ASP.NET Core のホスト」が詳しいので、それを見てください。

上にも書きましたが、「インプロセス ホスティング」と「アウトプロセス ホスティング」という 2 つのモデルがあって、前者には IISHttpServer が、後者には Kestrel が使用されるそうです。

他に、HTTP.sys(IIS の HTTP Protocol Stack HTTP.sys とは違うもののようです)を使うというオプションもあるそうです。自分は勉強不足で多くは語れませんので、Microsoft のドキュメント「ASP.NET Core での HTTP.sys Web サーバーの実装」を見てください。

OS が Windows 10 Pro 64-bit の PC のローカル IIS に ASP.NET Core 3.1 アプリを発行するのは実際にやってみました。

その手順は Microsoft のチュートリアル「IIS に ASP.NET Core アプリを発行する」の通りですが、概略を以下に書いておきます。結果「インプロセス ホスティング モデル」になっていると思います。

  1. C:\WebSites2019 というフォルダ下に AspNetCoreWebSite という名前のフォルダを作成。
  2. IIS Manager を起動してそのフォルダをサイトに設定。
  3. サイトバインド設定は、種類: http, IP アドレス: 未使用の IP アドレスすべて, ポート: 80, ホスト名: www.aspnetcorewebsite.com とした。
  4. hosts ファイルに 127.0.0.1 www.aspnetcorewebsite.com と設定。
  5. VS2019 のテンプレートで自動生成される Startup.cs に HTTPS 通信を実行させるミドルウェアを適用するコード app.UseHsts(); と app.UseHttpsRedirection(); が含まれている。自分の PC のローカル IIS では HTTPS で通信できないので、そのあたりの設定を変更する必要があるかと思ったが、実際試すとそうでもなかった。 Microsoft のドキュメント Enforce HTTPS in ASP.NET Core にポートを設定しないとリダイレクトされないというようなことが書いてある。そのため?
  6. Microsoft のドキュメント「IIS を使用した Windows での ASP.NET Core のホスト」に "IIS サーバーのオプションを構成するには、IISServerOptions 用のサービス構成を ConfigureServices に含めます" と書いてあるが、テンプレートで作ったアプリにはその設定はない。設定しなければデフォルトが使われるので問題なさそう。
  7. チュートリアルの「アプリを発行および配置する」セクションに従って、上のステップで作成した C:\WebSites2019\AspNetCoreWebSite フォルダにプロジェクトを発行。IIS Manager で見た結果は下の画像のようになる。

IIS に発行された Razor ページ

  1. 以上の設定でブラウザから http://www.aspnetcorewebsite.com/ を要求すると Razor ページが表示される。 ワーカープロセスに C:\WebSites2019\AspNetCoreWebSite フォルダへの権限は何も与える必要はなかった。
  2. web.config は元のプロジェクトには含まれないが、自動生成されて配置される。aspNetCore という名前の HTTP ハンドラが追加され、全ての要求に AspNetCoreModuleV2 ハンドラを使うよう設定されている。AspNetCoreModuleV2 というのは「ASP.NET Core モジュール」らしい。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" 
          modules="AspNetCoreModuleV2" 
          resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" 
        arguments=".\RazorApp.dll" stdoutLogEnabled="false" 
        stdoutLogFile=".\logs\stdout" 
        hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
<!--ProjectGuid: 8db7ea97-35b2-4168-81d5-7c68974ce862-->

Tags: , , ,

CORE

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar