workflows/wasm/host: prevent limiter and Wasmtime resource leaks - #2356
Conversation
|
👋 cfal, thanks for creating this pull request! To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team. Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks! |
📊 API Diff Results
|
| // and local testing has shown that with less than the min, some | ||
| // binaries may error sporadically. | ||
| modCfg.MaxMemoryMBs = uint64(math.Max(float64(modCfg.MinMemoryMBs), float64(modCfg.MaxMemoryMBs))) | ||
| limit := settings.Size(config.Size(modCfg.MaxMemoryMBs) * config.MByte) |
There was a problem hiding this comment.
removed because settings.Size only wrapped the raw bound with a parser and unit:
chainlink-common/pkg/settings/settings.go
Lines 160 to 163 in 6b01cb8
this call site constructed a Factory with only Logger, so Settings and Meter were nil:
chainlink-common/pkg/workflows/wasm/host/module.go
Lines 287 to 296 in 6b01cb8
GetOrDefault returns DefaultValue immediately when Settings is nil:
chainlink-common/pkg/settings/settings.go
Lines 216 to 229 in 6b01cb8
despite that, MakeUpperBoundLimiter started a global updater:
chainlink-common/pkg/settings/limits/bound.go
Lines 65 to 124 in 6b01cb8
each updater also started cancellation and ticker goroutines:
chainlink-common/pkg/settings/limits/updater.go
Lines 68 to 84 in 6b01cb8
chainlink-common/pkg/services/stop_!race.go
Lines 9 to 17 in 6b01cb8
chainlink-common/pkg/timeutil/ticker.go
Lines 14 to 20 in 6b01cb8
so these nine constant defaults created 27 goroutines per module to repeatedly resolve the same value. NewUpperBoundLimiter enforces the same bound without that unused update machinery.
callers that need dynamic settings already inject configured limiters through ModuleConfig:
There was a problem hiding this comment.
callers that need dynamic settings already inject configured limiters through ModuleConfig:
IIRC we expect all production callers to do this. This other case was only meant to be a non-production fallback. Do we have callers in production that are not injecting limiters?
There was a problem hiding this comment.
confidential-workflows creates a module per production execution without injecting any limiters:
WasmFileSpecFactory is another production path that supplies only Logger:
the standard workflow runtime also omits MaxSubscriptionsLimiter.
There was a problem hiding this comment.
Should we set them from confidential compute? Otherwise we cannot reconfigure and we don't get metrics.
The other one seems less important, given that it already has a null logger?
|
|
||
| cfg *ModuleConfig | ||
| cfg *ModuleConfig | ||
| defaultLimiters moduleLimiters |
There was a problem hiding this comment.
used to be written into the caller-owned ModuleConfig:
chainlink-common/pkg/workflows/wasm/host/module.go
Lines 293 to 368 in 6b01cb8
that isn’t safe as EvictableModule retains the same config pointer, the old module closes asynchronously, and a replacement can be constructed while that cleanup is still queued:
keeping defaults per-module means the old module can only close its own resources.
jmank88
left a comment
There was a problem hiding this comment.
Can you please add a description? I'm having trouble understanding the goal of this PR.
| if err != nil { | ||
| return nil, fmt.Errorf("failed to make metric payload size limiter: %w", err) | ||
| } | ||
| defaultLimiters.maxUserMetricPayload = limits.NewUpperBoundLimiter(config.Size(modCfg.MaxUserMetricPayloadBytes)) |
There was a problem hiding this comment.
Why switch from the *Make(Factory,* variants? The only effect should be losing logging, which is especially important while we still have "fail open" cases which only log errors rather than return them.
There was a problem hiding this comment.
there is no effective logging lost. Factory.Settings is nil, so GetOrDefault always returns the default with a nil error. the fail-open log path only applies to missing tenants on non-global settings, while these defaults are all global. bound violations weren't logged before either.
so NewUpperBoundLimiter preserves the observable behavior; it only removes the unused updater machinery.
There was a problem hiding this comment.
I don't like the idea of depending on implementation details that could change but if this is only for the non-production cases then that is probably fine. Can we update the injection from confidential compute?
Otherwise, if we are stuck with using these for production, then something seems misaligned, because we should want overrides and metrics in that case, no?
fixes resource leaks in the WASM host by explicitly closing module-owned limiters and Wasmtime resources.
confidential-workflows creates a new module per production execution without injecting limiters. the previous fallback limiters started 27 goroutines per module and were never closed.
this change: