Rust Workersは、RustをWebAssemblyにコンパイルすることでCloudflare Workersプラットフォーム上で動作しますが、私たちが発見したように、WebAssemblyにはいくつかの課題があります。パニックや予期せぬ中断が発生した場合、ランタイムが未定義の状態になることがあります。Rust Workersのユーザーにとって、パニックは従来から重大なことであり、インスタンスを汚染し、場合によってはWorkerを一定期間停止させる可能性さえありました。
これらの問題を検出して軽減することはできましたが、Rust Workerが予期せず失敗し、他のリクエストが失敗する可能性はわずかに残りました。Workerで処理されていないRustが1つのリクエストに影響を与えると、より広範な障害に発展して、同じ接続リクエストに影響を与えるか、あるいは新しい着信リクエストに影響を与え続ける可能性があります。この根本的な原因は、Rust Workersが依存するRustからJavaScriptへのバインディングを生成するコアプロジェクトwasm-bindgenと、組み込みの復旧セマンティックスの欠如でした。
この記事では、Rust Workersの最新バージョンが、この中断によるサンドボックスポイズニングを解決する包括的なWasmエラーリカバリをどのように処理するかをご紹介します。この成果は、昨年発足したwasm-bindgen組織内での共同作業の一環として、wasm-bindgenに還元されました。まず、panic=unwindサポートにより、単一の失敗したリクエストが他のリクエストに悪影響を及ぼすことがないようにし、次に、Wasm上のRustコードが中断後に再実行されないことを保証するアボートリカバリーメカニズムを備えています。
この分野における信頼性に対処するための最初の試みは、本番Rust WorkersでのRustパニックや中断によって引き起こされる障害を把握し、封じ込めることに焦点を当てました。Cloudflareでは、Worker内の障害状態を追跡し、後続のリクエストを処理する前にアプリケーション全体の再初期化をトリガーするカスタムRustパニックハンドラーを導入しました。JavaScript側では、これは、プロキシベースの間接費を使用して、RustとJavaScriptの呼び出し境界をラップし、すべてのエントリポイントが一貫してカプセル化されるようにする必要がありました。また、障害発生後にWebAssemblyモジュールを正しく再初期化するために、生成されたバインディングに的を絞った変更を行いました。
このアプローチはカスタムJavaScriptロジックに依存していましたが、信頼できる復旧が可能であることを実証し、実際に見られる永続的な障害モードを排除することができました。このソリューションは、バージョン0.6から開始するすべてのWorkersユーザーにデフォルトで出荷され、次のセクションで説明するより一般的なアップストリーム型の中断復旧メカニズムの基礎を築きました。
WebAssembly Exception Handlingを使用したpananc=unwindの実装
上記の中断リカバリメカニズムは、Workerが障害に耐えられることを保証しますが、それはアプリケーション全体を再初期化することで実現されます。ステートレスなリクエストハンドラであれば、これで問題ありません。しかし、Durable Objectsのように、メモリに意味のある状態を保持するワークロードにとっては、再初期化はその状態を完全に失うことを意味します。1つのリクエストの1つのパニックが、他の同時リクエストで使用されているメモリ内の状態を消去する可能性があります。
ほとんどのネイティブRust環境では、パニックが解消されるため、デストラクチャが実行され、プログラムは状態を失うことなく回復することができます。WebAssemblyでは、従来は全く異なるものでした。wasm32-unknown-unknown経由で Wasm にコンパイルされた Rust は、デフォルトでpanic=abortとなるため、そのため、Rust Worker 内でパニックが発生すると、unreachable命令によって突然トラップが発生し、WebAssembly.RuntimeErrorと共にWasm から JS へ戻ります。
インスタンスの状態を破棄せずにパニックから回復するには、2023年にエンジンで広くサポートされるようになったWebAssembly 例外処理提案によって可能になったwasm-bindgenのpanic=unwindサポートがwasm32-unknown-unknownに必要でした。
まず、RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-stdでコンパイルします。これにより、アンワインドをサポートする標準ライブラリが再構築され、適切なパニックアンワインドでコードが生成されます。たとえば:
struct HasDropA;
struct HasDropB;
extern "C" {
fn imported_func();
}
fn some_func() {
let a = HasDropA;
let b = HasDropB;
imported_func();
}
WebAssemblyをコンパイルします:
try
call <imported_func>
catch_all
call <drop_b>
call <drop_a>
rethrow
end
call <drop_b>
call <drop_a>
これにより、imported_func() がパニックに陥っても、デストラクタは引き続き実行されます。同様に、std::panic::catch_unwind(|| some_func()) は次のようにコンパイルされます。
try
call <some_func>
;; set result to Ok(return value)
catch
try
call <std::panicking::catch_unwind::cleanup>
;; set result to Err(panic payload)
catch_all
call <core::panicking::cannot_unwind>
unreachable
end
end
エンドツーエンドで機能させるには、wasm-bindgenツールチェーンにいくつかの変更を必要としました。WebAssemblyパーサーWalrusは、try/catch指示の処理方法を持っていなかったので、サポートを追加しました。また、記述子インタープリターにも、例外処理ブロックを含むコードを評価する方法を教育する必要がありました。その時点で、完全なアプリケーションはpanic=unwindで構築することができます。
最後のステップは、wasm-bindgenによって生成されたエクスポートを修正し、Rust-JavaScriptの境界でパニックをキャッチして、JavaScript PanicError 例外として表面化させることでした。微妙な点に、Rustはextern "C"関数を使って解凍するときに外部の例外をキャッチして中断するため、エクスポートはextern "C-unwind"とマークして、境界を越えた解除を明示的に許可する必要があります。未来のために、パニックがPanicErrorでJavaScriptプロミスを拒否します。
Closuresは、panic=unwindで構築された場合のみUnwindSafeをチェックする新しいMaybeUnwindSafeトレイトによって、アンウィンドの安全性が適切にチェックされるように特別な注意が必要でした。しかし、これにより問題がすぐに浮き彫りになりました。多くのクロージャは、解除された後も残る参照を捕捉するため、本質的に安全でない参照をリクエストすることができるのです。ユーザーがコンパイラを満足させるためだけに、AssertUnwindSafe でクロージャーを誤ってラップするよう奨励される状況を回避するために、Closure::new_aborting バリアントを追加しました。これは、アンワインドの安全性が保証できない場合に、アンワインドする代わりにパニックで終了するものです。
パニック解除有効化:
エクスポートされたRust関数のパニックはWasm-bindgenで捕捉されます。
JavaScriptに対するパニック領域。PanicError 例外
非同期エクスポートは、PanicErrorを使用して、返された約束を拒否します
Rustデストラクチャが正しく動作する
WebAssemblyインスタンスは有効で再利用可能なまま
このアプローチの詳細とwasm-bindgenでの使用方法については、Wasm Bindgen: Catching Panicsの最新ガイドページで取り上げられています。
panic=unwindをサポートしても、中断は依然として発生します。メモリ不足エラーは、よくある原因の一つです。ボットは解除できないため、ステートが回復できる可能性はまったくありませんが、少なくとも将来の操作のためにボットを検出して回復し、無効なステートが後続のリクエストをエラーにすることを回避することができます。
パニックに戻るサポートにより、リカバリーを中断する新しい問題が生じました。Wasmからエラーを受け取った場合、それがextern “C-unwind”外部エラーによるものなのか、それとも本物のAbortなのかはわかりません。WebAssemblyでは、Abortは様々な形を取ることができます。
これを技術的に解決する選択肢は2つありました。確実に中断されるすべてのエラーをマークするか、確実に解除されるすべてのエラーをマークするかのどちらかです。どちらも実現できましたが、私たちは後者を選択しました。既に私たちの海外例外処理は、生のWATレベル(WebAssemblyテキストフォーマット)の例外処理命令を直接使用していたため、外部例外の例外タグを実装することで、アンウィンド安全でない例外と区別する方が簡単であることがわかりました。
WebAssemblyの例外処理におけるこのException.Tag機能のおかげで、回復可能なエラーと回復不可能なエラーを明確に区別できるようになり、新しいアボートハンドラーとアボート再入可能ガードの両方を統合することができました。
新しいAbortフック、set_on_abortを初期化時に使用し、プラットフォーム埋め込みのニーズに応じて回復するハンドラをアタッチできます。
無効な実行状態を回避するには、パニックと中断処理の強化が重要です。WebAssemblyを使用すると、コールスタックが深く相互作用し、WasmがJavaScriptを呼び出すことができ、JavaScriptが任意の深さでWasmに再侵入することができますが、これとともに複数のタスクを同じインスタンスで機能させることができます。以前は、1つのタスクやネストされたスタックで発生した中断が、JSを通じて上位のスタックを無効化することが保証されず、未定義の動作につながる可能性がありました。実行モデルを保証できるように注意を払う必要があり、この分野での貢献は継続的に続いています。
中断は決して理想的ではなく、障害発生時の再初期化は絶対に最悪のシナリオですが、最終防御線としてクリティカルエラーリカバリーを実装することで、実行の正確性を確保し、将来の操作を成功させることができます。無効な状態は持続しないため、単一の障害が複数の障害につながることはありません。
拡張:wasm-bindgenライブラリの再初期化を中断する
これに取り組んでいるうちに、これはwasm-bindgenで構築されたJSが使用するライブラリに共通する問題であり、回復を実行できるようにABORTハンドラーを付加することで恩恵を受けることがわかりました。
しかし、WasmをESモジュールとして構築して直接インポートする場合(例:import { func }from 'wasm-dep' )、Wasm abortの復旧メカニズムは明確ではなく、すでにリンクされたfunc()を呼び出します。学習したと思いました。
厳密にはRust Workersのユースケースではありませんが、CloudflareのチームはRustに支えられたWasmライブラリの依存関係を実行する、JSベースのWorkersユーザーもサポートしています。この問題を同時に解決できれば、Cloudflare WorkersプラットフォームでのWasmの使用にも間接的に利益をもたらす可能性があります。
Wasmライブラリユースケースの自動中断リカバリをサポートするために、wasm-bindgen、--reset-state-functionに実験的な再初期化メカニズムのサポートを追加しました。これにより、Rustアプリケーションが、生成されたバインディングのコンシューマーがそれらを再インポートまたは作成する必要なく、次の呼び出しのために内部のWasmインスタンスを初期の状態にリセットすることを効果的にリクエストできる関数が公開されます。古いインスタンスのクラスインスタンスは、ハンドリングがオーファンになると破棄されますが、その後、新しいクラスを構築することができます。Wasmライブラリを使用したJSアプリケーションはエラーになりますが、ブリッキングされていません。
この機能の技術的な詳細とwasm-bindgenでの使用方法については、新しいwasm-bindgenガイドのセクションWasm Bindgen: Handling Abortsで説明されています。
この取り組みのアップストリームの貢献は、wasm-bindgenプロジェクトだけにとどまりませんでした。panwind=unwindを使用したWasmの構築には、実験的な夜間のRustターゲットが必要です。そのため、当社はRustのWasmのWebAssembly Exception処理のサポートを発展させ、安定したRustに移行することにも取り組んできました。
WebAssemblyの例外処理の仕様策定過程において、最終段階での仕様変更により、2つのバリエーションが生まれました。それは、従来の例外処理と、最終的に採用された最新の「exnref」を用いた例外処理です。現在も、RustのWebAssemblyターゲットは依然としてレガシーバリアントのコードをデフォルトで生成しています。従来の例外処理は広くサポートされていますが、現在は非推奨となっています。
最新のWebAssembly例外処理は、以下のJSプラットフォームリリースにてサポートされています:
ランタイム | バージョン | リリース日 |
v8 | 13.8.1 | 2025年4月28日 |
workerd | v1.20250620.0 | 2025年6月19日 |
Chrome | 138 | 2025年6月28日 |
Firefox | 131 | 2024年10月1日 |
Safari | 18.4 | 2025年3月31日 |
Node.js | 25.0.0 | 2025年10月15日 |
サポートマトリックスを調査していたところ、最大の懸念はNode.js 24 LTSのリリーススケジュールでした。これにより、エコシステム全体は2028年4月まで従来のWebAssembly例外処理から抜け出せません。
この矛盾を発見したことで、最新の例外処理をNode.js 24リリースにバックポートし、Node.js 22リリースラインでこのターゲットのサポートを確実にするために必要な修正をバックポートすることができました。これにより、最新の例外処理提案が来年のデフォルトのターゲットになるはずです。
今後数ヶ月の間に、安定したpanic=unwindと最新の例外処理への移行を、エンドユーザーに可能な限り見えないようにする作業を進めます。
エコシステムへのこのような長期的な投資には時間がかかりますが、Rust WebAssemblyコミュニティ全体のより強力な基盤を構築するのに役立ちます。こうして改善に貢献できることを嬉しく思います。
Rust Workersでパニック・アンワンドを使用する
Rust Workersのバージョン0.8.0より、新しい--panic-unwindフラグが利用可能になり、こちらの指示に従ってビルドコマンドに追加できます。
このフラグにより、パニックは完全に回復することができ、ボットリカバリーは新しいボット分類とリカバリーフックメカニズムを使用します。より安定したRust Workersの環境を実現するため、アップグレードして試してみることを強くお勧めします。また、今後のリリースではpanic=unwindをデフォルト設定にする予定です。panic=abort を引き続き使用するユーザーは、0.6.0 からの以前のカスタムリカバリーラッパー処理を引き続き利用できます。
この作業は、Rust Workersの安定版リリースに向けた継続的な取り組みの一部です。Wasmプラットフォーム基盤のこれらの急激なエッジを根本から解決し、それが理にかなっているエコシステムに貢献することで、当社のプラットフォームだけでなく、Rust、JS、Wasmのエコシステム全体のより強固な基盤を構築します。
Rust Workersについては今後、数多くの改善を計画しており、まもなくこれらの追加機能に関する最新情報をお伝えする予定です。これには、先月、当チームのGuy BedfordがWasm.ioでの「Rust & JS Interoperability」という講演で概要を紹介した、wasm-bindgenのジェネリクスや自動bindgenなども含まれます。
#rust‑on‑workersのCloudflare Discordでお会いしましょう。また、フィードバックや議論、特にworkers-rsとwasm-bindgenのGitHubプロジェクトへの新しいコントリビューターも歓迎します。