Rust Workers работают на платформе Cloudflare Workers путем компиляции Rust в WebAssembly, но, как мы выяснили, у WebAssembly есть свои острые углы. Когда что-то идет не так из-за паники (panic) или неожиданного аварийного завершения (abort), среда выполнения может оказаться в неопределенном состоянии. Для пользователей Rust Workers паники исторически были фатальными, "отравляя" экземпляр и потенциально даже выводя Воркер из строя на некоторое время.
Хотя мы могли обнаруживать и смягчать эти проблемы, оставалась небольшая вероятность, что Rust Worker неожиданно завершится с ошибкой и повлечет за собой сбой других запросов. Необработанное аварийное завершение Rust в Воркере, влияющее на один запрос, могло перерасти в более широкий сбой, затрагивающий параллельные запросы, или даже продолжать влиять на новые входящие запросы. Коренная причина этого заключалась в wasm-bindgen, основном проекте, который генерирует привязки Rust-to-JavaScript, от которых зависят Rust Workers, и в отсутствии у него встроенной семантики восстановления.
В этом посте мы расскажем, как последняя версия Rust Workers реализует комплексное восстановление после ошибок Wasm, решая проблему "отравления" песочницы, вызванного аварийным завершением. Эта работа была внесена обратно в wasm-bindgen в рамках нашего сотрудничества внутри организации wasm-bindgen, созданной в прошлом году. Сначала с поддержкой panic=unwind, которая гарантирует, что один неудачный запрос никогда не "отравит" другие запросы, а затем с механизмами восстановления после abort, которые гарантируют, что код Rust на Wasm никогда не сможет выполниться повторно после аварийного завершения.
Первоначальные меры по восстановлению
Наши первоначальные попытки решить проблему надежности в этой области были сосредоточены на понимании и сдерживании сбоев, вызванных паниками и аварийными завершениями Rust в рабочих Rust Workers в production. Мы внедрили пользовательский обработчик паники Rust, который отслеживал состояние сбоя внутри Воркера и запускал полную переинициализацию приложения перед обработкой последующих запросов. На стороне JavaScript это потребовало обернуть границу вызова Rust-JavaScript с использованием косвенности на основе Proxy, чтобы гарантировать, что все точки входа были последовательно инкапсулированы. Мы также внесли целевые изменения в генерируемые привязки, чтобы корректно переинициализировать модуль WebAssembly после сбоя.
Хотя этот подход опирался на пользовательскую логику JavaScript, он продемонстрировал, что надежное восстановление достижимо, и устранил устойчивые режимы сбоев, которые мы наблюдали на практике. Это решение по умолчанию было выпущено для всех пользователей workers‑rs, начиная с версии 0.6, и заложило основу для более общих, интегрированных в основную ветку (upstream) механизмов восстановления после abort, описанных в следующих разделах.
Реализация panic=unwind с обработкой исключений WebAssembly
Описанные выше механизмы восстановления после abort гарантируют, что Воркер может пережить сбой, но делают это путем переинициализации всего приложения. Для обработчиков запросов без состояния (stateless) это нормально. Но для рабочих нагрузок, которые хранят значимое состояние в памяти, таких как Durable Objects, переинициализация означает полную потерю этого состояния. Одна паника в одном запросе может стереть состояние в памяти, используемое другими параллельными запросами.
В большинстве нативных сред Rust паники могут быть "развернуты" (unwind), позволяя запуститься деструкторам и программе восстановиться без потери состояния. В WebAssembly исторически все выглядело иначе. Rust, скомпилированный в Wasm через wasm32-unknown-unknown, по умолчанию использует panic=abort, поэтому паника внутри Rust Worker резко приводила к trap с инструкцией unreachable и выходу из Wasm обратно в JS с ошибкой WebAssembly.RuntimeError.
Чтобы восстанавливаться после паник без потери состояния экземпляра, нам потребовалась поддержка panic=unwind для wasm32-unknown-unknown в wasm-bindgen, ставшая возможной благодаря предложению по обработке исключений WebAssembly (WebAssembly Exception Handling), которое получило широкую поддержку движков в 2023 году.
Мы начинаем с компиляции с помощью RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-std, что пересобирает стандартную библиотеку с поддержкой unwind и генерирует код с правильным разворачиванием паники. Например:
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>
;; устанавливаем результат в Ok(возвращаемое значение)
catch
try
call <std::panicking::catch_unwind::cleanup>
;; устанавливаем результат в Err(данные паники)
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", чтобы явно разрешить разворачивание через границу. Для future (асинхронных операций) паника отклоняет JavaScript Promise с ошибкой PanicError.
Замыкания потребовали особого внимания, чтобы гарантировать правильную проверку безопасности разворачивания (unwind safety) с помощью нового трейта MaybeUnwindSafe, который проверяет UnwindSafe только при сборке с panic=unwind. Однако это быстро выявило проблему: многие замыкания захватывают ссылки, которые сохраняются после разворачивания, что делает их по своей природе небезопасными для разворачивания. Чтобы избежать ситуации, когда пользователей поощряют некорректно оборачивать замыкания в AssertUnwindSafe только для удовлетворения компилятора, мы добавили варианты Closure::new_aborting, которые завершаются при панике вместо разворачивания в случаях, когда безопасность разворачивания не может быть гарантирована.
При включенном разворачивании паник:
-
Паники в экспортированных функциях Rust перехватываются wasm-bindgen
-
Паники проявляются в JavaScript как исключения PanicError
-
Асинхронные экспорты отклоняют возвращенные промисы с ошибкой PanicError
-
Деструкторы Rust выполняются корректно
-
Экземпляр WebAssembly остается действительным и пригодным для повторного использования
Полные детали подхода и как его использовать в wasm-bindgen описаны на последней странице руководства Wasm Bindgen: Перехват паник.
Восстановление после abort
Даже при поддержке panic=unwind аварийные завершения все равно происходят — одной из распространенных причин являются ошибки нехватки памяти. Поскольку abort не могут быть развернуты, нет никакой возможности восстановления состояния вообще, но мы можем, по крайней мере, обнаруживать и восстанавливаться после abort для будущих операций, чтобы избежать ошибок недопустимого состояния в последующих запросах.
Поддержка разворачивания паник создала новую проблему для восстановления после abort. Когда мы получаем ошибку из Wasm, мы не знаем, произошла ли она из-за иностранной ошибки в extern “C-unwind” или это было настоящее аварийное завершение. Аварийные завершения могут принимать множество форм в WebAssembly.
У нас было два технических варианта решения: либо пометить все ошибки, которые точно являются abort, либо пометить все ошибки, которые точно являются разворачиванием. Оба могли сработать, но мы выбрали последнее. Поскольку наша обработка иностранных исключений уже напрямую использовала инструкции обработки исключений на уровне сырого WAT (текстовый формат WebAssembly), нам было проще реализовать теги исключений (exception tags) для иностранных исключений, чтобы отличать их от abort-исключений, небезопасных для разворачивания.
Благодаря возможности четко различать восстанавливаемые и невосстанавливаемые ошибки с помощью этой функции Exception.Tag в обработке исключений WebAssembly, мы смогли интегрировать как новый обработчик abort, так и защиты от повторного входа (reentrancy guards) при abort.
Новый обработчик abort, set_on_abort, можно использовать во время инициализации, чтобы прикрепить обработчик, который восстанавливается в соответствии с потребностями платформы, в которую происходит встраивание (embedding).
Усиление обработки паник и аварийных завершений критически важно для избежания недопустимого состояния выполнения. WebAssembly допускает глубоко переплетенные стеки вызовов, где Wasm может вызывать JavaScript, а JavaScript может повторно входить в Wasm на произвольной глубине, и при этом несколько задач могут функционировать в одном экземпляре. Ранее abort, происходящий в одной задаче или вложенном стеке, не гарантировал инвалидацию более высоких стеков через JS, что приводило к неопределенному поведению. Требовалась осторожность, чтобы гарантировать нашу модель выполнения, и вклад в этой области остается продолжающимся.
Хотя аварийные завершения никогда не являются идеальными, а повторная инициализация при сбое — абсолютно наихудший сценарий, реализация восстановления после критических ошибок в качестве последней линии обороны гарантирует корректность выполнения и то, что будущие операции смогут завершиться успешно. Некорректное состояние не сохраняется, что обеспечивает невозможность превращения единичного сбоя в каскадный.
Расширение: аварийная повторная инициализация для библиотек wasm-bindgen
Работая над этим, мы поняли, что это распространённая проблема для библиотек, используемых JS и собранных с помощью wasm-bindgen, и что им также было бы полезно подключить обработчик аварийного завершения для возможности восстановления.
Но при сборке Wasm как ES-модуля и его прямом импорте (например, через import { func } from ‘wasm-dep’), неочевидно, каким должен быть механизм восстановления после аварийного завершения Wasm при вызове func() для уже слинкованной и инициализированной библиотеки, находящейся в пользовательском JS-приложении.
Хотя это не является строго сценарием использования Rust Workers, наша команда также поддерживает пользователей Workers на JS, которые запускают зависимости в виде Wasm-библиотек на Rust. Если бы мы могли решить эту проблему одновременно, это косвенно также принесло бы пользу использованию Wasm на платформе Cloudflare Workers.
Для поддержки автоматического восстановления после аварийного завершения в сценариях использования Wasm-библиотек мы добавили поддержку экспериментального механизма повторной инициализации в wasm‑bindgen — --reset-state-function. Он предоставляет функцию, которая позволяет Rust-приложению эффективно запросить сброс своего внутреннего Wasm-экземпляра в исходное состояние для следующего вызова, не требуя от потребителей сгенерированных привязок их повторного импорта или пересоздания. Экземпляры классов из старого экземпляра будут выбрасывать исключения, так как их дескрипторы становятся "осиротевшими", но затем можно будет создавать новые классы. JS-приложение, использующее Wasm-библиотеку, получает ошибку, но не выходит из строя полностью.
Полные технические детали этой функции и способы её использования в wasm-bindgen описаны в новом разделе руководства wasm-bindgen: Wasm Bindgen: Обработка аварийных завершений.
Созревание экосистемы обработки исключений Rust Wasm
Вклад в вышестоящие проекты для этой работы не ограничился wasm-bindgen. Сборка для Wasm с panic=unwind по-прежнему требует экспериментальной ночной цели Rust, поэтому мы также работали над продвижением поддержки WebAssembly Exception Handling в Rust, чтобы помочь вывести это в стабильную версию.
В ходе разработки WebAssembly Exception Handling изменение спецификации на позднем этапе привело к появлению двух вариантов: устаревшая обработка исключений и окончательная современная обработка исключений "с exnref". На сегодняшний день цели WebAssembly в Rust по умолчанию всё ещё генерируют код для устаревшего варианта. Хотя устаревшая обработка исключений широко поддерживается, теперь она считается устаревшей.
Современная обработка исключений WebAssembly поддерживается, начиная со следующих выпусков JS-платформ:
|
Среда выполнения |
Версия |
Дата выпуска |
|
v8 |
13.8.1 |
28 апреля 2025 |
|
workerd |
v1.20250620.0 |
19 июня 2025 |
|
Chrome |
138 |
28 июня 2025 |
|
Firefox |
131 |
1 октября 2024 |
|
Safari |
18.4 |
31 марта 2025 |
|
Node.js |
25.0.0 |
15 октября 2025 |
Изучая матрицу поддержки, мы пришли к выводу, что наибольшую озабоченность вызывает график выпуска Node.js 24 LTS, который оставил бы всю экосистему на устаревшей обработке исключений WebAssembly вплоть до апреля 2028 года.
Обнаружив это несоответствие, мы смогли перенести современную обработку исключений в выпуск Node.js 24 и даже перенести необходимые исправления, чтобы она работала в ветке Node.js 22, обеспечив поддержку для этой цели. Это должно позволить современному предложению по обработке исключений стать целью по умолчанию в следующем году.
В течение следующих месяцев мы будем работать над тем, чтобы переход к стабильному panic=unwind и современной обработке исключений был как можно менее заметен для конечных пользователей.
Хотя эти долгосрочные инвестиции в экосистему требуют времени, они помогают создать более прочный фундамент для всего сообщества Rust WebAssembly, и мы рады возможности внести вклад в эти улучшения.
Использование panic unwind в Rust Workers
Начиная с версии 0.8.0 Rust Workers, у нас появился новый флаг --panic-unwind, который можно добавить в команду сборки, следуя инструкциям здесь.
С этим флагом паники можно полностью обрабатывать, а восстановление после аварийного завершения будет использовать новый механизм классификации абортов и перехватчиков восстановления. Мы настоятельно рекомендуем обновиться и опробовать это для получения более стабильного опыта работы с Rust Workers и планируем сделать panic=unwind значением по умолчанию в одном из последующих выпусков. Пользователи, оставшиеся на panic=abort, по-прежнему будут пользоваться преимуществами предыдущей обработки с помощью пользовательской обёртки восстановления из версии 0.6.0.
Стремление к стабильности Rust Workers
Эта работа является частью наших постоянных усилий по достижению стабильного выпуска Rust Workers. Решая эти острые проблемы основ платформы Wasm в корне и внося свой вклад в экосистему там, где это уместно, мы создаём более прочную основу не только для нашей платформы, но и для всей экосистемы Rust, JS и Wasm.
У нас запланирован ряд будущих улучшений для Rust Workers, и вскоре мы поделимся новостями об этой дополнительной работе, включая дженерики wasm-bindgen и автоматический bindgen, которые Гай Бедфорд из нашей команды анонсировал в докладе о взаимодействии Rust & JS на Wasm.io в прошлом месяце.