Files
tasks/common/nexus/failure.go
zeekay 40351cce6f rip: delete temporal fork — native ZAP-only tasksd
Goes from 135 MB binary with 1153 files importing go.temporal.io and 224
importing google.golang.org/grpc to a 10 MB native binary with ZERO of
either.

Deleted:
- tasks/, tasktests/, service/, chasm/, client/, api/, proto/, tools/
- cmd/tools/, components/, docker/
- schema/{cassandra,elasticsearch,mysql,postgresql,sqlite}/
- 40 tainted common/ subdirs + 7 top-level common files
- tests/testutils (no longer referenced after surviving deps drop common)

New cmd/tasksd (~100 lines) boots tasks.Embed (zap.Node) + http.Server.
New pkg/tasks/embed.go registers ZAP handlers for the SDK opcode surface
(0x0060-0x00A5). Workflow handlers return a 501 "not yet implemented"
envelope; native engine lands behind this contract.

Build:  go build ./cmd/tasksd → 10 MB
Vet:    go vet ./... → clean
Boot:   tasksd --zap-port 9999 --http :7243
Probes: /healthz, /v1/tasks/health → 200
UI:     /_/tasks/, / → 200 (embedded Vite bundle)
ZAP:    _tasks._tcp on :9999 (mDNS disabled; direct connect)

go.mod: tidy. No go.temporal.io. No google.golang.org/grpc.
2026-04-26 14:06:21 -07:00

431 lines
14 KiB
Go

