Rust Workersの信頼性向上:wasm-bindgenにおけるパニックとアボート復旧
2026-04-22 | Guy Bedford、Hood Chatham、Logan Gatlin | 7分読了
Rust Workersは、RustをWebAssemblyにコンパイルしてCloudflare Workersプラットフォーム上で実行されます。しかし、WebAssemblyにはいくつかの落とし穴があります。パニックや予期しないアボートが発生すると、ランタイムが未定義の状態に陥る可能性があります。Rust Workersのユーザーにとって、パニックは歴史的に致命的であり、インスタンスを汚染し、一時的にWorkerを機能不全にする可能性さえありました。これらの問題を検出して軽減することはできましたが、Rust Workerが予期せず失敗し、他のリクエストも失敗させる可能性は残っていました。1つのリクエストに影響する未処理のRustアボートは、兄弟リクエストに影響する広範な障害にエスカレートしたり、新しい受信リクエストに継続して影響したりする可能性があります。
この根本原因は、Rust Workersが依存するRust-JavaScriptバインディングを生成するコアプロジェクトであるwasm-bindgenにあり、組み込みの復旧セマンティクスが不足していました。このポストでは、最新バージョンのRust Workersがこのアボート誘発サンドボックス汚染を解決する包括的なWasmエラー復旧をどのように処理するかについて説明します。このワークは、昨年設立されたwasm-bindgen組織内での協力の一部として、wasm-bindgenにフィードバックされています。まずpanic=unwindサポートで、単一の失敗したリクエストが他のリクエストを汚染しないことを保証し、次にアボート復旧メカニズムで、Wasm上のRustコードがアボート後に再実行されることがないことを保証します。
初期復旧対策
この領域の信頼性に対処するための初期の試みは、本番環境のRust Workersで発生するRustパニックとアボートによる障害を理解し、封じ込めることに焦点を当てていました。Worker内の障害状態を追跡し、後続のリクエストを処理する前に完全なアプリケーション再初期化をトリガーするカスタムRustパニックハンドラーを導入しました。JavaScriptの側では、Proxy ベースの間接参照を使用してRust-JavaScriptコール境界をラップして、すべてのエントリポイントが一貫してカプセル化されることを確認する必要がありました。また、障害後にWebAssemblyモジュールを正しく再初期化するために、生成されたバインディングに対象を絞った変更を加えました。このアプローチはカスタムJavaScriptロジックに依存していましたが、信頼性の高い復旧が達成可能であり、実際に見られていた永続的な障害モードを排除したことを実証しました。このソリューションはバージョン0.6以降、すべてのworkers-rsユーザーにデフォルトで配信され、以下のセクションで説明されているより一般的なアップストリームアボート復旧メカニズムの基礎を築きました。
WebAssembly例外処理によるpanic=unwindの実装
上記のアボート復旧メカニズムはWorkerが障害から生き残ることを保証しますが、アプリケーション全体を再初期化することでそうしています。ステートレスなリクエストハンドラーの場合、これで問題ありません。しかし、Durable Objectsなど、メモリに意味のある状態を保持するワークロードの場合、再初期化はその状態を完全に失うことを意味します。1つのリクエストのパニックは、他の同時リクエストで使用されているメモリ内状態を完全に消去する可能性があります。
ほとんどのネイティブRust環境では、パニックはアンワインドでき、デストラクタが実行され、プログラムが状態を失うことなく復旧できます。WebAssemblyでは、歴史的には状況が大きく異なっていました。wasm32-unknown-unknownを経由してWasmにコンパイルされたRustはデフォルトでpanic=abortになるため、Rust Worker内のパニックは到達不可能な命令で急激にトラップし、WebAssembly.RuntimeErrorでWasmからJSに終了します。
インスタンス状態を破棄せずにパニックから復旧するには、WebAssembly例外処理提案によって可能になったwasm32-unknown-unknownのwasm-bindgenでのpanic=unwindサポートが必要でした。これは2023年にエンジンの広範なサポートを獲得しました。
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>
catch
try
call <std::panicking::catch_unwind::cleanup>
catch_all
call <core::panicking::cannot_unwind>
unreachable
end
end
これをエンドツーエンドで機能させるには、wasm-bindgenツールチェーンにいくつかの変更が必要でした。WebAssemblyパーサーWalrusはtry/catch命令を処理する方法を知らなかったため、サポートを追加しました。記述子インタープリターも、例外処理ブロックを含むコードを評価する方法を学ぶ必要がありました。その時点で、完全なアプリケーションをpanic=unwindでビルドできました。最後のステップは、wasm-bindgenによって生成されたエクスポートを変更して、Rust-JavaScriptの境界でパニックをキャッチし、JavaScriptのPanicError例外として表示することでした。
1つの微妙な点:Rustは外部例外をキャッチし、extern "C"関数を通じてアンワインドするときにアボートするため、エクスポートは境界を越えたアンワインディングを明示的に許可するようにextern "C-unwind"でマークする必要がありました。
フューチャーの場合、パニックはJavaScript Promiseを PanicError で拒否します。クロージャーは、panic=unwindでビルドされた場合にのみUnwindSafeをチェックする新しいMaybeUnwindSafeトレイトを介して、アンワインドセーフティが適切にチェックされることを確認するために特別な注意が必要でした。
しかし、これはすぐに問題を露呈させました。多くのクロージャーはアンワインド後に残る参照をキャプチャするため、本質的にアンワインド不安全です。ユーザーがコンパイラを満たすためにクロージャーをAssertUnwindSafeで不正にラップするよう促される状況を避けるために、アンワインドセーフティを保証できない場合にアンワインドの代わりにパニックで終了するClosure::new_abortingバリアントを追加しました。
パニックアンワインディングが有効な場合:
- エクスポートされたRust関数のパニックはwasm-bindgenによってキャッチされます
- パニックはJavaScriptにPanicError例外として表示されます
- 非同期エクスポートは返されたPromiseをPanicErrorで拒否します
- Rustデストラクタが正しく実行されます
- WebAssemblyインスタンスは有効で再利用可能なままです
アプローチの完全な詳細とwasm-bindgenでの使用方法は、最新のWasm Bindgenガイドページ「Catching Panics」に記載されています。
アボート復旧
panic=unwindサポートがあっても、アボートは発生します。メモリ不足エラーが一般的な原因です。アボートはアンワインドできないため、状態復旧の可能性はまったくありませんが、少なくともアボートを検出して復旧し、無効な状態が後続のリクエストをエラーにするのを避けることができます。
パニックアンワインドサポートはアボート復旧に新しい問題をもたらしました。Wasmからエラーを受け取るとき、それがextern "C-unwind"外部エラーから来たのか、それとも本物のアボートなのかわかりません。
アボートはWebAssemblyで多くの形をとることができます。これを技術的に解決するための2つのオプションがありました。確実にアボートであるすべてのエラーをマークするか、確実にアンワインドであるすべてのエラーをマークするかです。どちらでも機能した可能性がありますが、後者を選択しました。外部例外処理は既にWAT レベル(WebAssemblyテキスト形式)の例外処理命令を直接使用していたため、外部例外の例外タグを実装して、アボート非アンワインドセーフ例外と区別する方が簡単だと判断しました。
WebAssembly例外処理のException.Tag機能のおかげで、復旧可能なエラーと復旧不可能なエラーを明確に区別する能力により、新しいアボートハンドラーとアボート再入性ガードの両方を統合することができました。
初期化時に設定できる新しいアボートフック set_on_abort を使用して、プラットフォーム埋め込みのニーズに応じて適切に復旧するハンドラーをアタッチできます。
パニックとアボート処理の強化は、無効な実行状態を避けるために重要です。WebAssemblyは深くインターリーブされたコールスタックを許可します。WasmはJavaScriptに呼び出し、JavaScriptは任意の深さでWasmに再入できます。一方、複数のタスクが同じインスタンスで機能しています。以前は、1つのタスクまたはネストされたスタックで発生するアボートは、JS経由で高いスタックを無効にすることが保証されていなかったため、未定義の動作につながっていました。実行モデルを保証できることを確認するために注意が必要でした。この領域への貢献は継続中です。
アボートは決して理想的ではなく、障害時の再初期化は絶対的な最悪のシナリオですが、最後の防衛線として重要なエラー復旧を実装することで、実行の正確性を確保し、将来の操作が成功できることを保証します。無効な状態は永続化されず、単一の障害が複数の障害にカスケードしないことを保証します。
拡張:wasm-bindgenライブラリのアボート再初期化
これに取り組んでいる間、これはwasm-bindgenで構築されたJSで使用されるライブラリの一般的な問題であり、アボートハンドラーをアタッチして復旧を実行できることからも利益を得ることに気付きました。しかし、Wasmを ES モジュールとしてビルドし、直接インポートする場合(例:import { func } from 'wasm-dep')、ユーザーJS アプリケーションで既にリンクおよび初期化されているライブラリを呼び出す際のWasmアボートの復旧メカニズムが何であるかは明確ではありません。
厳密にはRust Workersのユースケースではありませんが、私たちのチームは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」に記載されています。
Rust Wasm例外処理エコシステムの成熟
このワークのアップストリーム貢献はwasm-bindgenプロジェクトで止まりませんでした。panic=unwindでWasmをビルドするには、実験的なナイトリーRustターゲットが必要なため、WebAssembly例外処理のRustのWasmサポートを進めるためにも取り組んでいます。これにより、安定したRustにこれをもたらすのに役立ちます。
WebAssembly例外処理の開発中に、後期段階の仕様変更により、レガシー例外処理と最終的な最新の例外処理「exnrefを使用」の2つのバリアントが生じました。今日、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リリースラインでも機能させるために必要な修正をバックポートして、このターゲットのサポートを確保することができました。これにより、最新の例外処理提案がデフォルトになることが可能になるはずです。