555fe4c0ba
Client.Go-004: ran gofmt on alarms_test.go and galaxy_test.go; the tree is now gofmt-clean. Client.Go-005/009/010: migrated Dial/DialGalaxy off the deprecated grpc.DialContext/WithBlock to grpc.NewClient via a shared dial helper, with a DialTimeout-bounded readiness probe to keep fail-fast semantics; shared callContext deadline arithmetic; updated the stale Dial doc comment. Test harnesses use passthrough:///bufnet for the NewClient default-scheme change. Client.Go-006: added GatewayError.Code() and an IsTransient(err) helper so callers can classify transient gRPC failures. Client.Go-007: newCorrelationID no longer returns an empty id when crypto/rand fails — it falls back to a non-empty time+counter id. Client.Go-008: added coverage_test.go for transport-credential resolution, callContext deadline arithmetic, and native value/array edge kinds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
package mxgateway
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// ErrEventBufferOverflow is the terminal error delivered on the compatibility
|
|
// event channel returned by Session.Events / Session.EventsAfter when a slow
|
|
// consumer lets the bounded result buffer fill. It signals that the stream was
|
|
// cancelled and events were dropped, so a consumer can tell an overflow apart
|
|
// from a normal end-of-stream. Use Session.SubscribeEvents to block instead of
|
|
// dropping.
|
|
var ErrEventBufferOverflow = errors.New("mxgateway: event buffer overflow; compatibility stream cancelled and events dropped")
|
|
|
|
// GatewayError wraps transport-level gRPC failures.
|
|
type GatewayError struct {
|
|
// Op names the operation that failed (for example "dial" or "invoke").
|
|
Op string
|
|
// Err is the underlying gRPC or transport error.
|
|
Err error
|
|
}
|
|
|
|
// Error returns the formatted gateway error message.
|
|
func (e *GatewayError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if e.Op == "" {
|
|
return fmt.Sprintf("mxgateway: %v", e.Err)
|
|
}
|
|
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
|
}
|
|
|
|
// Unwrap returns the wrapped transport error.
|
|
func (e *GatewayError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Err
|
|
}
|
|
|
|
// Code returns the gRPC status code of the wrapped transport error. It returns
|
|
// codes.OK when the error is nil and codes.Unknown when the wrapped error does
|
|
// not carry a gRPC status. Callers can use it to write retry, timeout, and
|
|
// auth handling without manually unwrapping and re-parsing the error.
|
|
func (e *GatewayError) Code() codes.Code {
|
|
if e == nil || e.Err == nil {
|
|
return codes.OK
|
|
}
|
|
return status.Code(e.Err)
|
|
}
|
|
|
|
// IsTransient reports whether err is a transport failure that may succeed on
|
|
// retry — for example a gateway that is briefly Unavailable or a call that
|
|
// hit a DeadlineExceeded. Permanent failures (Unauthenticated, PermissionDenied,
|
|
// InvalidArgument, NotFound, and similar) return false. It unwraps through
|
|
// *GatewayError and any other error chain carrying a gRPC status, so callers
|
|
// do not need to call status.Code themselves.
|
|
func IsTransient(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
switch transientCode(err) {
|
|
case codes.Unavailable, codes.DeadlineExceeded, codes.ResourceExhausted, codes.Aborted:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// transientCode extracts a gRPC status code from err, preferring a wrapped
|
|
// *GatewayError's Code and otherwise falling back to status.Code on the chain.
|
|
func transientCode(err error) codes.Code {
|
|
var gatewayErr *GatewayError
|
|
if errors.As(err, &gatewayErr) {
|
|
return gatewayErr.Code()
|
|
}
|
|
return status.Code(err)
|
|
}
|
|
|
|
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
|
// command reply when one exists.
|
|
type CommandError struct {
|
|
// Op names the gateway operation that produced the non-OK status.
|
|
Op string
|
|
// Status carries the gateway-reported protocol status.
|
|
Status *ProtocolStatus
|
|
// Reply is the raw command reply, when one was returned alongside the status.
|
|
Reply *MxCommandReply
|
|
}
|
|
|
|
// Error returns the formatted command error message.
|
|
func (e *CommandError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
status := e.Status
|
|
if status == nil {
|
|
return fmt.Sprintf("mxgateway: %s failed with missing protocol status", e.Op)
|
|
}
|
|
if status.GetMessage() == "" {
|
|
return fmt.Sprintf("mxgateway: %s failed with protocol status %s", e.Op, status.GetCode())
|
|
}
|
|
return fmt.Sprintf("mxgateway: %s failed with protocol status %s: %s", e.Op, status.GetCode(), status.GetMessage())
|
|
}
|
|
|
|
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
|
type MxAccessError struct {
|
|
// Command is the wrapped CommandError when the protocol status carried one.
|
|
Command *CommandError
|
|
// Reply is the raw MXAccess command reply that surfaced the failure.
|
|
Reply *MxCommandReply
|
|
}
|
|
|
|
// Error returns the formatted MXAccess error message.
|
|
func (e *MxAccessError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if e.Command != nil && e.Command.Status != nil && e.Command.Status.GetMessage() != "" {
|
|
return e.Command.Error()
|
|
}
|
|
if e.Reply != nil && e.Reply.GetDiagnosticMessage() != "" {
|
|
return fmt.Sprintf("mxgateway: MXAccess command %s failed: %s", e.Reply.GetKind(), e.Reply.GetDiagnosticMessage())
|
|
}
|
|
if e.Reply != nil && e.Reply.Hresult != nil {
|
|
return fmt.Sprintf("mxgateway: MXAccess command %s failed with HRESULT 0x%08X", e.Reply.GetKind(), uint32(e.Reply.GetHresult()))
|
|
}
|
|
return "mxgateway: MXAccess command failed"
|
|
}
|
|
|
|
// Unwrap returns the wrapped CommandError, when one is present.
|
|
//
|
|
// When Command is nil (the HRESULT / MxStatusProxy path) it returns an
|
|
// untyped nil rather than a typed-nil *CommandError, so errors.As does not
|
|
// bind a nil pointer that a caller would then panic on.
|
|
func (e *MxAccessError) Unwrap() error {
|
|
if e == nil || e.Command == nil {
|
|
return nil
|
|
}
|
|
return e.Command
|
|
}
|
|
|
|
// EnsureProtocolSuccess returns a typed CommandError when status is non-OK.
|
|
func EnsureProtocolSuccess(op string, status *ProtocolStatus, reply *MxCommandReply) error {
|
|
if status == nil || status.GetCode() == pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK {
|
|
return nil
|
|
}
|
|
|
|
commandError := &CommandError{
|
|
Op: op,
|
|
Status: status,
|
|
Reply: reply,
|
|
}
|
|
if status.GetCode() == pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE {
|
|
return &MxAccessError{
|
|
Command: commandError,
|
|
Reply: reply,
|
|
}
|
|
}
|
|
return commandError
|
|
}
|
|
|
|
// EnsureMxAccessSuccess returns a typed MxAccessError for failing HRESULTs or
|
|
// MXSTATUS_PROXY entries.
|
|
func EnsureMxAccessSuccess(op string, reply *MxCommandReply) error {
|
|
if reply == nil {
|
|
return nil
|
|
}
|
|
if reply.Hresult != nil && reply.GetHresult() != 0 {
|
|
return &MxAccessError{Reply: reply}
|
|
}
|
|
for _, status := range reply.GetStatuses() {
|
|
if !StatusSucceeded(status) {
|
|
return &MxAccessError{Reply: reply}
|
|
}
|
|
}
|
|
return nil
|
|
}
|