package nexus
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"sync/atomic"
"github.com/nexus-rpc/sdk-go/nexus"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
failurepb "go.temporal.io/api/failure/v1"
nexuspb "go.temporal.io/api/nexus/v1"
"go.temporal.io/api/serviceerror"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
)
const (
// FailureSourceHeaderName is the header used to indicate from where the Nexus failure originated.
FailureSourceHeaderName = "Temporal-Nexus-Failure-Source"
// FailureSourceWorker indicates the failure originated from outside the server (e.g. bad request or on the Nexus worker).
FailureSourceWorker = "worker"
)
type failureSourceContextKeyType struct{}
var FailureSourceContextKey = failureSourceContextKeyType{}
func SetFailureSourceOnContext(ctx context.Context, response *http.Response) {
if response == nil || response.Header == nil {
return
}
failureSourceHeader := response.Header.Get(FailureSourceHeaderName)
if failureSourceHeader == "" {
return
}
failureSourceContext := ctx.Value(FailureSourceContextKey)
if failureSourceContext == nil {
return
}
if val, ok := failureSourceContext.(*atomic.Value); ok {
val.Store(failureSourceHeader)
}
}
var failureTypeString = string((&failurepb.Failure{}).ProtoReflect().Descriptor().FullName())
// ProtoFailureToNexusFailure converts a proto Nexus Failure to a Nexus SDK Failure.
func ProtoFailureToNexusFailure(failure *nexuspb.Failure) nexus.Failure {
nf := nexus.Failure{
Message: failure.GetMessage(),
StackTrace: failure.GetStackTrace(),
Metadata: failure.GetMetadata(),
Details: failure.GetDetails(),
}
if failure.GetCause() != nil {
cause := ProtoFailureToNexusFailure(failure.GetCause())
nf.Cause = &cause
}
return nf
}
// NexusFailureToProtoFailure converts a Nexus SDK Failure to a proto Nexus Failure.
// Always returns a non-nil value.
func NexusFailureToProtoFailure(failure nexus.Failure) *nexuspb.Failure {
pf := &nexuspb.Failure{
Message: failure.Message,
Metadata: failure.Metadata,
Details: failure.Details,
StackTrace: failure.StackTrace,
}
if failure.Cause != nil {
pf.Cause = NexusFailureToProtoFailure(*failure.Cause)
}
return pf
}
type serializedHandlerError struct {
Type string `json:"type,omitempty"`
RetryableOverride *bool `json:"retryableOverride,omitempty"`
// Bytes as base64 encoded string.
EncodedAttributes string `json:"encodedAttributes,omitempty"`
}
// TemporalFailureToNexusFailure converts an API proto Failure to a Nexus SDK Failure setting the metadata "type" field to
// the proto fullname of the temporal API Failure message or the standard Nexus SDK failure types.
// Returns an error if the failure cannot be converted.
// Mutates the failure temporarily, unsetting the Message and StackTrace fields to avoid duplicating the information in
// the serialized failure. Mutating was chosen over cloning for performance reasons since this function may be called
// frequently.
func TemporalFailureToNexusFailure(failure *failurepb.Failure) (nexus.Failure, error) {
var causep *nexus.Failure
if failure.GetCause() != nil {
var cause nexus.Failure
var err error
cause, err = TemporalFailureToNexusFailure(failure.GetCause())
if err != nil {
return nexus.Failure{}, err
}
causep = &cause
}
switch info := failure.GetFailureInfo().(type) {
case *failurepb.Failure_NexusHandlerFailureInfo:
var encodedAttributes string
if failure.EncodedAttributes != nil {
b, err := protojson.Marshal(failure.EncodedAttributes)
if err != nil {
return nexus.Failure{}, fmt.Errorf("failed to deserialize HandlerError attributes: %w", err)
}
encodedAttributes = base64.RawURLEncoding.EncodeToString(b)
}
var retryableOverride *bool
// nolint:exhaustive,revive // There are only two valid values other than unspecified.
switch info.NexusHandlerFailureInfo.GetRetryBehavior() {
case enumspb.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE:
val := true
retryableOverride = &val
case enumspb.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE:
val := false
retryableOverride = &val
}
handlerError := serializedHandlerError{
Type: info.NexusHandlerFailureInfo.GetType(),
RetryableOverride: retryableOverride,
EncodedAttributes: encodedAttributes,
}
details, err := json.Marshal(handlerError)
if err != nil {
return nexus.Failure{}, err
}
return nexus.Failure{
Message: failure.GetMessage(),
StackTrace: failure.GetStackTrace(),
Metadata: map[string]string{
"type": "nexus.HandlerError",
},
Details: details,
Cause: causep,
}, nil
}
// Unset message and stack trace so it's not serialized in the details.
var message string
message, failure.Message = failure.Message, ""
var stackTrace string
stackTrace, failure.StackTrace = failure.StackTrace, ""
data, err := protojson.Marshal(failure)
failure.Message = message
failure.StackTrace = stackTrace
if err != nil {
return nexus.Failure{}, err
}
return nexus.Failure{
Message: failure.GetMessage(),
StackTrace: failure.GetStackTrace(),
Metadata: map[string]string{
"type": failureTypeString,
},
Details: data,
Cause: causep,
}, nil
}
// NexusFailureToTemporalFailure converts a Nexus Failure to an API proto Failure.
// If the failure metadata "type" field is set to the fullname of the temporal API Failure message, the failure is
// reconstructed using protojson.Unmarshal on the failure details field. Otherwise, the failure is reconstructed
// based on the known Nexus SDK failure types.
// Returns an error if the failure cannot be converted.
// nolint:revive // cognitive-complexity is high but justified to keep each case together
func NexusFailureToTemporalFailure(f nexus.Failure) (*failurepb.Failure, error) {
apiFailure := &failurepb.Failure{
Message: f.Message,
StackTrace: f.StackTrace,
}
if f.Metadata != nil {
switch f.Metadata["type"] {
case failureTypeString:
opts := protojson.UnmarshalOptions{DiscardUnknown: true}
if err := opts.Unmarshal(f.Details, apiFailure); err != nil {
return nil, err
}
// Restore these fields as they are not included in the marshalled failure.
apiFailure.Message = f.Message
apiFailure.StackTrace = f.StackTrace
case "nexus.OperationError":
// Special case for OperationError that adapts from Nexus semantics to Temporal semantics.
// Note that Temporal -> Temporal doesn't go through this code path, operation errors are always used as empty
// wrappers for an underlying causes.
var operationError *nexus.OperationError
err := json.Unmarshal(f.Details, &operationError)
if err != nil {
return nil, fmt.Errorf("failed to deserialize OperationError: %w", err)
}
if operationError.State == nexus.OperationStateCanceled {
// Canceled operation errors are represented as CanceledFailure in Temporal.
apiFailure.FailureInfo = &failurepb.Failure_CanceledFailureInfo{
CanceledFailureInfo: &failurepb.CanceledFailureInfo{},
}
} else {
// Failed operation errors are represented as non-retryable ApplicationFailure in Temporal.
apiFailure.FailureInfo = &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: true,
Type: "OperationError",
},
}
}
case "nexus.HandlerError":
var se serializedHandlerError
err := json.Unmarshal(f.Details, &se)
if err != nil {
return nil, fmt.Errorf("failed to deserialize HandlerError: %w", err)
}
var retryBehavior enumspb.NexusHandlerErrorRetryBehavior
if se.RetryableOverride == nil {
retryBehavior = enumspb.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_UNSPECIFIED
} else if *se.RetryableOverride {
retryBehavior = enumspb.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE
} else {
retryBehavior = enumspb.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE
}
apiFailure.FailureInfo = &failurepb.Failure_NexusHandlerFailureInfo{
NexusHandlerFailureInfo: &failurepb.NexusHandlerFailureInfo{
Type: se.Type,
RetryBehavior: retryBehavior,
},
}
if len(se.EncodedAttributes) > 0 {
decoded, err := base64.RawURLEncoding.DecodeString(se.EncodedAttributes)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 HandlerError attributes: %w", err)
}
apiFailure.EncodedAttributes = &commonpb.Payload{}
if err := protojson.Unmarshal(decoded, apiFailure.EncodedAttributes); err != nil {
return nil, fmt.Errorf("failed to deserialize HandlerError attributes: %w", err)
}
}
default:
// We don't recognize this type, convert to a generic ApplicationFailure and preserve the original Nexus failure
// as serialized details.
applicationFailureInfo, err := nexusFailureMetadataToApplicationFailureInfo(f)
if err != nil {
return nil, fmt.Errorf("failed to serialize Nexus failure: %w", err)
}
apiFailure.FailureInfo = applicationFailureInfo
}
} else if len(f.Details) > 0 {
// We don't recognize this type, convert to a generic ApplicationFailure and preserve the original Nexus failure as
// serialized details.
applicationFailureInfo, err := nexusFailureMetadataToApplicationFailureInfo(f)
if err != nil {
return nil, fmt.Errorf("failed to serialize Nexus failure: %w", err)
}
apiFailure.FailureInfo = applicationFailureInfo
}
if f.Cause != nil {
var err error
apiFailure.Cause, err = NexusFailureToTemporalFailure(*f.Cause)
if err != nil {
return nil, err
}
}
return apiFailure, nil
}
func nexusFailureMetadataToApplicationFailureInfo(failure nexus.Failure) (*failurepb.Failure_ApplicationFailureInfo, error) {
var payloads *commonpb.Payloads
if len(failure.Metadata) > 0 || len(failure.Details) > 0 {
// Delete before serializing (note the failure here is passed by value).
failure.Message = ""
failure.StackTrace = ""
data, err := json.Marshal(failure)
if err != nil {
return nil, err
}
payloads = &commonpb.Payloads{
Payloads: []*commonpb.Payload{
{
Metadata: map[string][]byte{
"encoding": []byte("json/plain"),
},
Data: data,
},
},
}
}
return &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
Type: "NexusFailure",
Details: payloads,
},
}, nil
}
// ConvertGRPCError converts either a serviceerror or a gRPC status error into a Nexus HandlerError if possible.
// If exposeDetails is true, the error message from the given error is exposed in the converted HandlerError, otherwise,
// a default message with minimal information is attached to the returned error.
// Roughly taken from https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
// and
// https://github.com/grpc-ecosystem/grpc-gateway/blob/a7cf811e6ffabeaddcfb4ff65602c12671ff326e/runtime/errors.go#L56.
func ConvertGRPCError(err error, exposeDetails bool) error {
var st *status.Status
stGetter, ok := err.(interface{ Status() *status.Status })
if ok {
st = stGetter.Status()
} else {
st, ok = status.FromError(err)
if !ok {
// The Nexus SDK will translate this into an internal server error and will not expose the error details.
return err
}
}
errMessage := err.Error()
switch st.Code() {
case codes.AlreadyExists, codes.InvalidArgument, codes.FailedPrecondition, codes.OutOfRange:
if !exposeDetails {
errMessage = "bad request"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeBadRequest,
Message: errMessage,
}
case codes.Aborted, codes.Unavailable:
if !exposeDetails {
errMessage = "service unavailable"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeUnavailable,
Message: errMessage,
}
case codes.Canceled:
// TODO: This should have a different status code (e.g. 499 which is semi standard but not supported by nexus).
// The important thing is that the request is retryable, internal serves that purpose.
if !exposeDetails {
errMessage = "canceled"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeInternal,
Message: errMessage,
}
case codes.DataLoss, codes.Internal, codes.Unknown:
if !exposeDetails {
errMessage = "internal error"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeInternal,
Message: errMessage,
}
case codes.Unauthenticated:
if !exposeDetails {
errMessage = "authentication failed"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeUnauthenticated,
Message: errMessage,
}
case codes.PermissionDenied:
if !exposeDetails {
errMessage = "permission denied"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeUnauthorized,
Message: errMessage,
}
case codes.NotFound:
if !exposeDetails {
errMessage = "not found"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeNotFound,
Message: errMessage,
}
case codes.ResourceExhausted:
if !exposeDetails {
errMessage = "resource exhausted"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeResourceExhausted,
Message: errMessage,
}
case codes.Unimplemented:
if !exposeDetails {
errMessage = "not implemented"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeNotImplemented,
Message: errMessage,
}
case codes.DeadlineExceeded:
if !exposeDetails {
errMessage = "request timeout"
}
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeUpstreamTimeout,
Message: errMessage,
}
case codes.OK:
return nil
}
if !exposeDetails {
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeInternal,
Message: "internal error",
}
}
// Let the nexus SDK handle this for us (log and convert to an internal error).
return err
}
func AdaptAuthorizeError(permissionDeniedError *serviceerror.PermissionDenied) error {
if permissionDeniedError.Reason != "" {
return nexus.NewHandlerErrorf(nexus.HandlerErrorTypeUnauthorized, "permission denied: %s", permissionDeniedError.Reason)
}
return nexus.NewHandlerErrorf(nexus.HandlerErrorTypeUnauthorized, "permission denied")
}