142 lines
5.3 KiB
Go
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)
|
|
}
|