雑誌のテーマからプロジェクト実践へ
該当記事に関連するサービス・技術ページ
一見地味に見えるWindows Service in Delphiは、日常的にはバックグラウンドで動作し、ジョブを処理し、ログを書き、データベースやREST-APIにアクセスするだけの存在に見えます。誰かが「サービスの停止」をクリックするか、パッチ適用による再起動がかかると、サービスがきれいに停止しないことがあります。するとサービス管理コンソールは数分間「停止中…」と表示し、サービスはStop Pending状態でハングし、最悪の場合プロセスが強制終了されます。まさにこの場面で、Windows Service in Delphi Graceful Shutdownを明確なアーキテクチャの課題として扱う価値があります:明確なシャットダウン・シグナル、定義されたタイムアウト、それに確実に応答するスレッドです。
本稿はフレームワークの内部実装論ではなく、実運用で使えるパターンに焦点を当てます:TEventを停止シグナルとして使う(System.SyncObjsに近いカーネル同期オブジェクト)ことと、Windows Service Control Manager(SCM、つまりサービスを起動/停止するWindowsコンポーネント)と自身のワーカースレッドの双方を考慮したStop-Timeout戦略を組み合わせます。併せて典型的な周辺ケース、デバッグ手法、追加ロジックを導入する価値がある場面についても扱います。
Windows Service in Delphi Graceful Shutdownの実践
最も多い原因は単純です:サービスには少なくとも1つ、ブロッキング操作に入っており中断経路を持たないスレッドがあります。典型例:
- Sleepを伴うポーリングループ: 「while not Terminated do Sleep(1000)」。停止時にシグナルは届くものの、スレッドは最大で1秒(あるいは30秒…)後にしか反応しません。
- ブロッキングI/O:データベース呼び出し、HTTPリクエスト、名前付きパイプ、ファイルシステムの待機など、停止シグナルを考慮せずに「ただ待つ」処理全般。
- Wakeupの無いキュー消費者:ワーカーがキューを待っているが、停止時に起こされずに抜けられない。
- ロック順序/デッドロック:停止処理でクリーンアップを行う際、他のスレッドがまだロックを保持している。停止経路でのみ発生しやすく、通常動作時と順序が異なるのが原因です。
Windows SCMは、サービスが停止コマンドに迅速に反応し、状態を継続的に報告すること(SetServiceStatusを通じて)を期待します;Delphiはこれをサービスコンポーネントにカプセル化します。停止イベントは受け取るがスレッドをきれいにシャットダウンしないと、プロセスは生き続け、Windowsは「時間がかかり過ぎる」と判断します。結果として強制終了されるか、不明瞭な中間状態でサービスが残ることになります。
Grundprinzip: Ein Stop-Signal, das jeder Worker versteht
グレースフルシャットダウンは、次の条件を満たすシグナルがある場合にのみ機能します:
- すべての関連するスレッドから監視できること、
- ブロッキングの待機状態からでも効果を発揮する、
- Stopパスでは決定論的である(「いつか出てくるかもしれない」という期待ではない)、
- 明確なタイムアウト戦略を持っている。
In Delphi ist TEvent dafür ein sehr brauchbares Werkzeug: ein Event-Objekt, das intern über Windows-Handles umgesetzt ist (vergleichbar mit CreateEvent/SetEvent). Du kannst es als „Stop requested“-Signal verwenden. Jeder Worker wartet dann nicht einfach blind, sondern wartet „auf Arbeit oder auf Stop“.
TEvent richtig wählen: ManualReset vs. AutoReset
Bei Stop-Signalen willst du üblicherweise Manual Reset (manuell zurücksetzbar): Einmal gesetzt, bleibt das Event „signaled“, bis du es zurücksetzt. Damit ist sichergestellt, dass jeder Thread, der später in eine Wartephase kommt, das Stop-Signal trotzdem erkennt. Auto Reset wäre hier riskant, weil es das Signal nach einem wartenden Thread automatisch zurücksetzt und andere Threads das Stop-Signal verpassen könnten.
Delphi-Service-Lebenszyklus: Wo Stop wirklich ankommt
Ein Delphi-Windows- und Linux-Services basiert typischerweise auf TService (VCL/RTL). Der SCM schickt Kommandos (Start, Stop, Pause, Continue). Delphi ruft dann entsprechende Events/Methoden auf (je nach Template z. B. OnStart, OnStop, OnExecute).
Wichtig für die Architektur:
- OnStop ist kein Ort für langes Warten ohne Status-Updates. Es ist der Ort, an dem du den Shutdown anstößt und dann kontrolliert wartest – mit Timeout.
- OnExecute ist oft eine Schleife. Wenn du dort „endlos“ arbeitest, muss die Schleife auf ein Stop-Signal reagieren.
- Worker-Threads (TThread oder Thread-Pools) müssen auf dasselbe Stop-Signal reagieren, sonst ist der Service logisch gestoppt, aber physisch noch nicht fertig.
Sauberes Muster: Stop-Event + Join der Worker + harter Fallback
Das praxistaugliche Muster besteht aus vier Schritten:
- Stop anfordern: Stop-Event setzen, keine neuen Jobs mehr annehmen.
- Wakeups auslösen: Wenn Worker auf Queues oder Sleeps warten, müssen sie „aufwachen“ können (z. B. über Event/Queue-Signal).
- Geordnet beenden: Worker beenden ihre Loops, schließen Ressourcen (DB-Verbindungen, Dateien, Handles) und melden „fertig“.
- Timeout und Fallback: Wenn nicht alles rechtzeitig endet, musst du eine Entscheidung treffen: weiter warten (mit Status-Update) oder kontrolliert abbrechen/hart beenden (je nach Risiko).
Der Kern ist: kein Thread darf ausschließlich auf Zeit warten (Sleep) oder ausschließlich auf I/O blockieren, ohne parallel ein Stop-Signal zu berücksichtigen. Stattdessen arbeitest du mit Wartefunktionen, die mehrere Signale berücksichtigen (z. B. „Stop-Event oder Work-Event“), oder du kapselst I/O in Timeouts plus Stop-Checks.
Stop-Timeout richtig denken: SCM-Timeout vs. eigener Shutdown-Timeout
Hier passieren in Projekten die meisten Missverständnisse. Es gibt zwei verschiedene Timeout-Ebenen:
実務的にはつまり: サービスは速やかに新しい作業単位を開始しない状態に移行し、その後は稼働中の作業が終了するのを待つだけにするべきです – ただし無限に待ってはいけません。待機フェーズは小さな間隔で実行し、応答や必要に応じたログ記録ができるようにするべきです。
停止はどのくらい許容されるか?
常に当てはまる魔法の数値はありません。多くのビジネスサービスでは目標レンジとして5–30秒が現実的です: 「進行中」のデータに十分な時間を与えつつ、パッチ窓に対して短く保てます。定期的にそれ以上の時間が必要な場合、単位が大きすぎて一度に処理しているか、外部依存(DB/HTTP)にタイムアウトが設定されていないことが多いサインです。
TEventを用いた実装: 運用で安定する構成
Delphi-サービスにおける実績ある構成は次のようになります(フレームワークの詳細は省きます):
- Stop-Event (TEvent, Manual Reset), 停止時にセットされます。
- Worker-Threads を1つ以上用意し、メインループで定期的にStopをチェックします。
- 任意で作業を通知するWork-Eventまたはキュー。Workerは「WorkまたはStop」を待機します。
- Shutdown-Phase: Workerをjoin(終了を待つ)しますが、タイムアウトを設けます。
重要なのは、TThread、omnithreadlibrary、あるいは独自のプールのどれを使うかではなく、Workerが「盲目的に」動作しないことです。Workerループは構造的に次のようであるべきです: イベントを待つ → 小さなチャンクで作業 → チャンク間にStopを確認 → リソースを確実に解放.
落とし穴: Terminateだけでは不十分
多くのDelphi-スレッドは Terminate で「中断」されます。しかしそれは単なるフラグに過ぎません。スレッドがブロッキングAPIの中にいる場合は何も起きません。したがって独自のStop-Eventが有用で、待機呼び出しに組み込んで意図的にウェイクアップを発生させることができます。
落とし穴: サービスコンテキストでの FreeOnTerminate
サービスではしばしば FreeOnTerminate := True が見られます。これでも動作しますが、シャットダウンの制御が難しくなります。というのも、スレッド終了を待機したり障害状態を記録するための明確な参照が失われることが多いためです。制御された停止ロジックでは、スレッドを明示的に所有し、シャットダウン時に決定論的に待機して解放する方が安定します。
ブロッキング操作: 停止可能にする方法
厄介なのはイベント自体ではなく、サービスがブロックされる箇所です。代表的な3つのパターン:
1) Sleep/ポーリングの置換: Stopイベントによる待機
定期的に処理する(「10秒ごとにチェック」など)場合、Sleep(10000) を使うのではなく、タイムアウト付きのイベント待ちにしてください。そうすれば Stopイベント が待機を即座に解除できます。これにより停止のレイテンシが低減し、「サービスが応答しない」という印象を防げます。
2) キューのコンシューマ: Workイベント + Stopイベントを組み合わせる
Producer/Consumerアーキテクチャ(例: ジョブがキューに投入される)を採用している場合、コンシューマを起こすためのシグナルが必要です。多くの場合、それは別の TEvent(Work available)です。コンシューマは 2つのハンドル、すなわち「Work」か「Stop」を待ちます。停止時には Stopイベント をセットし、必要なら Workイベント もセットして、すべてのコンシューマが確実に待機から抜けられるようにします。
3) 外部呼び出し(DB/HTTP): タイムアウトと中断パス
データベースアクセスやHTTPコールは、サービスがクリーンに停止できるか否かを決定します。運用上の原則は: タイムアウトなしのコールは許容しない、です。タイムアウトは贅沢ではなく、制御可能性の前提条件です。さらに、再試行/バックオフの各フェーズの合間に常に停止要求を確認するべきです。さもないと典型的な状況になります: 「サービスが停止しない — というのも、現在10回の再試行で Sleep を行っているから」。
一部のライブラリでは中断を明示的にトリガできる(例: Query-Abbruch)ことがあります。そうでない場合は、少なくともシャットダウンのタイムアウトを超えないように、タイムアウトを十分短く設定する必要があります。
Stop Pending を正しく扱う: ステータス、ログ、期待値管理
サービスが停止する際、運用上重要なのはどこでハングしているのかを把握することです。これには次の2つが必要です:
- ログマーカー(Stopパス上): 「停止要求受信」、「新しいジョブなし」、「ワーカー待機中」、「Worker X 終了」、「シャットダウン完了」。
- 計測可能な時間: 停止にどれくらい時間がかかっているか?どのフェーズが時間を消費しているか?ここではしばしば GetTickCount64 や TStopwatch のような単調増加の時間計測で十分です(単調性 = システム時刻の変更によって歪まないこと)。
停止パスにログエントリを1行だけ「停止中…」と書くと、現場でのデバッグは推測ゲームになります。サービス運用では、ログが対話なしに得られる唯一の手がかりであることが多いです。
サービスで本当に役立つログは何か?
- サービスPID、起動時刻、Version/Build(過剰なオーバーヘッドなし)。
- アクティブなワーカー数、進行中のジョブ数(in-flight Jobs)。
- アクティブな外部依存:“DB-Call 実行中“、“HTTP リクエスト実行中“、“ファイルフラッシュ実行中“(詳細を逐一出すのではなく集約して)。
- 停止タイムアウト到達:どのワーカーがまだ終了していないか。
現場でのデバッグ:推測ではなく再現可能にする
停止問題は本番環境でしか発生しないことがよくあります:負荷が違う、レイテンシが違う、権限が違う、パッチ適用の時間帯が違う。実務で効果のあるいくつかの手段:
制御下でサービスをテストする
- アイドル時ではなく、処理中に停止する。
- 外部障害が発生している状態で停止する:DBが一時的に到達不能、HTTPエンドポイントが遅い、ファイル共有が切断される、など。
- 起動直後に停止する(レースコンディション:ワーカーがまだ初期化中)。
イベントビューアーとサービスコントロールマネージャーのシグナル
Windows はサービスイベントを書きますが、それは大まかな情報になりがちです。より良いのは、サービス自身がログファイルや Windows のイベントログに書くことです。重要なのは:停止パスでもログ出力が機能すること。シャットダウン時にロガーを早すぎて解放したり、フラッシュがブロックされると、まさに決定的な痕跡を失います。
ハングしているスレッドを可視化する
「停止タイムアウト」を繰り返し見る場合は、スレッド状態の確認が有効です(テスト環境でデバッガーや Procdump 等を使用)。多くの場合、信号が来ないハンドルを待っている待機状態のスレッドや、タイムアウトのないネットワーク呼び出し中のスレッドが見つかります。対処はたいてい「さらに Sleep を入れる」ことではなく、きちんとした中断経路を設けることです。
いつその手間をかけるべきか?
タイマーだけを持ち外部依存がない最小構成のサービスは、単純に停止できることがあります。しかし、次のいずれかに該当するなら、きちんとしたグレースフルシャットダウンを実装する価値はほぼ常にあります:
- サービスが副作用のあるジョブを処理する(ファイル書き込み、DBトランザクション、API呼び出し)。
- 複数のスレッドまたはプールがある。
- サービスがネットワークリソース(DB、REST、メッセージブローカー、ファイル共有等)に依存している。
- 運用側が計画的なメンテナンス窓(再起動、更新、フェイルオーバー)を求める。
ここで得られる価値は「見た目の優雅さ」ではなく運用の確実性です:強制的なプロセス終了の減少、不整合な中間状態の減少、手動介入の削減。
実務上の落とし穴:シャットダウンでよく起こる問題
1) 停止が設定されても新しいジョブが入ってくる
ソケット、ファイルトリガー、タイマーなどで着信作業を受け付けている場合、停止パスではまず新規作業の受け付けを停止する必要があります:リスナーを閉じる、タイマーを無効にする、スケジューラを停止する。そうしないと、常に新しいジョブが開始され続けて終わりを追いかけることになります。
2) クリーンアップがブロックされる(Flush、Close、Finalize)
「最後に全部フラッシュしてしまおう」は、ターゲット(ネットワークドライブ、リモートログ、DB)がたまたまハングしているときにサービスコンテキストでは危険です。したがって、クリーンアップは行うが時間を限定するべきです。場合によっては、完全に停止をブロックするよりも、どのデータをメモリ上で失うかを選択する判断が必要です。
3) ロックと順序
Stop時には、ワーカーと同じデータ構造(キュー、キャッシュ、ステート)にアクセスすることが多い。Stopスレッドがロックを保持したままワーカーの終了を待ち、同時にワーカーが同じロックを必要とする場合、Stop-Deadlockが発生する。対策:ロック保持時間を短くする、Stop-Pfadで「ロック下で待機」しない、明確な順序を定義する。
4) 二重Stopでの並行性
実務ではStopが複数回トリガされることがある(例:Stop + Shutdown、あるいはStopが再度発生)。あなたのStop-Pfadは冪等であるべきだ:Stop-Eventを設定するのは問題ないが、二重のJoin/Freeロジックは適切に保護する(例えばアトミックフラグで)。
運用視点:管理者とITリードがサービスに期待すること
運用・管理において最終的に重要なのは、コードがどれだけ「美しい」かではなく、サービスが以下を満たすかどうかだ:
- bei Stop 確実に終了する(予測可能でハングしない)、
- bei Stop 不整合なデータを生成しない(例:不完全なファイル、オープントランザクション)、
- im Fehlerfall 有用なログを提供する、
- bei Wartungsfenstern und Deployments 予測可能である。
これが、Stop-Timeoutの問題が単なる「開発者の問題」ではない理由だ:パッチサイクル、リカバリ時間、そして自動化されたデプロイがそもそも可能かどうかに影響する。
堅牢なシャットダウン設計のための具体的な指針
この課題を実務的に標準化したい場合、次の指針が有効だ:
- グローバルなStop-Event、Manual Reset、サービスライフサイクルの早期に作成し、遅めに解放する。
- WorkerループでのSleep禁止、Stopに対応した代替(タイムアウト付きのWait)を用意する。
- 外部呼び出しはすべてタイムアウトを設定(DB、HTTP、ファイル共有)。タイムアウトはShutdown-Timeoutに収まるように設定する。
- Stop-Timeoutを設定可能に(例:INI/Registry)、運用側が再コンパイルせずに対応できるようにする。
- 段階モデル:まずgraceful(実行中のジョブを完了させる)、次にオプションで「soft abort」(新しい処理を開始しない)、最後に最終手段として強制終了(harter Exit)。
- 適切なStopログ(フェーズと経過時間の計測を含む)。
Fazit: TEvent + Stop-Timeout ist kein Luxus, sondern Steuerbarkeit
ハングするStopは単一のバグというよりアーキテクチャ上の抜け穴であることが多い:処理がスレッドやブロッキング呼び出しで実行され、共通のStopシグナルを認識していない。明確なStop-Event(TEvent, Manual Reset)、Sleepの代わりにStop対応のWait、外部依存への一貫したタイムアウト、定義されたShutdown-Timeoutを組み合わせることで、日常運用で予測可能なサービスを得られる。
この設計は、サービスがメンテナンスウィンドウ、自動化されたデプロイ、または副作用の重大な本番環境で稼働している場合に特に有益だ。その場合、「Graceful Shutdown」は単なる見栄えではなく、安定運用と次回再起動時のエスカレーション軽減のための重要な要素である。
もし貴社のStop-Pfadを一度きちんと設計するか、既存のDelphi-サービスのシャットダウンロジックと運用安全性をレビューしたい場合、技術的なスパーリングコールが明確な対策に至る最も速い方法であることが多い:ご連絡ください。
このテーマに関しては、Delphi Windows Service と Tevent Delphi も重要だ。本稿はこれらの側面を分かりやすく整理し、日常で重視すべき点を示している。
次のステップ
テーマが実際のプロジェクトになる場合、アーキテクチャ、既存資産、運用は早期に一体として検討する必要があります。
私たちは単なる個別の問い合わせへの対応にとどまらず、ソースの断片やレガシー課題、ポータルの構想が堅牢な企業向けプロジェクトへと成長する段階まで支援します。
- 既存環境、目標像、技術的リスクを一体として評価します。
- REST、データアクセス、ポータル、およびロールアウトは後回しにされません。
- 早い段階で、どの選択肢が経済的かつ運用上実行可能であるかを見極められます。