ConsoleAppFrameworkでGraceful Shutdown
はじめに
Cysharp(サイゲームスのグループ会社Cysharpが管理)のOSSパッケージConsoleAppFrameworkの使い勝手が 良さそうだったので、Asyncなタスクを実行させるコンソールアプリを試作してみました。
基本的な使い方はGitHubのREADMEに書かれているので、本記事では少し詰まったポイント+Graceful Shutdownについて書きます。
ConsoleApp.AsyncRun
タスクを実行させる場合には、ConsoleApp.AsyncRun
を使います。
Graceful Shutdown
ConsoleApp.Run
と書式はほぼ変わりませんが、第二引数のデリゲートの引数にCancellationToken
を追加できます。
これを追加すると、Ctrl+C
などから送信されるSIGINT
等の強制終了シグナルを自動的にフックして、CancellationToken
をキャンセル状態にしてくれます。
下記のように、CancellationToken
を引数に追加することでGraceful Shutdownを実現できます。
await ConsoleApp.RunAsync(args, async (int foo, CancellationToken cancellationToken) =>{ // 10秒待ってHello Worldを表示 await Task.Delay(10000, cancellationToken); Console.WriteLine("Hello World!");});
OperationCanceledException
の例外処理を追加すれば、シグナル経由のシャットダウンであっても、キャンセル時の処理を簡単に実装できるようになります。
await ConsoleApp.RunAsync(args, async (CancellationToken cancellationToken) =>{ try { await Task.Delay(10000, cancellationToken); } catch (OperationCanceledException) { Console.WriteLine("Delayがキャンセルされました"); } Console.WriteLine("Hello World!");});
Functionのデフォルト引数
CancellationToken
を渡しつつ、コマンドライン引数にデフォルト値を提供したい場合は下記のようにdefault
を渡してあげます。そうじゃないと位置引数の関係で文法エラーになってしまうので。
await ConsoleApp.RunAsync(args, async (int foo = 10, int bar = 20, CancellationToken cancellationToken = default) =>{ // 10秒待ってHello Worldを表示 await Task.Delay(10000, cancellationToken); Console.WriteLine("Hello World!");});
他のキャンセルトークンと組み合わせたい場合は?
今回はCtrl+C
等のシグナル以外にも、「Qキーが押されたらメインタスクを終了」という仕様で実装を考えていました。
その場合はCreateLinkedTokenSource
で複数のCancellationToken
を組み合わせることで実現できます。
備忘録として、下記仕様を満たすようなコードを載せておきます。
- 1秒ごとに
count
回、文字列message
を表示し続けるメインタスク Q
キーが押されたらキャンセルCtrl+C
でもキャンセル- どちらのキャンセルでも
Successfully Canceled
というメッセージを表示
internal class Program{ static readonly CancellationTokenSource s_cts = new(); static async Task Main(string[] args) { await ConsoleApp.RunAsync(args, RunnerAsync); }
static async Task RunnerAsync(string message = "Sample Message", int count = 10, CancellationToken cancellationToken = default) { // キャンセルトークンを組み合わせる cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(s_cts.Token, cancellationToken).Token;
var cancelTask = Task.Run(() => { Console.WriteLine("Press Q to cancel."); while (Console.ReadKey().Key != ConsoleKey.Q) ; s_cts.Cancel(); }).WaitAsync(cancellationToken);
var showMessageTask = ShowMessageAsync(message, count, cancellationToken); try { await Task.WhenAll(showMessageTask, cancelTask); } catch (OperationCanceledException) { Console.WriteLine("Successfully Canceled"); } } static async Task ShowMessageAsync(string message, int count, CancellationToken cancellationToken) { // 1秒ごとにメッセージを表示 for (int i = 0; i < count; i++) { Console.WriteLine(message); await Task.Delay(1000, cancellationToken); } s_cts.Cancel(); }}
おわりに
ConsoleAppFrameworkを試すきっかけとなったのは、下記の記事でした。
(記事末尾にConsoleAppFrameworkに関する言及があります)
「コンソールアプリを汎用ホストで作るなら、StopApplication
をどっかから叩いて終了する必要があるよな?」、なんて調べているうちに辿り着きました。
感謝です🙏