start、wait、cancel、abortSignal、再接続の仕組み。
createTrainer は 3 メソッドを持つ Trainer オブジェクトを返します。arkor start と Studio の "Run training" ボタンはどちらも start() の後に wait() を呼びます。これらを自分で呼ぶのは、学習を自前のコード(サーバー、スクリプト、独自 CLI)に組み込むときだけです。
interface Trainer {
readonly name: string;
start(): Promise<{ jobId: string }>;
wait(): Promise<TrainingResult>;
cancel(): Promise<void>;
}
interface TrainingResult {
job: TrainingJob;
artifacts: unknown[];
}start()const { jobId } = await trainer.start();jobId は Studio で見えるもの、SDK の TrainingJob.id と同じです。start() を 2 回目に呼んでも再投入せず同じ jobId を返します(packages/arkor/src/core/trainer.ts:275-289)。wait() です。wait()const { job, artifacts } = await trainer.wait();training.completed か training.failed を報告したときに終端の TrainingResult で resolve します。start() を呼んでいなければ代わりに呼んでくれます。wait() 内から発火します。start() を呼んで wait() を呼ばないと、学習がバックエンドで進行していてもコールバックは動きません。cancel()await trainer.cancel();start() がまだ呼ばれていなければ cancel() は何もしません(:388-389 で早期 return)。cancel() は reject します。投機的に呼ぶなら try / catch で囲んでください。abortSignalconst controller = new AbortController();
const trainer = createTrainer({
name: "with-timeout",
model: "unsloth/gemma-4-E4B-it",
dataset: { type: "huggingface", name: "arkorlab/triage-demo" },
abortSignal: controller.signal,
});
// あとで、どこからでも:
controller.abort();abortSignal は あくまでローカルの wait() ループ をコントロールします。シグナルが Abort すると:
trainer.ts:325-328)。delay が signal.reason で reject される(trainer.ts:178)。handleFailure が再 throw する(trainer.ts:308)。wait() は Abort 時に resolve ではなく reject する。これは cancel() を呼ばず、バックエンドにも何も送りません。マネージド側ではジョブが GPU 時間を使い続けます。
両方の効果(ローカルでの待機停止と、バックエンドの学習停止)が欲しいなら別々にやります:
try {
await trainer.wait();
} catch (err) {
if (controller.signal.aborted) {
// 想定通り: wait() を止めるよう頼んだ
} else {
throw err;
}
}
await trainer.cancel(); // ベストエフォート、上記参照「この学習を待つのはもういい」(リクエストタイムアウト、親プロセス終了)なら abortSignal。「バックエンド側で学習を止めたい」なら cancel()。
wait() はデフォルトで一過性の障害を超えて SSE ストリームを生かし続けます:
ping フレームと不正なフレームは進捗として数えません)のクリーンなストリーム EOF は、即時再接続をベース遅延(initialReconnectDelayMs、デフォルト 1000 ms)で起こし、失敗回数にはカウントしません。ストリームは Last-Event-ID で再開します。handleFailure を経由します: 指数バックオフは initialReconnectDelayMs * 2 ** attempt、各試行の遅延は maxReconnectDelayMs(デフォルト 60 000 ms)にクランプ、連続失敗カウントは maxReconnectAttempts で上限。これは意図的です: ping やゴミだけを送って EOF するような壊れた中継が、ベース遅延で無限ループしないように失敗として計上します。401 / 403(認証)、410(gone)、426(アップグレード必須)のレスポンスは、再接続しても同じ結果になるため wait() を即座に reject します。404 は一過性として扱いリトライします(作成直後のジョブのイベントストリームがまだ可視でない場合があるため)。408、429、5xx も同様にリトライします。maxReconnectAttempts のデフォルトは undefined(連続失敗無制限)。TrainerInput から設定はできず、reconnectDelayMs と maxReconnectDelayMs も含め createTrainer の第 2 引数 context(@internal 注釈付き、変更され得る)からのみ設定できます。多くのプロジェクトでこれは、ジョブが走っている限り一過性 SSE 失敗が黙ってリトライされ続ける、ということを意味します。この経路は あくまで トランスポート障害のためのものです。ユーザーコールバックの throw はこの経路を通りません。再接続もリトライもせず、即座に wait() を reject します(ライフサイクルコールバック § 例外ハンドリング を参照)。throw を再接続経由にすると、既に進んだ失敗イベントの Last-Event-ID の先から再開してしまい、エラーを握り潰すことになります。決定的で致命的でないエラーハンドリングが必要なら、wait() の reject に頼るのではなくコールバック内で catch してください。
CLI 以外で使うときの典型的な形は、長寿命のトレーナー参照を持って自前のコードで start、wait、cancel を制御するものです:
import { createTrainer } from "arkor";
const controller = new AbortController();
process.on("SIGINT", () => controller.abort());
const trainer = createTrainer({
/* ... */
abortSignal: controller.signal, // abort() で wait() が実際に reject するよう接続
});
const { jobId } = await trainer.start();
console.log(`Started ${jobId}`);
try {
const { artifacts } = await trainer.wait();
console.log(`Finished with ${artifacts.length} artifact(s).`);
} catch (err) {
if (controller.signal.aborted) {
await trainer.cancel().catch(() => {});
throw new Error("Aborted by signal");
}
throw err;
}これは runTrainer のエントリー解決を除けば、arkor start がやっているのと機能的に同じです。
createTrainer: この Trainer を返す入力の型wait() 内から発火するものrunTrainer: start() + wait() をラップしてエントリー解決まで行うヘルパー