Repository: tetratelabs/wazero
Pull request #1157 (2023-02-24 00:56:50)

Adding waPC to general purpose plugins list
Adding waPC to the list of wazero users. Thank you, Wazero team!

/cc: @codefromthecrypt 


Patch:
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index 4e1d1a1c..16999429 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -636,6 +636,17 @@ func functionFromUintptr(ptr uintptr) *function {
 
 // Call implements the same method as documented on wasm.ModuleEngine.
 func (ce *callEngine) Call(ctx context.Context, callCtx *wasm.CallContext, params []uint64) (results []uint64, err error) {
+	if ce.fn.parent.withEnsureTermination {
+		select {
+		case <-ctx.Done():
+			// If the provided context is already done, close the call context
+			// and return the error.
+			callCtx.CloseWithCtxErr(ctx)
+			return nil, callCtx.FailIfClosed()
+		default:
+		}
+	}
+
 	tp := ce.initialFn.source.Type
 
 	paramCount := len(params)
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index 206fc902..bdcaaf69 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -777,6 +777,17 @@ func (ce *callEngine) Call(ctx context.Context, m *wasm.CallContext, params []ui
 }
 
 func (ce *callEngine) call(ctx context.Context, callCtx *wasm.CallContext, tf *function, params []uint64) (results []uint64, err error) {
+	if ce.compiled.parent.ensureTermination {
+		select {
+		case <-ctx.Done():
+			// If the provided context is already done, close the call context
+			// and return the error.
+			callCtx.CloseWithCtxErr(ctx)
+			return nil, callCtx.FailIfClosed()
+		default:
+		}
+	}
+
 	ft := tf.source.Type
 	paramSignature := ft.ParamNumInUint64
 	paramCount := len(params)
diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go
index c70c2e88..aa7deafe 100644
--- a/internal/wasm/call_context.go
+++ b/internal/wasm/call_context.go
@@ -73,29 +73,45 @@ func (m *CallContext) FailIfClosed() (err error) {
 //
 // Callers of this function must invoke the returned context.CancelFunc to release the spawned Goroutine.
 func (m *CallContext) CloseModuleOnCanceledOrTimeout(ctx context.Context) context.CancelFunc {
-	goroutineDone, cancelFn := context.WithCancel(context.Background())
-	go m.closeModuleOnCanceledOrTimeoutClosure(ctx, goroutineDone)()
-	return cancelFn
+	// Creating an empty channel in this case is a bit more efficient than
+	// creating a context.Context and canceling it with the same effect. We
+	// really just need to be notified when to stop listening to the users
+	// context. Closing the channel will unblock the select in the goroutine
+	// causing it to return an stop listening to ctx.Done().
+	cancelChan := make(chan struct{})
+	go m.closeModuleOnCanceledOrTimeout(ctx, cancelChan)
+	return func() { close(cancelChan) }
 }
 
-// closeModuleOnCanceledOrTimeoutClosure is extracted from CloseModuleOnCanceledOrTimeout for testing.
-func (m *CallContext) closeModuleOnCanceledOrTimeoutClosure(ctx, goroutineDone context.Context) func() {
-	return func() {
-		for {
-			select {
-			case <-ctx.Done():
-				if errors.Is(ctx.Err(), context.Canceled) {
-					// TODO: figure out how to report error here.
-					_ = m.CloseWithExitCode(ctx, sys.ExitCodeContextCanceled)
-				} else if errors.Is(ctx.Err(), context.DeadlineExceeded) {
-					// TODO: figure out how to report error here.
-					_ = m.CloseWithExitCode(ctx, sys.ExitCodeDeadlineExceeded)
-				}
-				return
-			case <-goroutineDone.Done():
-				return
-			}
+// closeModuleOnCanceledOrTimeout is extracted from CloseModuleOnCanceledOrTimeout for testing.
+func (m *CallContext) closeModuleOnCanceledOrTimeout(ctx context.Context, cancelChan <-chan struct{}) {
+	select {
+	case <-ctx.Done():
+		select {
+		case <-cancelChan:
+			// In some cases by the time this goroutine is scheduled, the caller
+			// has already closed both the context and the cancelChan. In this
+			// case go will randomize which branch of the outer select to enter
+			// and we don't want to close the module.
+		default:
+			m.CloseWithCtxErr(ctx)
 		}
+	case <-cancelChan:
+	}
+}
+
+// CloseWithExitCode closes the module with an exit code based on the type of
+// error reported by the context.
+//
+// If the context's error is unknown or nil, the module does not close.
+func (m *CallContext) CloseWithCtxErr(ctx context.Context) {
+	switch {
+	case errors.Is(ctx.Err(), context.Canceled):
+		// TODO: figure out how to report error here.
+		_ = m.CloseWithExitCode(ctx, sys.ExitCodeContextCanceled)
+	case errors.Is(ctx.Err(), context.DeadlineExceeded):
+		// TODO: figure out how to report error here.
+		_ = m.CloseWithExitCode(ctx, sys.ExitCodeDeadlineExceeded)
 	}
 }
 
diff --git a/site/content/community/users.md b/site/content/community/users.md
index f236b984..1e941d2e 100644
--- a/site/content/community/users.md
+++ b/site/content/community/users.md
@@ -10,9 +10,10 @@ considering their efforts before starting your own!
 
 ### General purpose plugins
 
-| Name           | Description                            |
-|:---------------|----------------------------------------|
-| [go-plugin][2] | use gRPC to define WebAssembly plugins |
+| Name           | Description                                                                                 |
+|:---------------|---------------------------------------------------------------------------------------------|
+| [go-plugin][2] | use gRPC to define WebAssembly plugins                                                      |
+| [waPC][5]      | uses an intuitive [IDL][6] and code generation to create WebAssembly plugins |
 
 ### HTTP
 
@@ -33,3 +34,5 @@ experience.
 [2]: https://github.com/knqyf263/go-plugin
 [3]: https://github.com/http-wasm/http-wasm-host-go
 [4]: https://http-wasm.io
+[5]: https://wapc.io
+[6]: https://apexlang.io


Test patch:
diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go
index 1f929a05..2775896f 100644
--- a/internal/integration_test/engine/adhoc_test.go
+++ b/internal/integration_test/engine/adhoc_test.go
@@ -117,6 +117,15 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) {
 		require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context canceled", t.Name()))
 	})
 
+	t.Run("context cancel in advance", func(t *testing.T) {
+		_, infinite := newInfiniteLoopFn(t)
+		ctx, cancel := context.WithCancel(context.Background())
+		cancel()
+		_, err = infinite.Call(ctx)
+		require.Error(t, err)
+		require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context canceled", t.Name()))
+	})
+
 	t.Run("context timeout", func(t *testing.T) {
 		_, infinite := newInfiniteLoopFn(t)
 		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go
index ab758a86..5806a14d 100644
--- a/internal/wasm/call_context_test.go
+++ b/internal/wasm/call_context_test.go
@@ -361,19 +361,69 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) {
 
 	t.Run("cancel works", func(t *testing.T) {
 		cc := &CallContext{Closed: new(uint64), module: &ModuleInstance{Name: "test"}, s: s}
-		goroutineDone, cancelFn := context.WithCancel(context.Background())
-		fn := cc.closeModuleOnCanceledOrTimeoutClosure(context.Background(), goroutineDone)
+		cancelChan := make(chan struct{})
 		var wg sync.WaitGroup
 		wg.Add(1)
 
-		// Ensure that fn returned by closeModuleOnCanceledOrTimeoutClosure exists after cancelFn is called.
+		// Ensure that fn returned by closeModuleOnCanceledOrTimeout exists after cancelFn is called.
 		go func() {
 			defer wg.Done()
-			fn()
+			cc.closeModuleOnCanceledOrTimeout(context.Background(), cancelChan)
 		}()
-		cancelFn()
+		close(cancelChan)
 		wg.Wait()
 	})
+
+	t.Run("no close on all resources canceled", func(t *testing.T) {
+		cc := &CallContext{Closed: new(uint64), module: &ModuleInstance{Name: "test"}, s: s}
+		cancelChan := make(chan struct{})
+		close(cancelChan)
+		ctx, cancel := context.WithCancel(context.Background())
+		cancel()
+
+		cc.closeModuleOnCanceledOrTimeout(ctx, cancelChan)
+
+		err := cc.FailIfClosed()
+		require.Nil(t, err)
+	})
+}
+
+func TestCallContext_CloseWithCtxErr(t *testing.T) {
+	s := newStore()
+
+	t.Run("context canceled", func(t *testing.T) {
+		cc := &CallContext{Closed: new(uint64), module: &ModuleInstance{Name: "test"}, s: s}
+		ctx, cancel := context.WithCancel(context.Background())
+		cancel()
+
+		cc.CloseWithCtxErr(ctx)
+
+		err := cc.FailIfClosed()
+		require.EqualError(t, err, "module \"test\" closed with context canceled")
+	})
+
+	t.Run("context timeout", func(t *testing.T) {
+		cc := &CallContext{Closed: new(uint64), module: &ModuleInstance{Name: "test"}, s: s}
+		duration := time.Second
+		ctx, cancel := context.WithTimeout(context.Background(), duration)
+		defer cancel()
+
+		time.Sleep(duration * 2)
+
+		cc.CloseWithCtxErr(ctx)
+
+		err := cc.FailIfClosed()
+		require.EqualError(t, err, "module \"test\" closed with context deadline exceeded")
+	})
+
+	t.Run("no error", func(t *testing.T) {
+		cc := &CallContext{Closed: new(uint64), module: &ModuleInstance{Name: "test"}, s: s}
+
+		cc.CloseWithCtxErr(context.Background())
+
+		err := cc.FailIfClosed()
+		require.Nil(t, err)
+	})
 }
 
 type mockCloser struct{ called int }