Files
go-sip/internal/agent/mock_failure_report.go
T

142 lines
5.3 KiB
Go

package agent
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"git.ipao.vip/rogee/go-sip/internal/contract"
"github.com/google/uuid"
"google.golang.org/protobuf/proto"
)
type MockFailureFactClient interface {
ReportExecutionEvent(context.Context, *agentpb.ReportExecutionEventRequest) (*agentpb.ReportExecutionEventResponse, error)
}
// MockUploadFailureCode returns empty for uncertain PUT outcomes. In particular
// a transport error, HTTP 408/429 or 5xx never claims that OSS rejected the
// object; the Dispatcher retains the unknown outcome until its deadline.
func MockUploadFailureCode(cause error) string {
switch {
case errors.Is(cause, ErrUploadGrantExpired):
return "upload_authorization_expired"
case errors.Is(cause, ErrUploadGrantInvalid):
return "upload_authorization_failed"
case errors.Is(cause, ErrUploadChecksumMismatch):
return "checksum_mismatch"
case errors.Is(cause, os.ErrNotExist):
return "upload_failed"
}
var response *UploadHTTPError
if errors.As(cause, &response) && response.StatusCode >= 400 && response.StatusCode < 500 &&
response.StatusCode != http.StatusRequestTimeout && response.StatusCode != http.StatusTooManyRequests {
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
return "upload_authorization_failed"
}
return "upload_failed"
}
return ""
}
func validateMockFailureActiveMeta(meta *agentpb.RequestMeta) error {
if meta == nil || meta.ProtocolVersion != "agent.v1" || meta.AgentId == "" || meta.CellId == "" ||
meta.BootId == "" || meta.DispatcherEpoch == "" || meta.SessionGeneration == 0 {
return errors.New("Mock failure requires the current activated Agent session")
}
return nil
}
// ReportMockUploadFailure persists a known, explicit failure before trying to
// notify the Dispatcher. It never retries a PUT or claims an uncertain result.
// The caller supplies metadata from its activated Agent session, not a newly
// invented boot identity. A pending fact is retried with the same ID after
// restart, while the request metadata uses the new active session.
func (s *Spool) ReportMockUploadFailure(ctx context.Context, client MockFailureFactClient, active *agentpb.RequestMeta, uploadID string, cause error) (bool, error) {
code := MockUploadFailureCode(cause)
if code == "" {
return false, nil
}
if err := validateMockFailureActiveMeta(active); err != nil {
return true, err
}
record, err := s.LoadUploadAttempt(uploadID)
if err != nil {
return true, err
}
if record.FailureFact == nil {
if record.Binding == nil || record.Asset == nil {
return true, errors.New("Mock failure has no bound recording attempt")
}
payload, err := json.Marshal(contract.LocalMockRecordingFailure{
SchemaVersion: contract.LocalMockRecordingFailureVersion,
UploadID: record.UploadID, RecordingID: record.Asset.AssetId, ErrorCode: code,
})
if err != nil {
return true, err
}
sum := sha256.Sum256(payload)
fact := &agentpb.ExecutionFact{
FactId: uuid.NewString(), Binding: proto.Clone(record.Binding).(*agentpb.ExecutionBinding),
Kind: agentpb.FactKind_FACT_KIND_RECORDING_PROGRESS, PayloadJson: payload,
ContentSha256: hex.EncodeToString(sum[:]), ObservedAtUnixMs: s.now().UTC().UnixMilli(),
SourceBootId: active.BootId, SourceSequence: 1,
}
if err := s.RecordUploadFailureFact(uploadID, fact); err != nil {
return true, fmt.Errorf("persist Mock failure before RPC: %w", err)
}
record, err = s.LoadUploadAttempt(uploadID)
if err != nil {
return true, err
}
}
return true, s.deliverMockFailure(ctx, client, active, record)
}
func (s *Spool) RecoverMockUploadFailures(ctx context.Context, client MockFailureFactClient, active *agentpb.RequestMeta) error {
pending, err := s.PendingUploadFailures()
if err != nil || len(pending) == 0 {
return err
}
if err := validateMockFailureActiveMeta(active); err != nil {
return err
}
for _, record := range pending {
if err := s.deliverMockFailure(ctx, client, active, record); err != nil {
return fmt.Errorf("recover Mock recording failure %s: %w", record.UploadID, err)
}
}
return nil
}
func (s *Spool) deliverMockFailure(ctx context.Context, client MockFailureFactClient, active *agentpb.RequestMeta, record UploadAttempt) error {
if client == nil || record.FailureFact == nil {
return errors.New("Mock failure reporter or durable fact is missing")
}
if err := validateMockFailureActiveMeta(active); err != nil {
return err
}
meta := proto.Clone(active).(*agentpb.RequestMeta)
meta.RequestId = record.FailureFact.FactId
meta.OperationId = record.FailureFact.FactId
meta.IdempotencyKey = "mock-upload-failure:" + record.FailureFact.FactId
response, err := client.ReportExecutionEvent(ctx, &agentpb.ReportExecutionEventRequest{
Meta: meta, Fact: proto.Clone(record.FailureFact).(*agentpb.ExecutionFact),
})
if err != nil {
return err
}
receipt := response.GetReceipt()
if receipt.GetResult() != agentpb.ResultCode_RESULT_CODE_ACCEPTED ||
receipt.GetFactId() != record.FailureFact.FactId || receipt.GetContentSha256() != record.FailureFact.ContentSha256 {
return errors.New("Dispatcher did not acknowledge the original Mock failure fact")
}
return s.CompleteUploadFailureReport(record.UploadID, record.FailureFact.FactId)
}