Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
262 lines
5.9 KiB
Go
262 lines
5.9 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type StreamEndReason string
|
|
|
|
const (
|
|
StreamEndReasonNone StreamEndReason = ""
|
|
StreamEndReasonDone StreamEndReason = "done"
|
|
StreamEndReasonTimeout StreamEndReason = "timeout"
|
|
StreamEndReasonClientGone StreamEndReason = "client_gone"
|
|
StreamEndReasonScannerErr StreamEndReason = "scanner_error"
|
|
StreamEndReasonHandlerStop StreamEndReason = "handler_stop"
|
|
StreamEndReasonEOF StreamEndReason = "eof"
|
|
StreamEndReasonPanic StreamEndReason = "panic"
|
|
StreamEndReasonPingFail StreamEndReason = "ping_fail"
|
|
)
|
|
|
|
// ResponseOutcome is the protocol-level result of one response, independent of
|
|
// how the transport ended. Adaptors mark it from the events they already parse.
|
|
type ResponseOutcome string
|
|
|
|
const (
|
|
ResponseOutcomeUnknown ResponseOutcome = ""
|
|
ResponseOutcomeCompleted ResponseOutcome = "completed"
|
|
ResponseOutcomeFailed ResponseOutcome = "failed"
|
|
ResponseOutcomeIncomplete ResponseOutcome = "incomplete"
|
|
ResponseOutcomeCancelled ResponseOutcome = "cancelled"
|
|
)
|
|
|
|
const maxStreamErrorEntries = 20
|
|
|
|
type StreamErrorEntry struct {
|
|
Message string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
type StreamStatus struct {
|
|
EndReason StreamEndReason
|
|
EndError error
|
|
endMu sync.Mutex
|
|
endSet bool
|
|
|
|
mu sync.Mutex
|
|
Errors []StreamErrorEntry
|
|
ErrorCount int
|
|
|
|
response ResponseOutcome
|
|
errorCode string
|
|
errorType string
|
|
errorStatus int
|
|
incompleteReason string
|
|
expectsTerminal bool
|
|
}
|
|
|
|
// StreamOutcome holds classification facts only; upstream messages never
|
|
// enter it because they may contain credentials or request content.
|
|
type StreamOutcome struct {
|
|
EndReason StreamEndReason
|
|
HasErrors bool
|
|
ExpectsTerminal bool
|
|
Response ResponseOutcome
|
|
ErrorCode string
|
|
ErrorType string
|
|
ErrorStatus int
|
|
IncompleteReason string
|
|
}
|
|
|
|
func NewStreamStatus() *StreamStatus {
|
|
return &StreamStatus{}
|
|
}
|
|
|
|
func (s *StreamStatus) SetEndReason(reason StreamEndReason, err error) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.endMu.Lock()
|
|
defer s.endMu.Unlock()
|
|
if s.endSet {
|
|
return
|
|
}
|
|
s.EndReason = reason
|
|
s.EndError = err
|
|
s.endSet = true
|
|
}
|
|
|
|
// SetClientGone preserves the client disconnect as the diagnostic reason when
|
|
// it races with a normal upstream terminal event. Fatal and handler-stop reasons
|
|
// remain authoritative.
|
|
func (s *StreamStatus) SetClientGone(err error) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.endMu.Lock()
|
|
defer s.endMu.Unlock()
|
|
if !s.endSet || s.EndReason == StreamEndReasonDone || s.EndReason == StreamEndReasonEOF {
|
|
s.EndReason = StreamEndReasonClientGone
|
|
s.EndError = err
|
|
s.endSet = true
|
|
}
|
|
}
|
|
|
|
func (s *StreamStatus) RecordError(msg string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.ErrorCount++
|
|
if len(s.Errors) < maxStreamErrorEntries {
|
|
s.Errors = append(s.Errors, StreamErrorEntry{
|
|
Message: msg,
|
|
Timestamp: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// RequireTerminal declares that the protocol always ends with an explicit
|
|
// terminal event, so a stream that ends without one was cut short.
|
|
func (s *StreamStatus) RequireTerminal() {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.expectsTerminal = true
|
|
}
|
|
|
|
// MarkCompleted, MarkIncomplete and MarkCancelled keep the first terminal seen;
|
|
// MarkFailed always wins because an error after completion is still a failure.
|
|
func (s *StreamStatus) MarkCompleted() {
|
|
s.markTerminal(ResponseOutcomeCompleted, "")
|
|
}
|
|
|
|
func (s *StreamStatus) MarkIncomplete(reason string) {
|
|
s.markTerminal(ResponseOutcomeIncomplete, reason)
|
|
}
|
|
|
|
func (s *StreamStatus) MarkCancelled() {
|
|
s.markTerminal(ResponseOutcomeCancelled, "")
|
|
}
|
|
|
|
func (s *StreamStatus) markTerminal(outcome ResponseOutcome, incompleteReason string) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.response != ResponseOutcomeUnknown {
|
|
return
|
|
}
|
|
s.response = outcome
|
|
s.incompleteReason = incompleteReason
|
|
}
|
|
|
|
// MarkFailed records a protocol failure. Empty details never erase details
|
|
// recorded earlier, so a bare error envelope keeps the structured error.
|
|
func (s *StreamStatus) MarkFailed(code, errorType string, status int) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.response = ResponseOutcomeFailed
|
|
if code != "" {
|
|
s.errorCode = code
|
|
}
|
|
if errorType != "" {
|
|
s.errorType = errorType
|
|
}
|
|
if status != 0 {
|
|
s.errorStatus = status
|
|
}
|
|
}
|
|
|
|
func (s *StreamStatus) ResponseOutcome() string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return string(s.response)
|
|
}
|
|
|
|
func (s *StreamStatus) ResponseFailed() bool {
|
|
return s.ResponseOutcome() == string(ResponseOutcomeFailed)
|
|
}
|
|
|
|
func (s *StreamStatus) OutcomeSnapshot() StreamOutcome {
|
|
if s == nil {
|
|
return StreamOutcome{}
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return StreamOutcome{
|
|
EndReason: s.EndReason,
|
|
HasErrors: s.ErrorCount > 0,
|
|
ExpectsTerminal: s.expectsTerminal,
|
|
Response: s.response,
|
|
ErrorCode: s.errorCode,
|
|
ErrorType: s.errorType,
|
|
ErrorStatus: s.errorStatus,
|
|
IncompleteReason: s.incompleteReason,
|
|
}
|
|
}
|
|
|
|
func (s *StreamStatus) HasErrors() bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.ErrorCount > 0
|
|
}
|
|
|
|
func (s *StreamStatus) TotalErrorCount() int {
|
|
if s == nil {
|
|
return 0
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.ErrorCount
|
|
}
|
|
|
|
func (s *StreamStatus) IsNormalEnd() bool {
|
|
if s == nil {
|
|
return true
|
|
}
|
|
s.endMu.Lock()
|
|
reason := s.EndReason
|
|
s.endMu.Unlock()
|
|
return reason == StreamEndReasonDone ||
|
|
reason == StreamEndReasonEOF ||
|
|
reason == StreamEndReasonHandlerStop
|
|
}
|
|
|
|
func (s *StreamStatus) Summary() string {
|
|
if s == nil {
|
|
return "StreamStatus<nil>"
|
|
}
|
|
b := &strings.Builder{}
|
|
s.endMu.Lock()
|
|
reason := s.EndReason
|
|
endError := s.EndError
|
|
s.endMu.Unlock()
|
|
fmt.Fprintf(b, "reason=%s", reason)
|
|
if endError != nil {
|
|
fmt.Fprintf(b, " end_error=%q", endError.Error())
|
|
}
|
|
s.mu.Lock()
|
|
if s.ErrorCount > 0 {
|
|
fmt.Fprintf(b, " soft_errors=%d", s.ErrorCount)
|
|
}
|
|
s.mu.Unlock()
|
|
return b.String()
|
|
}
|