Skip to content

Commit

Permalink
Preserve serializable custom errors from remote method calls. (#458)
Browse files Browse the repository at this point in the history
The code generator emits code to keep track of all error types that
contain an embedded weaver.AutoMarshal in a global table.  The sender
sends such errors by prefixing their serialization with they
corresponding key in the global table. The receiver looks up the
key in its global table, creates a value of the correct type,
and deserializes the error contents into that value.

This allows custom errors to be returned from remote methods without
losing type information.
  • Loading branch information
ghemawat authored Jul 14, 2023
1 parent d3da486 commit 3f2b58a
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 4 deletions.
8 changes: 8 additions & 0 deletions internal/tool/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1898,6 +1898,14 @@ func (g *generator) generateAutoMarshalMethods(p printFn) {
for _, inner := range innerTypes {
g.generateEncDecMethodsFor(p, inner)
}

// Register the type so it can be sent when the compile time type
// is an interface (like error). For now, we only do so for types
// that implement error. We could conceivably allow other types to
// be sent around as interfaces in the future.
if g.tset.implementsError(t) {
p("func init() { %s[%s]() }", g.codegen().qualify("RegisterSerializable"), ts(t))
}
}
}

Expand Down
13 changes: 13 additions & 0 deletions internal/tool/generate/testdata/automarshal_embeddings.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
// func (x *A) WeaverUnmarshal(dec *codegen.Decoder)
// func (x *B) WeaverMarshal(enc *codegen.Encoder)
// func (x *B) WeaverUnmarshal(dec *codegen.Decoder)
// func (x *customError) WeaverMarshal(enc *codegen.Encoder)
// func (x *customError) WeaverUnmarshal(dec *codegen.Decoder)
// RegisterSerializable[customError]()

// UNEXPECTED
// RegisterSerializable[A]()
// RegisterSerializable[B]()

// Verify that AutoMarshal works on a struct with an embedded struct that also
// embeds AutoMarshal.
Expand All @@ -37,6 +44,12 @@ type B struct {
weaver.AutoMarshal
}

type customError struct {
weaver.AutoMarshal
}

func (c customError) Error() string { return "custom" }

type foo interface {
M(context.Context, A, B) error
}
Expand Down
29 changes: 29 additions & 0 deletions internal/tool/generate/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,30 @@ func isInvalid(t types.Type) bool {
return t.String() == "invalid type"
}

// implementsError returns whether the provided type is a concrete type that
// implements error.
func (tset *typeSet) implementsError(t types.Type) bool {
if _, ok := t.Underlying().(*types.Interface); ok {
return false
}
obj, _, _ := types.LookupFieldOrMethod(t, true, tset.pkg.Types, "Error")
method, ok := obj.(*types.Func)
if !ok {
return false
}
sig, ok := method.Type().(*types.Signature)
if !ok {
return false
}
if args := sig.Params(); args.Len() != 0 {
return false
}
if results := sig.Results(); results.Len() != 1 || !isString(results.At(0).Type()) {
return false
}
return true
}

// isProto returns whether the provided type is a concrete type that implements
// the proto.Message interface.
func (tset *typeSet) isProto(t types.Type) bool {
Expand Down Expand Up @@ -865,6 +889,11 @@ func isWeaverAutoMarshal(t types.Type) bool {
return isWeaverType(t, "AutoMarshal", 0)
}

func isString(t types.Type) bool {
b, ok := t.(*types.Basic)
return ok && b.Kind() == types.String
}

func isContext(t types.Type) bool {
n, ok := t.(*types.Named)
if !ok {
Expand Down
10 changes: 10 additions & 0 deletions weavertest/internal/generate/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@ type behaviorType int
const (
appError behaviorType = iota
panicError
customError
noError
)

type customErrorValue struct {
weaver.AutoMarshal
key string
}

func (c customErrorValue) Error() string { return fmt.Sprintf("customError(%s)", c.key) }

type testApp interface {
Get(_ context.Context, key string, behavior behaviorType) (int, error)
IncPointer(_ context.Context, arg *int) (*int, error)
Expand All @@ -50,6 +58,8 @@ func (p *impl) Get(_ context.Context, key string, behavior behaviorType) (int, e
return 42, fmt.Errorf("key %v not found in the store", key)
case panicError:
panic("panic")
case customError:
return 0, customErrorValue{key: key}
case noError:
return 42, nil
}
Expand Down
12 changes: 12 additions & 0 deletions weavertest/internal/generate/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ func TestErrors(t *testing.T) {
t.Fatalf("client.Get: got %d, want 42", x)
}

// Check custom error.
_, err = client.Get(ctx, "custom", customError)
if err == nil {
t.Fatal(err)
}
var c customErrorValue
if !errors.As(err, &c) {
t.Errorf("did not get customError, got error %v of type %T", err, err)
} else if c.key != "custom" {
t.Errorf("customError contained wrong key %q, expecting %q", c.key, "custom")
}

// Trigger a panic.
_, err = client.Get(ctx, "foo", panicError)
if err == nil || !errors.Is(err, weaver.RemoteCallError) {
Expand Down
27 changes: 27 additions & 0 deletions weavertest/internal/generate/weaver_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions website/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2755,10 +2755,17 @@ type Pair[A any] struct {
To serialize generic structs, implement `BinaryMarshaler` and
`BinaryUnmarshaler`.

Finally note that while [Service Weaver requires every component method to
return an `error`](#components-interfaces), `error` is not a
serializable type. Service Weaver serializes `error`s in a way that does not
preserve any custom `Is` or `As` methods.
## Errors

Service Weaver requires every component method to [return an
error](#components-interfaces). If a non-nil error is returned, Service Weaver
by default transmits the textual representation of the error. Therefore any
custom information stored in the error value, or custom `Is` or `As` methods,
are not available to the caller.

Applications that need custom error information can embed a `weaver.AutoMarshal`
in their custom error type. Service Weaver will then serialize and deserialize
such errors properly and make them available to the caller.

# weaver generate

Expand Down

0 comments on commit 3f2b58a

Please sign in to comment.