Fixes race on engine creation with Cache API (#1021)

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Takeshi Yoneda
2023-01-11 09:46:55 +09:00
committed by GitHub
parent 3fc5392570
commit e216466331
6 changed files with 44 additions and 12 deletions

View File

@@ -76,6 +76,11 @@ jobs:
cache: true cache: true
- run: make test - run: make test
if: ${{ github.event_name == 'pull_request' }}
# Run tests with -race only on main branch push.
- run: make test go_test_options='-race'
if: ${{ github.event_name == 'push' }}
- name: "Generate coverage report" # only once (not per OS) - name: "Generate coverage report" # only once (not per OS)
if: runner.os == 'Linux' if: runner.os == 'Linux'

View File

@@ -22,6 +22,9 @@ main_sources := $(wildcard $(filter-out %_test.go $(all_testdata) $(all_testing
# Paths need to all start with ./, so we do that manually vs foreach which strips it. # Paths need to all start with ./, so we do that manually vs foreach which strips it.
main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f)))) main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f))))
# By default, we don't run with -race as it's costly to run on every PR.
go_test_options ?=
ensureCompilerFastest := -ldflags '-X github.com/tetratelabs/wazero/internal/integration_test/vs.ensureCompilerFastest=true' ensureCompilerFastest := -ldflags '-X github.com/tetratelabs/wazero/internal/integration_test/vs.ensureCompilerFastest=true'
.PHONY: bench .PHONY: bench
bench: bench:
@@ -45,7 +48,7 @@ build.bench:
.PHONY: test.examples .PHONY: test.examples
test.examples: test.examples:
@go test ./examples/... ./imports/assemblyscript/example/... ./imports/emscripten/... ./imports/go/example/... ./imports/wasi_snapshot_preview1/example/... @go test $(go_test_options) ./examples/... ./imports/assemblyscript/example/... ./imports/emscripten/... ./imports/go/example/... ./imports/wasi_snapshot_preview1/example/...
.PHONY: build.examples.as .PHONY: build.examples.as
build.examples.as: build.examples.as:
@@ -174,8 +177,8 @@ build.spectest.v2: # Note: SIMD cases are placed in the "simd" subdirectory.
.PHONY: test .PHONY: test
test: test:
@go test $$(go list ./... | grep -vE '$(spectest_v1_dir)|$(spectest_v2_dir)') -timeout 120s @go test $(go_test_options) $$(go list ./... | grep -vE '$(spectest_v1_dir)|$(spectest_v2_dir)') -timeout 120s
@cd internal/version/testdata && go test ./... -timeout 120s @cd internal/version/testdata && go test $(go_test_options) ./... -timeout 120s
.PHONY: coverage .PHONY: coverage
coverpkg = $(subst $(space),$(comma),$(main_packages)) coverpkg = $(subst $(space),$(comma),$(main_packages))
@@ -189,10 +192,10 @@ spectest:
@$(MAKE) spectest.v2 @$(MAKE) spectest.v2
spectest.v1: spectest.v1:
@go test $$(go list ./... | grep $(spectest_v1_dir)) -timeout 120s @go test $(go_test_options) $$(go list ./... | grep $(spectest_v1_dir)) -timeout 120s
spectest.v2: spectest.v2:
@go test $$(go list ./... | grep $(spectest_v2_dir)) -timeout 120s @go test $(go_test_options) $$(go list ./... | grep $(spectest_v2_dir)) -timeout 120s
golangci_lint_path := $(shell go env GOPATH)/bin/golangci-lint golangci_lint_path := $(shell go env GOPATH)/bin/golangci-lint

View File

@@ -8,6 +8,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
goruntime "runtime" goruntime "runtime"
"sync"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/filecache"
@@ -50,9 +51,14 @@ func NewCompilationCacheWithDir(dirname string) (CompilationCache, error) {
type cache struct { type cache struct {
// eng is the engine for this cache. If the cache is configured, the engine is shared across multiple instances of // eng is the engine for this cache. If the cache is configured, the engine is shared across multiple instances of
// Runtime, and its lifetime is not bound to them. Instead, the engine is alive until Cache.Close is called. // Runtime, and its lifetime is not bound to them. Instead, the engine is alive until Cache.Close is called.
eng wasm.Engine eng wasm.Engine
fileCache filecache.Cache fileCache filecache.Cache
once sync.Once
}
func (c *cache) initEngine(ne newEngine, ctx context.Context, features api.CoreFeatures) wasm.Engine {
c.once.Do(func() { c.eng = ne(ctx, features, c.fileCache) })
return c.eng
} }
// Close implements the same method on the Cache interface. // Close implements the same method on the Cache interface.

View File

@@ -125,13 +125,15 @@ func NewRuntimeConfig() RuntimeConfig {
return newRuntimeConfig() return newRuntimeConfig()
} }
type newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine
type runtimeConfig struct { type runtimeConfig struct {
enabledFeatures api.CoreFeatures enabledFeatures api.CoreFeatures
memoryLimitPages uint32 memoryLimitPages uint32
memoryCapacityFromMax bool memoryCapacityFromMax bool
isInterpreter bool isInterpreter bool
dwarfDisabled bool // negative as defaults to enabled dwarfDisabled bool // negative as defaults to enabled
newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine newEngine newEngine
cache CompilationCache cache CompilationCache
} }

View File

@@ -116,10 +116,7 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
if c := config.cache; c != nil { if c := config.cache; c != nil {
// If the Cache is configured, we share the engine. // If the Cache is configured, we share the engine.
cacheImpl = c.(*cache) cacheImpl = c.(*cache)
if cacheImpl.eng == nil { engine = cacheImpl.initEngine(config.newEngine, ctx, config.enabledFeatures)
cacheImpl.eng = config.newEngine(ctx, config.enabledFeatures, cacheImpl.fileCache)
}
engine = cacheImpl.eng
} else { } else {
// Otherwise, we create a new engine. // Otherwise, we create a new engine.
engine = config.newEngine(ctx, config.enabledFeatures, nil) engine = config.newEngine(ctx, config.enabledFeatures, nil)

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
_ "embed" _ "embed"
"errors" "errors"
"sync"
"testing" "testing"
"time" "time"
@@ -695,3 +696,21 @@ func (e *mockEngine) Close() (err error) {
e.closed = true e.closed = true
return return
} }
// TestNewRuntime_concurrent ensures that concurrent execution of NewRuntime is race-free.
// This depends on -race flag.
func TestNewRuntime_concurrent(t *testing.T) {
const num = 100
var wg sync.WaitGroup
config := NewRuntimeConfig().WithCompilationCache(NewCompilationCache())
wg.Add(num)
for i := 0; i < num; i++ {
go func() {
defer wg.Done()
r := NewRuntimeWithConfig(testCtx, config)
err := r.Close(testCtx)
require.NoError(t, err)
}()
}
wg.Wait()
}