Files
tasks/common/rpc/interceptor/request_error_handler.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

185 lines
5.4 KiB
Go

package interceptor
//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination request_error_handler_mock.go
import (
"errors"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/rpc/interceptor/logtags"
serviceerrors "go.temporal.io/server/common/serviceerror"
"go.temporal.io/server/common/tasktoken"
"google.golang.org/grpc/codes"
)
type (
// ErrorHandler defines the interface for handling request errors
ErrorHandler interface {
HandleError(
req any,
fullMethod string,
metricsHandler metrics.Handler,
logTags []tag.Tag,
err error,
nsName namespace.Name,
)
}
// RequestErrorHandler handles error recording and logging for RPC interceptors
RequestErrorHandler struct {
logger log.Logger
workflowTags *logtags.WorkflowTags
logAllReqErrors dynamicconfig.BoolPropertyFnWithNamespaceFilter
}
)
// NewRequestErrorHandler creates a new RequestErrorHandler
func NewRequestErrorHandler(
logger log.Logger,
logAllReqErrors dynamicconfig.BoolPropertyFnWithNamespaceFilter,
) *RequestErrorHandler {
return &RequestErrorHandler{
logger: logger,
workflowTags: logtags.NewWorkflowTags(tasktoken.NewSerializer(), logger),
logAllReqErrors: logAllReqErrors,
}
}
// HandleError handles error recording and logging
func (eh *RequestErrorHandler) HandleError(
req any,
fullMethod string,
metricsHandler metrics.Handler,
logTags []tag.Tag,
err error,
nsName namespace.Name,
) {
statusCode := serviceerror.ToStatus(err).Code()
if statusCode == codes.OK {
return
}
isExpectedError := isExpectedErrorByStatusCode(statusCode) || isExpectedErrorByType(err)
recordErrorMetrics(metricsHandler, err, isExpectedError)
eh.logError(req, fullMethod, nsName, err, statusCode, isExpectedError, logTags)
}
func (eh *RequestErrorHandler) logError(
req any,
fullMethod string,
nsName namespace.Name,
err error,
statusCode codes.Code,
isExpectedError bool,
logTags []tag.Tag,
) {
logAllErrors := nsName != "" && eh.logAllReqErrors(nsName.String())
// context errors may not be user errors, but still too noisy to log by default
if !logAllErrors && (isExpectedError ||
common.IsContextDeadlineExceededErr(err) ||
common.IsContextCanceledErr(err) ||
common.IsResourceExhausted(err)) {
return
}
logTags = append(logTags, tag.Stringer("grpc_code", statusCode))
logTags = append(logTags, eh.workflowTags.Extract(req, fullMethod)...)
eh.logger.Error("service failures", append(logTags, tag.Error(err))...)
}
func recordErrorMetrics(metricsHandler metrics.Handler, err error, isExpectedError bool) {
metrics.ServiceErrorWithType.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err))
var resourceExhaustedErr *serviceerror.ResourceExhausted
if errors.As(err, &resourceExhaustedErr) {
metrics.ServiceErrResourceExhaustedCounter.With(metricsHandler).Record(
1,
metrics.ResourceExhaustedCauseTag(resourceExhaustedErr.Cause),
metrics.ResourceExhaustedScopeTag(resourceExhaustedErr.Scope),
)
}
if isExpectedError {
return
}
metrics.ServiceFailures.With(metricsHandler).Record(1)
}
//nolint:revive // explicit cases in switch for documentation purposes
func isExpectedErrorByStatusCode(statusCode codes.Code) bool {
switch statusCode {
case codes.Canceled,
codes.InvalidArgument,
codes.NotFound,
codes.AlreadyExists,
codes.PermissionDenied,
codes.FailedPrecondition,
codes.OutOfRange,
codes.Unauthenticated:
return true
// We could just return false here, but making it explicit what codes are
// considered (potentially) server errors.
case codes.Unknown,
codes.DeadlineExceeded,
// the result for resource exhausted depends on the resource exhausted scope and
// will be handled by isExpectedErrorByType()
codes.ResourceExhausted,
codes.Aborted,
codes.Unimplemented,
codes.Internal,
codes.Unavailable,
codes.DataLoss:
return false
default:
return false
}
}
func isExpectedErrorByType(err error) bool {
// This is not a full list of service errors.
// Only errors with status code that fails the isExpectedErrorByStatusCode() check
// but are actually expected need to be explicitly handled here.
//
// Some of the errors listed below does not failed the isExpectedErrorByStatusCode() check
// but are listed nonetheless.
switch err := err.(type) {
case *serviceerror.ResourceExhausted:
return err.Scope == enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE
case *serviceerror.Canceled,
*serviceerror.AlreadyExists,
*serviceerror.CancellationAlreadyRequested,
*serviceerror.FailedPrecondition,
*serviceerror.NamespaceInvalidState,
*serviceerror.NamespaceNotActive,
*serviceerror.NamespaceNotFound,
*serviceerror.NamespaceAlreadyExists,
*serviceerror.InvalidArgument,
*serviceerror.WorkflowExecutionAlreadyStarted,
*serviceerror.WorkflowNotReady,
*serviceerror.NotFound,
*serviceerror.QueryFailed,
*serviceerror.ClientVersionNotSupported,
*serviceerror.ServerVersionNotSupported,
*serviceerror.PermissionDenied,
*serviceerror.NewerBuildExists,
*serviceerrors.StickyWorkerUnavailable,
*serviceerrors.TaskAlreadyStarted,
*serviceerrors.RetryReplication,
*serviceerrors.StalePartitionCounts,
*serviceerrors.SyncState:
return true
default:
return false
}
}