iop/packages/go/execution/failure.go
toki 7b90b7e5af refactor(chronos): IOP 경계를 분리한다
Chronos 이전 완료 증거를 보존하면서 IOP에는 provider 실행 책임만 남기기 위해 검토된 마일스톤 변경을 하나의 승격 커밋으로 고정한다.
2026-08-02 19:29:30 +09:00

140 lines
3.8 KiB
Go

package execution
import (
"context"
"encoding/json"
"errors"
"fmt"
)
const failureSchemaVersion = 1
// FailureCode is the stable, provider-neutral classification of an execution
// failure. Provider-specific diagnostics belong in Message or Metadata.
type FailureCode string
const (
FailureCodeUnknown FailureCode = "unknown"
FailureCodeCancelled FailureCode = "cancelled"
FailureCodeDeadlineExceeded FailureCode = "deadline_exceeded"
FailureCodeInvalidRequest FailureCode = "invalid_request"
FailureCodeUnavailable FailureCode = "unavailable"
FailureCodeQuotaExhausted FailureCode = "quota_exhausted"
FailureCodeProvider FailureCode = "provider_error"
FailureCodeInternal FailureCode = "internal"
)
// Failure is the public typed failure value shared by runtime hosts.
type Failure struct {
Code FailureCode `json:"code"`
Message string `json:"message,omitempty"`
Retryable bool `json:"retryable"`
Metadata map[string]string `json:"metadata,omitempty"`
}
func (f *Failure) Error() string {
if f == nil {
return ""
}
if f.Message != "" {
return f.Message
}
return string(f.Code)
}
type failureEnvelope struct {
SchemaVersion int `json:"schema_version"`
Failure *Failure `json:"failure"`
}
// EncodeFailure serializes a typed failure for durable host boundaries.
func EncodeFailure(f *Failure) (string, error) {
if f == nil {
return "", errors.New("execution: nil failure")
}
normalized := normalizeFailure(*f)
payload, err := json.Marshal(failureEnvelope{
SchemaVersion: failureSchemaVersion,
Failure: &normalized,
})
if err != nil {
return "", fmt.Errorf("execution: encode failure: %w", err)
}
return string(payload), nil
}
// DecodeFailure parses a durable failure envelope. Unknown future codes are
// preserved in metadata and normalized to FailureCodeUnknown.
func DecodeFailure(payload string) (*Failure, error) {
var envelope failureEnvelope
if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
return nil, fmt.Errorf("execution: decode failure: %w", err)
}
if envelope.SchemaVersion != failureSchemaVersion {
return nil, fmt.Errorf("execution: unsupported failure schema version %d", envelope.SchemaVersion)
}
if envelope.Failure == nil {
return nil, errors.New("execution: failure payload is missing")
}
normalized := normalizeFailure(*envelope.Failure)
return &normalized, nil
}
// FailureFromError maps cancellation boundaries and otherwise returns a
// provider-neutral unknown failure without guessing provider policy.
func FailureFromError(err error) *Failure {
if err == nil {
return nil
}
switch {
case errors.Is(err, ErrRunCancelled), errors.Is(err, context.Canceled):
return &Failure{Code: FailureCodeCancelled, Message: err.Error()}
case errors.Is(err, context.DeadlineExceeded):
return &Failure{Code: FailureCodeDeadlineExceeded, Message: err.Error(), Retryable: true}
default:
return &Failure{Code: FailureCodeUnknown, Message: err.Error()}
}
}
func normalizeFailure(f Failure) Failure {
if isKnownFailureCode(f.Code) {
return f
}
metadata := cloneStringMap(f.Metadata)
if metadata == nil {
metadata = make(map[string]string, 1)
}
if f.Code != "" {
metadata["original_code"] = string(f.Code)
}
f.Code = FailureCodeUnknown
f.Metadata = metadata
return f
}
func isKnownFailureCode(code FailureCode) bool {
switch code {
case FailureCodeUnknown,
FailureCodeCancelled,
FailureCodeDeadlineExceeded,
FailureCodeInvalidRequest,
FailureCodeUnavailable,
FailureCodeQuotaExhausted,
FailureCodeProvider,
FailureCodeInternal:
return true
default:
return false
}
}
func cloneStringMap(input map[string]string) map[string]string {
if len(input) == 0 {
return nil
}
cloned := make(map[string]string, len(input))
for key, value := range input {
cloned[key] = value
}
return cloned
}