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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`context.Context` as the first argument. `NewExecutableUnit` gains a leading
`ctx` too. The Extism compiler's `WithContext` option and the FromHTTP
loader's `GetReaderWithContext` helper are removed (now redundant).
- **BREAKING**: `polyscript.New[E]`, `extism.FromExtismLoader`,
`risor.FromRisorLoader`, and `starlark.FromStarlarkLoader` now take a
`context.Context` as the first argument. Cancelling that ctx reaches the
loader's I/O and (where the parser supports it) the compile path.

### Deprecated
- The twelve legacy top-level constructors (`FromRisorFile`,
Expand Down
25 changes: 13 additions & 12 deletions deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package polyscript

import (
"context"
"log/slog"

extismMachine "github.com/robbyt/go-polyscript/engines/extism"
Expand Down Expand Up @@ -35,7 +36,7 @@ func FromExtismFile(
return nil, err
}

return extismMachine.FromExtismLoader(l,
return extismMachine.FromExtismLoader(context.Background(), l,
extismMachine.WithLogHandler(logHandler),
extismMachine.WithEntryPoint(entryPoint),
)
Expand All @@ -57,7 +58,7 @@ func FromExtismFileWithData(
return nil, err
}

return extismMachine.FromExtismLoader(l,
return extismMachine.FromExtismLoader(context.Background(), l,
extismMachine.WithLogHandler(logHandler),
extismMachine.WithStaticData(staticData),
extismMachine.WithEntryPoint(entryPoint),
Expand All @@ -77,7 +78,7 @@ func FromExtismBytes(
return nil, err
}

return extismMachine.FromExtismLoader(l,
return extismMachine.FromExtismLoader(context.Background(), l,
extismMachine.WithLogHandler(logHandler),
extismMachine.WithEntryPoint(entryPoint),
)
Expand All @@ -99,7 +100,7 @@ func FromExtismBytesWithData(
return nil, err
}

return extismMachine.FromExtismLoader(l,
return extismMachine.FromExtismLoader(context.Background(), l,
extismMachine.WithLogHandler(logHandler),
extismMachine.WithStaticData(staticData),
extismMachine.WithEntryPoint(entryPoint),
Expand All @@ -118,7 +119,7 @@ func FromRisorFile(
return nil, err
}

return risorMachine.FromRisorLoader(l, risorMachine.WithLogHandler(logHandler))
return risorMachine.FromRisorLoader(context.Background(), l, risorMachine.WithLogHandler(logHandler))
}

// FromRisorFileWithData creates a Risor evaluator with both static and dynamic
Expand All @@ -135,7 +136,7 @@ func FromRisorFileWithData(
return nil, err
}

return risorMachine.FromRisorLoader(l,
return risorMachine.FromRisorLoader(context.Background(), l,
risorMachine.WithLogHandler(logHandler),
risorMachine.WithStaticData(staticData),
)
Expand All @@ -153,7 +154,7 @@ func FromRisorString(
return nil, err
}

return risorMachine.FromRisorLoader(l, risorMachine.WithLogHandler(logHandler))
return risorMachine.FromRisorLoader(context.Background(), l, risorMachine.WithLogHandler(logHandler))
}

// FromRisorStringWithData creates a Risor evaluator with both static and
Expand All @@ -170,7 +171,7 @@ func FromRisorStringWithData(
return nil, err
}

return risorMachine.FromRisorLoader(l,
return risorMachine.FromRisorLoader(context.Background(), l,
risorMachine.WithLogHandler(logHandler),
risorMachine.WithStaticData(staticData),
)
Expand All @@ -188,7 +189,7 @@ func FromStarlarkFile(
return nil, err
}

return starlarkMachine.FromStarlarkLoader(l, starlarkMachine.WithLogHandler(logHandler))
return starlarkMachine.FromStarlarkLoader(context.Background(), l, starlarkMachine.WithLogHandler(logHandler))
}

// FromStarlarkFileWithData creates a Starlark evaluator with both static and
Expand All @@ -205,7 +206,7 @@ func FromStarlarkFileWithData(
return nil, err
}

return starlarkMachine.FromStarlarkLoader(l,
return starlarkMachine.FromStarlarkLoader(context.Background(), l,
starlarkMachine.WithLogHandler(logHandler),
starlarkMachine.WithStaticData(staticData),
)
Expand All @@ -223,7 +224,7 @@ func FromStarlarkString(
return nil, err
}

return starlarkMachine.FromStarlarkLoader(l, starlarkMachine.WithLogHandler(logHandler))
return starlarkMachine.FromStarlarkLoader(context.Background(), l, starlarkMachine.WithLogHandler(logHandler))
}

// FromStarlarkStringWithData creates a Starlark evaluator with both static and
Expand All @@ -240,7 +241,7 @@ func FromStarlarkStringWithData(
return nil, err
}

return starlarkMachine.FromStarlarkLoader(l,
return starlarkMachine.FromStarlarkLoader(context.Background(), l,
starlarkMachine.WithLogHandler(logHandler),
starlarkMachine.WithStaticData(staticData),
)
Expand Down
10 changes: 5 additions & 5 deletions engines/extism/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ var ErrEntryPointRequired = errors.New("extism: entry point is required (use Wit
// If both [WithStaticData] and [WithDataProvider] are supplied,
// [WithDataProvider] takes precedence — pass exactly one of them per
// call to keep intent unambiguous.
func FromExtismLoader(ldr loader.Loader, opts ...Option) (*evaluator.Evaluator, error) {
//
// The supplied ctx flows into the loader's I/O and the Extism SDK's
// WASM compile + entry-point probe; cancelling it halts both.
func FromExtismLoader(ctx context.Context, ldr loader.Loader, opts ...Option) (*evaluator.Evaluator, error) {
Comment thread
robbyt marked this conversation as resolved.
cfg := &config{}
for _, opt := range opts {
if opt != nil {
Expand All @@ -96,10 +99,7 @@ func FromExtismLoader(ldr loader.Loader, opts ...Option) (*evaluator.Evaluator,
execUnitID = u.String()
}

// FromExtismLoader is a one-shot startup constructor; compile uses a
// fresh Background context. Callers needing cancellable compile
// should drive script.NewExecutableUnit directly.
execUnit, err := script.NewExecutableUnit(context.Background(), cfg.handler, execUnitID, ldr, comp, provider)
execUnit, err := script.NewExecutableUnit(ctx, cfg.handler, execUnitID, ldr, comp, provider)
if err != nil {
return nil, err
}
Expand Down
55 changes: 42 additions & 13 deletions engines/extism/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package extism

import (
"bytes"
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -42,7 +43,7 @@ func newErrorLoader(t *testing.T, msg string) *loaderMock {

func TestFromExtismLoader_RequiresEntryPoint(t *testing.T) {
mockLoader := new(loaderMock)
eval, err := FromExtismLoader(mockLoader)
eval, err := FromExtismLoader(t.Context(), mockLoader)
require.Error(t, err)
require.Nil(t, eval)
require.ErrorIs(t, err, ErrEntryPointRequired)
Expand All @@ -53,15 +54,15 @@ func TestFromExtismLoader_RequiresEntryPoint(t *testing.T) {

func TestFromExtismLoader_EmptyEntryPointStillRejected(t *testing.T) {
mockLoader := new(loaderMock)
eval, err := FromExtismLoader(mockLoader, WithEntryPoint(""))
eval, err := FromExtismLoader(t.Context(), mockLoader, WithEntryPoint(""))
require.Error(t, err)
require.Nil(t, eval)
require.ErrorIs(t, err, ErrEntryPointRequired)
}

func TestFromExtismLoader_NoOptionsBeyondEntryPoint(t *testing.T) {
mockLoader := newWASMLoader(t)
eval, err := FromExtismLoader(mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
eval, err := FromExtismLoader(t.Context(), mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
require.NoError(t, err)
require.NotNil(t, eval)
assert.Equal(t, "extism.Evaluator", eval.String())
Expand All @@ -71,7 +72,8 @@ func TestFromExtismLoader_NoOptionsBeyondEntryPoint(t *testing.T) {
func TestFromExtismLoader_WithLogHandler(t *testing.T) {
mockLoader := newWASMLoader(t)
handler := slog.NewTextHandler(os.Stdout, nil)
eval, err := FromExtismLoader(mockLoader,
eval, err := FromExtismLoader(
t.Context(), mockLoader,
WithEntryPoint(wasmdata.EntrypointGreet),
WithLogHandler(handler),
)
Expand All @@ -82,7 +84,8 @@ func TestFromExtismLoader_WithLogHandler(t *testing.T) {

func TestFromExtismLoader_NilLogHandler(t *testing.T) {
mockLoader := newWASMLoader(t)
eval, err := FromExtismLoader(mockLoader,
eval, err := FromExtismLoader(
t.Context(), mockLoader,
WithEntryPoint(wasmdata.EntrypointGreet),
WithLogHandler(nil),
)
Expand All @@ -93,7 +96,8 @@ func TestFromExtismLoader_NilLogHandler(t *testing.T) {

func TestFromExtismLoader_WithStaticData(t *testing.T) {
mockLoader := newWASMLoader(t)
eval, err := FromExtismLoader(mockLoader,
eval, err := FromExtismLoader(
t.Context(), mockLoader,
WithEntryPoint(wasmdata.EntrypointGreet),
WithStaticData(map[string]any{"input": "World"}),
)
Expand All @@ -105,7 +109,8 @@ func TestFromExtismLoader_WithStaticData(t *testing.T) {
func TestFromExtismLoader_WithDataProvider(t *testing.T) {
mockLoader := newWASMLoader(t)
provider := data.NewContextProvider("test_key")
eval, err := FromExtismLoader(mockLoader,
eval, err := FromExtismLoader(
t.Context(), mockLoader,
WithEntryPoint(wasmdata.EntrypointGreet),
WithDataProvider(provider),
)
Expand All @@ -117,7 +122,8 @@ func TestFromExtismLoader_WithDataProvider(t *testing.T) {
func TestFromExtismLoader_DataProviderBeatsStaticData(t *testing.T) {
mockLoader := newWASMLoader(t)
provider := data.NewContextProvider("sentinel")
eval, err := FromExtismLoader(mockLoader,
eval, err := FromExtismLoader(
t.Context(), mockLoader,
WithEntryPoint(wasmdata.EntrypointGreet),
WithStaticData(map[string]any{"ignored": true}),
WithDataProvider(provider),
Expand All @@ -130,7 +136,8 @@ func TestFromExtismLoader_DataProviderBeatsStaticData(t *testing.T) {
func TestFromExtismLoader_NilOption(t *testing.T) {
mockLoader := newWASMLoader(t)
var nilOpt Option
eval, err := FromExtismLoader(mockLoader,
eval, err := FromExtismLoader(
t.Context(), mockLoader,
nilOpt,
WithEntryPoint(wasmdata.EntrypointGreet),
)
Expand All @@ -140,7 +147,7 @@ func TestFromExtismLoader_NilOption(t *testing.T) {

func TestFromExtismLoader_LoaderError(t *testing.T) {
mockLoader := newErrorLoader(t, "failed to load WASM")
eval, err := FromExtismLoader(mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
eval, err := FromExtismLoader(t.Context(), mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
require.Error(t, err)
require.Nil(t, eval)
mockLoader.AssertExpectations(t)
Expand All @@ -151,7 +158,7 @@ func TestFromExtismLoader_NilSourceURL(t *testing.T) {
mockLoader.On("GetSourceURL").Return(nil)
mockLoader.On("GetReader", mock.Anything).Return(io.NopCloser(bytes.NewReader(wasmdata.TestModule)), nil)

eval, err := FromExtismLoader(mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
eval, err := FromExtismLoader(t.Context(), mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
require.NoError(t, err)
require.NotNil(t, eval)
mockLoader.AssertExpectations(t)
Expand All @@ -166,7 +173,7 @@ func TestFromExtismLoader_DiskLoader(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, diskLoader)

eval, err := FromExtismLoader(diskLoader, WithEntryPoint(wasmdata.EntrypointGreet))
eval, err := FromExtismLoader(t.Context(), diskLoader, WithEntryPoint(wasmdata.EntrypointGreet))
require.NoError(t, err)
require.NotNil(t, eval)
assert.Equal(t, "extism.Evaluator", eval.String())
Expand All @@ -177,6 +184,7 @@ func TestFromExtismLoader_RunsEndToEnd(t *testing.T) {
require.NoError(t, err)

eval, err := FromExtismLoader(
t.Context(),
scriptLoader,
WithEntryPoint(wasmdata.EntrypointGreet),
WithStaticData(map[string]any{"input": "World"}),
Expand All @@ -203,7 +211,7 @@ func TestFromExtismLoader_DefaultsToSlogDefault(t *testing.T) {
scriptLoader, err := loader.NewFromBytes(wasmdata.TestModule)
require.NoError(t, err)

eval, err := FromExtismLoader(scriptLoader, WithEntryPoint(wasmdata.EntrypointGreet))
eval, err := FromExtismLoader(t.Context(), scriptLoader, WithEntryPoint(wasmdata.EntrypointGreet))
require.NoError(t, err)
require.NotNil(t, eval)
}
Expand All @@ -224,3 +232,24 @@ func TestNewCompiler(t *testing.T) {
require.NotNil(t, comp)
})
}

// TestFromExtismLoader_LoaderCancelled verifies that ctx cancellation
// during the loader IO phase aborts the constructor. This is the
// headline behavior of #145 for engines whose WASM compile is too fast
// to reliably observe a pre-cancelled ctx (the wazero compile of the
// small embedded test module completes in microseconds).
func TestFromExtismLoader_LoaderCancelled(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(t.Context())
cancel() // pre-cancel; the loader's GetReader call observes it.

mockLoader := new(loaderMock)
mockLoader.On("GetSourceURL").Return((*url.URL)(nil))
mockLoader.On("GetReader", mock.Anything).Return(nil, ctx.Err())

eval, err := FromExtismLoader(ctx, mockLoader, WithEntryPoint(wasmdata.EntrypointGreet))
require.Error(t, err)
require.Nil(t, eval)
require.ErrorIs(t, err, context.Canceled)
}
Loading