Skip to content

Commit 9d7f476

Browse files
authored
Unify wasmtime configuration to fix precompilation (#3730)
1 parent e2da057 commit 9d7f476

10 files changed

Lines changed: 112 additions & 94 deletions

File tree

AGENTS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ exec --fail-on-error ${golem} build -P release --yes
4545
'''
4646
```
4747

48+
### Wasmtime configuration compatibility
49+
50+
All production engines that compile, load, or inspect components must use the shared constructors
51+
in `golem_common::wasmtime_config`. This includes the component compilation service, worker
52+
executor, component metadata extraction, and any future component tools. Do not set Wasmtime
53+
features or tunables independently at those call sites; change the shared constructor instead and
54+
verify every consumer still uses it.
55+
56+
The component compilation service serializes precompiled `.cwasm` artifacts that the worker
57+
executor deserializes. Wasmtime validates artifact-affecting feature and tunable settings during
58+
deserialization. A mismatch makes the executor reject the cached artifact and compile the original
59+
component again, causing a severe cold-start regression. When changing the Wasmtime version or
60+
configuration, run the cross-engine precompiled-component compatibility test in `golem-common`.
61+
62+
Minimal Wasmtime engines in unit tests may use a local configuration only when they exercise an
63+
isolated host primitive and neither load production components nor produce or consume precompiled
64+
artifacts. Tests intended to mirror production component behavior must use the shared constructor.
65+
4866
## Testing
4967

5068
Tests use [test-r](https://test-r.vigoo.dev). **Important:** Each test file must import `test_r::test` or tests will not run.

golem-common/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ pub mod retries;
6565
pub mod serialization;
6666
#[cfg(feature = "full")]
6767
pub mod tracing;
68+
#[cfg(feature = "full")]
69+
pub mod wasmtime_config;
6870

6971
mod macros;
7072

golem-common/src/model/agent/extraction.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::schema::agent::wit::{decode_agent_error_rejecting_quota_with, decode_
1919
use crate::schema::tool::Tool;
2020
use crate::schema::tool::validation::validate_tool;
2121
use crate::schema::tool::wit::{decode_tool, wire as tool_wire};
22+
use crate::wasmtime_config::create_wasmtime_config;
2223
use anyhow::anyhow;
2324
use golem_schema::schema::wit::{
2425
QuotaTokenHandleDropper, QuotaTokenHandleRep, SecretHandleDropper, SecretHandleRep,
@@ -133,24 +134,7 @@ async fn extract_component_metadata_impl(
133134
enable_fs_cache: bool,
134135
include_tools: bool,
135136
) -> anyhow::Result<ExtractedComponentMetadata> {
136-
let mut config = wasmtime::Config::default();
137-
config.wasm_multi_value(true);
138-
config.wasm_component_model(true);
139-
// Required for WASI p3: enables the async ABI (stream<T>, future<T>,
140-
// async lift/lower, error-context). Without this, components that use
141-
// any p3 async builtins fail to parse via `Component::from_file`.
142-
config.wasm_component_model_async(true);
143-
config.wasm_component_model_error_context(true);
144-
config.epoch_interruption(true);
145-
config.consume_fuel(true);
146-
config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
147-
148-
if enable_fs_cache {
149-
config.cache(Some(
150-
wasmtime::Cache::new(wasmtime::CacheConfig::new()).expect("Failed to initialize cache"),
151-
));
152-
}
153-
137+
let config = create_wasmtime_config(enable_fs_cache);
154138
let engine = Engine::new(&config)?;
155139
let mut linker: Linker<Host> = Linker::new(&engine);
156140
linker.allow_shadowing(true);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2024-2026 Golem Cloud
2+
//
3+
// Licensed under the Golem Source License v1.1 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://license.golem.cloud/LICENSE
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use wasmtime::{Config, WasmBacktraceDetails};
16+
17+
/// Creates the Wasmtime configuration shared by every Golem component engine.
18+
///
19+
/// A component compiled by one Golem service can be deserialized by another,
20+
/// so compilation-affecting settings must not be configured independently at
21+
/// individual call sites.
22+
pub fn create_wasmtime_config(enable_fs_cache: bool) -> Config {
23+
let mut config = Config::default();
24+
25+
config.wasm_multi_value(true);
26+
config.wasm_component_model(true);
27+
config.wasm_component_model_async(true);
28+
config.wasm_component_model_error_context(true);
29+
config.wasm_threads(false);
30+
config.shared_memory(false);
31+
config.epoch_interruption(true);
32+
config.consume_fuel(true);
33+
config.concurrency_support(true);
34+
config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
35+
36+
if enable_fs_cache {
37+
config.cache(Some(
38+
wasmtime::Cache::new(wasmtime::CacheConfig::new()).expect("Failed to initialize cache"),
39+
));
40+
}
41+
42+
config
43+
}
44+
45+
pub fn create_wasmtime_config_with_fs_cache() -> Config {
46+
create_wasmtime_config(true)
47+
}
48+
49+
pub fn create_wasmtime_config_without_fs_cache() -> Config {
50+
create_wasmtime_config(false)
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use super::*;
56+
use test_r::test;
57+
use wasmtime::Engine;
58+
use wasmtime::component::Component;
59+
60+
#[test]
61+
fn precompiled_components_are_compatible_across_engines() -> anyhow::Result<()> {
62+
let compilation_engine = Engine::new(&create_wasmtime_config_with_fs_cache())?;
63+
let precompiled_component = compilation_engine.precompile_component(b"(component)")?;
64+
let executor_engine = Engine::new(&create_wasmtime_config_without_fs_cache())?;
65+
66+
// SAFETY: these bytes were produced by Wasmtime in this process and are
67+
// deserialized without being modified.
68+
unsafe { Component::deserialize(&executor_engine, precompiled_component) }?;
69+
70+
Ok(())
71+
}
72+
}

golem-common/tests/agent_extraction.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
use assert2::assert;
1616
use golem_common::model::agent::extraction::extract_agent_type_schemas;
17+
use golem_common::wasmtime_config::create_wasmtime_config_without_fs_cache;
1718
use std::path::PathBuf;
1819
use std::str::FromStr;
1920
use test_r::test;
@@ -51,10 +52,7 @@ async fn can_extract_agent_type_schemas_from_component_importing_p3_http() -> an
5152
// Guard: the fixture must actually import P3 `wasi:http`, otherwise a
5253
// future rebuild of the component would silently defeat the purpose of
5354
// this regression test.
54-
let mut config = wasmtime::Config::default();
55-
config.wasm_component_model(true);
56-
config.wasm_component_model_async(true);
57-
config.wasm_component_model_error_context(true);
55+
let config = create_wasmtime_config_without_fs_cache();
5856
let engine = wasmtime::Engine::new(&config)?;
5957
let component = wasmtime::component::Component::from_file(&engine, &wasm_path)?;
6058
let imports_p3_http = component

golem-component-compilation-service/src/lib.rs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ use tonic::codec::CompressionEncoding;
3636
use tonic_tracing_opentelemetry::middleware;
3737
use tonic_tracing_opentelemetry::middleware::filters;
3838
use tracing::{Instrument, info};
39-
use wasmtime::WasmBacktraceDetails;
4039

4140
pub mod config;
4241
mod grpc;
@@ -168,24 +167,5 @@ async fn start_grpc_server(
168167
}
169168

170169
fn create_wasmtime_config(engine_config: &config::EngineConfig) -> wasmtime::Config {
171-
let mut config = wasmtime::Config::default();
172-
173-
config.wasm_multi_value(true);
174-
config.wasm_component_model(true);
175-
// Must match the executor's enabled wasm features so that components
176-
// pre-compiled here are accepted at runtime. WASI p3 components use the
177-
// async ABI (stream<T>, future<T>, async lift/lower, error-context).
178-
config.wasm_component_model_async(true);
179-
config.wasm_component_model_error_context(true);
180-
config.epoch_interruption(true);
181-
config.consume_fuel(true);
182-
config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
183-
184-
if engine_config.enable_fs_cache {
185-
config.cache(Some(
186-
wasmtime::Cache::new(wasmtime::CacheConfig::new()).expect("Failed to initialize cache"),
187-
));
188-
}
189-
190-
config
170+
golem_common::wasmtime_config::create_wasmtime_config(engine_config.enable_fs_cache)
191171
}

golem-worker-executor/src/bootstrap.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,12 @@ pub async fn run(
5757

5858
#[cfg(test)]
5959
mod tests {
60-
use super::*;
61-
use crate::services::golem_config::EngineConfig;
6260
use test_r::test;
6361
use wasmtime::{Engine, Module};
6462

6563
#[test]
6664
fn production_engine_rejects_shared_memory_modules() {
67-
let config = <ServerBootstrap as Bootstrap<Context>>::create_wasmtime_config(
68-
&ServerBootstrap,
69-
&EngineConfig::default(),
70-
);
65+
let config = golem_common::wasmtime_config::create_wasmtime_config_without_fs_cache();
7166
let engine = Engine::new(&config).unwrap();
7267

7368
assert!(Module::new(&engine, "(module (memory 1 2 shared))").is_err());

golem-worker-executor/src/lib.rs

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use crate::services::card::{CardService, CardServiceDefault};
5555
use crate::services::component::ComponentService;
5656
use crate::services::events::Events;
5757
use crate::services::golem_config::{
58-
EngineConfig, GolemConfig, HttpClientConfig, IndexedStorageConfig, KeyValueStorageConfig,
58+
GolemConfig, HttpClientConfig, IndexedStorageConfig, KeyValueStorageConfig,
5959
KeyValueStorageInnerConfig, SchedulerStorageConfig,
6060
};
6161
use crate::services::key_value::{DefaultKeyValueService, KeyValueService};
@@ -135,8 +135,8 @@ use tonic::transport::Server;
135135
use tonic_tracing_opentelemetry::middleware;
136136
use tonic_tracing_opentelemetry::middleware::filters;
137137
use tracing::{Instrument, info};
138+
use wasmtime::Engine;
138139
use wasmtime::component::{HasSelf, Linker};
139-
use wasmtime::{Config, Engine, WasmBacktraceDetails};
140140

141141
pub struct RunDetails {
142142
pub http_port: u16,
@@ -463,34 +463,6 @@ pub trait Bootstrap<Ctx: WorkerCtx> {
463463
))
464464
}
465465

466-
/// Can be overridden to customize the wasmtime configuration
467-
fn create_wasmtime_config(&self, engine_config: &EngineConfig) -> Config {
468-
let mut config = Config::default();
469-
470-
config.wasm_multi_value(true);
471-
config.wasm_component_model(true);
472-
// Required for WASI p3: enables the async ABI (stream<T>, future<T>,
473-
// async lift/lower, error-context). Without this, components that use
474-
// any p3 async builtins fail to instantiate.
475-
config.wasm_component_model_async(true);
476-
config.wasm_component_model_error_context(true);
477-
// Golem does not expose wasi-threads or a durable shared-memory runtime.
478-
config.wasm_threads(false);
479-
config.shared_memory(false);
480-
config.epoch_interruption(true);
481-
config.consume_fuel(true);
482-
config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
483-
484-
if engine_config.enable_fs_cache {
485-
config.cache(Some(
486-
wasmtime::Cache::new(wasmtime::CacheConfig::new())
487-
.expect("Failed to initialize cache"),
488-
));
489-
}
490-
491-
config
492-
}
493-
494466
/// This method is responsible for linking all the host function implementations the worker
495467
/// executor supports.
496468
fn create_wasmtime_linker(&self, engine: &Engine) -> anyhow::Result<Linker<Ctx>> {
@@ -853,7 +825,8 @@ pub async fn create_worker_executor_impl<
853825
shutdown_token.clone(),
854826
);
855827

856-
let config = bootstrap.create_wasmtime_config(&golem_config.engine);
828+
let config =
829+
golem_common::wasmtime_config::create_wasmtime_config(golem_config.engine.enable_fs_cache);
857830
let engine = Arc::new(Engine::new(&config)?);
858831
let linker = bootstrap.create_wasmtime_linker(&engine)?;
859832

golem-worker-executor/tests/concurrent_delivery_order.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
3838
use test_r::test;
3939
use wasmtime::component::{Accessor, Component, Linker};
40-
use wasmtime::{Config, Engine, Store};
40+
use wasmtime::{Engine, Store};
4141

4242
/// Host-side state driving the completion order of the bespoke `call` host
4343
/// function.
@@ -49,12 +49,7 @@ struct DeliveryState {
4949
}
5050

5151
fn engine() -> Engine {
52-
let mut config = Config::default();
53-
// Mirror the production component-model-async configuration (see
54-
// `Golem::create_wasmtime_config`).
55-
config.wasm_component_model(true);
56-
config.wasm_component_model_async(true);
57-
config.wasm_component_model_error_context(true);
52+
let config = golem_common::wasmtime_config::create_wasmtime_config_without_fs_cache();
5853
Engine::new(&config).expect("failed to create engine")
5954
}
6055

@@ -116,6 +111,8 @@ async fn delivery_order_for(schedule: Vec<u32>) -> Vec<u32> {
116111
.expect("register golem:cmtest/host#call");
117112

118113
let mut store = Store::new(&engine, DeliveryState { schedule, step: 0 });
114+
store.set_fuel(u64::MAX).expect("set test fuel");
115+
store.set_epoch_deadline(u64::MAX);
119116
let instance = linker
120117
.instantiate_async(&mut store, &component)
121118
.await

golem-worker-executor/tests/concurrent_runtime_events.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,10 @@ use std::sync::{Arc, Mutex};
4444

4545
use test_r::test;
4646
use wasmtime::component::{Accessor, Component, Linker};
47-
use wasmtime::{Config, Engine, Store, StoreContextMut};
47+
use wasmtime::{Engine, Store, StoreContextMut};
4848

4949
fn engine() -> Engine {
50-
let mut config = Config::default();
51-
// Mirror the production component-model-async configuration (see
52-
// `Golem::create_wasmtime_config`).
53-
config.wasm_component_model(true);
54-
config.wasm_component_model_async(true);
55-
config.wasm_component_model_error_context(true);
50+
let config = golem_common::wasmtime_config::create_wasmtime_config_without_fs_cache();
5651
Engine::new(&config).expect("failed to create engine")
5752
}
5853

@@ -168,6 +163,8 @@ async fn run_cancel_one() -> CancelShared {
168163
.expect("register golem:cmtest/host#observed");
169164

170165
let mut store = Store::new(&engine, ());
166+
store.set_fuel(u64::MAX).expect("set test fuel");
167+
store.set_epoch_deadline(u64::MAX);
171168
let instance = linker
172169
.instantiate_async(&mut store, &component)
173170
.await
@@ -344,6 +341,8 @@ async fn run_observe_completions(schedule: Vec<u32>) -> (Vec<u32>, ObserveShared
344341
}
345342

346343
let mut store = Store::new(&engine, ());
344+
store.set_fuel(u64::MAX).expect("set test fuel");
345+
store.set_epoch_deadline(u64::MAX);
347346
let instance = linker
348347
.instantiate_async(&mut store, &component)
349348
.await

0 commit comments

Comments
 (0)