サーバースクリプトの処理を非同期に実行する
通常・拡張サーバースクリプトで行っている集計や外部連携を、画面の応答から切り離したいときは、処理要求をテーブルへ保存し、バックグラウンドサーバースクリプトで後から実行する構成にできます。本体の改修は不要です。
通常・拡張サーバースクリプトそのものを非同期化する設定はありません。「作成後」「更新後」も同期実行です。受付処理だけをその条件に残し、時間のかかる処理を別のスクリプトへ移します。
同期実行になる理由
通常と拡張のスクリプトは、同じ実行経路で結合され、ScriptEngine.Execute で実行されます。呼び出し元へ戻る前に V8 エンジンが破棄され、実行結果の値が反映されます(ServerScriptUtilities.cs#L1167-L1286)。拡張スクリプトも ServerScript に変換されるため、別の非同期実行経路はありません(SiteSettings.cs#L6204-L6244)。
同期実行の入口は ScriptEngine.Execute です。スクリプトを起動してすぐ戻る API ではなく、V8 の実行をその場で呼び出しています。
public void Execute(string code, bool debug)
{
v8ScriptEngine?.Execute(
new DocumentInfo()
{
Flags = debug ? DocumentFlags.AwaitDebuggerAndPause : DocumentFlags.None
},
code);
}通常・拡張の違いは実行対象の集め方にあり、実行するエンジンは共通です。呼び出し元の ServerScriptUtilities は using の範囲内でこの実行を行い、範囲を抜けたあとでモデルの値を取り込みます。スクリプトに async と書くだけでは、モデルの反映を待つこの HTTP リクエストの経路は変わりません。
| 書き方・設定 | 実際の扱い |
|---|---|
AfterCreate / AfterUpdate | レコードを保存した後、同じリクエスト内で実行する。スクリプトの終了を待つ |
async 関数 / Promise | 実行側には返された Promise を待つ実装がない。後続処理の完了を保証する方法には使えない |
httpClient.Post() | 内部で SendAsync(...).Result を待つため、呼び出しただけでは処理を切り離せない |
Background: true | 通常スクリプトの実行を別スレッドへ移すスイッチではない。拡張サーバースクリプトの設定項目にもない |
根拠は ScriptEngine.cs#L20-L69、ServerScriptModelHttpClient.cs#L43-L79、ExtendedServerScript.cs#L1-L29 です。保存後の実行位置は 操作別の実行順 で確認できます。
httpClient の内部では非同期 API を使っていますが、呼び出し結果を待っています。
var request = CreateHttpRequest(method, content);
HttpResponseMessage response;
try
{
response = _httpClient.SendAsync(request, cts.Token).Result;
}SendAsync の後の .Result が、応答を待ってから処理を続ける箇所です。レスポンス本文の取得も ReadAsStringAsync().Result です。HTTP 通信の内部実装が非同期であることと、保存要求が外部連携を待たずに完了することは別です。後者のためには、処理要求を保存して、別の実行契機で読む設計にします。
公式の拡張サーバスクリプトの説明は登録方法を、バックグラウンドサーバスクリプトの説明は定期実行の設定を扱います。このページでは、両者をつなぐ処理要求テーブルと失敗時の扱いを説明します。
処理の流れ
図を読み込み中…
バックグラウンド実行は Quartz のジョブから専用の Context でスクリプトを呼びます。内部の JavaScript は同期実行でも、レコード保存の HTTP リクエストとは別に動きます(BackgroundServerScriptJob.cs#L20-L91)。
ワーカーは、保存中のレコードのモデルを引き継いで実行する仕組みではありません。バックグラウンドのジョブが、専用の条件で同じ実行ユーティリティを呼びます。
var ServerScriptModelRow = ServerScriptUtilities.Execute(
context: sqlContext,
ss: ss,
gridData: null,
itemModel: null,
view: null,
scripts: scripts.ToArray(),
condition: ServerScriptModel.ServerScriptConditions.BackgroundServerScript,
debug: paramScripts != null && targetScript.Debug);itemModel と view は null、実行条件は BackgroundServerScript です。そのため業務処理に必要な受付時点の値は、保存処理の model を後から使う前提にせず、要求レコードの JSON に保存します。このサンプルの title はワーカー実行時の最新値ではなく、受付時に保存した値です。
前提とテーブルの準備
このサンプルは、ワーカーを実行するアプリケーションを 1 台に限定し、処理するバックグラウンドスクリプトも 1 つにした構成用です。クラスタや複数ワーカーでの排他取得は実装していません。
- 処理の起点になるテーブルを用意します。サンプルのサイト ID は
10001です。 - 別の記録テーブルを作り、処理要求の保存先にします。サンプルのサイト ID は
12345です。 - 次の項目を処理要求テーブルのエディタと一覧に追加します。
| 項目 | 表示名の例 | 内容 |
|---|---|---|
Title | タイトル | 非同期処理要求 |
ClassA | 処理状態 | Pending / Running / Done / Failed |
DescriptionA | 処理要求 | 受付時のデータを JSON で保存 |
DescriptionB | 処理結果 | 結果または失敗時の案内 |
ClassA は単一選択にし、選択肢は次の 4 行を登録します。内部の値もこの文字列にします。
Pending
Running
Done
Failed受付するユーザーには処理要求テーブルの作成権限、バックグラウンドの実行ユーザーには読み取り・更新権限を付けます。処理要求には元レコードのタイトルが含まれるため、閲覧範囲も元の業務データに合わせます。バックグラウンド処理は受付ユーザーではなく、バックグラウンドスクリプトに指定した実行ユーザーの権限で動きます(BackgroundServerScriptJob.cs#L110-L133、ServerScriptUtilities.cs#L1401-L1435)。
実行ユーザーも、バックグラウンド用の Context で作り直します。
var context = new Context(
tenantId: tenantId,
userId: userId,
deptId: user.DeptId,
request: false,
setAuthenticated: true);
context.SetTenantProperties(force: true);
context.BackgroundServerScript = true;request: false なので、受付時のブラウザの要求を使っていません。要求 JSON の requestedBy は受付者を記録する値であり、ワーカーの権限を受付者へ切り替える値ではありません。ワーカーが対象レコードを読めるか、結果を更新できるかは、バックグラウンドの実行ユーザーの権限で確認します。
通常のサーバースクリプトから受付する
起点テーブルの「テーブルの管理」→「サーバスクリプト」に、実行条件を「作成後」「更新後」としたスクリプトを登録します。本文は次のコードです。sourceSiteId と queueSiteId を実際のサイト ID に変更してください。
(function () {
var sourceSiteId = 10001;
var queueSiteId = 12345;
if (Number(context.SiteId) !== sourceSiteId) return;
var payload = {
sourceSiteId: sourceSiteId,
sourceId: Number(context.Id),
title: String(model.Title),
requestedBy: Number(context.UserId)
};
var created = items.Create(queueSiteId, JSON.stringify({
ApiVersion: 1.1,
Title: "非同期処理要求",
ClassHash: { ClassA: "Pending" },
DescriptionHash: { DescriptionA: JSON.stringify(payload) }
}));
if (!created) {
logs.LogUserError("非同期処理要求の登録に失敗しました。");
context.AddMessage("保存後の処理要求を登録できませんでした。管理者に確認してください。", "alert-error");
return;
}
context.AddMessage("処理要求を受け付けました。結果は処理要求テーブルで確認できます。");
})();items.Create は成功・失敗を bool で返します。作成した ID や業務処理の完了結果を返すメソッドではありません(ServerScriptModelApiItems.cs#L160-L183)。成功した場合のメッセージも「受付」とし、完了したと案内しません。
タイトルは受付時の値を保存しています。実行時に元レコードを読み直す設計へ変える場合は、受付後の更新や削除をどう扱うかも決めてください。
業務レコードの保存と処理要求の登録は別の処理
この例は業務レコードの保存後に items.Create を呼びます。要求の登録に失敗したときに、業務レコードの保存を取り消す仕組みはありません。受付失敗のログを確認し、要求を作り直す手順を用意してください。業務データと要求を必ず同時に確定する仕組みが必要なら、この例だけでは満たせません。
拡張サーバースクリプトから受付する
通常のスクリプトの代わりに、次の JSON を App_Data/Parameters/ExtendedServerScripts/AsyncEnqueue.json に置きます。SiteIdList を起点テーブルの ID に変更してください。
{
"Name": "AsyncEnqueue",
"SiteIdList": [10001],
"Controllers": ["items"],
"Actions": ["create", "update"],
"AfterCreate": true,
"AfterUpdate": true
}受付スクリプト enqueue.js を AsyncEnqueue.json.js に改名して、JSON と同じフォルダへ置きます。配置後にアプリケーションを再起動します。JSON と .json.js の組み合わせの詳細は 拡張サーバースクリプトの適用条件 を参照してください。
Controllers / Actions は小文字です。処理要求テーブルを SiteIdList に含めないことで、要求の登録・状態更新による再帰実行を防ぎます。
受付はどちらか一方に登録する
通常と拡張の両方に登録すると、同じ保存操作から要求が 2 件できます。このサンプルでは保存操作ごとに 1 件を作成し、連続保存・再送の重複は除去しません。
バックグラウンドで実行する
既存の App_Data/Parameters/Script.json の次の項目を有効にし、アプリケーションを再起動します。他の項目は既存の設定を保持してください。
{
"ServerScript": true,
"BackgroundServerScript": true
}実行環境の制限など、追加の有効化条件は バックグラウンドサーバースクリプトの仕組み を確認してください。
特権ユーザーでテナント管理のサーバスクリプトからバックグラウンドスクリプトを作成します。
- 実行ユーザーを指定し、「共有」「無効」はオフにします。
- 次のコードを本文に設定します。
queueSiteIdを実際のサイト ID に変更します。 - スケジュールを追加して保存します。例えば、毎時の
00・05・10…55分に動かすには、毎時のスケジュールを 12 個登録します。画面の「毎時 05 分」は毎時間の 05 分であり、それだけで 5 分ごとにはなりません。
(function () {
var queueSiteId = 12345;
var jobs = items.Get(queueSiteId, JSON.stringify({
ApiVersion: 1.1,
PageSize: 10,
View: {
ColumnFilterHash: { ClassA: JSON.stringify(["Pending"]) },
ColumnSorterHash: { ResultId: "asc" }
}
}));
function saveState(id, state, result) {
if (!items.Update(id, JSON.stringify({
ApiVersion: 1.1,
ClassHash: { ClassA: state },
DescriptionHash: { DescriptionB: result }
}))) {
throw new Error("処理要求の状態を保存できませんでした。");
}
}
function processJob(payload) {
if (typeof payload.title !== "string" || !(payload.sourceId > 0)) {
throw new Error("処理要求の形式が不正です。");
}
// 動作確認用。実際の集計・外部連携処理はここに移します。
return "受付時のタイトル: " + payload.title;
}
for (var i = 0; i < jobs.Length; i++) {
var job = jobs[i];
var id = Number(job.ResultId);
// 開始状態を保存できなければ、業務処理には進みません。
saveState(id, "Running", "");
try {
var result = processJob(JSON.parse(String(job.DescriptionA)));
saveState(id, "Done", result);
} catch (e) {
// 外部サービスの応答・入力値をそのまま公開しない固定メッセージ。
saveState(id, "Failed", "処理に失敗しました。管理者による確認が必要です。");
logs.LogUserError("非同期処理要求 " + id + " が失敗しました。");
}
}
})();このコードの processJob は動作確認用に、受付時のタイトルを結果へ書き込みます。実際に切り離したい集計・外部連携を、この関数に移してください。元スクリプトに残すのは要求の登録までです。
items.Get の戻り値は .NET の配列なので、件数は Length で取得します。PageSize とフィルターで未処理の要求を最大 10 件取得します。次回も Pending だけを取得するため、処理が進めば残りの要求へ進みます(ServerScriptModelApiItems.cs#L20-L31、ResultUtilities.cs#L3158-L3202)。API キーは使わず、スクリプトの実行ユーザーで items を操作します。
応答時間と処理時間は別
画面から待たなくてよくなるのは、バックグラウンドへ移した処理の時間です。要求を DB に登録する時間は画面側でも待ちます。また、実際の開始時刻は次回のスケジュール以降になります。
受付時の値と、ワーカーが読む値
受付側の payload は、context.Id と model.Title を読み、要求レコードの DescriptionA に JSON として保存します。ワーカーは元レコードを取り直さず、JSON.parse(String(job.DescriptionA)) でこの JSON を読みます。そのため、受付後に元レコードのタイトルを変えても、すでに登録した要求のタイトルは変わりません。受付時点の値で処理する設計です。
items.Create が失敗した場合は受付失敗のメッセージを返します。一方、作成に成功した時点では processJob はまだ実行されていません。成功のメッセージを「受付」にしているのは、この二つの成功を区別するためです。業務処理の成功は、要求レコードが Done になり、結果が保存された時点で確認します。
状態の保存位置が失敗時の見え方を決める
ワーカーのループは、まず saveState(id, "Running", "") を実行し、その後に try へ入ります。開始状態を保存できなければ例外がこの try の外へ出て、業務処理を始めません。Running へ更新できた要求だけを processJob へ渡す構造です。
try の中には、業務処理と Done の保存の両方があります。業務処理が成功しても結果保存が失敗すれば catch へ進み、Failed の保存を試みます。したがって Failed は、外部処理が一度も成功していないことの保証にはなりません。外部登録などを追加する場合は、この状態だけで再送の可否を決めず、外部側の結果も確認します。
catch 内の失敗状態の保存にも失敗すれば、エラー記録の行へ進まず、例外が外へ出ます。また、プロセス停止なら catch 自体を実行できません。状態を保存する順序と try の範囲を見れば、Running が残る理由や、自動で再実行してよいかを判断できます。
失敗・タイムアウト・再実行
| 状態 | 意味と確認すること |
|---|---|
Pending | 未処理。スケジュール、実行ユーザーの権限、ワーカーの有効化を確認する |
Running | 開始状態を保存済み。停止・タイムアウトで残ることがある |
Done | 業務処理と結果の保存が成功した |
Failed | 捕捉した例外の後、失敗状態の保存まで成功した |
開始状態の保存に失敗した場合は業務処理へ進まず、ワーカーが例外で終了します。結果の保存自体にも失敗したときや、V8 のタイムアウト・プロセス停止時には Running が残り得ます。すべての失敗が必ず Failed になる設計ではありません。
バックグラウンドでも通常と同じタイムアウトが使われます。ServerScriptTimeOut の既定値は 10000 ミリ秒です。1 回の実行時間は 10 件分の合計なので、実際の業務処理に合わせて件数と時間を調整します(Script.cs#L7-L19、タイムアウト)。
再実行する場合は、外部側で処理済みになっていないかを確認してから、対象の ClassA を Pending に戻します。このサンプルは自動再試行も、1 回だけの実行保証も実装していません。メール送信や外部登録のように二重処理が問題になる処理は、要求レコードの ResultId を外部へ渡すキーにするなど、同じ要求を再実行しても重複しない仕組みを業務処理側に用意します。
同じ Quartz ジョブの同時実行は制限されていますが、別のジョブ、即時実行、独立した複数台のスケジューラを含めてテーブル取得を排他化する仕組みではありません(ClusterExecutionTimerBase.cs#L5-L17)。この例の Running 更新にも「まだ Pending なら取得する」という原子的な条件はありません。複数ワーカーで動かす場合は、DB 上の排他取得などを追加した別の設計が必要です。
動作確認
掲載コードは、プリザンター 1.5.8.1 の検証環境で、通常・拡張それぞれの API による作成・更新からの要求登録と、バックグラウンドのスケジュール実行による正常終了・不正 JSON の失敗を確認しています。processJob を実際の業務処理に変更した後は、処理時間・権限・再実行時の挙動も確認してください。
- 起点のレコードを作成し、処理要求が
Pendingで 1 件できることを確認します。 - タイトルを変更して保存し、別の要求に変更後の値が保存されることを確認します。
- ワーカーを実行し、要求が
Doneになり、DescriptionBに受付時のタイトルが入ることを確認します。 - 検証用の要求に不正な JSON を入れて実行し、その要求が
Failedになることを確認します。 - 処理要求の作成・状態更新によって、追加の処理要求が生成されないことを確認します。