Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions initialization_rollback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package component_test

import (
"context"
_ "embed"
"errors"
"os"
"path/filepath"
"testing"

component "github.com/wago-org/component-model"
)

//go:embed testdata/initialization_resource_failure.wasm
var initializationResourceFailure []byte

func TestGraphInitializationFailureClosesHostResources(t *testing.T) {
for _, cleanupFails := range []bool{false, true} {
name := "successful-cleanup"
if cleanupFails {
name = "cleanup-error"
}
t.Run(name, func(t *testing.T) {
rt, ref := loadService(t, nil)
defer rt.Close()
setupErr := errors.New("initializer setup failed")
cleanupErr := errors.New("resource cleanup failed")
const iface = "test:initialization/resources"
const tag uint32 = 700
root := t.TempDir()
var files []*os.File
var dropped []uint32
opts := []component.Option{
component.WithResourceTag(iface, "resource", tag),
component.WithImport(iface, "acquire", func(context.Context, []component.Value) ([]component.Value, error) {
file, err := os.CreateTemp(root, "resource-")
if err != nil {
return nil, err
}
files = append(files, file)
t.Cleanup(func() { _ = file.Close() })
return []component.Value{uint32(len(files))}, nil
}, nil, []component.TypeDesc{component.OwnDesc{ResourceType: tag}}),
component.WithImport(iface, "fail", func(context.Context, []component.Value) ([]component.Value, error) { return nil, setupErr }, nil, nil),
component.WithHostResourceDtor(tag, func(ctx context.Context, rep uint32) error {
if ctx.Err() != nil {
t.Errorf("resource cleanup context canceled: %v", ctx.Err())
}
dropped = append(dropped, rep)
err := files[rep-1].Close()
if cleanupFails && rep == 1 {
return errors.Join(err, cleanupErr)
}
return err
}),
}
entered := false
err := ref.With(func(service component.Service) error {
return service.WithInstance(context.Background(), initializationResourceFailure, func(*component.Instance) error { entered = true; return nil }, opts...)
})
if entered {
t.Fatal("callback ran after initialization failure")
}
if !errors.Is(err, setupErr) {
t.Fatalf("initialization error = %v, want setup error", err)
}
if len(files) != 2 {
t.Fatalf("acquired resources = %d, want 2", len(files))
}
if len(dropped) != 2 || dropped[0] == dropped[1] {
t.Errorf("destructed resources = %v, want both resources exactly once", dropped)
}
for _, file := range files {
if _, statErr := file.Stat(); statErr == nil || file.Fd() != ^uintptr(0) {
t.Errorf("resource %s after failed initialization = %v, want closed", filepath.Base(file.Name()), statErr)
}
}
if cleanupFails && !errors.Is(err, cleanupErr) {
t.Errorf("initialization error = %v, want joined cleanup error", err)
}
})
}
}
8 changes: 8 additions & 0 deletions internal/instance/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package instance

import (
"context"
"errors"
"fmt"
"strconv"
"strings"
Expand Down Expand Up @@ -315,7 +316,14 @@ func instantiateGraph(ctx context.Context, r wazy.Runtime, comp *binary.Componen
}
}
fail := func(err error) (*Instance, error) {
// Initializers can acquire host resources before graph construction
// fails. Drain those resources before closing their core modules, just
// as Instance.Close does, even when the caller canceled setup.
closeErr := resources.close(context.WithoutCancel(ctx))
closeAll()
if closeErr != nil {
return nil, errors.Join(err, closeErr)
}
return nil, err
}

Expand Down
Binary file added testdata/initialization_resource_failure.wasm
Binary file not shown.
24 changes: 24 additions & 0 deletions testdata/initialization_resource_failure.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
;; The initializer acquires two ordinary host resources, then reports a
;; setup error. The runtime must destroy both resources before returning.
(component
(import "test:initialization/resources" (instance $host
(export "resource" (type (sub resource)))
(export "acquire" (func (result (own 0))))
(export "fail" (func))))
(alias export $host "acquire" (func $acquire))
(alias export $host "fail" (func $fail))
(core func $acquire-core (canon lower (func $acquire)))
(core func $fail-core (canon lower (func $fail)))
(core instance $imports
(export "acquire" (func $acquire-core))
(export "fail" (func $fail-core)))
(core module $initializer
(import "host" "acquire" (func $acquire (result i32)))
(import "host" "fail" (func $fail))
(func $initialize
call $acquire drop
call $acquire drop
call $fail)
(start $initialize))
(core instance $initialized
(instantiate $initializer (with "host" (instance $imports)))))
Loading