Files
core/internal/history/manual_failure.go

118 lines
3.7 KiB
Go

package history
import (
"context"
"crypto/sha1"
"database/sql"
"encoding/hex"
"fmt"
"strings"
"time"
)
type RegisterManualComponentFailureCommand struct {
ComponentID string
MachineID *string
FailureTime time.Time
Description *string
FailureType string
FailureSource string
FailureExternal string
SourceRef *string
IdempotencyKey *string
}
type RegisterManualComponentFailureResult struct {
HistoryApply ApplyPatchResult `json:"history_apply"`
FailureEventID string `json:"failure_event_id"`
FailureExternal string `json:"failure_external_id"`
}
func (s *Service) RegisterManualComponentFailure(ctx context.Context, cmd RegisterManualComponentFailureCommand) (RegisterManualComponentFailureResult, error) {
if s == nil || s.db == nil {
return RegisterManualComponentFailureResult{}, fmt.Errorf("history service unavailable")
}
componentID := strings.TrimSpace(cmd.ComponentID)
if componentID == "" {
return RegisterManualComponentFailureResult{}, fmt.Errorf("%w: component_id is required", ErrInvalidPatch)
}
failureAt := cmd.FailureTime.UTC()
if failureAt.IsZero() {
return RegisterManualComponentFailureResult{}, fmt.Errorf("%w: failure_time is required", ErrInvalidPatch)
}
failureType := strings.TrimSpace(cmd.FailureType)
if failureType == "" {
failureType = "component_failed_manual"
}
failureSource := strings.TrimSpace(cmd.FailureSource)
if failureSource == "" {
failureSource = "manual_ui"
}
failureExternal := strings.TrimSpace(cmd.FailureExternal)
if failureExternal == "" {
failureExternal = buildManualFailureExternalID(componentID, failureAt, cmd.Description)
}
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return RegisterManualComponentFailureResult{}, err
}
defer func() { _ = tx.Rollback() }()
patch := []PatchOp{
{Op: "replace", Path: "/runtime/health_status", Value: "FAILED"},
{Op: "replace", Path: "/runtime/health_status_at", Value: failureAt.Format(time.RFC3339Nano)},
}
applyRes, err := s.ApplyComponentPatchWithTx(ctx, tx, ApplyPatchCommand{
EntityID: componentID,
ChangeType: "COMPONENT_STATUS_SET",
EffectiveAt: &failureAt,
SourceType: "user",
SourceRef: cmd.SourceRef,
ActorType: "user",
IdempotencyKey: cmd.IdempotencyKey,
Patch: patch,
})
if err != nil {
return RegisterManualComponentFailureResult{}, err
}
if err := s.UpsertFailureProjectionWithTx(ctx, tx, FailureProjectionInput{
Source: failureSource,
ExternalID: failureExternal,
PartID: componentID,
MachineID: normalizeStringPtr(cmd.MachineID),
FailureType: failureType,
FailureTime: failureAt,
Details: normalizeStringPtr(cmd.Description),
}); err != nil {
return RegisterManualComponentFailureResult{}, err
}
var failureEventID string
if err := tx.QueryRowContext(ctx,
`SELECT id FROM failure_events WHERE source = ? AND external_id = ?`,
failureSource, failureExternal,
).Scan(&failureEventID); err != nil {
return RegisterManualComponentFailureResult{}, err
}
if err := tx.Commit(); err != nil {
return RegisterManualComponentFailureResult{}, err
}
return RegisterManualComponentFailureResult{
HistoryApply: applyRes,
FailureEventID: failureEventID,
FailureExternal: failureExternal,
}, nil
}
func buildManualFailureExternalID(componentID string, failureAt time.Time, description *string) string {
normDesc := ""
if description != nil {
normDesc = strings.TrimSpace(*description)
}
sum := sha1.Sum([]byte(strings.ToLower(componentID) + "|" + failureAt.UTC().Format(time.RFC3339Nano) + "|" + strings.ToLower(normDesc)))
return "manual_ui:" + componentID + ":" + failureAt.UTC().Format(time.RFC3339Nano) + ":" + hex.EncodeToString(sum[:4])
}